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. DEPENDENCY INJECTION
    understand that once and for all!

    View full-size slide

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

    View full-size slide

  3. (stutterer is "GAGO" in Portuguese)

    View full-size slide

  4. https://github.com/corcel/corcel

    View full-size slide

  5. https://www.glofox.com/careers

    View full-size slide

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

    View full-size slide

  7. DEPENDENCY INJECTION?

    View full-size slide

  8. 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/

    View full-size slide

  9. 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

    View full-size slide

  10. 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

    View full-size slide

  11. 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

    View full-size slide

  12. 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.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  15. HOW TO INJECT DEPENDENCIES?
    Constructor Injection
    Setter Injection
    Method Injection

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. 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

    View full-size slide

  23. Now we also need to upload avatars to Dropbox.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  30. 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

    View full-size slide

  31. (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

    View full-size slide

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

    View full-size slide

  33. 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.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  39. $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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  44. 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

    View full-size slide

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

    View full-size slide

  46. BONUS
    Testing using DI

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  51. THANK YOU!
    RATE THIS TALK ON JOIND.IN

    https://bit.ly/2IrLJbN
    http://twitter.com/junior_grossi

    View full-size slide