If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIn MixRepository
, it would be cool if we didn't need to specify the host name when we make the HTTP request. Like, it'd be great if that were preconfigured and we only needed to include the path. Also, pretty soon, we're going to configure an access token that will be used when we make requests to the GitHub API. We could pass that access token manually here in our service, but how cool would it be if the HttpClient service came preconfigured to always include the access token?
So, does Symfony have a way for us to, sort of, "preconfigure" the HttpClient service? It does! It's called "scoped clients": a feature of HttpClient where you can create multiple HttpClient services, each preconfigured differently.
Here's how it works. Open up config/packages/framework.yaml
. To create a scoped client, under the framework
key, add http_client
followed by scoped_clients
. Now, give your scoped client a name, like githubContentClient
... since we're using a part of their API that returns the content of files. Also add base_uri
, go copy the host name over here... and paste:
... line 1 | |
framework: | |
... lines 3 - 19 | |
http_client: | |
scoped_clients: | |
githubContentClient: | |
base_uri: https://raw.githubusercontent.com | |
... lines 24 - 30 |
Remember: the purpose of these config files is to change the services in the container. The end result of this new code is that a second HttpClient service will be added to the container. We'll see that in a minute. And, by the way, there's no way that you could just guess that you need http_client
and scoped_clients
keys to make this work. Configuration is the kind of thing where you really need to rely on the documentation.
Anyways, now that we've preconfigured this client, we should be able to go into MixRepository
and make a request directly to the path:
... lines 1 - 9 | |
class MixRepository | |
{ | |
... lines 12 - 18 | |
public function findAll(): array | |
{ | |
return $this->cache->get('mixes_data', function(CacheItemInterface $cacheItem) { | |
... line 22 | |
$response = $this->httpClient->request('GET', '/SymfonyCasts/vinyl-mixes/main/mixes.json'); | |
... lines 24 - 25 | |
}); | |
} | |
} |
But if we head over and refresh... ah...
Invalid URL: scheme is missing [...]. Did you forget to add "http(s)://"?
I didn't think we forgot... since we configured it via the base_uri
option... but apparently that didn't work. And you may have guessed why. Find your terminal and run:
php bin/console debug:autowiring client
There are now two HttpClient services in the container: The normal, non-configured one and the one that we just configured. Apparently, in MixRepository
, Symfony is still passing us the unconfigured HttpClient service.
How can I be sure? Well, think back to how autowiring works. Symfony looks at the type-hint of our argument, which is Symfony\Contracts\HttpClient\HttpClientInterface
, and then looks in the container to find a service whose ID is an exact match. It's that simple
So... if there are multiple services with the same "type" in our container, is only the main one autowireable? Fortunately, no! We can use something called "named autowiring"... and it's already showing us how. If we type-hint an argument with HttpClientInterface
and name the argument $githubContentClient
, Symfony will pass us the second one.
Let's try it: change the argument from $httpClient
to $githubContentClient
:
... lines 1 - 9 | |
class MixRepository | |
{ | |
public function __construct( | |
private HttpClientInterface $githubContentClient, | |
... lines 14 - 16 | |
) {} | |
... lines 18 - 27 | |
} |
and now... it doesn't work. Whoops...
Undefined property:
MixRepository::$httpClient
That's... just me being careless. When I changed the argument name, it changed the property name. So... we need to adjust the code below:
... lines 1 - 9 | |
class MixRepository | |
{ | |
public function __construct( | |
private HttpClientInterface $githubContentClient, | |
... lines 14 - 16 | |
) {} | |
... line 18 | |
public function findAll(): array | |
{ | |
return $this->cache->get('mixes_data', function(CacheItemInterface $cacheItem) { | |
... line 22 | |
$response = $this->githubContentClient->request('GET', '/SymfonyCasts/vinyl-mixes/main/mixes.json'); | |
... lines 24 - 25 | |
}); | |
} | |
} |
And now... it's alive! We just autowired a specific HttpClientInterface service!
Next, let's tackle another tricky problem with autowiring by learning how to fetch one of the many services in our container that is totally not available for autowiring.
Hey @wh!
SUCH a good question. As you know, the purpose of any of this config is to help "configure and add services to the container". The short answer to your question is that the bundle that provides the http client service is FrameworkBundle. By using framework
, we are passing this config to FrameworkBundle. The same is true for cache config: it also lives under framework
because FrameworkBundle provides the cache services.
In other situations, other bundles provide other services. For example, when we want to configure something about Twig (e.g. add a global variable or add a form theme), we do that under the twig
key because TwigBundle provides the Twig services.
But, how could a user possibly know which services come from which bundles? Fair question! FrameworkBundle is huge, and it provides MANY of core services you use, which is why so many pieces of config live under framework
. And, there's no huge, scientific reason why, for example, "twig services" were put into a separate TwigBundle bug "http client" services were put into the core FrameworkBundle. The real answer to your question about config is: read the docs -e.g. search for "how do I use Symfony's HTTP Client" and you'll find docs with the configuration. Because, even if you knew that the HTTP client services came from FrameworkBundle, the correct config still can't be guessed: it's definitely one spot where you should lean on the docs.
But, hopefully this answer gives you some insight into why framework
in this case but other root keys for other services.
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"knplabs/knp-time-bundle": "^1.18", // v1.19.0
"symfony/asset": "6.1.*", // v6.1.0-RC1
"symfony/console": "6.1.*", // v6.1.0-RC1
"symfony/dotenv": "6.1.*", // v6.1.0-RC1
"symfony/flex": "^2", // v2.1.8
"symfony/framework-bundle": "6.1.*", // v6.1.0-RC1
"symfony/http-client": "6.1.*", // v6.1.0-RC1
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/runtime": "6.1.*", // v6.1.0-RC1
"symfony/twig-bundle": "6.1.*", // v6.1.0-RC1
"symfony/ux-turbo": "^2.0", // v2.1.1
"symfony/webpack-encore-bundle": "^1.13", // v1.14.1
"symfony/yaml": "6.1.*", // v6.1.0-RC1
"twig/extra-bundle": "^2.12|^3.0", // v3.4.0
"twig/twig": "^2.12|^3.0" // v3.4.0
},
"require-dev": {
"symfony/debug-bundle": "6.1.*", // v6.1.0-RC1
"symfony/maker-bundle": "^1.41", // v1.42.0
"symfony/stopwatch": "6.1.*", // v6.1.0-RC1
"symfony/web-profiler-bundle": "6.1.*" // v6.1.0-RC1
}
}
Why does the httpClient has to be under the
framework
key? When I want to do that with other Symfony services, like... idk pre-set some cache value or whatever. How do I know under which key to create it on?