JesusValera

Why and How to decouple your classes

 Found a typo? Edit me

caravaca-fuentes-marques-1

There are some ways to couple your code, some frameworks like Laravel embrace you to do using their own façades or even using ORM as ActiveRecord, but this is also possible using PHP native methods like time() beyond others.

Unfortunately, in the long run, if you don't invert the dependencies, the code will get messy faster/easier, hurting your application. To avoid this, you must write tests. Writing tests help you realize how coupled is your business logic to infrastructure code.

Examples of classes that should be decoupled

Belongs to the Programming language itself like

Belongs to the infrastructure code like

Decoupling a real example

For the next example, let’s try to use a Laravel facade and a PHP function.

Imagine we have the following service, and we really want to invert the dependencies. As you can see, we have the trans() facade and the date() PHP function.

namespace Infrastructure;

final class WelcomeController
{
    /**
     * Output: "hello {$name} @ {Y-m-d}"
     */
    public function __invoke(string $name): string
    {
        return trans('messages.welcome', [
            'name' => $name,
            'date' => date('Y-m-d'),
        ]);
    }
}

The first thing we should do is to move the trans() Laravel function logic to a different service, in order to do that, we have to introduce an interface as follows:

namespace Domain;

interface TranslatorInterface
{
   function trans(string $message, string ...$placeholders): string;
}

And we can place in this class the original Laravel implementation.

namespace Infrastructure;

final class LaravelTranslator implements TranslatorInterface
{
   public function trans(string $message, string ...$placeholders)
   {
      return trans($message, ...$placeholders);
   }
}

So, from this point on, we are able to switch the translator class to our Laravel custom one or a third-party one by implementing the TranslatorInterface.

Finally, we can inject the new Translator service in our Controller defining in the service container which implementation we want to resolve when the framework faces the TranslatorInterface in the constructor.

namespace Infrastructure;

namespace Infrastructure;

final class WelcomeController
{
    public function __construct(
        private TranslatorInterface $translator,
    ) {}

    public function __invoke(Request $request): void
    {
        $name = $request->get('name');
        return $this->translator->trans(
            'messages.welcome',
            ['name' => $name, 'date' => date('Y-m-d')],
       );
    }
}

The next step is to do the same but with the date() method.

namespace Domain;

interface DateInterface
{
   function toString(): string;
}

And the PHP implementation:

namespace Infrastructure;

final class SystemDate implements DateInterface
{
    public function toString(): string
    {
        return date('Y-m-d');
    }
}

Let’s do the same as we did previously, but this time we can inject our own SystemDate implementation through the DateInterface interface.

namespace Infrastructure;

final class WelcomeController
{
    public function __construct(
        private TranslatorInterface $translatorService,
        private DateInterface $date,
    ) {}

    public function __invoke(Request $request): void
    {
        $name = $request->get('name');
        return $this->translatorService->trans(
            'messages.welcome',
            ['name' => $name, 'date' => $this->date->toString()],
       );
    }
}

⚠️ Do not forget to add both mapping interfaces between TranslatorInterface and LaravelTranslator, and DateInterface and SystemDate in the ServiceProvider class.

Finally, we are up to creating our controller test.

namespace Test/Infrastructure;

final class WelcomeControllerTest
{
    public function test_invoke(): void
    {
        $translate = new LaravelTranslator();

        $date = $this->createStub(DateInterface::class);
        $date->method('toString')->willReturn('2022-02-22');

        $request = $this->createStub(Request::class);
        $request->method('get')->willReturn('World');

        $controller = new WelcomeController($translate, $date);
        $response = $controller->__invoke($request);

        self::assertSame('hello World @ 2022-02-22', $response);
    }
}

caravaca-fuentes-marques-2