Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dependency Injection in PHP: understand that once and for all 🇺🇸

Dependency Injection in PHP: understand that once and for all 🇺🇸

Dependency Injection in PHP is very known, mainly by the newest frameworks. However, the container's essence, how it works, and how it can help your project, mainly for testing, are still very "magic" for us, developers. This talk has the goal to explain in a very practice way what it is and how works a DI.

Junior Grossi

June 08, 2019
Tweet

More Decks by Junior Grossi

Other Decks in Programming

Transcript

  1. Thorben Janssen "Dependency injection is a programming technique that makes

    a class independent of its dependencies. It achieves that by decoupling the usage of an object from its creation. This helps you to follow SOLID’s dependency inversion and single responsibility principles." https://stackify.com/dependency-injection/
  2. class AvatarRequestUploader { private AwsS3Client $s3Client; // PHP 7.4 public

    function __construct() { $this->s3Client = new AwsS3Client( // credentials + configs ); } public function upload(Request $request): string { $avatar = $this->findAvatarInRequest($request); $avatarUrl = $this->s3Client->store($avatar); return $avatarUrl; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
  3. public function __construct() { $this->s3Client = new AwsS3Client( // credentials

    + configs ); } class AvatarRequestUploader 1 { 2 private AwsS3Client $s3Client; // PHP 7.4 3 4 5 6 7 8 9 10 11 public function upload(Request $request): string 12 { 13 $avatar = $this->findAvatarInRequest($request); 14 $avatarUrl = $this->s3Client->store($avatar); 15 16 return $avatarUrl; 17 } 18 } 19
  4. public function __construct(AwsS3Client $s3Client) { $this->s3Client = $s3Client; } class

    AvatarRequestUploader 1 { 2 private AwsS3Client $s3Client; 3 4 5 6 7 8 9 public function upload(Request $request): string 10 { 11 $avatar = $this->findAvatarInRequest($request); 12 $avatarUrl = $this->s3Client->store($avatar); 13 14 return $avatarUrl; 15 } 16 } 17
  5. CURRENT STATUS Now, to instantiate AvatarRequestUploader we need to inject

    an instance of AwsS3Client as well. The AvatarRequestUploader class now depends on the AwsS3Client one.
  6. DEPENDENCY INJECTION A NEW UPLOADER CLASS? (I can share the

    same instance with other classes) $s3Client = new AwsS3Client([ /* parameters */]); $avatarRequestUploader = new AvatarRequestUploader($s3Client); $avatarRequestUploader->upload($request); $avatarMiddlewareUploader = new AvatarMiddlewareUploader($s3Client); $avatarMiddlewareUploader->upload($request);
  7. Dependency Injection is basically moving all the dependencies to the

    __construct(). (the __construct should be the only injection point)
  8. CONSTRUCTOR INJECTION ✔ the best place for that class A

    { private Foo $foo; private Bar $bar; public function __construct(Foo $foo, Bar $bar) { $this->foo = $foo; $this->bar = $bar; } }
  9. SETTER INJECTION ⚠ add many permissions / can be risky

    class Authorizer { private Logger $logger; public function setLogger(Logger $logger) { $this->logger = $logger; } }
  10. METHOD INJECTION ⚠ add dependency to a single method, not

    class class UserController { public function create(SaveUserRequest $request) { $data = $request->validated(); // Code... } }
  11. Robert C. Martin (Uncle Bob) Design Principles and Design Patterns

    "Classes should depend upon Abstractions. Do not depend upon concretions." https://bit.ly/2W6XAVm/
  12. ABSTRACTIONS (INTERFACE) What your class should do. It's a contract.

    CONCRETIONS (IMPLEMENTATIONS) How your class does. It's the logic behind the action.
  13. public function __construct(AwsS3Client $s3Client) { $this->s3Client = $s3Client; } class

    AvatarRequestUploader 1 { 2 private AwsS3Client $s3Client; 3 4 5 6 7 8 9 public function upload(Request $request): string 10 { 11 $avatar = $this->findAvatarInRequest($request); 12 $avatarUrl = $this->s3Client->store($avatar); 13 14 return $avatarUrl; 15 } 16 } 17
  14. if ($uploadPlace === 's3') { $s3Client->store($avatar); } elseif ($uploadPlace ===

    'dropbox') { $randomName = uniqid() . time(); $dropboxClient->send($randomName, $avatar); }
  15. class AvatarRequestUploader { private AwsS3Client $s3Client; public function __construct(AwsS3Client $s3Client)

    { $this->s3Client = $s3Client; } public function upload(Request $request): string { $avatar = $this->findAvatarInRequest($request); $avatarUrl = $this->s3Client->store($avatar); return $avatarUrl; } }
  16. AwsS3Client is a concrete class DropboxClient it's another concrete class

    Solution? CloudStorageInterface (abstraction) Abstraction = Interface
  17. class AwsS3Storage implements CloudStorageInterface { private AwsS3Client $client; public function

    __contruct(AwsS3Client $client) { $this->client = $client; } public function store(string $content): string { return $this->client->store($content); } }
  18. class DropboxStorage implements CloudStorageInterface { private DropboxClient $client; public function

    __contruct(DropboxClient $client) { $this->client = $client; } public function store(string $content): string { $name = $this->generateRandomName(); $result = $this->client->send($name, $content); if (!$result) { throw new CloudStorageUploadException(); } return $this->getUrlFor($name); } }
  19. public function __construct(CloudStorageInterface $cloudStorage) { $this->cloudStorage = $cloudStorage; } $avatarUrl

    = $this->cloudStorage->store($avatar); class AvatarRequestUploader 1 { 2 private CloudStorageInterface $cloudStorage; 3 4 5 6 7 8 9 public function upload(Request $request): string 10 { 11 $avatar = $this->findAvatarInRequest($request); 12 13 14 return $avatarUrl; 15 } 16 } 17
  20. (new AvatarRequestUploader($s3Storage)) ->upload($request); (new AvatarRequestUploader($dropboxStorage)) ->upload($request); $s3Client = new AwsS3Client([

    /* parameters */]); 1 $s3Storage = new AwsS3Storage($s3Client); 2 3 4 5 6 $dropboxClient = new DropboxClient([ /* parameters */]); 7 $dropboxStorage = new DropboxStorage($dropboxClient); 8 9 10 11
  21. PROCESS You "teach" the container how to create your objects,

    telling about all the dependencies you need. You ask for an instance to the Container. The Container knows how to resolve it, then returns the desired instance.
  22. Old way: Now just ask to the Container: $s3Client =

    new AwsS3Client([ /* parameters */]); $s3Storage = new AwsS3Storage($s3Client); (new AvatarRequestUploader($s3Storage)) ->upload($request); $container = Container::instance(); $avatarUploader = $container->get(AvatarRequestUploader::class); $avatarUploader->upload($request);
  23. Or use it as another class dependency: class ChangeAvatarAction {

    private AvatarRequestUploader $avatarUploader; public function __construct(AvatarRequestUploader $avatarUploader) { $this->avatarUploader = $avatarUploader; } public function __invoke(RequestInterface $request): ResponseInterface { $avatarUrl = $this->avatarUploader->upload($request); return new JsonResponse([ 'avatar' => $avatarUrl, ], 201); } }
  24. PSR-11: CONTAINER INTERFACE - PHP-FIG Psr\Container\ContainerInterface Methods: get() and has()

    Psr\Container\ContainerExceptionInterface Psr\Container\NotFoundExceptionInterface
  25. namespace Acme; class Foo { public Bar $bar; public function

    __construct(Bar $bar) { $this->bar = $bar; } } class Bar {}
  26. Service Provider is not only for Laravel $container = new

    League\Container\Container; $container->addServiceProvider( Acme\ServiceProvider\SomeServiceProvider::class ); $foo = $container->get(Acme\Foo::class);
  27. class SomeServiceProvider extends AbstractServiceProvider { protected array $provides = [

    Acme\Foo::class, Acme\Bar::class, ]; public function register(): void { $container = $this->getContainer(); $container->add(Acme\Foo::class)->addArgument(Acme\Bar::class); $container->add(Acme\Bar::class); } }
  28. Auto wiring is also not only for Laravel It works

    only with object dependencies! $container = new League\Container\Container; $container->delegate( new League\Container\ReflectionContainer ); $foo = $container->get(Acme\Foo::class);
  29. Alert: avoid using the Container as dependency ⚠ It's too

    much freedom for your class! This can hurt SRP! class Foo { private Bar $foo; private Baz $baz; public function __construct(ContainerInterface $container) { $this->bar = $container->get(Bar::class); $this->baz = $container->get(Baz::class); } }
  30. FINAL CONSIDERATIONS DI You will add flexibility to the architecture

    It's easier to change between components Centralize all your objects in a single place (why?)
  31. Unit Test (mock Interfaces / final classes) public function test_avatar_can_be_uploaded_through_the_request():

    void { $cloudStorageMock = \Mockery::mock(CloudStorageInterface::class); $cloudStorageMock->shouldReceive('store')->andReturn('http://avatar.com'); $uploader = new AvatarRequestUploader($cloudStorageMock); $avatarUrl = $uploader->upload(new Request(['avatar' => 'foo'])); $this->assertEquals('http://avatar.com', $avatarUrl); }
  32. Integration/Feature Test public function test_user_can_change_avatar(): void { $avatarUploaderMock = \Mockery::mock(AvatarRequestUploader::class);

    $avatarUploaderMock->shouldReceive('upload')->andReturn('http://avatar.com'); $container = Container::instance(); $container->replace(AvatarRequestUploader::class, $avatarUploaderMock); $response = $this->json('PATCH', '/1.0/users/change-avatar', [ 'avatar' => 'foo', // base64 ]); $result = json_decode($response->getContent()->getBody(), $assoc = true); $this->assertEquals('http://avatar.com', $result['avatar']); }
  33. Testing Events / Listeners > Feature Test: POST /1.0/users private

    array $events = [ Events\UserWasCreated::class => [ Listeners\SendWelcomeEmail::class, Listeners\RegisterUserAtIntercom::class, Listeners\UploadFuckingAvatar::class, Listeners\SendWelcomeSlackNotification::class, ], ];