Ian Littman / @ian@phpc.social / @iansltx
follow along at https://ian.im/ditek23
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);
}
}
class OrderProcessor {
function __construct() {
$this->orderRepository = new MysqlOrderRepository();
}
function completeOrder($order) {
$order->completedAt = new \DateTimeImmutable;
\Logger::info("Order {$order->id} is complete");
$this->orderRepository->save($order);
Mailer::sendOrderCompleteEmail($order);
}
}
class OrderProcessor {
function __construct() {
$this->orderRepository = new MysqlOrderRepository();
}
function completeOrder($order) {
$order->completedAt = new \DateTimeImmutable;
log_info("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: What'll it take to swap the mailer from SES to Mailgun?
A: ...have fun with that.
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/call 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!"
Instead, if your options are optional:
class ClassThatUsesLoggingButDoesNotRequireIt { private ?LoggerInterface $logger = null; public function setLogger(Logger $logger) { $this->logger = $logger; } }
class ClassThatMightWantAThingCalledALogger { public $logger = null; }
class SaferClassThatWantsALogger { 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()
) {}
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 ): AThing|ALaravel|Controller|CanReturn { /** do something, return a response **/ }
public function handle( \App\Services\OrderService $orderService ): void { /** do some things in a CLI context **/ }
* 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
Interfaces support multiple inheritance
PHP 8.1 lets you ask for intersection types
PHP 8.2 lets you ask for DNF types
class PhoneBurner implements SmsProvider {}
class InfobipSMS implements SmsProviderInterface {}
class PhoneBurner implements SmsProvider {}
class InfobipSMS implements SmsProviderInterface {}
Doesn't matter either way. Pick a convention and stick with it.
class TESTMORE implements HasAcceleratorPedal { public function pressAccelerator(): void; } // namespace Grumpy\Vehicles class MuskMobile implements HasAcceleratorPedal { public function pressAccelerator(): void; } // namespace Grumpy\Vehicles
class TESTMORE implements HasAcceleratorPedal { public function pressAccelerator(): Vroom; } // namespace Grumpy\Vehicles class MuskMobile implements HasAcceleratorPedal { public function pressAccelerator(): Zoom; } // namespace Grumpy\Vehicles
class MysqlUserRepo implements UserRepository { public function getById(int $id): ?User {} } class UsersInPostgres 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); }
namespace Psr\Container; interface ContainerInterface { public function get(string $id); // mixed public function has(string $id): bool; }
class Container implements Psr\Container\ContainerInterface{
private $s=[];
function __set($k,$c){$this->s[$k]=$c;}
function __get($k){return $this->s($k)($this);}
function get(string $k){return $this->s[$k]($this);}
function has(string $k):bool{return isset($this->s[$k]);}
}
class Container implements Psr\Container\ContainerInterface{
private $s=[];
function __set($k,$c){$this->s[$k]=$c;}
function __get($k){return $this->get($k);}
function get(string $id){return $this->s[$id]($this);}
function has(string $id):bool{return isset($this->s[$id]);}
}
class Container extends ArrayObject implements Psr\Container\ContainerInterface {
function __set($k,$c){$this[$k]=$c;}
function __get($k){return $this->get($k);}
function get(string $id){return $this[$id]($this);}
function has(string $id): bool {return isset($this[$id]);}
}
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 = fn () => new Fizz;
$c->buzz = fn () => new Buzz;
$c->fizzBuzz = fn ($c)
=> 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] = fn ($c) => new NeedsALogger($c['logger']); $c['logger'] = fn () => new Logger; var_dump($c[NeedsALogger::class]); // NeedsALogger
$app = Slim\Factory\AppFactory::create();
$app->get('/', function(Request $req, Response $res) {
$userRepository = new UserRepository(new PDO(/* */));
$users = $userRepository->listAll();
return $res->withJson($users);
});
$c = new \Pimple\Container();
$c['db'] = function($c) { return new PDO(/* */); };
Slim\Factory\AppFactory::setContainer($c);
$app = Slim\Factory\AppFactory::create();
$app->get('/', function(Request $req, Response $res) {
$userRepository = new UserRepository($this->get('db'));
$users = $userRepository->listAll();
return $res->withJson($users);
});
// snip
$c[UserRepository::class] = function($c) {
return new UserRepository($c['db']);
};
// snip
$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"; }