Disco - A Fresh Look at DI

Stephan Hochdörfer // 18.07.2017

About me


  • Stephan Hochdörfer
  • Head of Technology, bitExpert AG
    (Mannheim, Germany)
  • S.Hochdoerfer@bitExpert.de
  • @shochdoerfer

  • #PHP, #DevOps, #Automation
  • #phpugffm, #phpugmrn, #unKonf

bitexpert/disco




« [...] a PSR-11 compatible, annotation based
dependency injection container. » - Disco

container-interop PSR-11?




« to standardize how frameworks and libraries make use
of a container to obtain objects and parameters. » - PSR-11

Side note: service-provider




« [...] tries to find a solution for cross-framework modules
(aka bundles) through standard container configuration. »
- service-provider

Annotations?

Annotations in Disco



  • @Configuration
  • @Bean
  • @Parameter
  • @Alias
  • @BeanPostProcessor

Why another DI container?





« [...] to make DI in PHP great again. » - Me

Because awesome!

Non-code Configuration





« @rdohms says XML is best for DI config. » - @benmarks

Non-code Configuration

<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services 
    http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="dependency" class="\My\Dependency">
        </service>

        <service id="manager" class="\My\Manager">
            <call method="setDependency">
                <argument type="service" id="dependency" />
            </call>
        </service>
    </services>
</container>

Configuration as code

<?php

use Symfony\Component\DependencyInjection\Reference;

// ...
$container
    ->register('dependency', \My\Dependency::class);

$container
    ->register('manager', \My\Manager::class)
    ->addMethodCall('setDependency', array(new Reference('dependency')));

All that to just express this?

<?php

$manager = new \My\Manager();
$manager->setDependency(new \My\Dependency());

How to get started...

$ composer.phar require bitexpert/disco
<?php

require __DIR__ .'/vendor/autoload.php';

use bitExpert\Disco\AnnotationBeanFactory;
use bitExpert\Disco\BeanFactoryRegistry;

$beanFactory = new AnnotationBeanFactory(Config::class);
BeanFactoryRegistry::register($beanFactory);
$beanFactory->get('productService');
$beanFactory->has('productService');

Basic configuration class

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /** @Bean */
    public function productService() : ProductService
    {
        $db = new Database('mysql://user:secret@localhost/mydb');
        return new ProductService($db);
    }
}

Basic configuration class

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /** @Bean */
    protected function database() : Database
    {
        return new Database('mysql://user:secret@localhost/mydb');
    }

    /** @Bean */
    public function productService() : ProductService
    {
        return new ProductService($this->database());
    }
}

public vs. protected Beans

<?php

require __DIR__ .'/vendor/autoload.php';

use bitExpert\Disco\AnnotationBeanFactory;
use bitExpert\Disco\BeanFactoryRegistry;

$beanFactory = new AnnotationBeanFactory(\Demo\Config::class);
BeanFactoryRegistry::register($beanFactory);
var_dump($beanFactory->has('productService'));
bool(true)
var_dump($beanFactory->has('database'));
bool(false)

Dependency Injection Types




  • Property Injection
  • Constructor Injection
  • Setter Injection
  • Interface Injection

Bean lifecycle




  • Singleton vs. Non-Singleton / Prototype
  • Non-Lazy vs. Lazy
  • Request Scope vs. Session Scope

Singleton instances

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /** @Bean */
    protected function database() : Database
    {
        return new Database('mysql://user:secret@localhost/mydb');
    }

    /** @Bean({"singleton" = true}) */
    public function productService() : ProductService
    {
        return new ProductService($this->database());
    }
}

Singleton instances

<?php

var_dump($instance = $beanFactory->get('productService'));

var_dump($instance2 = $beanFactory->get('productService'));
object(Demo\ProductService)#9 (0) {
}

object(Demo\ProductService)#9 (0) {
}

Non-Singleton instances

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /** @Bean */
    protected function database() : Database
    {
        return new Database('mysql://user:secret@localhost/mydb');
    }

    /** @Bean({"singleton" = false}) */
    public function productService() : ProductService
    {
        return new ProductService($this->database());
    }
}

Non-Singleton instances

<?php

var_dump($instance = $beanFactory->get('productService'));

var_dump($instance2 = $beanFactory->get('productService'));
object(Demo\ProductService)#9 (0) {
}

object(Demo\ProductService)#11 (0) {
}

Non-Lazy instances

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /** @Bean */
    protected function database() : Database
    {
        return new Database('mysql://user:secret@localhost/mydb');
    }

    /** @Bean({"lazy" = false}) */
    public function productService() : ProductService
    {
        return new ProductService($this->database());
    }
}

Non-Lazy instances

<?php

var_dump($instance = $beanFactory->get('productService'));
object(Demo\ProductService)#9 (0) {
}

Lazy instances

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /** @Bean */
    protected function database() : Database
    {
        return new Database('mysql://user:secret@localhost/mydb');
    }

    /** @Bean({"lazy" = true}) */
    public function productService() : ProductService
    {
        return new ProductService($this->database());
    }
}

Lazy instances

<?php

var_dump($instance = $beanFactory->get('productService'));
object(ProxyManagerGeneratedProxy\__PM__\ProductService\Gen1d5)#143 (3) {
}

Lazy instances. Why?




  1. "Expensive" dependencies
  2. Circular dependencies
  3. Sessionaware bean
    dependencies

Request scope

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /** @Bean */
    protected function database() : Database
    {
        return new Database('mysql://user:secret@localhost/mydb');
    }

    /** @Bean({"scope" = "request"}) */
    public function productService() : ProductService
    {
        return new ProductService($this->database());
    }
}

Session scope

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /** @Bean */
    protected function database() : Database
    {
        return new Database('mysql://user:secret@localhost/mydb');
    }

    /** @Bean({"scope" = "session"}) */
    public function productService() : ProductService
    {
        return new ProductService($this->database());
    }
}

Session scope

<?php

use bitExpert\Disco\AnnotationBeanFactory;
use bitExpert\Disco\BeanFactoryRegistry;

$config = new \bitExpert\Disco\BeanFactoryConfiguration(
    sys_get_temp_dir()
);

$beanFactory = new AnnotationBeanFactory(Config::class, [], $config);
BeanFactoryRegistry::register($beanFactory);
$beanStore = serialize($config->getBeanStore());

Session scope

<?php

use bitExpert\Disco\AnnotationBeanFactory;
use bitExpert\Disco\BeanFactoryRegistry;

$config = new \bitExpert\Disco\BeanFactoryConfiguration(
    sys_get_temp_dir()
);

$beanFactory = new AnnotationBeanFactory(Config::class, [], $config);
BeanFactoryRegistry::register($beanFactory);
$beanStore = unserialize($beanStore);
$config->setBeanStore($beanStore);
var_dump($instance = $beanFactory->get('productService'));

Bean parameters

Config with parameters

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
use bitExpert\Disco\Annotations\Parameter;

/** @Configuration */
class Config
{
    /** 
     * @Bean({
     *   "parameters"={
     *      @Parameter({"name" = "db.dsn"})
     *   }
     * })
     */
    protected function database($dsn = '') : Database
    {
        return new Database($dsn);
    }
    // [...]
}

Config with parameters

<?php

$config = [
    'db' => [
        'dsn' => 'mysql://user:secret@localhost/mydb'
    ]
];

$beanFactory = new AnnotationBeanFactory(Config::class, $config);
BeanFactoryRegistry::register($beanFactory);

Config with default params

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
use bitExpert\Disco\Annotations\Parameter;

/** @Configuration */
class Config
{
    /** 
     * @Bean({
     *   "parameters"={
     *      @Parameter({"name" = "db.dsn", "default" = "mysql://..."})
     *   }
     * })
     */
    protected function database($dsn = '') : Database
    {
        return new Database($dsn);
    }
    // [...]
}

Config with parameters

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
use bitExpert\Disco\Annotations\Parameter;

/** @Configuration */
class Config
{
    // [...]
    /** @Bean */
    public function productService() : ProductService
    {
        // no need to pass parameters to the database method!
        return new ProductService($this->database());
    }
}

vlucas/phpdotenv

$ composer.phar require vlucas/phpdotenv
DB_DSN="mysql://user:secret@localhost/mydb"
<?php

require __DIR__ .'/vendor/autoload.php';

use bitExpert\Disco\AnnotationBeanFactory;
use bitExpert\Disco\BeanFactoryRegistry;

$dotenv = new Dotenv\Dotenv(__DIR__);
$dotenv->load();

$beanFactory = new AnnotationBeanFactory(Config::class, $_ENV);
BeanFactoryRegistry::register($beanFactory);

Bean Alias

<?php

use bitExpert\Disco\Annotations\Alias;
use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /**
     * @Bean({
     *   "aliases"={
     *      @Alias({"name" = "\Identifier\With\Namespace"})
     *   }
     * })
     */
    public function productService() : ProductService
    {}
    // [...]
}

Bean Alias

<?php

var_dump($instance = $beanFactory->get('productService'));

var_dump($instance2 = $beanFactory->get('\Identifier\With\Namespace'));
object(Demo\ProductService)#9 (0) {
}

object(Demo\ProductService)#9 (0) {
}

Bean Type Alias

<?php

use bitExpert\Disco\Annotations\Alias;
use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /**
     * @Bean({
     *   "aliases"={
     *      @Alias({"type" = true})
     *   }
     * })
     */
    public function productService() : ProductService
    {}
    // [...]
}

Bean Type Alias

<?php

var_dump($instance = $beanFactory->get('\Demo\ProductService'));
object(Demo\ProductService)#9 (0) {
}

Aliases are public by default

<?php

use bitExpert\Disco\Annotations\Alias;
use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class Config
{
    /**
     * @Bean({
     *   "aliases"={
     *      @Alias({"alias" = "\My\Db"})
     *   }
     * })
     */
    protected function database() : Database
    {
        return new Database('mysql://user:secret@localhost/mydb');
    }
}

Aliases are public by default

<?php

var_dump($instance = $beanFactory->get('\My\Db'));
object(Demo\Database)#11 (0) {
}

Structure the configuration

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class BeanConfiguration
{
    /** @Bean */
    protected function database() : Database
    {}
    /** @Bean */
    public function productRepository() : productRepository
    {}
    /** @Bean */
    public function productService() : ProductService
    {}
    /** @Bean */
    public function productAction() : ProductAction
    {}
}

Structure the configuration

/home/shochdoerfer/Workspace/disco-demo
   |-src
   |---Demo
   |-----Config.php
   |-----SliceA
   |-------SliceAConfigTrait.php
   |-----SliceB
   |-------SliceBConfigTrait.php
   |-----SliceC
   |-------SliceCConfigTrait.php

Structure the configuration

<?php
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class BeanConfiguration
{
    use ProductSlice;
}
<?php
use bitExpert\Disco\Annotations\Bean;

trait ProductSlice
{
    /** @Bean */
    public function productService() : ProductService
    {
        $db = new Database('mysql://user:secret@localhost/mydb');
        return new ProductService($db);
    }
}

Structure the configuration

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class BeanConfiguration
{
    use ProductSlice;

    /** @Bean */
    public function productService() : ProductService
    {
        return new CustomProductService();
    }
}

Structure the configuration

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;

/** @Configuration */
class BeanConfigurationForTests extends BeanConfiguration
{
    /** @Bean */
    public function productService() : ProductService
    {
        return new ImpossibleToMockProductService();
    }
}

BeanPostProcessor




« [... ] allows you to implement custom logic after
Disco finishes instantiating, configuring, and
initializing a bean. »

BeanPostProcessor

<?php

namespace bitExpert\Disco;

interface BeanPostProcessor
{
    /**
     * Apply this BeanPostProcessor to the given new bean instance 
     * after the bean got created.
     *
     * @param object $bean
     * @param string $beanName
     */
    public function postProcess($bean, $beanName);
}

BeanPostProcessor

<?php

use bitExpert\Disco\BeanPostProcessor;

class DatabaseBeanCharSetPostProcessor implements BeanPostProcessor
{
    protected $charset;

    public function __construct($charset)
    {
        $this->charset = $charset
    }

    public function postProcess($bean, $beanName)
    {
        if ($bean instanceof Database) {
            $bean->setCharset($this->charset);
        }
    }
}

BeanPostProcessor

<?php

use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\BeanPostProcessor;
use bitExpert\Disco\Annotations\Configuration;
use bitExpert\Disco\Annotations\Parameter;

/** @Configuration */
class Config
{
    /** 
     * @BeanPostProcessor({
     *   "parameters"={
     *      @Parameter({"name" = "db.charset"})
     *   }
     * })
     */
    public function charset($cst = '') : DatabaseBeanCharSetPostProcessor
    {
        return new DatabaseBeanCharSetPostProcessor($cst);
    }
}

Tuning for production

<?php

$projectTmpDir = '/some/path/on/the/server/';

$config = new \bitExpert\Disco\BeanFactoryConfiguration($projectTmpDir);
$config->setProxyAutoloader(
    new \ProxyManager\Autoloader\Autoloader(
        new \ProxyManager\FileLocator\FileLocator($projectTmpDir),
        new \ProxyManager\Inflector\ClassNameInflector('Disco')
    )
);

$beanFactory = new \bitExpert\Disco\AnnotationBeanFactory(Config::class,
    [],$config);
BeanFactoryRegistry::register($beanFactory);

Disco - the code





https://github.com/bitExpert/disco

Disco forever!




Disco - in the news




sitepoint.com: A Fresh Look at Dependency Injection
php[architect]: Your Dependency Injection Needs a Disco

Disco - what others say




« Hmmm, ok. » - @ocramius
« Best developer experience, nice feature set » - @codeliner
« Disco is great! Personally I really love it! » - @heiglandreas

Thank you! Questions?


  • Stephan Hochdörfer
  • Head of Technology, bitExpert AG
    (Mannheim, Germany)
  • S.Hochdoerfer@bitExpert.de
  • @shochdoerfer

  • #PHP, #DevOps, #Automation
  • #phpugffm, #phpugmrn, #unKonf