4 PHPStan Rules that Bring Order to Nette Injects

Do you have at least one @inject property or inject*() method in your Nette project? If no, stop reading and have fun with another post.

If you do, you probably have some internal rules where what and how to use them and where to avoid them. But how do you keep order?

If you talk about your project's standards that everyone should follow, remember the old lazy programmer's saying:

"In CI,
or won't happen."

Injection Everywhere?

Using injection makes sense in some cases, like avoiding circular dependencies. Another use case is an abstract class with dozens of child classes, but mostly it's a killer for a healthy dependency tree.

In a one Nette project I worked on within the last five years, they had a very "cool" DI extension. A DI extension that enables @inject everywhere:

final class SomeClass
{
    /**
     * @var EventDispatcher
     * @inject
     */
    public $eventDispatcher;
}

Why needs __construct(). right? Get PHP 8 promoted properties today.

"If it's not forbidden,
it's allowed."

Luckily, nowadays, we have a Symplify PHPStan rules to help us.

1. Do Not Combine Nette Inject and __construct()

If there is a constructor already, there an exact working way to get dependencies:

class SomeClass
{
    private $someType;

    private $anotherType;

    public function __construct(AnotherType $anotherType)
    {
        $this->anotherType = $anotherType;
        // ...
    }

    public function injectSomeType(SomeType $someType)
    {
        $this->someType = $someType;
    }
}


It's better to use far cleaner __construct() inection:

class SomeClass
{
    private $someType;

    private $anotherType;

    public function __construct(AnotherType $anotherType, SomeType $someType)
    {
        $this->anotherType = $anotherType;
        $this->someType = $someType;
    }
}

Covered by Symplify\PHPStanRules\Rules\NoNetteInjectAndConstructorRule rule.


2. Use Single Nette inject*() Method

Using the inject*() method is an equal alternative to @inject property that does not require a property to be public. It like an extra __construct() that is caller by the framework to set other dependencies.

Single __construct() has its meaning. Imagine this use case:

class SomeClass
{
    private $type;

    private $anotherType;

    public function injectOne(SameType $type)
    {
        $this->type = $type;
    }

    public function injectTwo(SameType $anotherType)
    {
        $this->anotherType = $anotherType;
    }
}

Having multiple inject*() methods allow us to accidentally put two dependencies in the same class. This has performance hit, maintenance hit, and duplicated parasitic code that needs our attention for no gain.


We can solve all this with a single inject*() method per class:

class SomeClass
{
    private $someType;

    public function injectSomeClass(SomeType $someType)
    {
        $this->someType = $someType;
    }
}

Covered by Symplify\PHPStanRules\Rules\SingleNetteInjectMethodRule rule.


3. Validate @inject Format

I'm proud to admit I made this bug last week. With IDE autocomplete, class annotations, and PHP 8 attributes, I'm removing my skill to type precisely the word:

class SomeClass
{
    /**
     * @injects
     * @var SomeDependency
     */
    public  $someDependency;
}

...and the property was null.


Let's not do that ever again and validate letter by letter of @inject annotation:

 class SomeClass
 {
     /**
-     * @injects
+     * @inject
      * @var SomeDependency
      */
     public $someDependency;
}

Covered by Symplify\PHPStanRules\Rules\ValidNetteInjectRule rule.

4. No @inject on final Class

Last but not least, this rule finally put an order to our codebase.

A specific need for injects is an abstract class with a couple of children:

abstract class AbstractPresenter
{
}

final class ProductPresenter extends AbstractPresenter
{
}

Where would you allow injects?


What would be the consequences:

The ideal options is 3). Using inject only abstract classes make children cleaner and less coupled to the framework. There are more children classes than abstract, so code base quality will be much higher if children classes are strict and clean.

final class ProductPresenter extends AbstractPresenter
{
    /**
     * @inject
     * @var AnotherDependency
     */
    public $anotherDependency;
}

Covered by Symplify\PHPStanRules\Rules\NoInjectOnFinalRule rule.


That's it for today. Try the rule one by one and run PHPStan to see how it helps your project:

composer require symplify/phpstan-rules --dev

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!