How to avoid @inject thanks to Decorator feature in Nette

I often find @inject being overused in projects I review while mentoring. They often bring less writing, but in exchange they break SOLID principles.

Today I will show you solution that will keep your code both small and clean - Decorator feature in Nette.

As Derek Simons says says...

...Start with "Why"

Why am I writing this article? I try to improve knowledge interoperability between frameworks so it is easier to understand and use each other. The goal is to discourage Nette- (or any framework-) specific things in favor of those that may be common.

Today, I will try to agree on setter injection with you.

@Inject Overuse is Spreading

This code is common to 80 % Nette applications I came across in last year:

// app/Presenter/ProductPresenter.php

namespace App\Presenter;

final class ProductPresenter extends AbstractBasePresenter
{
    /**
     * @inject
     * @var ProductRepository
     */
    public $productRepository;
}

Using @inject annotations over constructor injection is fast, short and it just works.

Ok, why not use it everywhere:

// app/Repository/ProductRepository.php

namespace App\Repository;

class ProductRepository
{
    /**
     * @inject
     * @var Doctrine
     */
    public $entityManager;
}

and

# app/config/config.neon

services:
    -
        class: App\Repository\ProductRepository
        inject: on

Your Code is Seen as Manual How to Write

Why? Because "what you see is what you write". New programmer joins the teams, sees this handy @inject feature and uses when possible and handy.

Some of you, who already talked about @inject method usage already there are some and only few specific places to use it.

Where to only @inject?

To prevent constructor hell. If you meet this term first time, go read this short explanation by David Grudl.

The best use case is AbstractBasePresenter. Let's say I need Translator service in all of my presenters.

// app/Presenter/AbstractBasePresenter.php

namespace App\Presenter;

abstract class AbstractBasePresenter extends Nette\Application\UI\Presenter
{
    /**
     * @inject
     * @var Translator
     */
    public $translator;
}

And I can use it in ProductPresenter along with constructor injection

// app/Presenter/ProductPresenter.php

namespace App\Presenter;

final class ProductPresenter extends AbstractBasePresenter
{
    /**
     * @var ProductRepository
     */
    private $productRepository;

    public function __construct(ProductRepository $productRepository)
    {
        $this->productRepository = $productRepository;
    }
}

This is quite clean and easy to use, because presenters have injects enabled by default.

Level up

But what if we have other objects that:

2 common case pop to my mind:

Let's take the first one:

// app/Repository/AbstractBaseRepository.php

namespace App\Repository;

use Doctrine\ORM\EntityManagerInterface;

abstract class AbstractBaseRepository
{
    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    public function setEntityManager(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }
}

And specific repository with some dependency:

// app/Repository/ProductRepository.php

namespace App\Repository;

use App\Model\Product\ProductSorter;

final ProductRepository extends AbstractBaseRepository
{
    /**
     * @var ProductSorter
     */
    private $productSorter;

    public function __construct(ProductSorter $productSorter)
    {
        $this->productSorter = $productSorter;
    }
}

So our config would look like:

# app/config/config.neon

services:
    -
        class: App\Repository\ProductRepository
        setup:
            - setEntityManager
    # and for other repositories
    -
        class: App\Repository\UserRepository
        setup:
            - setEntityManager
    -
        class: App\Repository\CategoryRepository
        setup:
            - setEntityManager

SO much writing!

It is cleaner, but with so much writing? Thanks, but no, thanks. Let's go back to @inject...

Wait! Before any premature conclusion, let's set the goal first.

What is Desired Result?

# app/config.config.neon

services:
    - App\Repository\ProductRepository
    - App\Repository\UserRepository
    - App\Repository\CategoryRepository

That would be great, right? Is that possible in Nette while keeping the code clean?

Decorator Extension to the Rescue

This feature is in Nette since 2014 (<= the best documentation for it so far).

How does it work?

# app/config/config.neon

decorator: # keyword used by Nette
    App\Repository\AbstractBaseRepository: # 1. find every service this type
        setup: # same setup as we use in service configuration
            - setEntityManager # 2. call this setter injection on it

    # or do you need to call "setTranslator" on every component?
    App\Component\AbstractBaseControl:
        setup:
            - setTranslator

That's it!

What Have You Learned Today?

In next article, we will look at other practical use cases for Decorator Extension.


How do you use @inject, constructor injection or Decorator Extension? Let me know in the comments, I'm curious.


Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!