Inyección de Dependencias Automática

En Webman, la inyección de dependencias automática es una funcionalidad opcional, la cual está desactivada por defecto. Si necesitas la inyección de dependencias automática, se recomienda usar php-di. A continuación se muestra el uso de Webman en combinación con php-di.

Instalación

composer require psr/container ^1.1.1 php-di/php-di ^6.3 doctrine/annotations ^1.14

Modifica la configuración en config/container.php, su contenido final será el siguiente:

$builder = new \DI\ContainerBuilder();
$builder->addDefinitions(config('dependence', []));
$builder->useAutowiring(true);
$builder->useAnnotations(true);
return $builder->build();

config/container.php debe devolver finalmente una instancia de contenedor que cumple con la norma PSR-11. Si no deseas usar php-di, puedes crear y devolver aquí otra instancia de contenedor que cumpla con la norma PSR-11.

Inyección por Constructor

Crea app/service/Mailer.php (si el directorio no existe, créalo) con el siguiente contenido:

<?php
namespace app\service;

class Mailer
{
    public function mail($email, $content)
    {
        // Código de envío de correo omitido
    }
}

El contenido de app/controller/UserController.php será el siguiente:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;

class UserController
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', '¡Hola y bienvenido!');
        return response('ok');
    }
}

Normalmente, necesitarías el siguiente código para instanciar app\controller\UserController:

$mailer = new Mailer;
$user = new UserController($mailer);

Cuando usas php-di, los desarrolladores no necesitan instanciar manualmente Mailer en el controlador; Webman lo hará automáticamente. Si en el proceso de instanciar Mailer hay otras dependencias, Webman también las instanciará e inyectará automáticamente. Los desarrolladores no necesitan hacer ningún trabajo de inicialización.

Nota
Deben ser instancias creadas por el framework o php-di para completar la inyección de dependencias automática; instancias creadas con new no podrán completar la inyección automática. Si necesitas inyección, debes usar la interfaz support\Container en lugar de la declaración new, por ejemplo:

use app\service\UserService;
use app\service\LogService;
use support\Container;

// Instancias creadas con new no pueden ser inyectadas
$user_service = new UserService;
// Instancias creadas con new no pueden ser inyectadas
$log_service = new LogService($path, $name);

// Instancias creadas por Container pueden ser inyectadas
$user_service = Container::get(UserService::class);
// Instancias creadas por Container pueden ser inyectadas
$log_service = Container::make(LogService::class, [$path, $name]);

Inyección por Anotaciones

Aparte de la inyección automática por constructor, también podemos usar la inyección por anotaciones. Continuando con el ejemplo anterior, app\controller\UserController se modificará de la siguiente manera:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;
use DI\Annotation\Inject;

class UserController
{
    /**
     * @Inject
     * @var Mailer
     */
    private $mailer;

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', '¡Hola y bienvenido!');
        return response('ok');
    }
}

Este ejemplo utiliza la anotación @Inject para la inyección y declara el tipo de objeto con la anotación @var. Este ejemplo produce el mismo efecto que la inyección por constructor, pero el código es más conciso.

Nota
Webman no admite la inyección de parámetros del controlador en versiones anteriores a 1.4.6; por ejemplo, el siguiente código no será admitido en Webman <= 1.4.6:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;

class UserController
{
    // La inyección de parámetros del controlador no es soportada antes de la versión 1.4.6
    public function register(Request $request, Mailer $mailer)
    {
        $mailer->mail('hello@webman.com', '¡Hola y bienvenido!');
        return response('ok');
    }
}

Inyección personalizada por Constructor

A veces los parámetros pasados al constructor pueden no ser instancias de clases, sino cadenas, números, arreglos, etc. Por ejemplo, el constructor de Mailer puede necesitar pasar la IP y puerto del servidor SMTP:

<?php
namespace app\service;

class Mailer
{
    private $smtpHost;

    private $smtpPort;

    public function __construct($smtp_host, $smtp_port)
    {
        $this->smtpHost = $smtp_host;
        $this->smtpPort = $smtp_port;
    }

    public function mail($email, $content)
    {
        // Código de envío de correo omitido
    }
}

En este caso, no se puede utilizar directamente la inyección automática por constructor mencionada anteriormente, porque php-di no puede determinar cuáles son los valores de $smtp_host y $smtp_port. En este caso, se puede intentar la inyección personalizada.

Añade el siguiente código en config/dependence.php (si el archivo no existe, créalo):

return [
    // ... Otras configuraciones omitidas

    app\service\Mailer::class =>  new app\service\Mailer('192.168.1.11', 25);
];

De esta forma, cuando la inyección de dependencias necesite obtener una instancia de app\service\Mailer, se utilizará automáticamente la instancia de app\service\Mailer creada en esta configuración.

Notamos que se utilizó new para instanciar la clase Mailer en config/dependence.php. Esto no presenta ningún problema en este ejemplo, pero imagina que si la clase Mailer dependiera de otras clases o utilizara inyección por anotaciones, la inicialización con new no realizaría la inyección automática. La solución es utilizar la inyección de interfaz personalizada mediante los métodos Container::get(Clase) o Container::make(Clase, [Parámetros del constructor]) para inicializar la clase.

Inyección de Interfaz Personalizada

En proyectos reales, preferimos programar orientados a interfaces, no a clases concretas. Por ejemplo, en app\controller\UserController debería importar app\service\MailerInterface en lugar de app\service\Mailer.

Define la interfaz MailerInterface.

<?php
namespace app\service;

interface MailerInterface
{
    public function mail($email, $content);
}

Define la implementación de la interfaz MailerInterface.

<?php
namespace app\service;

class Mailer implements MailerInterface
{
    private $smtpHost;

    private $smtpPort;

    public function __construct($smtp_host, $smtp_port)
    {
        $this->smtpHost = $smtp_host;
        $this->smtpPort = $smtp_port;
    }

    public function mail($email, $content)
    {
        // Código de envío de correo omitido
    }
}

Importa la interfaz MailerInterface en vez de la implementación concreta.

<?php
namespace app\controller;

use support\Request;
use app\service\MailerInterface;
use DI\Annotation\Inject;

class UserController
{
    /**
     * @Inject
     * @var MailerInterface
     */
    private $mailer;

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', '¡Hola y bienvenido!');
        return response('ok');
    }
}

config/dependence.php definirá la interfaz MailerInterface de la siguiente manera.

use Psr\Container\ContainerInterface;
return [
    app\service\MailerInterface::class => function(ContainerInterface $container) {
        return $container->make(app\service\Mailer::class, ['smtp_host' => '192.168.1.11', 'smtp_port' => 25]);
    }
];

De esta forma, cuando el negocio necesite usar la interfaz MailerInterface, automáticamente se usará la implementación de Mailer.

La ventaja de programación orientada a interfaces es que, al cambiar un componente, no necesitamos modificar el código de negocio, solo cambiar la implementación concreta en config/dependence.php. Esto también es muy útil para realizar pruebas unitarias.

Otras Inyecciones Personalizadas

config/dependence.php no solo puede definir dependencias de clases, también puede definir otros valores, como cadenas, números, arreglos, etc.

Por ejemplo, config/dependence.php puede definirse de la siguiente manera:

return [
    'smtp_host' => '192.168.1.11',
    'smtp_port' => 25
];

En este caso, podemos usar @Inject para inyectar smtp_host y smtp_port en las propiedades de una clase.

<?php
namespace app\service;

use DI\Annotation\Inject;

class Mailer
{
    /**
     * @Inject("smtp_host")
     */
    private $smtpHost;

    /**
     * @Inject("smtp_port")
     */
    private $smtpPort;

    public function mail($email, $content)
    {
        // Código de envío de correo omitido
        echo "{$this->smtpHost}:{$this->smtpPort}\n"; // Esto imprimirá 192.168.1.11:25
    }
}

Nota: @Inject("key") debe incluir la clave entre comillas dobles.

Más Contenido

Por favor consulta el manual de php-di