Dependency Injection
for Mere Humans
Alternate title
opinions and implementations on
Decoupled software design
Shout out to @jcarouth and his presentation back in 2015
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 build one
- How to configure/use one
- ...and a few antipatterns along the way
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 external objects, resources, or functions an object uses to accomplish its responsibility.
- Per-class level <- we'll be here
- Library level (handled by Composer)
- OS level (handled by e.g. apt, apk, yum)
Some common Dependencies
- Databases/Entity Managers (e.g. PDO, Doctrine)
- Caching (e.g. Redis/Memcached adapters)
- File storage (e.g. local FS, S3, Flysystem)
- Notifications (email, SMS, push)
- Over-the-network APIs (or SDKs that wrap them)
- Logging
Some common Dependencies
- The current web request
- Environment variables
- The current user
- Session/API scope information
- 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();
}
function completeOrder($order) {
$logger = \App::make('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) {
$logger = Logger::getInstance();
$order->completedAt = new \DateTimeImmutable;
$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 completeOrder() without sending emails?
A: ¯\_(ツ)_/¯ *
* Yes, there are workarounds, but this isn't a testing talk.
WHat's dependency injection, anyway?
Dependency injection is 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
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
- Could be the wrong point in the object life cycle
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 */}
Antipattern #1: __construct($options)
"I know, I'll just throw everything in an $options array!"
Setter injection
class ClassThatUsesLoggingButDoesNotRequireIt { private $logger = null; public function setLogger(Logger $logger) { $this->logger = $logger; } }
Yolo Property injection
class ClassThatMightWantAThingCalledALogger { public $logger = null; }
class SlightlySaferClassThatWantsALogger { public Logger $logger = null; } // PHP 7.4+
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 **/ } }
Don't Implement This Null Logger...
...because there's one built into PSR/log
Traits + Setter Injection
- Add setter to trait
- Import trait into classes
- Implement interface on classes
- Configure DI container to setter-inject based on interface
- Profit!
e.g. Psr\Log\{LoggerAwareInterface, LoggerAwareTrait}
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 **/ }
Parameter Injection
public function orderAction( \Illuminate\Http\Request $request, \App\Services\OrderService $orderService) { /** do something, return a Response **/ }
Pros
- What you need when you need it
Cons
- Moves the problem of dependency management to the caller*
* Some frameworks take care of this for you for controllers/commands
Mix & Match as needed
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.
Should I use "interface" in my interface names?
class Twilio implements SmsProvider {}
class NexmoSMS implements SmsProviderInterface {}
Should I use "interface" in my interface names?
class Twilio implements SmsProvider {}
class NexmoSMS implements SmsProviderInterface {}
Doesn't matter either way. Pick a convention and stick with it.
Abstractions should not be leaky
class Camry implements HasGasPedal { public function pressAccelerator(); } // namespace Toyota\Vehicles class Model3 implements HasGasPedal { public function pressAccelerator(); } // namespace Tesla\Vehicles
Abstractions should not be leaky
class MysqlUserRepo implements UserRepository { public function getById(int $id): ?User {} } class ElasticUserRepo implements UserRepository {
public function getById(int $id): ?User {}
}
class gRPCUserAPI implements UserRepository {
public function getById(int $id): ?User {}
} interface User { /** various signatures **/ }
Let's do some refactoring
class OrderProcessor {
function __construct() {
$this->orderRepository = new MysqlOrderRepository();
}
function completeOrder($order) {
$logger = Logger:getInstance();
$order->completedAt = new \DateTimeImmutable;
$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,
\DateTimeInterface $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);
}
}
class OrderProcessor {
function __construct(OrderRepository $orderRepo,
Logger $logger,
Mailer $mailer) {
$this->orderRepository = $orderRepo;
$this->logger = $logger;
$this->mailer = $mailer;
}
function completeOrder($order, \DateTimeInterface $now) {
$order->completedAt = $now;
$this->logger->log("Order {$order->id} is complete");
$this->orderRepository->save($order);
$this->mailer->sendOrderCompleteEmail($order);
}
}
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 to be used as a Service Locator
PSR-11 (fka Container-Interop)
namespace Psr\Container; interface ContainerInterface { public function get($id); public function has($id); }
class OrderProcessor {
function __construct(ContainerInterface $c) {
$this->orderRepository = $c->get('OrderRepository');
$this->logger = $c->get('Logger');
$this->mailer = $c->get('Mailer');
}
function completeOrder($order, \DateTimeInterface $now) {
$order->completedAt = $now;
$this->logger->log("Order {$order->id} is complete");
$this->orderRepository->save($order);
$this->mailer->sendOrderCompleteEmail($order);
}
}
Antipattern #2: Service Location
class OrderProcessor {
protected $c;
function __construct(ContainerInterface $c) {
$this->orderRepository = $c->get('OrderRepository');
$this->c = $c;
}
function completeOrder($order, \DateTimeInterface $now) {
$order->completedAt = $now;
$this->c->get('log')->log("Order {$order->id} is complete");
$this->orderRepository->save($order);
$this->c->get('mail')->sendOrderCompleteEmail($order);
}
}
ANTIPATTERN #2: Service Location
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); // includes the Logger
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";
}
...but don't use Twittee++
- 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
...but don't use Twittee++
- 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
$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
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.
- Use $c->raw(dep) if you want to get a closure you set without using protect()
- Not much magic
- Default container of Slim 3
Let's refactor a Slim route
$app = new Slim\App();
$app->get('/', function(Request $req, Response $res) {
$userRepository = new UserRepository(new PDO(/* */));
$users = $userRepository->listAll();
return $res->withJson($users);
});
Let's refactor a Slim route
$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); });
Let's refactor a Slim route
// 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);
});
Let's refactor a Slim route
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);
}
};
};
Let's refactor a Slim route
$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') ->addArgument('LoggerInterface');
var_dump($c->get('myClass')); // NeedsALogger
- Use $c->share() 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.
-
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') ->addMethodCall('setLogger', ['LoggerInterface']);
// MyClass, MyOtherClass implement // LoggerAware, which has setLogger() $c = new League\Container\Container; $c->share('LoggerInterface', 'Logger') $c->add('MyClass'); $c->add('MyOtherClass'); $c->inflector('LoggerAware') ->invokeMethod('setLogger', ['LoggerInterface']);
Thanks! Questions?
- https://ian.im/dlsun19 - these slides
- https://joind.in/talk/7f2db - rate this talk!
- https://twitter.com/iansltx - me!
- https://github.com/iansltx - my code!
- https://ian.im/la0319 - upcoming Illuminate Container talk
Dependency Injection for Mere Humans - SunshinePHP 2019
By Ian Littman
Dependency Injection for Mere Humans - SunshinePHP 2019
What’s the difference between service location and dependency injection? Why is this dependency injection thing such a big deal anyway, and how do you use that tool correctly? I’ll answer these questions and more, including real-world examples of refactoring an application toward the more explicit, testable, closer-to-SOLID applications.
- 2,008