Skip to content

Latest commit

 

History

History
199 lines (140 loc) · 5.98 KB

best-practices.md

File metadata and controls

199 lines (140 loc) · 5.98 KB
layout tab current_menu
documentation
best-practices
best-practices

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:

  • inject the container in the controllers, and call $container->get(...)

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

  • inject dependencies in properties

This is the solution we recommend.

Example:

class UserController
{
    /**
     * @Inject
     * @var FormFactoryInterface
     */
    private $formFactory;

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

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

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

  • injecting in a private property breaks encapsulation
  • it is not an explicit dependency: there is no contract saying your class need the property to be set to work
  • if you use PHP-DI's annotations to mark the dependency to be injected, your class is dependent on the container (see the 2nd rule above)

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:

  • you will not unit-test it (that doesn’t mean you won’t write functional tests on the interface though)
  • you will not need to reuse it elsewhere
  • if you change the framework, you may have to rewrite it (or parts of it) anyway (because most dependencies like Request, Response, templating system, etc. will have changed)

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

Writing services

Given a service is intended to be reused, tested and independent of your framework, we do not recommend using annotations 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:

  • define the whole injection of the method/class (i.e. every parameters).

Example:

<?php
// config.php
return [
    // ...
    OrderService::class => DI\object()
        ->constructor(DI\get(SomeOtherService::class), 'a value'),
];
  • or define just the scalar parameter and let PHP-DI use autowiring for the rest.

Example:

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

This solution is generally preferred give 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\object(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.