Dependency Injection

How to do it right

@MrDanack

What is dependency injection?

  • Fancy way of saying passing parameters
  • Easier to test
  • Easier to write & refactor code
  • Easier to add new services
  • More reasonable code
Reasonable   Understandable
Obvious from reading the code Documentation / colleague can explain how it works
Errors happen at start of execution Errors can happen during middle of execution
Simpler parts, more of them Fewer parts, but more complex code

Not using DI is bad

m'kay?

Globals


function foo()
{
    global $fileUploader;
    $fileUploader->putFile();
}

Hard code all the things!


function foo()
{
    $storage = new S3Storage();
}

or

function foo()
{
    $storage = S3Storage::create();
}

Service locator


function foo($sl) {
    $storage = $sl->get('fileUploader');
    …
    …
    …
    if ($criticalError) {
        $notification = $sl->get('ErrorNotifier');
    }
}

Also a Service locator


class FooController extends ContainerAware {
    function foo() {
         $db = $this->get('storage');
    }
}

Really a hard coded service locator

So....how to do DI right?

Depdendency Injection is literally just passing in parameters


    
function backupFile(S3Storage $s3Storage, EntityManager $em)
{
    // ...
}
    
backupFile($s3Storage, $entityManager);
    

Code shouldn't be aware of injector

Typehints


function backupFile(S3Storage $s3Storage, EntityManager $em)
{
    // ...
}

Interfaces are best, so ignore that PSR-2 bye-law

Actually, can we just start calling them types now?

Constructor injection


$fooController = new FooController();
    
// Any code that touches $fooController between constructor
// and setter will behave...poorly

$foo->setDBConnection($dbConnection);

Use types for all dependencies


    $logger = $di->make('log_service');

vs

    $logger = $di->make(Psr\Log\LoggerInterface::class);

No really - use types for all dependencies


class S3Config
{
    public $key;
    public $secret;
    public $region;
    ...
}


class TmpPath
{
    public $value;
    function __construct($value) {
        $this->value = $value
    }
}

Use a Dependency Injection Container

https://github.com/rdlowrey/auryn

Using a 'Dependency Injection Container' makes life easy


function backupFile(S3Storage $s3Storage, EntityManager $em)
{
    // ...
}

$injector->execute('backupFile');    

'Dependency injection container' is a rubbish name. Going to just use the word 'injector' as opposed to saying DIC 100 times.

Injector - makes life easy


class BackupController {
    function backupFile(...) {
    }
}

$instance = $injector->make('BackupController');
$instance->backupFile();

Or just invoke directly

$injector->execute(['BackupController', 'backupFile']);

Injector - makes life easy


$route = $router->matchRequest();

$callable = $route->getCallable();

$injector->execute($callable);

Configuring the injector


$injector->alias('FileUploader', 'S3FileUploader');


$injector->share('S3FileUploader');


$injector->define(
    'S3Config', 
    array(
        ':aws_key'    => 'abcde12345',
        ':aws_secret' => 'fghij67890'
        ':region'     => 'EU_WEST'
    )
);

Configuring the injector


function createS3Config() {
    $key    = getenv('aws.s3.key');
    $secret = getenv('aws.s3.secret');
    $region = getenv('aws.s3.region');

    return new S3Config($key, $secret, $region)
}

$injector->delegate('S3Config', 'createS3Config');

Delegation is awesome


    
function getClientFromSession(Session $session, ClientRepository $clientRepo) {
    $clientID = $session->getClientID();

    return $clientRepo->getClientByID($clientID);

};
    
function createFileUploader(Client $client, Injector $injector) {
    $storageType = $client->getFileUploaderType();

    return $injector->make($storageType);
};

$injector->delegate('Client', 'getClientFromSession');
$injector->delegate('FileUploader', 'createFileUploader');

Configuring the injector


function prepareS3Client(S3Client $s3Client) {
    $s3Client->setTimeOut(10000);
}

$injector->prepare('S3Client', 'prepareS3Client');

'Optional' 'dependency' ?


function foo(ObjectCache $cache = null)
{
    if ($cache) {
        if ($value = $cache->get('bar')) {
            return $value;
        }
    }

    $value = bar();

    if ($cache) {
        $cache->put('bar', $value);
    }

    return $value;
}

No such thing as optional dependency


function foo(ObjectCache $cache)
{
    if ($value = $cache->get('bar')) {
        return $value;
    }

    $value = bar();
    $cache->put('bar', $value);

    return $value;
}

    

No such thing as optional dependency!


class NullCache implements ObjectCache
{
    function put($key, $object)
    {
    }

    function get()
    { 
        return null;
    }
}

Q: Does this break LSP?

Does this break LSP?

“What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.”

I think this turns on whether you interpret 'behaviour' as either 'technical behaviour' or as 'conforming to the business needs behaviour'.

Taken from http://www.objectmentor.com/resources/articles/lsp.pdf

Default implementation is okay though



class RouteCollection {
    public function __construct(RouteFactory $route_factory = null) {
        if ($route_factory == null) {
            $route_factory = new Aura\Router\RouteFactory();    
        }
    }
}


function backupFile(S3Storage $s3Storage,
    Request $request, \Doctrine\ORM\EntityManager $entityManager)
{
    $backupRepository = $entityManager->getRepository('Backup');
    $sourceFile = $backupRepository->getNextFilename();
    $backupFilename = $sourceFile.'_'.date("Y_m_d_H_i_s");

    try {
        $s3Storage->putFile($sourceFile, $backupFilename);
    }
    catch(S3Exception $s3Exception) {
        return new ServerErrorResponse("Somethings borked");
    }
    $responseData = ['backupFilename' => $backupFilename];
    $acceptHeader = $request->getHeader('Accept');
    if (strcmp($acceptHeader, "text/xml") === 0) {
        return new XMLResponse($responseData);
    }

    return new JsonResponse($responseData);
}

Wrong dependency


function backupFile(\Doctrine\ORM\EntityManager $em)
{
    ...
    $backupRepository = $entityManager->getRepository('Backup');
    ...
}

Right dependency

function backupFile(BackupRepository $backupRepository)
{
    ...
}

The Law of Demeter - "Only talk to your immediate friends."

Delegation removes coupling


function getBackupRepository(EntityManager $em)
{
    return $em->getRepository('Backup');
}

$injector->delegate(BackupRepository::class, 'getBackupRepository');

new is a smell...


function backupFile(..., Request $request, ...)
{
    ...
    $acceptHeader = $request->getHeader('Accept');

    if (strcmp($acceptHeader, "text/xml") === 0) {
        return new XMLResponse($responseData);
    }
    return new JsonResponse($responseData);
}

Factories are objects!!1!

  • They encapsulate information. In this case neither neither JsonResponse nor XMLResponse need to know about 'Request'.
  • Named constructors are fine - http://verraes.net/2014/06/named-constructors-in-php/


class RequestResponseFactory implements ResponseFactory
{
    private $request;

    function __construct(Request $request) {
        $this->request = $request;
    }
    
    function create(array $responseData) {
        $acceptHeader = $this->request->getHeader('Accept');
        if (strpos($acceptHeader, "text/xml") === 0) {
            return new XMLResponse($responseData);
        }

        return new JsonResponse($responseData);	
    }
}

$injector->alias('ResponseFactory', 'RequestResponseFactory');

Depending on an implementation...


function backupFile(S3Client $s3Client, ...)
{
    ...
    try {
        $s3Client->putFile($sourceFile, $backupFilename);
    }
    catch(S3Exception $s3Exception) {
        return new ServerErrorResponse("Somethings borked");
    }
    ...
}

Interface has...


interface FileUploader {
    function putFile($srcFilename, $dstFilename);
}

  • Less coupling
  • Clearer semantic meaning
  • Fewer methods so simpler to reason about
Two types of interfaces
  • Technical ones - obvious at start of project
  • 'Business' driven - emerge during project

function backupFile class S3Client
function backupFile S3FileUploader implements FileUploader class S3Client

This is the D in SOLID. "Dependency inversion principle" == "Depend upon Abstractions. Do not depend upon concretions.”

Interface has less coupling


class S3FileUploader implements FileUploader
{
    function __construct(S3Client $s3Client) {
        $this->s3Client = $s3Client;
    }

    function putFile($sourceFilename, $backupFilename) {
        try {
            $this->s3Client->putFile($sourceFilename, $backupFilename));
        }
        catch(\Exception $exception) {
           throw new FileUploaderException(
               "FileUploader exception: ".$exception->getMessage(),
               $exception->getCode(),
               $exception
           );
        }
    }
}

After refactoring


function backupFile(FileUploader $fileUploader,
    ResponseFactory $responseFactory, 
    BackupRepository $backupRepository)
{
    $sourceFile = $backupRepository->getNextFilename();
    $backupFilename = $sourceFile.'_'.date("Y_m_d_H_i_s");

    try {
        $fileUploader>putFile($sourceFile, $backupFilename);
    }
    catch(FileResponseException $exception) {
        return new ServerErrorResponse("Somethings borked");
    }

    return $responseFactory->create(['backupFilename' => $backupFilename]);
}

DI not appropriate ... for trivial apps

DI not appropriate ... where the dependencies can't be resolved cleanly


function archiveOldData(DBConnection $liveDB, DBConnection $archiveDB) {
   ...
}

    
Application / injector Execute callable Next callable + DI info Execute callable Next callable + DI info

This is a form of flow-based programming......probably.
http://www.jpaulmorrison.com/fbp/

Application / injector Execute Router callable Controller + query params Execute Controller callable View + model Execute View callable Response body

Demo time

No time....example app is at www.github.com/danack/Tier

Fin

  • Pass parameters in
  • Depend on the correct things
  • Use an injector https://github.com/rdlowrey/auryn
  • J.B. Rainsberger - Integrated Tests Are A Scam http://vimeo.com/80533536
  • Questions?

Encapsulated Context Pattern

"The ability to bind the lifecycle and interactions of stateful components to well-defined but extensible lifecycle contexts."


class MarketContext {
    public $tradeExecutor;
    public $logManager;
    public $priceValidator;

}

Stops lower tier code from being able to look up services, making it easier to reason about what is injected. Probably useful for views.

http://accu.org/index.php/journals/246

Why not other DICs?

  • Use names rather than types
  • Lack recursive delegation to functions
  • Lack ability to alias interfaces to classes
  • Bad (imo) syntax

$container['session_storage'] = function ($c) {
    return new SessionStorage('SESSION_ID');
};

$container['session'] = function ($c) {
    return new Session($c['session_storage']);
};

Current static methods:


interface Info {
    static function getInfo();
}

class Foo implements Info {
    static function getInfo() {
    }
}

“For years there has been a theory that millions of monkeys typing at random on millions of typewriters would reproduce the entire works of Shakespeare. The Internet has proven this theory to be untrue.”