- DI on demand
- DI starting in controllers
- DI starting at every entry point
class GitHubService {
public function getRepository($owner, $repository): \Psr\Http\Message\ResponseInterface
{
return (new \GuzzleHttp\Client())
->get("/repos/${owner}/${repository}");
}
}
use GuzzleHttp\ClientInterface;
class GitHubService {
/** @var ClientInterface */
private $guzzle;
public function __construct()
{
$this->guzzle = Container::getInstance()->get(ClientInterface::class);
}
public function getRepository($owner, $repository): \Psr\Http\Message\ResponseInterface
{
return $this->guzzle->get("/repos/${owner}/${repository}");
}
}
Pros
- No refactoring necessary
- Decoupled (mostly)
Cons
- Still hard coupling to the container class
- Not testable in a nice way
class PlaceOrderController {
public function placeAction()
{
$order = new Order($_POST); // Don't do this!
$orderService = new OrderService();
$orderService->place($order);
// Don't do this either!
echo json_encode(['id' => $order->getId()]);
die;
}
}
class OrderService {
public function place(Order $order)
{
$db = new Db();
$db->query('INSERT INTO order ...');
$order->setId($db->lastInsertId());
}
}
class PlaceOrderController {
public function __construct()
{
$this->orderService = Container::get_instance()->get(OrderServiceInterface::class);
$this->orderFactory = Container::get_instance()->get(OrderFactory::class);
}
public function placeAction()
{
$order = $this->orderFactory->create($_POST); // Don't do this!
$this->orderService->place($order);
// Don't do this either!
echo json_encode(['id' => $order->getId()]);
die;
}
}
class OrderService {
private $db;
public function __construct(DbInterface $db)
{
$this->db = $db;
}
public function place(OrderInterface $order)
{
$this->db->query('INSERT INTO order ...');
$order->setId($db->lastInsertId());
}
}
Pros
- No refactoring necessary
- Decoupled (mostly)
- Only coupled to container inside controller
Cons
- Still hard coupling to the container class inside controller
- Controller is still not testable in a nice way
// index.php
class App {
public function run()
{
//
$moduleClassName = '\\App\\Controller\\' . $module;
$handler = new $moduleClassName();
$handler->run();
}
}
$app = new App();
$app->run();
// index.php
class App {
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function run()
{
//
$moduleClassName = '\\App\\Controller\\' . $module;
$handler = $this->container->get($moduleClassName);
$handler->run();
}
}
$app = Container::getInstance()->get(App::class);
$app->run();
Pros
- Userland codes benefits from DI right from the beginning
- No use of the container itself (besides factories)
Cons
- Refactoring of internal framework code
How did we tackle the problem?
Avoid implicit internal state in code!
namespace DoNotTryAtHome;
class BadExample {
$isInitialized = false;
public function initialize(): void {
[...]
$this->isInitialized = true;
}
public function doSomething(): bool {
if (!$this->isInitialized) {
return true;
}
return false;
}
}
Make state explicit!
class BetterExample {
private $someService;
public function __construct(SomeService $someService) {
$this->someService = $someService;
}
public function doSomething(string $withThisString): SomeResultSet {
$someMethodResult = $this->someService->someMethod($withThisString);
$status = !empty($someMethodResult);
return new SomeResultSet(
$status,
[
'someMethodResult' => $someMethodResult,
]
);
}
}
If state is necessary, make instances
non-shared!
foreach ($foo as $bar) {
$myNonSharedInstance = $container->build(MyNonSharedInstance::class);
$myNonSharedInstance->doSomethingWith($bar);
}
Equivalent to:
[
'factories' => [
MyNonSharedInstance::class => MyNonSharedInstanceFactory::class
],
'shared => [
MyNonSharedInstance::class => false,
],
]
If state is necessary, make instances immutable!
class Employee implements MyImmutableClassInterface {
public function __construct(string $name, int $age) {...}
public function withName(string $newName): MyImmutableClassInterface {
return new static($newName);
}
public function withAge(int $age): MyImmutableClassInterface {
return new static($newName);
}
}
$employeeJohnDoe = new Employee('John doe', 18);
{...}
$employeeJohnDoePensioner = $employeeJohnDoe->withAge(66);
{...}
sql_object_hash($employeeJohnDoe) === spl_object_hash($employeeJohnDoePensioner);