Slide 1

Slide 1 text

DEPENDENCY INJECTION understand that once and for all!

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

(stutterer is "GAGO" in Portuguese)

Slide 4

Slide 4 text

KEEP CALM!

Slide 5

Slide 5 text

https://github.com/corcel/corcel

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

https://www.glofox.com/careers

Slide 9

Slide 9 text

SCHEDULE Dependency Injection Dependency Inversion (SOLID) Dependency Injection Container

Slide 10

Slide 10 text

DEPENDENCY INJECTION?

Slide 11

Slide 11 text

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/

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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.

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Dependency Injection is basically moving all the dependencies to the __construct(). (the __construct should be the only injection point)

Slide 18

Slide 18 text

HOW TO INJECT DEPENDENCIES? Constructor Injection Setter Injection Method Injection

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

SETTER INJECTION ⚠ add many permissions / can be risky class Authorizer { private Logger $logger; public function setLogger(Logger $logger) { $this->logger = $logger; } }

Slide 21

Slide 21 text

METHOD INJECTION ⚠ add dependency to a single method, not class class UserController { public function create(SaveUserRequest $request) { $data = $request->validated(); // Code... } }

Slide 22

Slide 22 text

DEPENDENCY INVERSION? (SOLID) "Dependency Inversion Principle (DIP)"

Slide 23

Slide 23 text

Robert C. Martin (Uncle Bob) Design Principles and Design Patterns "Classes should depend upon Abstractions. Do not depend upon concretions." https://bit.ly/2W6XAVm/

Slide 24

Slide 24 text

ABSTRACTIONS (INTERFACE) What your class should do. It's a contract. CONCRETIONS (IMPLEMENTATIONS) How your class does. It's the logic behind the action.

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Now we also need to upload avatars to Dropbox.

Slide 27

Slide 27 text

if ($uploadPlace === 's3') { $s3Client->store($avatar); } elseif ($uploadPlace === 'dropbox') { $randomName = uniqid() . time(); $dropboxClient->send($randomName, $avatar); }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

AwsS3Client is a concrete class DropboxClient it's another concrete class Solution? CloudStorageInterface (abstraction) Abstraction = Interface

Slide 30

Slide 30 text

interface CloudStorageInterface { public function store(string $content): string; }

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

(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

Slide 35

Slide 35 text

DEPENDENCY INJECTION CONTAINER "Recipe Book" Source: https://bit.ly/2ExbFDS

Slide 36

Slide 36 text

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.

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

PSR-11: CONTAINER INTERFACE - PHP-FIG Psr\Container\ContainerInterface Methods: get() and has() Psr\Container\ContainerExceptionInterface Psr\Container\NotFoundExceptionInterface

Slide 40

Slide 40 text

league/container A SIMPLE BUT POWERFUL PSR-11 DEPENDENCY INJECTION CONTAINER http://container.thephpleague.com

Slide 41

Slide 41 text

namespace Acme; class Foo { public Bar $bar; public function __construct(Bar $bar) { $this->bar = $bar; } } class Bar {}

Slide 42

Slide 42 text

$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

Slide 43

Slide 43 text

Service Provider is not only for Laravel $container = new League\Container\Container; $container->addServiceProvider( Acme\ServiceProvider\SomeServiceProvider::class ); $foo = $container->get(Acme\Foo::class);

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

BONUS Testing using DI

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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']); }

Slide 52

Slide 52 text

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, ], ];

Slide 53

Slide 53 text

USE DI AND DI CONTAINER It's a must have. It's the first step to SOLID!

Slide 54

Slide 54 text

THANK YOU! RATE THIS TALK ON JOIND.IN ⭐ https://bit.ly/2IrLJbN http://twitter.com/junior_grossi