Dependency Injection
for Mere Humans

AustinPHP January 2018

Ian Littman / @iansltx

follow along at https://ian.im/diatx0118

Alternate title

opinions and implementations on
Decoupled software design

Shout out to @jcarouth

What We'll Cover

  • Types of dependencies
  • What dependency injection is and why it's a good thing
  • Types of dependency injection, pros + cons
  • Refactoring to a dependency-injected architecture
  • Dependency Injection Containers
    • What they are
    • How to use them
    • How not to use them
    • Autowiring for fun and profit

Highly coupled code is bad

highly coupled code is bad

but some coupling is unavoidable

Decoupled Code is good

  • Easier to test
  • Easier to debug
  • Easier to reason about

What's a dependency, anyway?

Dependencies are the objects, resources, or functions an object uses to accomplish its responsibility.

 

For this talk, we'll be looking at dependencies at a per-class level, rather than at the library level (handled by Composer) or OS level (handled by e.g. apt, yum).

Some common Dependencies

  • Databases/Entity Managers (e.g. PDO, Doctrine)
  • Caching (e.g. Redis/Memcached adapters)
  • File system abstraction layers (e.g. Flysystem)
  • HTTP API clients (or SDKs that wrap them)
  • Logging
  • Notifications (email, SMS, push)

Some common Dependencies

  • The current web request
  • Environment variables
  • Session information
  • The current user
  • The current time
  • Static method calls

Spot the dependencies

class OrderProcessor {
    function __construct() {
        $this->orderRepository = new MysqlOrderRepository();
    }

    function completeOrder($order) {
        global $logger;
        $order->completedAt = new \DateTimeImmutable;
        $logger->log("Order {$order->id} is complete");
        $this->orderRepository->save($order);
        Mailer::sendOrderCompleteEmail($order);
    }
}

Spot the dependencies

class OrderProcessor {
    function __construct() {
        $this->orderRepository = new MysqlOrderRepository();
    }

    function completeOrder($order) {
        global $logger;
        $order->completedAt = new \DateTimeImmutable;
        $logger->log("Order {$order->id} is complete");
        $this->orderRepository->save($order);
        Mailer::sendOrderCompleteEmail($order);
    }
}

Spot the dependencies

class OrderProcessor {
    function __construct() {
        $this->orderRepository = new MysqlOrderRepository();
        $this->logger = Logger:getInstance();
    }

    function completeOrder($order) {
        $order->completedAt = new \DateTimeImmutable;
        $this->logger->log("Order {$order->id} is complete");
        $this->orderRepository->save($order);
        Mailer::sendOrderCompleteEmail($order);
    }
}

Hard questions

Q: What does OrderProcessor depend on?

A: Read the entire class to find out!
 

Q: Where is Mailer used in my application?

A: Grep everything in your project for "Mailer::".

Q: How can I test OrderProcessor's without sending emails?

A: ¯\_(ツ)_/¯ *

 

* Yes, there are workarounds, but this isn't a testing talk.

WHat's dependency injection, anyway?

It's the practice of pushing (injecting) dependencies into an object, rather than having objects find their dependencies on their own.

 

This isn't the same as the Dependency Inversion Principle in SOLID. We'll get to that later.

Types of Dependency Injection

  • Constructor
  • Setter + Property
  • Parameter (sorta)

Constructor injection

class ClassThatNeedsLogging {
    private $logger;

    public function __construct(Logger $logger) {
        $this->logger = $logger;
    }
}

Pros

  • Makes dependencies explicit
  • Can't modify dependencies after instantiation
  • Discourages violation of Single Responsibility Principle

Cons

  • May have a lot of constructor parameters
  • May never use most dependencies
public function __construct(
    Twig $view,
    AuthorizationServer $auth,
    LogRepository $errorRepo,
    AuthRequestRepository $authRequestRepo,                                
    ClientRepository $clientRepo,
    UserRepository $userRepo,
    ResourceServer $resAuth,
    AccessTokenRepository $accessTokenRepo,
    UserValidatorInterface $validator
) {/* a whole bunch of property assigns */}

How not to solve this

"I know, I'll just throw everything in an $options array!"

Setter injection

class ClassThatKindaSortaMightWantLogging {
    private $logger = null;

    public function setLogger(Logger $logger) {
        $this->logger = $logger;
    }
}

Yolo Property injection*

class ClassThatMightWantAThingCalledALogger {
    public $logger = null;
}

* Don't do this.

Pros

  • Lazier loading
  • Works well with optional dependencies
  • Flexible across class lifecycle
    • Don't need every dependency for every method call
    • Can change dependencies without re-instantiating the class

Cons

  • Harder to quickly see which dependencies a class has
  • Existence of dependencies in fully instantiated class not guaranteed
    • Null checks inside the code
    • Conditional injection outside the code

Which would you rather have?

public function fileTPSReport(Report $rpt) {
    /* do some stuff */
    if ($this->logger instanceof Logger) {
        $this->logger->log('Did a thing');
    }
    /* do more stuff */
    if ($this->logger instanceof Logger) {
        $this->logger->log('Did things');
    }
}

Which would you rather have?

public function fileTPSReport(Report $rpt) {
    /* do some stuff */
    if ($this->logger) {
        $this->logger->log('Did a thing');
    }
    /* do more stuff */
    if ($this->logger) {
        $this->logger->log('Did things');
    }
}

Which would you rather have?

public function fileTPSReport(Report $rpt) {
    /* do some stuff */
    $this->logger->log('Did a thing');
    /* do more stuff */
    $this->logger->log('Did things');
}

 

class NullLogger implements Logger {
    public function log($message) {
        /** noop **/
    }
}

One good thing about setter injection

Setter injection is useful when a bunch of classes have the same (preferably optional) dependency.

 

Add the setter + getter to a trait, then import the trait into the class. Then tag with a related interface for easy bulk injection.

 

Sorry, you can't have an interface implement a trait directly.

Parameter Injection

public function __invoke(
    \Slim\Http\Request $request,
    \Slim\Http\Response $response,
    array $args = [])
{
    /** do something, return a Response **/
}

Pros

  • What you need when you need it

Cons

  • Moves the problem of dependency management to the caller

You don't have to use just one Method

Dependency Inversion Principle

High level modules should not depend on low level modules; both should depend on abstractions. Abstractions should not depend on details.  Details should depend upon abstractions.

Dependency Inversion Principle

High level modules should not depend on low level modules; both should depend on abstractions. Abstractions should not depend on details.  Details should depend upon abstractions.

 

Tl;dr: use, and expose, interfaces with just enough functionality to get the job done.

Both Are Acceptable

class TwilioSms implements SmsProvider {}

class NexmoSms implements SmsProviderInterface {}

Abstractions should not be leaky

class FordExplorer implements HasGasPedal {
    public function pressAccelerator();
}

class TeslaModel3 implements HasGasPedal {
    public function pressAccelerator();
}

Abstractions should not be leaky

class MysqlUserRepo implements UserRepository {
    public function getById(int $id): ?User;
}

class RedisUserRepo implements UserRepository {
    public function getById(int $id): ?User;
}

class gRPCUserAPI implements UserRepository {
    public function getById(int $id): ?User;
}

Let's do some refactoring

class OrderProcessor {
    function __construct() {
        $this->orderRepository = new MysqlOrderRepository();
        $this->logger = Logger:getInstance();
    }

    function completeOrder($order) {
        $order->completedAt = new \DateTimeImmutable;
        $this->logger->log("Order {$order->id} is complete");
        $this->orderRepository->save($order);
        Mailer::sendOrderCompleteEmail($order);
    }
}

Let's do some refactoring

class OrderProcessor {
    function __construct(OrderRepository $orderRepo,
                         Logger $logger) {
        $this->orderRepository = $orderRepo;
        $this->logger = $logger;
    }

    function completeOrder($order) {
        $order->completedAt = new \DateTimeImmutable;
        $this->logger->log("Order {$order->id} is complete");
        $this->orderRepository->save($order);
        Mailer::sendOrderCompleteEmail($order);
    }
}
class OrderProcessor {
    function __construct(OrderRepository $orderRepo,
                         Logger $logger,
                         \DateTimeImmutable $now,
                         Mailer $mailer) {
        $this->orderRepository = $orderRepo;
        $this->logger = $logger;
        $this->now = $now;
        $this->mailer = $mailer;
    }
    function completeOrder($order) {
        $order->completedAt = $this->now;
        $this->logger->log("Order {$order->id} is complete");
        $this->orderRepository->save($order);
        $this->mailer->sendOrderCompleteEmail($order);
    }
}

Dependency Injection Containers

  • Every major framework has one
    • Symfony (DependencyInjection component)
    • Zend (ServiceManager)
    • Laravel (Illuminate\Container)
  • Standalone ones for use elsewhere
    • Pimple (used in Slim)
    • League\Container
    • Aura.Di
    • Disco

Dependency Injection Containers

  • Every major framework has one
    • Symfony (DependencyInjection component)
    • Zend (ServiceManager)
    • Laravel (Illuminate\Container)
  • Standalone ones for use elsewhere
    • Pimple (used in Slim)
    • League\Container
    • Aura.Di
    • Disco

What's a Dependency Injection Container?

A dependency injection container is an object used to manage the instantiation of other objects.

 

If you have one, it will be the place where the "new" keyword gets used more than anywhere else in your app, either via configuration code you write or under the hood.

What's a Dependency Injection Container NOT?

  • Not the only place you can (or should) use the "new" keyword in your application.
    • Factories
    • Value objects
  • Not required for dependency injection.
  • Not supposed to be used as a Service Locator

Twittee: A container in a 140 tweet

class Container {
 protected $s=array();
 function __set($k, $c) { $this->s[$k]=$c; }
 function __get($k) { return $this->s[$k]($this); }
}
    

Using Twittee

class NeedsALogger {
    private $logger;
    public function __construct(Logger $logger) {
        $this->logger = $logger;
    }
}
class Logger {}

$c = new Container;
$c->logger = function() { return new Logger; };
$c->myService = function($c) {
    return new NeedsALogger($c->logger);
};

var_dump($c->myService);

PSR-11 (fka Container-Interop)

namespace Psr\Container;

interface ContainerInterface {
    public function get($id);
    public function has($id);
}

Twittee++: A PSR-11 container in a 280 tweet

class Container implements Psr\Container\ContainerInterface {
 protected $s = [];
 function __set($k, $c) { $this->s[$k]=$c; }
 function __get($k) { return $this->s[$k]($this); }
 function get($k) { return $this->s[$k]($this); }
 function has($k) { return isset($s[$k]); }
}

Dependency Injected Fizzbuzz

class Fizz { public function __toString() { return 'Fizz'; } }
class Buzz { public function __toString() { return 'Buzz'; } }
class FizzBuzz {
    protected $fizz; protected $buzz;
    public function __construct(Fizz $fizz, Buzz $buzz) {
        $this->fizz = $fizz; $this->buzz = $buzz;
    }
    public function __invoke(int $i) {
        if (!($i % 15)) return $this->fizz . $this->buzz;
        if (!($i % 3)) return (string) $this->fizz;
        if (!($i % 5)) return (string) $this->buzz;
        return (string) $i;
    }
}

Dependency Injected Fizzbuzz

$c = new Container;
$c->fizz = function() { return new Fizz; }
$c->buzz = function() { return new Buzz; }
$c->fizzBuzz = function($c) {
    return new FizzBuzz($c->fizz, $c->buzz);
}


foreach (range(1, 15) as $i) {
    echo $c->get('fizzBuzz')($i) . "\n";
}
class OrderProcessor {
    function __construct(Container $c) {
        $this->orderRepository = $c->get('OrderRepository');
        $this->logger = $c->get('Logger');
        $this->now = $c->get('currentDate');
        $this->mailer = $c->get('mailer');
    }
    function completeOrder($order) {
        $order->completedAt = $this->now;
        $this->logger->log("Order {$order->id} is complete");
        $this->orderRepository->save($order);
        $this->mailer->sendOrderCompleteEmail($order);
    }
}

Service Location is an Antipattern

class OrderProcessor {
    protected $c;
    function __construct(Container $c) {
        $this->orderRepository = $c->get('OrderRepository');
        $this->logger = $c->get('Logger');
        $this->c = $c;
    }
    function completeOrder($order) {
        $order->completedAt = $this->c->get('currentDate');
        $this->logger->log("Order {$order->id} is complete");
        $this->orderRepository->save($order);
        $this->c->get('mail')->sendOrderCompleteEmail($order);
    }
}

Service Location is an Antipattern

$c = new Pimple\Container;
$c[NeedsALogger::class] = function($c) {
    return new NeedsALogger($c['logger']);
};
$c['logger'] = function() {
    return new Logger;
};

var_dump($c[NeedsALogger::class]); // NeedsALogger

Pimple - Now PSR-11 Compliant

Notes on Pimple

  • Use $c->factory(callable) if you don't want the same instance every time you ask for a dependency.
  • Use $c->protect(callable) if you want to add a closure to your container.
  • You can set parameters on the container directly
  • Not much magic
  • Default container of Slim 3

Speaking of Slim...

$app = new Slim\App();
$app->get('/', function(Request $req, Response $res) {
    $userRepository = new UserRepository(new PDO(/* */));
    $users = $userRepository->listAll();
    return $res->withJson($users);
});

Speaking of Slim...

$app = new Slim\App();
$c = $app->getContainer();
$c['db'] = function($c) { return new PDO(/* */); };

$app->get('/', function(Request $req, Response $res) use ($c) {
    $userRepository = new UserRepository($c['db']);
    $users = $userRepository->listAll();
    return $res->withJson($users);
});

Speaking of Slim...

// snip
$c[UserRepository::class] = function($c) {
    return new UserRepository($c['db']);
};

$app->get('/', function(Request $req, Response $res) {
    $userRepository = $this->get(UserRepository::class);   
    $users = $userRepository->listAll();
    return $res->withJson($users);
});

Speaking of Slim...

class GetUsersAction {
    protected $userRepository;

    public function __construct(UserRepository $repo) {
        $this->userRepository = $repo;
    }

    public function __invoke(Request $req, Response $res) {
            $users = $this->userRepository->listAll();
            return $res->withJson($users);
        }
    };
};

Speaking of Slim...

$c['getUsers'] = function($c) {
    return new GetUsersAction($c[UserRepository::class]);
};

$app->get('/', 'getUsers');
$c = new League\Container\Container;
$c->share('LoggerInterface', 'Logger')
$c->add('myClass', 'NeedsALogger')
  ->withArgument('LoggerInterface');

 

var_dump($c->get('myClass')); // NeedsALogger
  • Use $c->share(callable) if you do want the same instance every time you ask for a dependency.
  • Use $c->add(callable) if you want to use a closure to build your dependency, a la Pimple.
  • Use League\Container\Argument\RawArgument to pass configuration strings etc.

  • Check out jenssegers/lean to use with Slim
// NeedsALogger __construct(LoggerInterface $log)

$c = new League\Container\Container;
$c->share('LoggerInterface', 'Logger')
$c->delegate(
    new League\Container\ReflectionContainer
);
$c->get('NeedsALogger'); // NeedsALogger w\Logger
$c = new League\Container\Container;
$c->share('LoggerInterface', 'Logger')
$c->add('myClass', 'MightWantALogger')
  ->withMethodCall('setLogger',
                   ['LoggerInterface']);
// myClass, myOtherClass implement
// WantsALogger, which has setLogger()

$c = new League\Container\Container;
$c->share('LoggerInterface', 'Logger')
$c->add('myClass');
$c->add('myOtherClass');

$c->inflector('WantsALogger')
  ->invokeMethod('setLogger',
                 ['LoggerInterface']);

Thanks! Questions?

Dependency Injection for Mere Humans - AustinPHP January 2018

By Ian Littman

Dependency Injection for Mere Humans - AustinPHP January 2018

  • 487
Loading comments...

More from Ian Littman