Symfony Dependency Injection & Services

Dependency Injection

SOLID

As specific as possible

and

as general as needed

What is a Service?

As a rule, a PHP object is a service if it is used globally in your application. A single Mailer service is used globally to send email messages whereas the many Message objects that it delivers are not services. Similarly, a Product object is not a service, but an object that persists Product objects to a database is a service.

Symfony Service Container

Creation & Configuration

# app/config/services.yml

parameters:
  app.mailer.class: AppBundle\Mailer # stop it!

services:
    app.mailer:
        class:        AppBundle\Mailer
        arguments:    [sendmail]

Multiple Configuration Files

# app/config/services.yml

imports:
    - { resource: '@AcmeHelloBundle/Resources/config/common_services.yml' }
    - { resource: '@AcmeHelloBundle/Resources/config/event_services.yml' }
    - { resource: '@AcmeHelloBundle/Resources/config/security_services.yml' }
    # ...

Access

class HelloController extends Controller
{
    // ...

    public function sendEmailAction()
    {
        // ...
        $mailer = $this->get('app.mailer');
        $mailer->send('ryan@foobar.net', ...);
    }
}

In this example, the controller extends Symfony's base Controller, which gives you access to the service container itself. You can then use the get method to locate and retrieve the app.mailer service from the service container. You can also define your controllers as services. This is a bit more advanced and not necessary, but it allows you to inject only the services you need into your controller.

Controllers as Services?

Access the right way

class HelloController
{
    /** @var Mailer $mailer */
    private $mailer;

    /** @var Logger logger */
    private $logger;

    public function __construct(Mailer $mailer) {
        $this->mailer = $mailer;
    }

    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }

    public function sendEmailAction()
    {
        // ...
        $this->mailer->send('ryan@foobar.net', ...);
        if ($this->logger) {
            $this->logger->info('Mail sent to ryan@foobar.net');
        }
    }
}

Config the right way

# app/config/services.yml
services:
    app.hello_controller:
        class: AppBundle\Controller\HelloController
        arguments: ["@app.mailer"]
        calls:
            - [setLogger, ["@logger"]]

For the sake of completeness...

# app/config/services.yml
services:
    third_party.service:
        class: Some\Damn\Old\LibraryWithPublicProperties
        properties:
            mailer: '@my_mailer'
namespace Some\Damn\Old;

class LibraryWithPublicProperties {
    public $mailer;

    // ...
}

Services?

$ php app/console container:debug

[container] Public services
 Service ID                    Scope     Class name
 annotation_reader             container Doctrine\Common\Annotations\FileCacheReader
 doctrine.orm.entity_manager   n/a       alias for "doctrine.orm.default_entity_manager"
 logger                        container Symfony\Bridge\Monolog\Logger
 request                       request
 templating.helper.assets      request   Symfony\Component\Templating\Helper\CoreAssetsHelper

Symfony Services

Lazy Loaded

Singletonish

Non Shared Services

# app/config/services.yml

### Symfony >= 2.8 ###

services:
    app.some_not_shared_service:
        class: ...
        shared: false

### Symfony < 2.8 ###

services:
    app.some_not_shared_service:
        class: ...
        scope: prototype

Service Inheritance

# ...
services:
    # ...
    mail_manager:
        abstract:  true
        calls:
            - [setMailer, ["@my_mailer"]]
            - [setEmailFormatter, ["@my_email_formatter"]]

    newsletter_manager:
        class:  "NewsletterManager"
        parent: mail_manager

    greeting_card_manager:
        class:  "GreetingCardManager"
        parent: mail_manager

Factories

Services out of Factories

# app/config/services.yml
# Symfony >= 2.6
services:
    app.repo.car:
        class: 'AppBundle\Repository\CarRepository'
        factory: [@doctrine.orm.entity_manager, 'getRepository']
        arguments: ['AppBundle\Entity\Car']

# Symfony < 2.6
services:
    app.repo.car:
        class: 'AppBundle\Repository\CarRepository'
        factory_class: doctrine.orm.entity_manager # no @ here
        factory_method: 'getRepository'
        arguments:
            - 'AppBundle\Entity\Car'

Tags

Some Example

class TransportChain
{
    private $transports;

    public function __construct()
    {
        $this->transports = array();
    }

    public function addTransport(\Swift_Transport $transport)
    {
        $this->transports[] = $transport;
    }
}
services:
    acme_mailer.transport_chain:
        class: TransportChain

Tagged Services

services:
    acme_mailer.transport.smtp:
        class: \Swift_SmtpTransport
        arguments:
            - '%mailer_host%'
        tags:
            -  { name: acme_mailer.transport }
    acme_mailer.transport.sendmail:
        class: \Swift_SendmailTransport
        tags:
            -  { name: acme_mailer.transport }

Compiler Pass

class TransportCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->has('acme_mailer.transport_chain')) {
            return;
        }

        $definition = $container->findDefinition(
            'acme_mailer.transport_chain'
        );

        $taggedServices = $container->findTaggedServiceIds(
            'acme_mailer.transport'
        );
        foreach ($taggedServices as $id => $tags) { // id = service-id
            $definition->addMethodCall(
                'addTransport',
                array(new Reference($id))
            );
        }
    }
}

Compile

class AcmeMailerBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new CustomCompilerPass());
    }
}

Tags and more...

class TransportChain
{
    // ...

    public function addTransport(\Swift_Transport $transport, $alias)
    {
        $this->transports[$alias] = $transport;
    }

    public function getTransport($alias)
    {
        if (array_key_exists($alias, $this->transports)) {
            return $this->transports[$alias];
        }
    }
}

How to get the Alias?

services:
    acme_mailer.transport.smtp:
        class: \Swift_SmtpTransport
        arguments:
            - '%mailer_host%'
        tags:
            -  { name: acme_mailer.transport, alias: foo }
    acme_mailer.transport.sendmail:
        class: \Swift_SendmailTransport
        tags:
            -  { name: acme_mailer.transport, alias: bar }

Modify the Compiler Pass

class TransportCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('acme_mailer.transport_chain')) {
            return;
        }

        $definition = $container->getDefinition(
            'acme_mailer.transport_chain'
        );

        $taggedServices = $container->findTaggedServiceIds(
            'acme_mailer.transport'
        );
        foreach ($taggedServices as $id => $tags) { // != name
            foreach ($tags as $attributes) {
                $definition->addMethodCall(
                    'addTransport',
                    array(new Reference($id), $attributes["alias"])
                );
            }
        }
    }
}

Magic

The Request in Controllers

class HelloController
{
    public function sendEmailAction(Request $request, $some, $thing)
    {
        // ...
        $postData = $request->request;
    }
}

Links

Symfony Dependency Injection

By Ole Rößner

Symfony Dependency Injection

A preparation for the Symfony Certification

  • 1,085