Best practices

This is an opinionated guide on how to use PHP-DI and dependency injection for the best.

While it may not cover every case, and satisfy everybody, it can serve as a canvas to help you getting started with dependency injection.

If you disagree with anything explained in that guide, that's OK. It's opinionated, and you should make your own opinion on all these subjects ;). It shouldn't prevent you to use PHP-DI the way you want.

Rules for using a container and dependency injection

Here are some basic rules to follow:

  1. never get an entry from the container directly (always use dependency injection)
  2. more generally, write code decoupled from the container
  3. type-hint against interfaces, configure which implementation to use in the container's configuration

Writing controllers

Using dependency injection in controllers is usually where it is the most painful.

If we take Symfony 2 as an example (but this generally applies to every framework), here are your options:

This is bad, see the rule n°1.

This is painful when you have more than 5 dependencies, and your constructor is 15 lines of boilerplate code

This is the solution we recommend.

Example:


class UserController
{
    #[Inject]
    private FormFactoryInterface $formFactory;

    public function createForm($type, $data, $options)
    {
        // $this->formFactory->...
    }
}

As you can see, this solution requires very little code, is simple to understand and benefits from IDE support (autocompletion, refactoring, …).

Property injection is generally frowned upon, and for good reasons:

BUT

if you follow general best practices on how to write your application, your controllers will not contain business logic (only routing calls to the models and binding returned values to view).

So:

This solution offers many benefits for no major drawback, so we recommend using attributes in controllers.

Writing services

Given a service is intended to be reused, tested and independent of your framework, we do not recommend using attributes for injecting dependencies.

Instead, we recommend using constructor injection and autowiring:

class OrderService implements OrderServiceInterface
{
    private $paymentService;

    public function __construct(PaymentServiceInterface $paymentService)
    {
        $this->paymentService = $paymentService;
    }

    public function processOrder($order)
    {
        $this->paymentService->...
    }
}

By using autowiring (enabled by default), you save yourself binding every parameter of the constructor in the configuration. PHP-DI will guess which object it needs to inject by checking the types of your parameters.

In some cases, autowiring will not be enough because some parameter will be a scalar (string, int, …). At that point, you will need to define explicitly what to inject in that scalar parameter, and for this you can either:

Example:

<?php
// config.php
return [
    // ...
    OrderService::class => DI\create()
        ->constructor(DI\get(SomeOtherService::class), 'a value'),
];

Example:

<?php
// config.php
return [
    // ...
    OrderService::class => DI\autowire()
        ->constructorParameter('paramName', 'a value'),
];

This solution is generally preferred given it avoids redefining everything.

Side note: as explained in rule n°3, we recommend type-hinting against interfaces. In that case, you will need to map interfaces to the implementation the container should use in the configuration:

<?php
// config.php
return [
    // ...
    OrderServiceInterface::class => DI\get(OrderService::class),
];

Using libraries

When using libraries, like loggers, ORMs, … you sometimes need to configure them.

In that case, we advise you to define these dependencies in your configuration file. We also recommend using an anonymous function when the configuration gets a bit complex.

The anonymous function allows you to write real PHP code, which is great, because you can use the library's documentation, you get IDE support, and you are a PHP developer so you already know the language ;).

Here is an example with Monolog, a PHP logger:

<?php
// config.php

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

return [
    // ...

    Psr\Log\LoggerInterface::class => DI\factory(function () {
        $logger = new Logger('mylog');

        $fileHandler = new StreamHandler('path/to/your.log', Logger::DEBUG);
        $fileHandler->setFormatter(new LineFormatter());
        $logger->pushHandler($fileHandler);

        return $logger;
    }),
];

Of course, as you can see, we used the PSR-3 interface for injections. That way we can replace Monolog with any PSR-3 logger anytime we want, just by changing this configuration.