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:
- inherit from abstract parent
- needs 1 service available everywhere
2 common case pop to my mind:
AbstractBaseRepository
for all our repositoriesAbstractBaseControl
for all our components
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?
- It is easy to overuse
@inject
in places where it doesn't solve any problem - The problem
@inject
/inject<method>
method were born to solve is called dependency hell - If you need to decorate service of some type, use Decorator Extension
- This will lead to better framework understandability and usability
- ...and world peace in time :)
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.