Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

OCP: Autoconfiguration & tagged_iterator

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 $8.00

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

Login Subscribe

When we went to the "submit" page, we got this gigantic error. It's the middle that's most relevant:

Cannot autowire service SightingScorer, argument $scoringFactors of method __construct is type-hinted array. You should configure its value explicitly.

That makes sense! We haven't told Symfony what to pass to the new argument of SightingScorer.

Manually Wiring the Argument

What do we want to pass there? An array of all of our "scoring factor" services. The simplest way to do that is to configure it manually in config/services.yaml. Down at the bottom, we want to configure the App\Service\SightingScorer... service and we want to control its arguments:, specifically this $scoringFactors argument. Copy that, paste, and this will be an array: I'll use the multi-line syntax. Each entry in the array with be one of the scoring factor services. So @App\Scoring\TitleFactor, copy that, paste... fix the indentation... then pass DescriptionFactor and CoordinatesFactor.

... lines 1 - 7
services:
... lines 9 - 32
App\Service\SightingScorer:
arguments:
$scoringFactors:
- '@App\Scoring\TitleFactor'
- '@App\Scoring\DescriptionFactor'
- '@App\Scoring\CoordinatesFactor'

This will now pass an array with these three service objects inside.

Try it again. Refresh and... the error is gone... and now it kicked us to the log-in page. Copy the email above, enter the password, hit "sign in" and... beautiful! The page loads. Let's give it a try. Fill in the details of your most recent interaction with Bigfoot. Oh, but before I submit this, I'm going to add some keywords to the description that I know our scoring factor is looking for.

Submit and... it works! Ah man, a believability score of only 10!? I really thought that was a Bigfoot.

Enabling Autoconfiguration

Before we talk more about OCP, on a technical, Symfony level, there is one other way to inject these services. It's called a "tagged iterator"... and it's a pretty cool idea. It's also commonly used in the core of Symfony itself.

Open up src/Kernel.php. I know, we almost never open this file. Inside, go to Code -> Generate, or Command + N on a Mac, and select Override methods. Override one called build()... let me find it. There it is.

This is a hook where we can do extra processing on the container while it's being built. The parent method is empty... but I'll leave the parent call. Add $container->registerForAutoconfiguration(), pass this ScoringFactorInterface::class, then ->addTag('scoring.factor').

49 lines src/Kernel.php
... lines 1 - 4
use App\Scoring\ScoringFactorInterface;
... lines 6 - 11
class Kernel extends BaseKernel
{
... lines 14 - 40
protected function build(ContainerBuilder $container)
{
parent::build($container);
$container->registerForAutoconfiguration(ScoringFactorInterface::class)
->addTag('scoring.factor');
}
}

Thanks to this, any autoconfigurable service, which is all of our services, that implements ScoringFactorInterface, will automatically be tagged with scoring.factor. That scoring.factor is a name that I totally just made up.

This line, on its own, won't make any real change. But now, back in services.yaml we can simplify: set the $scoringFactors argument to a special YAML syntax: !tagged_iterator scoring.factor.

... lines 1 - 7
services:
... lines 9 - 32
App\Service\SightingScorer:
arguments:
$scoringFactors: !tagged_iterator scoring.factor # Inject all services tagged with "scoring.factor"

This says: please inject all services that are tagged with scoring.factor. So autoconfiguration adds the tag to our scoring factor services... and this handles passing them in. Pretty cool, right?

The only gotcha is that we need to change the type-hint in SightingScorer to be an iterable. This won't pass us an array... but it will pass us something that we can foreach over. As a bonus, it's a "lazy" iterable: the scoring factor services won't be instantiated until and unless we run the foreach. Oh, and change the property type to iterable also.

... lines 1 - 8
class SightingScorer
{
... lines 11 - 13
private iterable $scoringFactors;
... line 15
public function __construct(iterable $scoringFactors)
... lines 17 - 18
}
... lines 20 - 29
}

Next: now that we understand the type of change that OCP wants us to make to our code, let's talk about why we should care - or not care - about OCP and when we should and should not follow it.

Leave a comment!

11
Login or Register to join the conversation

Great tuto so far. I love when I learn new things unrelated to the main topic in tutos (like this auto tag trick).

1 Reply

Hell there, nice tutorial, thanks. 😊
Question: as far as I know we can tag the interface in the services.yaml file as well, does it make any difference to do it in kernel.php?
Here is the code I like to use for tagging the interface:(sorry for indentation problem, can't type it here, idk why)


services:
_defaults:
autowire: true
autoconfigure: true
_instanceof:
App\Scoring\ScoringFactorInterface:
tags:
- { name: scoring.factor }
1 Reply
Michael B. Avatar
Michael B. Avatar Michael B. | alireza | posted 1 year ago

In fact I was about to write the same question here :).

1 Reply

Hey Amin Behravesh

That's a great question and you made me dug to be honest. If you tag your interface like that, the only downside is only the services defined in that services.yaml file will get benefit of it. If you have services defined in other file, or perhaps they come from a bundle, then your tag won't be automatically applied. It's likely that you won't be hit by this issue but it's the only difference I could find.

Cheers!

Reply

Hey Diego Aguiar, thank you for the reply, good to know that. ;)

Reply
Michael B. Avatar

Hey! So, if I register an Interface as a service - it should be available as a service inside the whole application? I mean this is the main reason for registering services, isn't it?

For me it was interesting to see, that you can do this inside the kernel.php but I think there will be really no difference exept of having a way to create services and configure them before symfony does it.

Reply

Hey Michael Brauner

Yes, in this case, I think there should not be any difference unless a third party bundle implements your interface and then, in a different config file you register that service. In that situation, I think Symfony won't automatically tag such service but it's very unlikely to happen because third-party bundles do not depend on interfaces of your application, it's usually the other way around

Cheers!

1 Reply

I had another question though, how we should do it in symfony 3.4, how we should inject all the services with specific interface to a service.

Reply

Hey Amin,

It should work this way: https://symfony.com/blog/ne... , you just need to tag them.

Cheers!

Reply

Thanks Victor Bocharsky , I think it was cache problem, I had the same code, but wasn't work, but suddenly started to work. :)

Reply

Hey Amin,

Awesome! Then it was an easy fix ;) Yeah, my first rule in any weird case - clear the cache and see if it helped :)

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4 || ^8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.12.1
        "doctrine/doctrine-bundle": "^2", // 2.3.1
        "doctrine/doctrine-migrations-bundle": "^3", // 3.1.1
        "doctrine/orm": "^2", // 2.8.4
        "knplabs/knp-time-bundle": "^1.15", // v1.16.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^6.0", // v6.1.2
        "symfony/console": "5.2.*", // v5.2.6
        "symfony/dotenv": "5.2.*", // v5.2.4
        "symfony/flex": "^1.9", // v1.18.7
        "symfony/form": "5.2.*", // v5.2.6
        "symfony/framework-bundle": "5.2.*", // v5.2.6
        "symfony/http-client": "5.2.*", // v5.2.6
        "symfony/mailer": "5.2.*", // v5.2.6
        "symfony/property-access": "5.2.*", // v5.2.4
        "symfony/property-info": "5.2.*", // v5.2.4
        "symfony/security-bundle": "5.2.*", // v5.2.6
        "symfony/serializer": "5.2.*", // v5.2.4
        "symfony/twig-bundle": "5.2.*", // v5.2.4
        "symfony/validator": "5.2.*", // v5.2.6
        "symfony/webpack-encore-bundle": "^1.6", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.5
        "twig/cssinliner-extra": "^3.3", // v3.3.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.0
        "twig/twig": "^2.12|^3.0" // v3.3.0
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.2", // 3.4.0
        "fakerphp/faker": "^1.13", // v1.14.1
        "symfony/debug-bundle": "^5.2", // v5.2.4
        "symfony/maker-bundle": "^1.13", // v1.30.2
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/stopwatch": "^5.2", // v5.2.4
        "symfony/var-dumper": "^5.2", // v5.2.6
        "symfony/web-profiler-bundle": "^5.2" // v5.2.6
    }
}