Disco - A Fresh Look at DI

Stephan Hochdörfer // 18.04.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
  • @Parameters
  • @Parameter
  • @BeanPostProcessor

Annotations in Disco




« The library is independent and can be used
in your own libraries to implement doc block
annotations. » - Doctrine Annotations

Why another DI container?





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

Because awesome!

Configuration as non-code





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

Configuration as non-code

<?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());

Not so much type-hinted...

<?php

$container = new \Pimple\Container();

$container['database'] = function ($c) {
    return new Database('mysql://user:secret@localhost/mydb');
};

$container['productService'] = function ($c) {
    return new ProductService($c['database']);
};

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');

Configuration example

<?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);
    }
}

Configuration example

<?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('database'));
var_dump($beanFactory->has('productService'));
bool(false)
bool(true)

"Factory Beans"

<?php

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

/** @Configuration */
class Config
{
    /** @Bean */
    public function productService1() : ProductService
    {
        return $this->productServiceFactory(
            new Database('mysql://user:secret@localhost/mydb1'));
    }
    /** @Bean */
    public function productService2() : ProductService
    {
        return $this->productServiceFactory(
            new Database('mysql://user:secret@localhost/mydb2'));
    }

"Factory Beans"

    private function productServiceFactory(Database $db) : ProductService
    {
        return new ProductService($db);
    }
}

Config with parameters

<?php

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

/** @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;
use bitExpert\Disco\Annotations\Parameters;

/** @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;
use bitExpert\Disco\Annotations\Parameters;

/** @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);

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) {
}

Singleton vs. Non-Singleton

Non-Lazy instances

<?php

namespace Demo;

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

namespace Demo;

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 - the why?




  • "Expensive" dependencies
  • Circular dependencies
  • Dependencies of session-
    aware beans

Lazy - under the hood




« This library aims at providing abstraction
for generating various kinds of proxy classes. »
- ocramius/proxy-manager

Lazy - under the hood

<?php

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

use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use ProxyManager\Proxy\LazyLoadingInterface;

$factory     = new LazyLoadingValueHolderFactory();
$initializer = function (& $wrappedObject, LazyLoadingInterface $proxy,
    $method, array $parameters, & $initializer) {

    $initializer   = null;
    $wrappedObject = new ProductService();

    return true;
};

$proxy = $factory->createProxy('ProductService', $initializer);

Request scope

<?php

namespace Demo;

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

namespace Demo;

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

require __DIR__ .'/vendor/autoload.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

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

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

$beanStore = unserialize($beanStore);
$config = new \bitExpert\Disco\BeanFactoryConfiguration(
    sys_get_temp_dir()
);
$config->setBeanStore($beanStore);

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

Bean Alias

<?php

namespace Demo;

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({"alias" = "\Identifier\With\Namespace"}) */
    public function productService() : ProductService
    {
        return new ProductService($this->database());
    }
}

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) {
}

Aliases are public by default

<?php

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

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

Using primitives

<?php

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

/** @Configuration */
class BeanConfiguration
{
    /**
     * @Bean
     */
    public function disco() : string
    {
        return 'Disco rocks!';
    }
}

Using primitives

<?php

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

/** @Configuration */
class BeanConfiguration
{
    /**
     * @Bean
     * @Parameters({
     *    @Parameter({"name" = "disco"})
     * })
     */
    public function disco($disco = '') : string
    {
        return $disco;
    }
}

Structure the configuration

<?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);
    }
}
<?php
use bitExpert\Disco\Annotations\Configuration;

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

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

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

BeanPostProcessor

<?php

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

/** @Configuration */
class Config
{
    /** @BeanPostProcessor */
    public function charset() : DatabaseBeanCharSetPostProcessor
    {
        return new DatabaseBeanCharSetPostProcessor();
    }
}

BeanPostProcessor

<?php

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

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

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);
        }
    }
}

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 repository





https://github.com/bitExpert/disco

Disco in action





https://github.com/bitExpert/disco-demos

Disco in the news




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

Roadmap


  • Add Disco to
    zend-expressive installer
  • Add support for
    .phpstorm.meta.php
  • Add support for
    service-interop
  • Add support for
    custom annotations

Thank you! Questions?


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

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