Service Autowiring

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

The next new feature is called autowiring. And yes, it is magic. And yes, it's super controversial - it's like the celebrity gossip of the Symfony world, right along side annotations.

Autowiring makes registering services easier. Normally, a service needs a name, a class and its constructor arguments:

... lines 1 - 5
services:
weird_authenticator:
class: AppBundle\Security\WeirdFormAuthenticator
arguments: ['@doctrine.orm.entity_manager', '@router']

What autowiring says is, "maybe we don't need to give it the arguments?". So remove the arguments key and instead type autowire: true:

... lines 1 - 5
services:
weird_authenticator:
class: AppBundle\Security\WeirdFormAuthenticator
# arguments: ['@doctrine.orm.entity_manager', '@router']
autowire: true

Head back to the browser, refresh the login page and press the login button. It doesn't explode! How was it able to create the WeirdFormAuthenticator!!!??? That's autowiring.

How does it Work?

It works via type-hints. In WeirdFormAuthenticator we're type-hinting the first argument with EntityManager and the second argument with RouterInterface:

... lines 1 - 2
namespace AppBundle\Security;
... lines 4 - 18
class WeirdFormAuthenticator extends AbstractGuardAuthenticator
{
... lines 21 - 23
public function __construct(EntityManager $em, RouterInterface $router)
{
... lines 26 - 27
}
... lines 29 - 106
}

Behind the scenes, well, in a "compiler pass" if you're super geeky and curious, autowiring looks in the container and says "could you please show me all the services type-hinted with EntityManager?". Since there is only one, it auto-wires that service as the first argument.

The same is true for RouterInterface: it sees that there is only one service whose class implements RouterInterface, so it injects the router service.

What if there are Multiple Services with the Class?

But what if there are multiple services that implement an interface or match a class? Well, frankly, your computer will catch on fire....

I'm kidding! Autowiring won't work, but it will throw a clear exception. Autowiring is magical... but not completely magical: it won't try to guess which entity manager you want. It's the Symfony-spin on auto-wiring.

If you do hit this problem, there is a way for you to tell the container which service you want to inject for the type-hinted class.

But the real beauty of autowiring is when it just works. The whole point is to save time so you can rapidly develop.

Moar Magic: Injecting Non-Existent Services

Autowiring has one more interesting trick. In the Security directory, create a new super important class called EvilSecurityRobot. Give it one method: public function doesRobotAllowAccess():

... lines 1 - 2
namespace AppBundle\Security;
class EvilSecurityRobot
{
public function doesRobotAllowAccess()
{
return rand(0, 10) >= 5;
}
}

Basically, the evil robot will decide, randomly, whether or not we can login. So even if I enter all the login fields correctly, the evil security robot could still say "NOPE! You're not getting in and I'm not sorry. <3 The Evil Robot".

The EvilSecurityRobot is ready. To use this in WeirdFormAuthenticator, pass it as the third argument to the constructor: EvilSecurityRobot $robot. Now create a $robot property and set it:

... lines 1 - 18
class WeirdFormAuthenticator extends AbstractGuardAuthenticator
{
... lines 21 - 22
private $robot;
... line 24
public function __construct(EntityManager $em, RouterInterface $router, EvilSecurityRobot $robot)
{
... lines 27 - 28
$this->robot = $robot;
}
... lines 31 - 114
}

In checkCredentials(), if (!$this->robot->doesRobotAllowAccess()) then throw a really clear new CustomUserMessageAuthenticationException() that says "RANDOM SECURITY ROBOT SAYS NO!":

... lines 1 - 60
public function checkCredentials($credentials, UserInterface $user)
{
if (!$this->robot->doesRobotAllowAccess()) {
throw new CustomUserMessageAuthenticationException(
'RANDOM SECURITY ROBOT SAYS NO!'
);
}
... lines 68 - 85
}
... lines 87 - 116

And I'll even put quotes around that.

This is when we would normally go to services.yml and update the arguments key to inject the EvilSecurityRobot. But wait! Before we do that, we need to register the EvilSecurityRobot as a service first.

Resist the urge! Instead, do nothing: absolutely nothing. Refresh the page. Fill in weaverryan and try to login until you se the error. Bam!

RANDOM SECURITY ROBOT SAYS NO!

It works! What the heck is going on?

Remember, weird_authenticator is autowired. Because of that, Symfony sees that the third argument is type-hinted with EvilSecurityRobot. Next, it looks in the container, but finds no services with this class. But instead of failing, it creates a private service in the container automatically and injects that. This actually works pretty well: EvilSecurityRobot itself is created with autowiring. So if it had a couple of constructor arguments, it would try to autowire those automatically.

Oh, and if you have multiple autowired services that need an EvilSecurityRobot, the container will create just one private service and re-use it.

Some people will love this new feature and some people will hate it and complain on Twitter. But it's cool: like most things, you're not forced to use it. But, if you're doing some rapid application development, try it! It won't work in all cases, and isn't able to inject configuration yet, but when it works, it can save time, just like it did here for us.

Wield this new weapon wisely.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.0.*", // v3.0.0
        "doctrine/orm": "~2.5@dev", // 2.7.x-dev
        "doctrine/doctrine-bundle": "~1.6@dev", // 1.10.x-dev
        "doctrine/doctrine-cache-bundle": "~1.2@dev", // 1.3.2
        "symfony/security-acl": "~3.0@dev", // dev-master
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.8
        "symfony/monolog-bundle": "~2.7@dev", // dev-master
        "sensio/distribution-bundle": "~5.0@dev", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.11
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.2
        "doctrine/doctrine-fixtures-bundle": "^2.3", // v2.4.1
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "require-dev": {
        "sensio/generator-bundle": "~3.0", // v3.0.0
        "symfony/phpunit-bridge": "~2.7" // v2.7.6
    }
}