Dependency injection is a powerful technique allowing different parts of a system to collaborate with each other. Injection is the passing of a dependency (such as a service or database connection) to an object that would use it. This way, the object need not change because the outside service changed. This often also allows the object to be more easily tested by injecting a mock or stub service as the dependency.
6. 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);
}
}
7. 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);
}
}
8. 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.
9. What is Dependency Injection?
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.
12. 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
13. Many Constructor Parameters
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 */}
17. 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
18. 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
19. Checking Property
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');
}
}
20. Null Property
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 **/
}
}
22. TRAITS + SETTER INJECTION
Add setter to trait
Import trait into classes
Implement interface on classes
Configure DI container to setter-inject based on interface
e.g. PsrLog{LoggerAwareInterface, LoggerAwareTrait}
Sorry, you can't have an interface implement a trait directly.
23. Parameter Injection
public function __invoke(
SlimHttpRequest $request,
SlimHttpResponse $response,
array $args = [])
{
/** do something, return a Response **/
}
24. Parameter Injection
public function orderAction(
IlluminateHttpRequest $request,
AppServicesOrderService $orderService)
{
/** do something, return a Response **/
}
25. Pros
Does not clutter your object with refs to collaborators which are not needed
CONS
Almost everything else about it
Moves the problem of dependency management to the caller
27. 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.
28. Abstrations Should Not Be Leaky
class Camry implements HasGasPedal {
public function pressAccelerator();
} // namespace ToyotaVehicles
class Model3 implements HasGasPedal {
public function pressAccelerator();
} // namespace TeslaVehicles
29. Abstrations 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 **/ }
30. Let’s Do Some Refactoring
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);
}
}
31. 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);
}
}
32. Let’s Do Some Refactoring
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);
}
}
33. Let’s Do Some Refactoring
class OrderProcessor {
function __construct(OrderRepository $orderRepo, Logger $logger,
Mailer $mailer) {
$this->orderRepository = $orderRepo;
$this->logger = $logger;
$this->now = $now;
}
function completeOrder($order, DateTimeImmutable $now) {
$order->completedAt = $now;
$this->logger->log(“Order {$order->id} is complete");
$this->orderRepository->save($order);
$this->mailer->sendOrderCompleteEmail($order);
}
}
34. What is a DI 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.
35. What a DI Container is 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
37. Twittee: A DI 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); }
}
38. Using Twitter
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
39. Twittee++: A PSR-11 Container in a
280 Tweet
class Container implements PsrContainerContainerInterface {
protected $s=array();
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($this->s[$k]); }
}
40. Full DI Containers
Every major framework has one
Symfony (DependencyInjection component)
Zend (ServiceManager)
Laravel (IlluminateContainer)
Standalone ones for use elsewhere
Pimple (used in Slim)
LeagueContainer
Aura.Di
Disco
42. Antipattern #2” Service Location
class OrderProcessor {
function __construct(Container Interface $c) {
$this->orderRepository = $c->get(‘OrderRepository’);
$this->logger = $c->get(‘Logger’);
$this->mailer = $c->get(‘Mailer’);
}
function completeOrder($order, DateTimeImmutable $now) {
$order->completedAt = $now;
$this->logger->log(“Order {$order->id} is complete");
$this->orderRepository->save($order);
$this->mailer->sendOrderCompleteEmail($order);
}
}
43. Antipattern #2” Service Location
class OrderProcessor {
protected $c;
function __construct(Container Interface $c) {
$this->orderRepository = $c->get(‘OrderRepository’);
$this->c = $c;
}
function completeOrder($order, DateTimeImmutable $now) {
$order->completedAt = $now;
$this->c->get(‘Logger’)—>log(“Order {$order->id} is complete");
$this->orderRepository->save($order);
$this->c->get(‘Mailer’)—>sendOrderCompleteEmail($order);
}
}
44. Using a Container
In a typical application you will use the container from
within your “controllers” and use them to inject
dependencies into your “models”.
45. Pimple - PSR-11 Compliant
$c = new PimpleContainer;
$c[NeedsALogger::class] = function($c) {
return new NeedsALogger($c['logger']);
};
$c['logger'] = function() {
return new Logger;
};
var_dump($c[NeedsALogger::class]); // NeedsALogger
46. 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
47. Refactor a Slim Route
$app = new SlimApp();
$app->get('/', function(Request $req, Response $res) {
$userRepository = new UserRepository(new PDO(/* */));
$users = $userRepository->listAll();
return $res->withJson($users);
});
48. Refactor a Slim Route
$app = new SlimApp();
$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);
});
50. Refactor a Slim Route
class GetUsersAction implements SlimControllerInterface {
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);
}
};
};
51. Refactor a Slim Route
$app = new SlimApp();
$c = $app->getContainer();
$c['db'] = function($c) { return new PDO(/* */); };
$c['userRepository'] = function($c) {
return new UserRepository($c['db']);
};
$c['getUsers'] = function($c) {
return new GetUsersAction(UserRepository $c['userRepository']);
};
$app->get('/', 'getUsers');
52. Recap
Dependencies in code are unavoidable, but that
doesn’t mean they need to be unmanageable
Inverting dependencies is a way to create more
flexible software
DI containers are a helpful tool for maintaining single
responsibility within objects
54. Alena Holligan
• Wife, and Mother of 3 young children
• PHP Teacher at Treehouse
• Portland PHP User Group Leader
• Cascadia PHP Conference (cascadiaphp.com)
@alenaholligan alena@holligan.us https://joind.in/talk/b024a