Dependency Injection in a legacy monolith & Object state hell

Michel Petiton & Marcel Kempf
Marcel Kempf
github.com/R4c00n
twitter.com/Coding_R4c00n

  • Clean code 👩‍💻
  • Nice food 🍜
  • Tea 🍵
  • Trash (pandas) 🦝
Michel Petiton
github.com/demichl68 (tumbleweed)

  • BBQ
  • Italian bikes
  • Clean Architecture

Dependency inversion principle

  • High-level modules should not depend on low-level modules. Both should depend on abstraction.
  • Abstractions should not depend on details. Details should depend on abstractions.

High-level modules should not depend on low-level modules. Both should depend on abstraction.

High-level depending on low-level
High-level depending on abstraction

High-level modules should not depend on low-level modules. Both should depend on abstraction.

Abstractions should not depend on details. Details should depend on abstractions.

Abstraction depending on details
							
								class GitHubService {
								  public function getRepository($owner, $repository): \Psr\Http\Message\ResponseInterface
								  {
								      return (new \GuzzleHttp\Client())
								        ->get("/repos/${owner}/${repository}");
								  }
								}
							
						
We can do better!
							
								use GuzzleHttp\ClientInterface;

								class GitHubService {
								  /** @var ClientInterface */
								  private $guzzle;

								  public function __construct(ClientInterface $guzzle)
								  {
								    $this->guzzle = $guzzle;
								  }

								  public function getRepository($owner, $repository): \Psr\Http\Message\ResponseInterface
								  {
								      return $this->guzzle->get("/repos/${owner}/${repository}");
								  }
								}
							
						
The monolith

... Well, let's at least improve it.

A few stats

  • 6572 PHP files
  • 969624 Lines of PHP
  • Lots of static methods
  • Magic everywhere 🧙
  • Stateful everything!
  • Tight coupling
  • No modules

0 (Real) Unit tests (Before DI) 😔

So, where do we start?

  • DI on demand
  • DI starting in controllers
  • DI starting at every entry point
Explicit Content
							
								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
  • src/
    • public
      • index.php
      • api.php
  • bin
    • console
                            
                                // 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

Details details details

  • DI Framework: https://github.com/zendframework/zend-servicemanager
  • Auto wiring: https://github.com/reinfi/zf-dependency-injection
							
								(new autowire_migration_tool())->execute(
								    __DIR__ . '/../vendor/autoload.php',
								    __DIR__ . '/../config/di_legacy.php'
								);
							
						
							
								return [
									'factories' => [
										GitHubService::class => AutoWiringFactory::class,
										OrderRepository::class => OrderRepositoryFactory::class,
									],
									'aliases' => [
										OrderRepositoryInterface::class => OrderRepository::class,
									],
									'abstract_factories' => [
										FallbackAutoWiringFactory::class,
									],
								];
							
						

The monolith today

  • 5524 Unit tests
  • Middleware routing (in some parts)
  • (Zend) modules
  • Explicit configuration instead of weird convention magic

Everything went better than expected! Or not?

"A customer is complainging, he's getting a lot of emails lately!"
Picture of the customer

Conclusion: We have a bug somewhere!

Thanks, captain obvious!

What exactly happened?

  • A customer received 2000 Emails instead of 2000 customers receiving 1 Email each. Twice!
  • 							
    								/**
    								  * send mail
    								  */
    								public function send(): bool {
    									[...]
    									if ($this->is_created === false) {
    										if (trim($this->get_mail()->get_body()) !== '') {
    											$this->is_created = true;
    										} else {
    											if ($this->build_email() === false) {
    												return false;
    											}
    										}
    										[...]
    									}
    									[...]
    
    									return $this->get_email()->send();
    								}
    							
    						
    MIND BLOWN

    Mind = blown!

    How did we tackle the problem?

    Awareness!


    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!
    I dont like to share!

    							
    								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);
    							
    						

    Lessons learned!

  • Better understanding of what state actually means.
  • ServiceManager internals.
  • Better code reviews.
  • Thank you!

    Thank you