Dependency Injection & Containers

Hi!

Matthieu Napoli

Piwik Pro

Leading open source web analytics platform

Wat?

  • Dependency injection
  • Containers & PHP-DI
  • Making the switch

Survey

What is a dependency?

Coupling between 2 modules, i.e. classes


class ReportGenerator {
    public function generateReport() {
        $db = Database::getInstance();
        $data = $db->selectAll('some_table');

        Logger::info('Generating the report');

        $pdf = new PdfGenerator();
        return $pdf->generate($data);
    }
}
                    

Not Composer dependencies

DI ≠ DIC

Dependency Injection: design pattern

Container: tool

Dependency injection

Usual patterns

New:


$generator = new PdfReportGenerator();
$generator->generate();
                    

Static method:


PdfReportGenerator::generate();
                        

Singleton:


$generator = PdfReportGenerator::getInstance();
$generator->generate();
                        

Usual workflow

Application:


$controller = new ReportController();
$controller->viewAction();
                    

ReportController:


public function viewAction() {
    $generator = new PdfReportGenerator();
    echo $generator->generate();
}
                        

PdfReportGenerator:


public function generate() {
    $db = new Database();
    $data = $db->selectAll('sales');
    return /* ... */;
}
                        

Hardcoded dependencies


public function doSomething() {
    $cache = new Redis();
    // ...
}
                    

Coupling

I want to change the caching backend => I have to change it everywhere…

Hardcoded dependencies


class PdfReportGenerator
{
    public function generate() {
        $data = Db::selectAll('sales');
        return /* ... */;
    }
}
                    

Tests

hard to test code using a webservice, database, cache…

Hardcoded dependencies


public function copyData($fromDatabase, $toDatabase) {
    $data = Db::selectAll('my_data');
    Db::insert($data);
    // D'oh!
}
                    

Extensibility

I want X to use a different Y than the one by default…

Dependency injection

Before:


class Foo {
    public function run() {
        $bar = new Bar(); // or $bar = Bar::getInstance()
        $bar->doSomething();
    }
}
                    

After:


class Foo {
    private $bar;

    public function __construct(Bar $bar) {
        $this->bar = $bar;
    }

    public function run() {
        $this->bar->doSomething();
    }
}
                    

Dependency injection workflow

Application:


$db = new Database();
$generator = new PdfReportGenerator($db);
$controller = new ReportController($generator);

$controller->viewAction();
                    

ReportController:


public function viewAction() {
    echo $this->generator->generate();
}
                        

PdfReportGenerator:


public function generate() {
    $data = $this->db->selectAll('sales');
    return /* ... */;
}
                        

Dependency injection

  • Choose and replace injected instances (tests!)
  • Configuration separated from logic (SRP)

SOLID

The dependency inversion principle:

Code against abstractions

class StoreService
{
    public function __construct(GoogleMaps $geolocator) { … }

    // ...
}
                    

class GoogleMaps
{
    public function getCoordinates($address) { … }
}
                    

Code against abstractions


class StoreService {
    public function __construct(GeolocationService $geolocationService) { … }
}
                    

interface GeolocationService {
    public function getCoordinates($address);
}

class GoogleMaps implements GeolocationService { … }

class OpenStreetMap implements GeolocationService { … }
                    

Dependency injection is great

But


$db = new Database();
$generator = new PdfReportGenerator($db);
$controller = new ReportController($generator);
                    

Containers

Workflow using a DI container

Application:


$controller = $container->get('ReportController');

$controller->viewAction();
                    

ReportController:


public function viewAction() {
    echo $this->generator->generate();
}
                        

PdfReportGenerator:


public function generate() {
    $data = $this->db->selectAll('sales');
    return /* ... */;
}
                        

$controller = $container->get('ReportController');
                    

=


$db = new Database();
$generator = new PdfReportGenerator($db);
$controller = new ReportController($generator);
                    

The container builds object graphs

but how?


$db = new Database();
$generator = new PdfReportGenerator($db);
$controller = new ReportController($generator);
                    

Container configuration

  • Symfony DI
  • Pimple
  • Zend\Di
  • Zend\ServiceManager
  • Aura DI
  • Laravel IoC
  • Mouf
  • PHP-DI

Common API


$container = new Container();

$mailer = new Mailer();
$reportSender = new ReportSender($mailer);
$reportController = new ReportController($reportSender);

$container->set('Mailer', $mailer);
$container->set('ReportSender', $reportSender);
$container->set('ReportController', $reportController);
                    

Pimple


$container['Mailer'] = function () {
    return new Mailer();
};

$container['ReportSender'] = function ($container) {
    return new ReportSender($container['Mailer']);
};

$reportSender = $container['ReportSender'];
                    

Lazy graphs

Symfony


services:
    mailer:
        class:     Acme\Hello\Mailer
    report_sender:
        class:     Acme\Hello\ReportSender
        arguments: ["@mailer"]
                    

<services>
    <service id="mailer" class="Acme\Hello\Mailer" />
    <service id="report_sender" class="Acme\Hello\ReportSender">
        <argument>@mailer</argument>
    </service>
</services>
                    

Aura.DI


$container->set('Mailer', $container->lazyNew('Mailer'));

$container->set('ReportSender', $container->lazyNew('ReportSender'));
$container->params['ReportSender']['mailer'] = $container->lazyGet('Mailer');
                    

Mouf PHP

Why?

The dependency injection container for humans

 

  • Practical
  • Copy good stuff from others
  • Framework-agnostic
  • Autowiring
  • Annotations
  • PHP configuration

Autowiring


class ReportSender
{
    public function __construct(Mailer $mailer)
    {
    }
}
                    

~ 80% of the use cases -> no configuration!

Annotations


class ReportController {
    /**
     * @Inject
     * @var ReportSender
     */
    private $reportSender;

    public function sendReportNow() {
        $this->reportSender->sendReport($_GET['id']);
        // ...
    }
}
                    

Not meant to be used everywhere, but very useful in controllers

PHP configuration


<?php

return [
    'Psr\Log\LoggerInterface' => DI\object('Monolog\Logger')
        ->constructor('webservice', Monolog\Logger::LEVEL_DEBUG),

    'My\Service' => DI\factory(function () {
        $object = new Stuff();
        $object->init('foo', 'bar');
        return $object;
    }),
];
                    

Future

Make the switch

Legacy codebase

  • Singletons
  • Static classes/methods
  • "new" everywhere

DI extremism

Choose a container

Don't rewrite one…

Make it static

Make it static


/**
 * @deprecated
 */
class StaticContainer
{
    private static $container;

    public static function getContainer()
    {
        if (self::$container === null) {
            self::$container = self::createContainer();
        }
        return self::$container;
    }

    // ...
                    

Remove it ASAP

Singletons


class Database
{
    /**
     * @deprecated
     */
    public static function getInstance()
    {
        return StaticContainer::getInstance()->get('Database');
    }

    // ...
                    

Move the old getInstance() to the container config

Static classes

Turn the static class into a static proxy:


class OldDbClass
{
    /**
     * @deprecated
     */
    public static function selectAll($table)
    {
        $db = StaticContainer::getInstance()->get('NewDbClass');
        return $db->selectAll($table);
    }

    // ...
                    

Inject dependencies

Remove deprecated classes/methods

Use an IDE

Separate configuration from logic

Before:


$logFile = PIWIK_PATH . '/tmp/piwik.log';
$customId = Config::get('customId');
if ($customId !== null) {
    $tmpPath = Common::rewriteTmpPathWithCustomId($id, $tmpPath);
}
                    

After


# default config
tmpPath: /tmp
logFile: $tmpPath/piwik.log
                    

# plugin config
tmpPath: /tmp/123
                    

Don't call the container


$mailer = $container->get('Mailer');
                    

Service Locator anti-pattern

The end (finally)

Is that really worth it?

It depends

Questions?

mnapoli.fr / github.com/mnapoli

php-di.org