switch ($texan) { case "y'all": return "some of you"; case "all y'all": return "all of you"; default: return $texan; }
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();
$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);
}
}
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.
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; public function __construct(Logger $logger) { $this->logger = $logger; } }
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 */}
"I know, I'll just throw everything in an $options array!"
class ClassThatUsesLoggingButDoesNotRequireIt { private $logger = null; public function setLogger(Logger $logger) { $this->logger = $logger; } }
class ClassThatMightWantAThingCalledALogger { public $logger = null; }
* Don't do this until we get typed properties in PHP 7.4,
and probably not even then
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 **/ } }
Setter injection is useful when a bunch of classes have the same (preferably optional) dependency.
Add the setter to a trait, then import the trait into the class, then tag the class with a related interface for easy bulk injection.
e.g. Psr\Log\{LoggerAwareInterface, LoggerAwareTrait}
Sorry, you can't have an interface implement a trait directly.
public function __invoke( \Slim\Http\Request $request, \Slim\Http\Response $response, array $args = []) { /** do something, return a Response **/ }
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.
class Twilio implements SmsProviderInterface {}
class Two10CansAndStr implements SmsProvider {}
class Focus implements HasGasPedal { public function pressAccelerator(); } // namespace Ford\Vehicles class Leaf implements HasGasPedal { public function pressAccelerator(); } // namespace Nissan\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();
$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);
}
}
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);
}
}
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 Container { protected $s=array(); function __set($k, $c) { $this->s[$k]=$c; } function __get($k) { return $this->s[$k]($this); } }
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
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 {
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;
}
}
$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);
}
}
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);
}
}
$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 {
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);
}
};
};
$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
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']);