Problematic Multi-Class Services
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeSince autowiring works by finding a service whose id exactly matches the type-hint, it means that in the future, if someone type-hints MessageGenerator
as an argument, this first, auto-registered service will be used! And actually, in this case, that's not a huge problem: MessageGenerator
has a required constructor argument:
// ... lines 1 - 4 | |
class MessageGenerator | |
{ | |
// ... lines 7 - 8 | |
public function __construct(array $messages) | |
{ | |
// ... line 11 | |
} | |
// ... lines 13 - 17 | |
} |
So our app would explode with an exception. But still, that's a little unexpected... we really don't want this third service.
Fix 1) Don't auto-register the Service
There are three ways to fix this, depending on your app. First, you can explicitly exclude it. At the end of exclude
, add Service/MessageGenerator.php
:
// ... lines 1 - 8 | |
services: | |
// ... lines 10 - 16 | |
AppBundle\: | |
// ... lines 18 - 20 | |
exclude: '../../src/AppBundle/{Entity,Repository,Service/MessageGenerator.php}' | |
// ... lines 22 - 52 |
Now, go back to your terminal:
php bin/console debug:container --show-private
Boom! Back to just the two services. And because neither id is set to the class name, if someone tries to autowire using the MessageGenerator
type-hint, they'll see a clear exception asking them to explicitly wire the value they want for that argument. In other words, MessageGenerator
cannot be used for autowiring... but that's ok!
Fix 2) Choose one Service for Autowiring
Another solution is to choose one of your services to be the one that's used for autowiring. For example, suppose the app.encouraging_message_generator
is used much more often, so you want that to be autowired for the MessageGenerator
type-hint:
// ... lines 1 - 8 | |
services: | |
// ... lines 10 - 40 | |
app.encouraging_message_generator: | |
class: AppBundle\Service\MessageGenerator | |
arguments: | |
- ['You can do it!', 'Dude, sweet!', 'Woot!'] | |
public: true | |
// ... lines 46 - 52 |
Cool! Copy the old service id. Then, open legacy_aliases.yml
and do the same thing we did before. I'll use the class as the service id, then setup an alias:
// ... lines 1 - 8 | |
services: | |
// ... lines 10 - 40 | |
AppBundle\Service\MessageGenerator: | |
// ... lines 42 - 50 |
services: | |
// ... lines 2 - 6 | |
app.encouraging_message_generator: '@AppBundle\Service\MessageGenerator' |
We still have two services in the container for the same class. But the first will be used for autowiring. If you need to pass the second instance as an argument, you'll need to explicitly configure that. We'll see how in a few minutes.
Fix 3) Refactor to a Single Class
The final option - which is really practical, but a bit more controversial - is to refactor your application to avoid this situation. For example, if you downloaded the start code, you should have a tutorial/
directory with a MessageManager.php
file inside. Copy that and paste it into the Service/
directory:
// ... lines 1 - 2 | |
namespace AppBundle\Service; | |
class MessageManager | |
{ | |
private $encouragingMessages = array(); | |
private $discouragingMessages = array(); | |
public function __construct(array $encouragingMessages, array $discouragingMessages) | |
{ | |
$this->encouragingMessages = $encouragingMessages; | |
$this->discouragingMessages = $discouragingMessages; | |
} | |
public function getEncouragingMessage() | |
{ | |
return $this->encouragingMessages[array_rand($this->encouragingMessages)]; | |
} | |
public function getDiscouragingMessage() | |
{ | |
return $this->discouragingMessages[array_rand($this->discouragingMessages)]; | |
} | |
} |
This class has two arguments - $encouragingMessages
and $discouragingMessages
- and a method to fetch a message from each. It's basically a combination of our two MessageGenerator
services.
Technically, this class is already registered as a service. But of course, these two arguments can't be autowired. So, configure the service explicitly: AppBundle\Service\MessageManager:
and under arguments
, pass the encouraging messages and the discouraging messages:
// ... lines 1 - 8 | |
services: | |
// ... lines 10 - 40 | |
AppBundle\Service\MessageManager: | |
arguments: | |
- ['You can do it!', 'Dude, sweet!', 'Woot!'] | |
- ['We are *never* going to figure this out', 'Why even try again?', 'Facepalm'] | |
public: true |
Now that we have MessageManager
, let's remove all the MessageGenerator
stuff completely! Copy the old discouraging service id. Then, search for it:
git grep app.discouraging_message_generator
Ah, this is only used in GenusAdminController
. In fact, both MessageGenerator
services are only used there. Let's use the new MessageManager
service - which I've purposely made public for now:
// ... lines 1 - 8 | |
services: | |
// ... lines 10 - 40 | |
AppBundle\Service\MessageManager: | |
arguments: | |
- ['You can do it!', 'Dude, sweet!', 'Woot!'] | |
- ['We are *never* going to figure this out', 'Why even try again?', 'Facepalm'] | |
public: true |
In GenusAdminController
, use that: $this->get(MessageGenerator::class)->getEncouragingMessage()
:
// ... lines 1 - 16 | |
class GenusAdminController extends Controller | |
{ | |
// ... lines 19 - 64 | |
public function editAction(Request $request, Genus $genus) | |
{ | |
// ... lines 67 - 70 | |
if ($form->isSubmitted() && $form->isValid()) { | |
// ... lines 72 - 77 | |
$this->addFlash( | |
'success', | |
$this->get(MessageManager::class)->getEncouragingMessage() | |
); | |
// ... lines 82 - 90 | |
} | |
// ... lines 92 - 95 | |
} | |
} |
Then the same below: $this->get(MessageGenerator::class)->getDiscouragingMessage()
:
// ... lines 1 - 16 | |
class GenusAdminController extends Controller | |
{ | |
// ... lines 19 - 64 | |
public function editAction(Request $request, Genus $genus) | |
{ | |
// ... lines 67 - 70 | |
if ($form->isSubmitted() && $form->isValid()) { | |
// ... lines 72 - 77 | |
$this->addFlash( | |
'success', | |
$this->get(MessageManager::class)->getEncouragingMessage() | |
); | |
// ... lines 82 - 85 | |
} elseif ($form->isSubmitted()) { | |
$this->addFlash( | |
'error', | |
$this->get(MessageManager::class)->getDiscouragingMessage() | |
); | |
} | |
// ... lines 92 - 95 | |
} | |
} |
Time to celebrate! Delete the discouraging service, and go remove the legacy alias too: neither is being used. This refactoring was optional, but it makes life a little bit easier: I can now safely type-hint any argument with MessageManager
and let autowiring do its magic.
And, our app isn't broken, which is always nice. Oh, and I'll delete the unused MessageGenerator
class.
Next, we'll try out a special new type of dependency injection for controllers and clean up the rest of our service config.
From which course is the discouraging_message_generator and encuraging_message_generator? I can't find it via Google on knpuniversity