Ian Littman / @ian@phpc.social / @iansltx
follow along at https://ian.im/diuk23
code samples at https://ian.im/dicode
Shout out to @jcarouth and his presentation back in 2015
Dependencies are the external objects, resources, or functions an object uses to accomplish its responsibility.
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);
}
}
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);
}
}
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);
}
}
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);
}
}
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 exactly a testing/mocking talk.
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.
class ClassThatNeedsLogging { private Logger $logger; public function __construct(Logger $logger) { $this->logger = $logger; } }
class ClassThatNeedsLogging { public function __construct( private Logger $logger ) { // constructor property promotion // available since PHP 8.0 } }
public function __construct( private Twig $view, private AuthorizationServer $auth, private LogRepository $errorRepo, private AuthRequestRepository $authRequestRepo, private ClientRepository $clientRepo, private UserRepository $userRepo, private ResourceServer $resAuth, private AccessTokenRepository $accessTokenRepo, private UserValidatorInterface $validator ) {}
"I know, I'll just throw everything in an $options array!"
If your options are optional, make them optional parameters
and maybe use named parameters (PHP 8.0+) to set them.
class ClassThatUsesLoggingButDoesNotRequireIt { private ?LoggerInterface $logger = null; public function setLogger(Logger $logger) { $this->logger = $logger; } }
class ClassThatMightWantAThingCalledALogger { // can't do initialization as default public $logger = null; }
class SlightlySaferClassThatWantsALogger { public ?Logger $logger = null; } // as of 7.4
Can't use readonly here
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'); }
}
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'); }
}
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 **/ } }
public function __construct(
private LoggerInterface $logger
= new NullLogger()
) {} // PHP >= 8.1
e.g. Psr\Log\{LoggerAwareInterface, LoggerAwareTrait}
Sorry, you can't have an interface implement a trait directly.
public function __invoke( ServerRequestInterface $request, ResponseInterface $response, array $args = [] ): ResponseInterface { /** do something, return a ResponseInterface **/ }
public function getCollection( \Illuminate\Http\Request $request, \App\Services\OrderService $orderService ) { /** do something, return a Response **/ }
* Some frameworks take care of this for you for controllers/commands
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.
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.
Pro tip: interfaces support multiple inheritance
Pro tip 2: PHP 8.1 lets you ask for intersection types
class Vonage implements SmsProvider {}
class RedSMS implements SmsProviderInterface {}
class Vonage implements SmsProvider {}
class RedSMS implements SmsProviderInterface {}
Doesn't matter either way. Pick a convention and stick with it.
class Camry implements HasAcceleratorPedal { public function pressAccelerator(): void; } // namespace Toyota\Vehicles class Model3 implements HasAcceleratorPedal { public function pressAccelerator(): void; } // namespace Tesla\Vehicles
class Camry implements HasAcceleratorPedal { public function pressAccelerator(): Vroom; } // namespace Toyota\Vehicles class Model3 implements HasAcceleratorPedal { public function pressAccelerator(): Zoom; } // namespace Tesla\Vehicles
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 {}
} interface User { /** various signatures **/ }
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);
}
}
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);
}
}
class OrderProcessor {
function __construct(
private OrderRepository $orderRepo,
private Logger $logger,
private Mailer $mailer
) {
// constructor property promotion, PHP 8.0+
}
function completeOrder($order, \DateTimeInterface $now) {
$order->completedAt = $now;
$this->logger->log("Order {$order->id} is complete");
$this->orderRepository->save($order);
$this->mailer->sendOrderCompleteEmail($order);
}
}
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.
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);
}
}
class OrderProcessor {
function __construct(protected ContainerInterface $c) {
$this->orderRepository = $c->get('OrderRepository');
}
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);
}
}
class Container { protected $s=array(); function __set($k, $c) { $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } }
class NeedsALogger { public function __construct( private 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
namespace Psr\Container; interface ContainerInterface { public function get($id); public function has($id); }
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]); } }
class Fizz { public function __toString() { return 'Fizz'; } }
class Buzz { public function __toString() { return 'Buzz'; } }
class FizzBuzz {
public function __construct(
private Fizz $fizz,
private Buzz $buzz
) {}
public function __invoke(int $i): string {
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;
}
}
$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";
}
$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
$app = new Slim\App();
$app->get('/', function(Request $req, Response $res) {
$userRepository = new UserRepository(new PDO(/* */));
$users = $userRepository->listAll();
return $res->withJson($users);
});
$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); });
// 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);
});
class GetUsersAction {
public function __construct(
private UserRepository $repo
) {}
public function __invoke(Request $req, Response $res) {
$users = $this->userRepository->listAll();
return $res->withJson($users);
}
};
};
$c['getUsers'] = function($c) { return new GetUsersAction($c[UserRepository::class]); }; $app->get('/', 'getUsers');
use function DI\create;
$c = new \DI\Container();
$c->set('LoggerInterface', create('Logger'));
// returns a NeedsALogger
var_dump($c->get('NeedsALogger'));
use function DI\autowire;
$builder = new \DI\ContainerBuilder();
$builder->enableCompilation('/tmp');
$builder->addDefinitions([
'LoggerInterface' => autowire('Logger'),
'NeedsALogger' => autowire()
]);
$c = $builder->build();
var_dump($c->get('NeedsALogger')); // cached!
require 'FizzBuzz.php';
$c = new \DI\Container();
foreach (range(1, 15) as $i) { echo $c->get('fizzBuzz')($i) . "\n"; }
$c = new League\Container\Container;
$c->share('LoggerInterface', 'Logger');
$c->add('myClass', 'NeedsALogger') ->addArgument('LoggerInterface');
var_dump($c->get('myClass')); // NeedsALogger
// NeedsALogger __construct(LoggerInterface $log) $c = new League\Container\Container; $c->share('LoggerInterface', 'Logger'); $c->delegate( new League\Container\ReflectionContainer );
// NeedsALogger w\Logger var_dump($c->get('NeedsALogger'));
$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']);