Dependency Injection 101

Stephan Hochdörfer // August 15, 2017

Hi, I am Stephan Hochdörfer

@shochdoerfer

I speak at conferences (a lot)

I work for...

Head of Technology

Knowledge transfer

unKonf host & organiser

Co-Org. of PHP UG FFM

Co-Org. of PHP UG MRN

This talk is not about...

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation=
    "urn:magento:framework:ObjectManager/etc/config.xsd">

    <preference for="Magento\Catalog\Api\Data\ProductInterface"
        type="Magento\Catalog\Model\Product" />

    <type name="Magento\Customer\Model\ResourceModel\Visitor">
        <plugin name="catalogLog"
            type="Magento\Catalog\Model\Plugin\Log" />
    </type>
</config>

The new hope?

Code used to look like this

<?php

class Mage_Catalog_Product_CompareController extends
    Mage_Core_Controller_Front_Action
{
    public function indexAction()
    {
        $items = $this->getRequest()->getParam('items');
        // [...]
        if ($items) {
            $items = explode(',', $items);
            $list = Mage::getSingleton('catalog/product_compare_list');
            $list->addProducts($items);
            $this->_redirect('*/*/*');
            return;
        }
        $this->loadLayout();
        $this->renderLayout();
    }
    // [...]
}

Tightly coupled code

Let's run this code...

<?php

$request = new Zend_Controller_Request_Http();
$request->setParamSources(['items' => '1,2,3']);

$response = new Zend_Controller_Response_Http();

$controller = new Mage_Catalog_Product_CompareController(
    $request,
    $response,
    []
);
$controller->indexAction();
[14-Aug-2017 13:05:13 Europe/Berlin] PHP Fatal error:  Uncaught Error:
Class 'Mage' not found in ...

No component re-use

What about testing?

<?php

class Mage_Catalog_Product_CompareController extends
    Mage_Core_Controller_Front_Action
{
    public function indexAction()
    {
        $items = $this->getRequest()->getParam('items');
        // [...]
        if ($items) {
            $items = explode(',', $items);
            $list = Mage::getSingleton('catalog/product_compare_list');
            $list->addProducts($items);
            $this->_redirect('*/*/*');
            return;
        }
        $this->loadLayout();
        $this->renderLayout();
    }
    // [...]
}

No isolation, not testable!

Type safety?

<?php

$list = Mage::getSingleton('catalog/product_compare_list');
<?php

/** @var Mage_Catalog_Model_Product_Compare_List $list */
$list = Mage::getSingleton('catalog/product_compare_list');

Inversion of Control





« [...] is a design principle in which custom-written portions
of a computer program receive the flow of control from a
generic framework. » - Wikipedia

Inversion of Control

<?php

class Mage_Catalog_Product_CompareController extends
    Mage_Core_Controller_Front_Action
{
    private $list;

    public function __construct(
        // [...]
        Mage_Catalog_Model_Product_Compare_List $list
    ) {
        parent::__construct($request, $response, $invokeArgs);
        $this->list = $list;
    }

    public function indexAction()
    {
        // [...]
        $this->list->addProducts($items);
        // [...]
    }
}

Let's run this code...

<?php

$request = new Zend_Controller_Request_Http();
$request->setParamSources(['items' => '1,2,3']);

$response = new Zend_Controller_Response_Http();

$controller = new Mage_Catalog_Product_CompareController(
    $request,
    $response,
    []
);
[14-Aug-2017 13:05:13 Uncaught TypeError: Argument 4 passed to
Mage_Catalog_Product_CompareController::__construct() must be an
instance of Mage_Catalog_Model_Product_Compare_List, none given [...]

S.O.L.I.D.





« High level modules should not depend upon
low level modules. Both should depend upon
abstractions.» - Robert C. Martin

S.O.L.I.D.





« Abstractions should not depend upon details.
Details should depend upon abstractions. »
- Robert C. Martin

Dependency Injection?

Dependency Injection types




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

Property Injection

<?php

class Mage_Catalog_Product_CompareController extends
    Mage_Core_Controller_Front_Action
{
    private $list;

    public function __construct(
        Zend_Controller_Request_Abstract $request,
        Zend_Controller_Response_Abstract $response,
        array $invokeArgs = array()
    ) {
        parent::__construct($request, $response, $invokeArgs);
    }

    // [...]
}

Property Injection

<?php

class Mage_Catalog_Product_CompareController extends
    Mage_Core_Controller_Front_Action
{
    /** 
     * @Inject 
     * @var Mage_Catalog_Model_Product_Compare_List
     */
    private $list;

    public function __construct(
        Zend_Controller_Request_Abstract $request,
        Zend_Controller_Response_Abstract $response,
        array $invokeArgs = array()
    ) {
        parent::__construct($request, $response, $invokeArgs);
    }

    // [...]
}

Let's run this code...

<?php

$request = new Zend_Controller_Request_Http();
$request->setParamSources(['items' => '1,2,3']);

$response = new Zend_Controller_Response_Http();

$controller = new Mage_Catalog_Product_CompareController(
    $request,
    $response,
    []
);
$controller->indexAction();
[14-Aug-2017 13:05:13 PHP Fatal error: Uncaught Error: Call to
a member function addProducts() on null [...]

Property Injection





« Repeat after me: field injection
is a dumb idea... *sigh* » - @olivergierke

Setter Injection

<?php

class Mage_Catalog_Product_CompareController extends
    Mage_Core_Controller_Front_Action
{
    private $list;

    public function setProductCompareList(
        Mage_Catalog_Model_Product_Compare_List $list
    ) {
        $this->list = $list;
    }

    // [...]
}

Let's run this code...

<?php

$request = new Zend_Controller_Request_Http();
$request->setParamSources(['items' => '1,2,3']);

$response = new Zend_Controller_Response_Http();

$controller = new Mage_Catalog_Product_CompareController(
    $request,
    $response,
    []
);
$controller->indexAction();
[14-Aug-2017 13:05:13 PHP Fatal error: Uncaught Error: Call to
a member function addProducts() on null [...]

..but optional dependencies?





« The concept of optional dependencies is a lie! » - Me

Interface Injection

<?php

interface Mage_Catalog_Model_Product_Compare_ListAware
{
    public function setProductCompareList(
         Mage_Catalog_Model_Product_Compare_List $list
    );
}
class Mage_Catalog_Product_CompareController
    extends Mage_Core_Controller_Front_Action
    implements Mage_Catalog_Model_Product_Compare_ListAware
{
    private $list;

    public function setProductCompareList(
        Mage_Catalog_Model_Product_Compare_List $list
    ) {
        $this->list = $list;
    }
    // [...]
}

Constructor Injection

<?php

class Mage_Catalog_Product_CompareController extends
    Mage_Core_Controller_Front_Action
{
    private $list;

    public function __construct(
        Zend_Controller_Request_Abstract $request,
        Zend_Controller_Response_Abstract $response,
        array $invokeArgs = array()
        Mage_Catalog_Model_Product_Compare_List $list
    ) {
        parent::__construct($request, $response, $invokeArgs);
        $this->list = $list;
    }
}

Dependency Injection config



  • Config as code

  • Config as non-code

    • XML, YAML, ...

    • Annotations

Config as code - Disco

<?php

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

/** @Configuration */
class BeanConfiguration
{
    /** @Bean */
    public function request(): Zend_Controller_Request_Http
    {
        return new Zend_Controller_Request_Http();
    }
    // [...]
    /** @Bean */
    public function cntrller(): Mage_Catalog_Product_CompareController
    {
        return new Mage_Catalog_Product_CompareController(
            $this->request(), $this->response(), []
        );
    }
}

Config as code - Pimple

<?php

use Pimple\Container;

$container = new Container();

$container['request'] = function ($c) {
    return new Zend_Controller_Request_Http();
};

$container['response'] = function ($c) {
    return new Zend_Controller_Response_Http();
};

$container['cntrller'] = function ($c) {
    return new Mage_Catalog_Product_CompareController(
        $c['request'], $c['response'], []
    );
};

Config as non-code - @

<?php

class Mage_Catalog_Product_CompareController extends
    Mage_Core_Controller_Front_Action
{
    /** 
     * @Inject 
     * @var Zend_Controller_Request_Abstract
     */
    private $request;
   /** 
     * @Inject 
     * @var Zend_Controller_Response_Abstract
     */
    private $response;
    /** 
     * @Inject 
     * @var Mage_Catalog_Model_Product_Compare_List
     */
    private $list;
    // [...]
}

Config as non-code - XML

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation=
    "urn:magento:framework:ObjectManager/etc/config.xsd">

    <preference for="Magento\Catalog\Api\Data\ProductInterface"
        type="Magento\Catalog\Model\Product" />

    <type name="Magento\Customer\Model\ResourceModel\Visitor">
        <plugin name="catalogLog"
            type="Magento\Catalog\Model\Plugin\Log" />
    </type>
</config>

Magento 2 ObjectManager

<?php

namespace Magento\Framework;

/** @api */
interface ObjectManagerInterface
{
    /** Create new object instance */
    public function create($type, array $arguments = []);

    /** Retrieve cached object instance */
    public function get($type);

    /** Configure object manager */
    public function configure(array $configuration);
}

Magento prohibits the direct use of the ObjectManager in
your code because it hides the real dependencies of a class.

OM: Constructor Injection

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation=
    "urn:magento:framework:ObjectManager/etc/config.xsd">

    <type name="Magento\Core\Model\Session">
        <arguments>
            <argument name="sessionName" xsi:type="string">
                adminhtml
            </argument>
        </arguments>
    </type>

</config>

OM: Constructor Injection

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation=
    "urn:magento:framework:ObjectManager/etc/config.xsd">

    <type name="Magento\Backend\Block\Context">
        <arguments>
            <argument name="urlBuilder" xsi:type="object">
                Magento\Backend\Model\Url
            </argument>
        </arguments>
    </type>

</config>

OM: Object lifestyle config

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation=
    "urn:magento:framework:ObjectManager/etc/config.xsd">

    <type name="Magento\Backend\Block\Context" shared="false">
       <arguments>
         <argument name="urlBuilder" xsi:type="object" shared="false">
             Magento\Backend\Model\Url
         </argument>
       </arguments>
    </type>

</config>

OM: Proxy Generation

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation=
    "urn:magento:framework:ObjectManager/etc/config.xsd">

    <type name="Magento\Backend\Block\Context">
        <arguments>
            <argument name="urlBuilder" xsi:type="object">
                Magento\Backend\Model\Url\Proxy
            </argument>
        </arguments>
    </type>

</config>

OM: Type Preferences

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation=
    "urn:magento:framework:ObjectManager/etc/config.xsd">

    <preference for="\Magento\Framework\Mail\Template\TransportBuilder"
        type="\bitExpert\Magento\Mail\Template\TransportBuilder" />

</config>

 





« IoC is a good idea. Like all good ideas,
it should be used with moderation. »
- @unclebobmartin

Depend on what you need...

<?php

namespace Magento\Framework\View\Element\Template;

class Context extends \Magento\Framework\View\Element\Context
{
    public function __construct(
        \Magento\Framework\App\RequestInterface $request,
        \Magento\Framework\View\LayoutInterface $layout,
        \Magento\Framework\Event\ManagerInterface $eventManager,
        \Magento\Framework\UrlInterface $urlBuilder,
        \Magento\Framework\App\CacheInterface $cache,
        \Magento\Framework\View\DesignInterface $design,
        \Magento\Framework\Session\SessionManagerInterface $session,
        \Magento\Framework\Session\SidResolverInterface $sidResolver,
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Framework\View\Asset\Repository $assetRepo,
        // [...]
    ) {
        // [...]
    }
}

ObjectManager is a lie!

<?php

namespace Magento\Framework\View\Element\Template;

class Context extends \Magento\Framework\View\Element\Context
{
    public function __construct(
        \Magento\Framework\ObjectManagerInterface $objectManager
    ) {
        // [...]
    }
}

ContainerAware is a lie!





« ContainerAware is the new Singleton. #php » - @tobySen

But I need lazy-loading...

To new or not to new...

<?php

namespace Magento\Framework\Mail\Template;

class TransportBuilder
{
    // [...]

    protected function reset()
    {
        $this->message = $this->objectManager->create(
            \Magento\Framework\Mail\Message::class
        );
        $this->templateIdentifier = null;
        $this->templateVars = null;
        $this->templateOptions = null;
        return $this;
    }
}

To new or not to new...

<?php

namespace Magento\Framework\Mail;

class Message implements MailMessageInterface
{
    /**
     * Initialize dependencies.
     *
     * @param string $charset
     */
    public function __construct($charset = 'utf-8')
    {
        $this->zendMessage = new \Zend\Mail\Message();
        $this->zendMessage->setEncoding($charset);
    }

    // [...]
}

To new or not to new...

<?php

namespace Magento\Framework\Mail\Template;

class TransportBuilder
{
    // [...]

    protected function reset()
    {
        $this->message = new \Magento\Framework\Mail\Message();
        $this->templateIdentifier = null;
        $this->templateVars = null;
        $this->templateOptions = null;
        return $this;
    }
}

What is a dependency?

Is a logger a dependency?

Is a logger a dependency?

<?php

namespace Magento\Framework\View\Element\Template;

class Context extends \Magento\Framework\View\Element\Context
{
    public function __construct(
        // [...]
        \Psr\Log\LoggerInterface $logger,
        // [...]
    ) {
        // [...]
        $this->_logger = $logger;
        // [...]
    }

    public function getLogger()
    {
        return $this->_logger;
    }
    // [...]
}

bitexpert/slf4psrlog

$> composer.phar require bitexpert/slf4psrlog
<?php

namespace Magento\Framework\View\Element\Template;

class Context extends \Magento\Framework\View\Element\Context
{
    public function __construct(
        // [...]
    ) {
        // [...]
        $this->_logger = \bitExpert\Slf4PsrLog\LoggerFactory::getLogger(
            __CLASS__
        );
    }

    // [...]
}

The glue code...

<?php

\bitExpert\Slf4PsrLog\LoggerFactory::registerFactoryCallback(
    function($channel) {
        if (!\Monolog\Registry::hasLogger($channel)) {
            \Monolog\Registry::addLogger(
                new \Monolog\Logger($channel)
            );
        }

        return \Monolog\Registry::getInstance($channel);
    }
);

Architecture is tool agnostic

 





« Dependency Injection is a key element
of agile architecture » - Ward Cunningham

Thank you! Questions?


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

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