This tutorial is built using Symfony 4, but most of the concepts apply fine to Symfony 5!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"doctrine/annotations": "^1.8", // v1.8.0
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knpuniversity/lorem-ipsum-bundle": "*@dev", // dev-master
"nexylan/slack-bundle": "^2.0,<2.2", // v2.0.1
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.1.6
"symfony/asset": "^4.0", // v4.0.6
"symfony/console": "^4.0", // v4.0.6
"symfony/flex": "^1.0", // v1.21.6
"symfony/framework-bundle": "^4.0", // v4.0.6
"symfony/lts": "^4@dev", // dev-master
"symfony/twig-bundle": "^4.0", // v4.0.6
"symfony/web-server-bundle": "^4.0", // v4.0.6
"symfony/yaml": "^4.0", // v4.0.6
"weaverryan_test/lorem-ipsum-bundle": "^1.0" // v1.0.0
},
"require-dev": {
"easycorp/easy-log-handler": "^1.0.2", // v1.0.4
"sensiolabs/security-checker": "^4.1", // v4.1.8
"symfony/debug-bundle": "^3.3|^4.0", // v4.0.6
"symfony/dotenv": "^4.0", // v4.0.6
"symfony/maker-bundle": "^1.0", // v1.1.1
"symfony/monolog-bundle": "^3.0", // v3.2.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.3
"symfony/stopwatch": "^3.3|^4.0", // v4.0.6
"symfony/var-dumper": "^3.3|^4.0", // v4.0.6
"symfony/web-profiler-bundle": "^3.3|^4.0" // v4.0.6
}
}
12 Comments
What is the difference between adding this code `$container->registerForAutoconfiguration(WordProviderInterface::class)->addTag('knpu_ipsum_word_provider');` inside the DI extension KnpULoremIpsumExtension::build or inside LoremIpsumBundle::build ! is there a recommanded place ?
Yo ahmedbhs!
Great question! My instinct was that it makes no difference, but I actually didn't know the answer to this one! Fortunately the code holds the secret - and here it is: https://github.com/symfony/...
As you can see, the "extension" class is called first on each bundle and *then* the build() method. But that doesn't make any difference. It is not until after BOTH of these that the "compiler passes" are executed, and it is a compiler pass that ultimately processes the results from registerForAutoconfiguration(). So, both spots are "early enough" and it should make zero difference :). I have seen people (and I don't disagree) sometimes skip creating an extension class entirely and instead just put things in their bundle's build() method for simplicity.
Cheers!
Hi, I have a question :)
Everything works OK: I see words from even two custom words providers, created by me in the main app, <b>and</b> words from
KnpUWordProvider. But what if I want users of my bundle totally exclude words provided byKnpUWordProvider? Before tags system implementation it was easy: in custom providers we could just <i>not to call</i>$words = parent::getWordList();. But after tags implementationKnpUWordProvideris also tagged in bundle's "services.xml", so it looks like no way for bundle users to exclude words, whichKnpUWordProviderprovides, from final words set. At least, it looks no way for me, 'cause I'm noobie in Symfony bundles/container/other stuff :) Maybe, it could be possible for bundle's user to callContainerBuilder::removeDefinition()somewhere, but I'm not sure that this is a good idea. Need an advice.Hey @Boolean_Type!
Hmm, for a "noobie" you're asking some pretty good questions on some pretty advanced stuff ;).
So, you're correct that there's no way to do this directly with the tags system. And you're also correct that the user could do something crazy with a compiler pass and
ContainerBuilder::removeDefinitionbut we don't want them to do that :).The "tags" system is really a way to allow end-users to add more stuff to our system. In this project, we've done this but (as you know) we've also decided to always add our own
KnpUWordProvider. Suppose we release the bundle like this, and for 6 months, everyone is happy. Suddenly, we get a feature request where someone wants to not include this (this exactly what you're asking for).The fix is to make our bundle more flexible by adding more configuration. Here's how it would look:
1) Add some new option in our
Configurationclass so that users can have some config like this:You would default this value to "true" to keep the current behavior by default.
2) Then, remove the
knpu_ipsum_word_providertag in our bundle's XML file for theKnpUWordProviderservice. At this moment, the class would never have that tag.3) In the
KnpULoremIpsumExtensionclass, use the newdefault_providerconfig. If it IS set to true (and only if it is set to true), manually find theknpu_lorem_ipsum.knpu_word_providerservice and add the tag.That's it! So you're basically adding a new option that allows users to control whether or not this tag is there. The cool thing is that your users don't even need to understand what a "tag" is or that you're using it (unless they want to add their own custom providers). A user just sees that they can set
default_provider: falseto not include the default words. They don't need to care how it works internally.Let me know if that helps!
Cheers!
Thank you so much, @weaverryan !
I've tried your proposition - it works :)
<i>knpu_lorem_ipsum.yaml:</i>
<i>services.xml:</i>
<i>KnpU\LoremIpsumBundle\DependencyInjection\Configuration:</i>
<i>KnpU\LoremIpsumBundle\DependencyInjection\KnpULoremIpsumExtension::load():</i>
I've also tried this in
KnpULoremIpsumExtension::load()instead of block above:But for some reason uknown for me it didn't work.
Woohoo! Nice work! You did everything perfectly.
That last block of code didn’t work only because you were... sort of doing something different.... but close. That code said “please find all services that match the class KnpUWordProvider and automatically tag them with knpu_ipsum_word_provider”. That sounds like it should work, but it will only apply to services that have “autoconfigure true”, which our bundle’s services don’t. And that’s ok and on purpose :). It’s better for our bundle to do things explicitly, which is what you did by specifically adding the tag to the service.
Cheers!
I sometimes find that this doesn't always automatically tag implemented classes. For some reason, I seem to have no problem with any of my Repository interfaces (that have nothing to do with Doctrine), but I end up having to tag stuff in the application configuration. I can see when I use
findTaggedServiceIds()the array is empty until I manually tag it. Is there a good way to debug this? I was having a problem with a Uuid ParamConverter but I was able to track it down by dump/dying in the Manager class. Debugging this is a bit harder because everything seems a bit obfuscated by the container cache. (Besides that, I don't really know how my <i>successful</i> auto tags are ending up in there, so there's no convenient class to just check if my implementation is getting scanned or what not.)As a second, unrelated question, is it possible to pass configuration into a class you inevitably wiring this way, or does that not make any sense?
Hey ash-m!
Sorry for my slow reply!
Ok, so this entire feature is powered by "autoconfiguration". "Autoconfigure" is an option that you can set on each individual service. If "autoconfigure" is enabled for a service, then any matching "autoconfiguration rules" (i.e. the thing we add with
registerForAutoconfiguration()) are applied to that service.So, the first thing we should look at is: is autoconfiguration enabled for your services? The autoconfigure option is disabled by default on services. However, in practice, it's enabled in applications thanks to the default services.yaml file: https://github.com/symfony/recipes/blob/fb10d2ac4cf54486a1b171685f4a3d6311196938/symfony/framework-bundle/4.4/config/services.yaml#L8-L12
But if you're building a bundle, then you might have this setting. And, in general, like autowiring, the "best practice" is for bundles to not rely on autoconfiguration. Basically, if your bundle needs something to have a tag, you should configure it manually. But if you would like to make it easier for "end-users" to "auto-tag" their services (which is a good thing!), then you would call
registerForAutoconfiguration(). So basically, you add this autoconfiguration rule for your end users... but then we typically don't actually rely on it in the bundle (though, there is no rule against that).Let me know if this helps... or if i'm way off ;).
Cheers!
This helps, but doesn't explain why some services autowire and others don't. But first, to clarify, is
registerForAutoConfiguration()not best practice?Second, perhaps there is some tagging magic happening with Doctrine, but I will have an unimplemented repository interface (eg
FooRepository) in my bundle and I'll useregisterForAutoConfiguration()on it and the App's implementation (egInMemoryFooRepository) will appear in the CompilerPass. But some (most*) other services do not show up until I explicitly add tags in the App's services.yaml file.Most of these services do not have default implementations, so they're not defined in the bundle's services.xml (it's expected that the developer implement the interfaces to use the bundle). Maybe there is some relationship I don't understand between services.xml and the CompilerPass? If it's <i>better</i> to tag stuff in the App's services.yaml, I'm okay with that, but it doesn't seem to be necessary for any of the other bundles I've used.
Note: as far as my unrelated question, I decided to map all configuration variables to <b>parameters</b> in the Extension class, and then use those in the CompilerPass for the
setArgument()calls. Is this okay? Is there a better way?Hey ash-m!
This absolutely IS a best-practice. This allows users of your bundle to have their service's auto-tagged. What's not a best practice is to rely on this in your own bundle. Instead, you should manually/explicitly tag your services.
Um, hmm. I think I'll need to see some code. But also, I would run:
And then see what the "Autoconfigured" option says - see if it says "yes" or "no". If it says "no", then we know that the problem is that autoconfiguration is not enabled for this service, and so that's why it isn't automatically getting the tag.
Yes, this is ok... it's a great way (actually) to "store" that config so that you can read it in your compiler pass :).
Cheers!
So, just to clarify, stuff ending in
Repositoryseem to (mostly) work as expected (except, for some reason when I inject them in listeners instead of handlers, but that probably has something to do with what's being defined in services.xml... ¯_(ツ)_/¯... ) but it looks like the problem is mostly with <i>factories</i> which I usually define next to the thing they create. So anApp\Entity\ConcreteFooFactorywill implementAshm\ALib\Foo\FooFactoryand of course that interface will be tagged and registered for auto-configuration in the bundle's extension class.When I debug like you suggest (without removing the tag from services.yaml, because otherwise it breaks saying that a handler depends on a non-existent service), then it indeed shows that the class is autoconfigured <b>and</b> autowired (and of course tagged with the explicit tag).
<s>I am just now realizing that the services that fail to get tagged in the Compiler without being explicit are all in the library I abstracted (which the bundle requires).</s> (edit: actually this is inconsistent as there is also a factory interface in the bundle whose implementation requires a tag in the App... I should probably think of a way to fix that... )
To further clarify, when I say, "it doesn't show up", if I remove the tag, from services.yaml, like I mentioned, the app will crash, but if I
var_dump($container->findTaggedServivceIds($tag)); die()(where$tagis the tag defined in the extension) I will see an empty array. If I leave the tag alone andvar_dump, then I see theApp\Entity\ConcreteFooFactory.Any input would be great. Thank you.
Hey ash-m!
Any chance you'd be able to share some code? I could probably spot the problem, but you've got a complex enough setup that I can't quite picture it. It sounds like you are doing most everything correctly, which is why I think there may be some minor problem or minor "lost in translation" tweak that you need :).
Cheers!
"Houston: no signs of life"
Start the conversation!