Symfony — how to inject a service by interface and argument name

Filip Horvat
3 min readMar 8, 2024

--

I am referring to cases where you have code like this:

public function __construct(
private readonly CacheInterface $first,
private readonly CacheInterface $second,
)
{
}

And you want to inject a different service (interface implementation) for the $first and $second parameters.

This behavior can be found in Symfony’s ‘Cache Component

You can have more cache pools defined:

# config/packages/cache.yaml
framework:
cache:
pools:
custom_thing.cache:
adapter: cache.app
custom_thing2.cache:
adapter: cache.adapter.filesystem

And then you can inject the first, second, or both cache pools into your service by name. Here is the code that demonstrates how you can inject the first cache pool:


public function __construct(
private readonly CacheInterface $customThingCache,
)
{
}

And you can also inject both:

public function __construct(
private readonly CacheInterface $customThingCache,
private readonly CacheInterface $customThing2Cache,
)
{
}

Now, I will show you how you can do the same in your code, allowing you to use it for your needs or simply understand how it works.

We will create a dummy cache system for this purpose, starting with the CacheInterface:

<?php

namespace App\Service\DummyCache\Adapter;

interface CacheInterface
{
public function get($name);
}

There are two implementations for File and Redis:

<?php
namespace App\Service\DummyCache\Adapter;

class CacheFile implements CacheInterface
{
public function get($name)
{
echo 'get cache from FILE';
}
}
<?php
namespace App\Service\DummyCache\Adapter;

class CacheRedis implements CacheInterface
{
public function get($name)
{
echo 'get cache from REDIS';
}
}

Finally, we have one dummy service where the above adapters will be used:

<?php
namespace App\Service\DummyCache;

use App\Service\DummyCache\Adapter\CacheInterface;

class DummyService
{
public function __construct(
private readonly CacheInterface $cacheFile,
private readonly CacheInterface $cacheRedis,
private readonly CacheInterface $cacheUnknown,
)
{
}

public function doWork()
{
dd(
$this->cacheFile->get('testing'),
$this->cacheRedis->get('testing'),
$this->cacheUnknown->get('testing'),
);
}
}

If you run this code now, you will encounter an error because the Service Container doesn’t know which implementation to inject in places where CacheInterface is typehinted. This is due to the existence of two implementations that use that interface.

In cases where only one service (class) implements that interface, the Service Container will inject that class. However, when there are multiple implementations, it cannot guess which one you want.

You can instruct the Service Container to inject CacheRedis wherever CacheInterface is typehinted by using an alias:

App\Service\DummyCache\Adapter\CacheInterface: '@App\Service\DummyCache\Adapter\CacheRedis'

Or with default bind in services.yaml:

services:
_defaults:
bind:
App\Service\DummyCache\Adapter\CacheInterface: '@App\Service\DummyCache\Adapter\CacheFile'

In this example, with the default bind in place, if you run the code from the service after the above changes, you will see:

get cache from FILE
get cache from FILE
get cache from FILE

Because in all three variables, ‘CacheFile’ is injected.

Now, we will instruct the container to inject ‘CacheFile’ implementation where the argument is:

CacheInterface $cacheFile,

And “CacheRedis” implementation where argument is:

CacheInterface $cacheRedis,

Here is the code:

services:
_defaults:
bind:
App\Service\DummyCache\Adapter\CacheInterface $cacheFile: '@App\Service\DummyCache\Adapter\CacheFile'
App\Service\DummyCache\Adapter\CacheInterface $cacheRedis: '@App\Service\DummyCache\Adapter\CacheRedis'

Now if we run the code from the service we will get an error, because the third argument:

private readonly CacheInterface $cacheUnknown,

cannot be injected because we did not define it anywhere, and the service container is unaware. We have only specified for the service container what to inject when the typehinted class is ‘CacheInterface’ and the argument name is “$cacheFile” or “$cacheRedis”. For all other cases, Symfony doesn’t know what to inject because there are multiple implementations of that interface.

You can instruct the Service Container to inject the default Cache implementation when the name does not match the previously defined rules.

services:
_defaults:
bind:
App\Service\DummyCache\Adapter\CacheInterface $cacheFile: '@App\Service\DummyCache\Adapter\CacheFile'
App\Service\DummyCache\Adapter\CacheInterface $cacheRedis: '@App\Service\DummyCache\Adapter\CacheRedis'
App\Service\DummyCache\Adapter\CacheInterface: '@App\Service\DummyCache\Adapter\CacheRedis'

The output is now:

get cache from FILE
get cache from REDIS
get cache from REDIS

That’s all I hope you enjoyed!

--

--

Filip Horvat

Senior Software Engineer, Backend PHP Developer, Located at Croatia, Currently working at myzone.com