Last time we covered the basics of Symfony’s service container. The service container has been with Symfony since the early days of Symfony 2, and will likely always be at the heart of the Symfony system.
However — programming fashion changes. These days it’s hard to walk out your PHP door and not trip over the phrase dependency injection. If you’re not familiar with automatic dependency injection it can be intimidating. Your programming language will seem to behave in a way that’s impossible.
Symfony developers face an additional challenge with dependency injection. The service container system was never designed with automatic dependency injection in mind, and this can lead to some configuration paths that are, (initially), not intuitive.
Hopefully by the end of this article we’ll have you set on the right path, and you’ll see that dependency injection isn’t all that complicated, and you’ll understand how Symfony’s configured to allow this magic feature into its convention based culture.
Dependency Injection
So what is dependency injection? I’m not the first person to say this, but the idea of dependency injection is a lot simpler than its fancy pants name. Another way to describe dependency injection is passing values into functions.
Consider the following code
function someFunction($foo) {
// some stuff happens
// ...
// an object is instantiated and used
$object = new \Foo\Baz\Bar($foo, $otherParam, etc...);
$value = $object->doSomeThing();
// some other stuff happens
// ...
// we return
return $someValue;
}
This code directly instantiates an object inside a function, between a lot of other code. This pattern of work ends up being hard to maintain and change over time. This function is dependent on the Foo\Baz\Bar
object, and dependent on the values Foo\Baz\Bar
is instantiated with.
The idea behind dependency injection is it’s better to write code like this
function someFunction($foo, \Foo\Baz\BarInterface $object) {
// some stuff happens
// ...
// The object that's injected is used
$value = $object->doSomeThing();
// some other stuff happens
// ...
// we return
return $someValue;
}
This code passes (or “injects”) the object instead of instantiating it directly in the function. This means we can pass this function whatever object we want, so long as it implements the Foo\Baz\BarInterface
interface (either an explicit interface like the one in the above example, or an implicit interface in an object that implements all the methods that get called in the function).
This pattern gives you more flexible functions, and functions that are easier to test. They’re easier to test because we can control what’s returned by the doSomething
method, and ensure our unit tests handle different results from this method.
Where dependency injection gets tricky is something actually needs to instantiate the objects. When a system like Laravel, Magento, or Symfony tout dependency injection as a feature, they’re usually referring to systems that automatically create objects for us and pass them into object constructors, or pass them into methods or functions that are called automatically, (like controller action methods).
So, now that we’re sorted as to what dependency injection is, lets take a look at a basic example in Symfony.
Starting from Zero
The first thing we’re going to do is temporarily remove the default service configurations that ship with a Sylius (or a Symfony) system. These defaults turn on certain service features that we’re not ready to have turned on yet.
First, make a backup copy of your service configuration
$ cp config/services.yaml config/services.yaml.bak
Then, remove everything in your config/services.yaml
file except the parameters
section.
#File: config/services.yaml
parameters:
locale: 'en'
This locale
parameter is required by the Symfony framework.
Now we’re ready to start.
Setting up The Services
Similar to last time, we’re going to setup a controller endpoint that, using the global kernel object’s container reference, will instantiate a service and call its sayHello
method. This service will call a second service.
If that was confusing be sure to read our previous article in this series. Also — don’t worry, you’ll be able to follow this “cookbook style” and still get the gist of what we’re doing.
So let’s configure our route.
#File: config/routes.yaml
dependency_injection_playground:
path: /pulsestorm_di
methods: [GET]
controller: App\Controller\Pulsestorm\Di::run
and create the controller it references
#File: src/Controller/Pulsestorm/Di.php
<?php
namespace App\Controller\Pulsestorm;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class Di extends AbstractController {
public function run() {
global $kernel;
$container = $kernel->getContainer();
$service = $container->get('App\Services\ServiceA');
echo $service->sayHello();
exit;
}
}
Then, we’ll configure two services
#File: config/services.yaml
services:
App\Services\ServiceA:
public: true
arguments:
- '@App\Services\ServiceB'
App\Services\ServiceB:
public: true
And create their class files
#File: src/Services/ServiceA.php
<?php
namespace App\Services;
class ServiceA {
protected $service;
public function __construct($service) {
$this->service = $service;
}
public function sayHello() {
return $this->service->getMessage();
}
}
#File: src/Services/ServiceB.php
<?php
namespace App\Services;
class ServiceB {
public function getMessage() {
return 'Hello Soon to Be Di Injected Services';
}
}
and finally, we’ll load the following URL in our Sylius or Symfony system (replacing sylius.example.com
with whatever URL you’re using in your system).
http://sylius.example.com/pulsestorm_di
We should see the Hello Soon to Be Di Injected Services message.
Nothing much new here, so let’s get on with the interesting bits.
Service Names and Class Names
You may have noticed something slightly different with our example today, and that’s our service names.
In our original article, we named our services with a generic lowercase string. Here we’ve named our services with a string that matches a PHP class name. This is perfectly legal, and in fact is required for Symfony’s automatic constructor dependency injection system to work.
Changing our service to inject the second service automatically is a two step process. First, we need to include a constructor method in our service class, with a type hinted argument for each service we want to inject. Let’s edit service A so its constructor looks like this
#File: src/Services/ServiceA.php
public function __construct(
\App\Services\ServiceB $service
) {
$this->service = $service;
}
and remove the service arguments
from the configuration for service A.
#File: config/services.yaml
services:
App\Services\ServiceA:
public: true
App\Services\ServiceB:
public: true
Here we’ve type hinted the constructor argument with the class name of the service we want to inject. This is also the Symfony service name. By having our service name match the name of a valid PHP class, Symfony will be able to see the class name of the type hint, use this name to lookup a service name, and automatically pass the instantiated service when it (Symfony) instantiates the App\Services\ServiceA
class.
All that said — there’s still work to do. If we reload the page with the above code in place, we’ll get an error
Too few arguments to function App\Services\ServiceA::__construct(), 0 passed in /path/to/var/cache/dev/Container9Nwhdth/getServiceAService.php
Unlike Magento or Laravel’s automatic dependency injection systems, Symfony services will not automatically inject service instances into a constructor. Instead, we need to tell Symfony which services should have automatic constructor dependency injection.
Automatic Wiring
Symfony’s name for its automatic constructor dependency injection system is autowiring — as in automatic wiring. Normally, with a Symfony service, you need to configure each argument manually. These are the “wires”. With autowiring, Symfony will look at the type hint and inject the right service for you.
To turn on autowiring for a service, you set its autowiring
configuration to true
. For example, to have our App\Services\ServiceA
service automatically wired, we’d use the following configuration.
#File: config/services.yaml
services:
App\Services\ServiceA:
public: true
autowire: true
App\Services\ServiceB:
public: true
If you make that change and reload your page, you’ll see an exception message is no longer thrown. We see our text! Symfony successfully injects a service instance in the constructor (using the type hint to decide which service to load) and we’ll see the Hello Soon to Be Di Injected Services output!
Congratulations, you just autowired your first Symfony service!
Autowiring and Services for All
Generally speaking, the philosophy of the Symfony project has been to prefer explicit configuration over automatically enabling features. The usual arguments for this are these features might impact performance, or might provide surprising behavior to end-user-programmers. However, in recent years, configuration options have started to appear that allow Symfony engineers to quickly turn on features as though Symfony was a more “magic by default” system.
Put another way — with more recent versions of Symfony, it’s possible to configure your system such that all services are autowired, and all PHP classes in your system will be automatically considered as services.
Remember our backup file, services.yaml.bak
? Let’s take a look at that file again.
#File: config/services.yaml.bak
_defaults:
autowire: true # Automatically injects dependencies in your services.
/*...*/
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
Based on what we’ve talked about so far, you might think this configuration configures a service named _defaults
and a service named App\
. However, that’s not the case.
You can’t have a service named _defaults
in Symfony. Instead, this key will set default properties for all your services. In the above example, every service we configure will have autowiring enabled unless the service explicitly disables it with a autowiring:false
.
The App\:
key is a little trickier. The plain english of this configuration
#File: config/services.yaml.bak
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
would be
For every class in the
../src
folder (relative from the service file) whose full class name begins withApp\
, automatically make that class a service if that class is referenced as a service in Symfony’s container or autowiring parameters. However, do NOT do this if the class path matches theglob
pattern../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}
.
This exclude
property is important. Many classes and patterns that predate Symfony’s use of autowiring will have constructors whose parameters include type hints, but aren’t intended to be autowired. Autowiring these services will likely lead to errors.
Let’s try this with our configuration. First, remove our service configuration from service.yaml
— we’ll do this with comments below
#File: config/service.yaml
parameters:
locale: 'en'
#
#services:
# App\Services\ServiceA:
# public: true
# autowire: true
# App\Services\ServiceB:
# public: true
Reload your page, and Symfony will complain about no defined services with the following error message
You have requested a non-existent service “App\Services\ServiceA”.
Now, add the following configuration to our services file
#File: config/services.yaml
services:
_defaults:
autowire: true
public: true
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
We’ve set all services to autowire by default, and be public by default. We’ve told Symfony that every class in our src/*
folder should be considered for use as a service, expect for the class files that match the pattern defined in the file src/Kernel.php
.
Reload the page with the above in place and you’ll see that Symfony is automatically able to recognize our classes as services — i.e. our message is still printed out.
With this configuration in place by default, many developers won’t even realize their classes are being registered as Symfony services. However, if you’re going to be able to reason about your system, it’s important to understand that all your automatically injected class instances are still Symfony services, and subject to additional service configuration.
No Global Service Container
At the risk of getting too far ahead of ourselves, there’s one last thing to cover. We’re still doing global shenanigans to get our service container.
#File: src/Controller/Pulsestorm/Di.php
public function run() {
global $kernel;
$container = $kernel->getContainer();
$service = $container->get('App\Services\ServiceA');
echo $service->sayHello();
exit;
}
This isn’t how things are done in Symfony. Instead, we can actually inject our service container with autowiring. Building off what we’ve done above, give the following controller a try
#File: src/Controller/Pulsestorm/Di.php
<?php
namespace App\Controller\Pulsestorm;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class Di extends AbstractController {
private $fullServiceContainer;
public function __construct(
\Symfony\Component\DependencyInjection\ContainerInterface $container
) {
$this->fullServiceContainer = $container;
}
public function run() {
$service = $this->fullServiceContainer->get('App\Services\ServiceA');
echo $service->sayHello();
exit;
}
}
The string Symfony\Component\DependencyInjection\ContainerInterface
is a service setup by default in Symfony applications that will contain a reference to the current container object. We’re able to use autowiring in our controller class because we told Symfony that all our classes should be considered as services.
Finally — symfony comes with a huge number of services — you can check them out (as well as the services your own configuration has added), using the Symfony console’s debug:container
command.
$ php bin/console debug:container
# ... list of all your services ...
Another common approach here would be to avoid the service container entirely, and inject our App\Services\ServiceA
class/service directly. We’ll leave that one as an exercise for the reader.
Wrap Up
That’s where we’ll leave it for today. Next time we’ll we’ll take a brief lookg at every possible service configuration paramater. After that, we’ll be ready to start looking at Sylius code in earnest.