Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Named Autowiring & Scoped HTTP Clients

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

In 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.

Creating a Scoped Client

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

Fetching the Named Version of a Service

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.

Leave a comment!

2
Login or Register to join the conversation
wh Avatar

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?

Reply

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!

3 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice