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.

7bb3a66a199daec275d5ad339724c754?s=128

Junior Grossi

June 08, 2019
Tweet

More Decks by Junior Grossi

Other Decks in Programming

Transcript

  1. DEPENDENCY INJECTION understand that once and for all!

  2. Hi! I'm Junior Grossi twitter.com/junior_grossi github.com/jgrossi

  3. (stutterer is "GAGO" in Portuguese)

  4. KEEP CALM!

  5. https://github.com/corcel/corcel

  6. None
  7. None
  8. https://www.glofox.com/careers

  9. SCHEDULE Dependency Injection Dependency Inversion (SOLID) Dependency Injection Container

  10. DEPENDENCY INJECTION?

  11. 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/
  12. 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
  13. 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
  14. 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
  15. 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.
  16. 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);
  17. Dependency Injection is basically moving all the dependencies to the

    __construct(). (the __construct should be the only injection point)
  18. HOW TO INJECT DEPENDENCIES? Constructor Injection Setter Injection Method Injection

  19. 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; } }
  20. SETTER INJECTION ⚠ add many permissions / can be risky

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

    class class UserController { public function create(SaveUserRequest $request) { $data = $request->validated(); // Code... } }
  22. DEPENDENCY INVERSION? (SOLID) "Dependency Inversion Principle (DIP)"

  23. Robert C. Martin (Uncle Bob) Design Principles and Design Patterns

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

    CONCRETIONS (IMPLEMENTATIONS) How your class does. It's the logic behind the action.
  25. 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
  26. Now we also need to upload avatars to Dropbox.

  27. if ($uploadPlace === 's3') { $s3Client->store($avatar); } elseif ($uploadPlace ===

    'dropbox') { $randomName = uniqid() . time(); $dropboxClient->send($randomName, $avatar); }
  28. 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; } }
  29. AwsS3Client is a concrete class DropboxClient it's another concrete class

    Solution? CloudStorageInterface (abstraction) Abstraction = Interface
  30. interface CloudStorageInterface { public function store(string $content): string; }

  31. 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); } }
  32. 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); } }
  33. 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
  34. (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
  35. DEPENDENCY INJECTION CONTAINER "Recipe Book" Source: https://bit.ly/2ExbFDS

  36. 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.
  37. 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);
  38. 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); } }
  39. PSR-11: CONTAINER INTERFACE - PHP-FIG Psr\Container\ContainerInterface Methods: get() and has()

    Psr\Container\ContainerExceptionInterface Psr\Container\NotFoundExceptionInterface
  40. league/container A SIMPLE BUT POWERFUL PSR-11 DEPENDENCY INJECTION CONTAINER http://container.thephpleague.com

  41. namespace Acme; class Foo { public Bar $bar; public function

    __construct(Bar $bar) { $this->bar = $bar; } } class Bar {}
  42. $container = new League\Container\Container; $container->add(Acme\Foo::class)->addArgument(Acme\Bar::class); $container->add(Acme\Bar::class); $foo = $container->get(Acme\Foo::class); var_dump($foo

    instanceof Acme\Foo); // true var_dump($foo->bar instanceof Acme\Bar); // true
  43. Service Provider is not only for Laravel $container = new

    League\Container\Container; $container->addServiceProvider( Acme\ServiceProvider\SomeServiceProvider::class ); $foo = $container->get(Acme\Foo::class);
  44. 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); } }
  45. 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);
  46. 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); } }
  47. OTHER PACKAGES illuminate/container pimple/pimple php-di/php-di https://github.com/illuminate/container https://github.com/silexphp/Pimple https://github.com/PHP-DI/PHP-DI

  48. 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?)
  49. BONUS Testing using DI

  50. 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); }
  51. 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']); }
  52. 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, ], ];
  53. USE DI AND DI CONTAINER It's a must have. It's

    the first step to SOLID!
  54. THANK YOU! RATE THIS TALK ON JOIND.IN ⭐ https://bit.ly/2IrLJbN http://twitter.com/junior_grossi