How to avoid @inject thanks to Decorator feature in Nette

Found a typo? Edit me

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!


Have you find this post useful? Do you want more?

Follow me on Twitter, RSS or support me on GitHub Sponsors.