Definiendo librerías externas en Symfony

Symfony permite utilizar librerías de terceros si las definimos en el fichero services.yml, incluso con parámetros. Hoy vamos a conocer el componente Dependency Injection para conseguir usarlas en nuestro proyecto con ejemplos.
Definiendo librerías externas en Symfony

Muchas veces, cuando estamos trabajando en un proyecto PHP/Symfony, necesitamos inyectar librerías de terceros. Sin embargo, la función autowire del componente de inyección de dependencias de Symfony no funciona correctamente: no encuentra la nueva biblioteca. Esto se debe a que Symfony no hace autowire con librerías externas. En el artículo de hoy te voy a enseñar a definir e inicializar librerías de terceros como un servicio más en un proyecto Symfony o, si no usas Symfony, al menos inicializarlas usando este genial componente.

Pero no todo va a ser Symfony. Desde hace un par de versiones, Symfony dio un giro bastante aplaudido por los desarrolladores comenzando a romper el monolito de Symfony en componentes. Esto significa que puedes usarlos en tu proyecto PHP sin tener que usar el núcleo del framework. Y uno de estos componentes es el Dependency Injection, un componente increíble que permite centralizar y estandarizar las dependencias de las clases que componen tu aplicación. El artículo de hoy habla principalmente de este componente y sobre cómo puedes configurarlo para inicializar las librerías, con o sin parámetros. Así que tanto si utilizas el framework de Symfony o este componente, esto te va a interesar.

Problema del autowire en Symfony con librerías externas

Symfony es un framework genial para PHP. Podemos criticar si es muy pesado o grande (tema que resolvieron cuando enfocaron la arquitectura de componentes), es muy complejo, etc. Pero que quita muchísimo trabajo al desarrollador es algo en lo que creo que todos estaremos de acuerdo.

En cada nueva versión añaden algún componente o utilidad (que generalmente puedes utilizar por separado del núcleo de Symfony) que hace que programar en PHP sea más sencillo y podamos ir directos al grano. Uno de esos cambios drásticos que introdujeron fue el autowire: no tenías que declarar los servicios que hubieses programado en ningún YML: Symfony los encontraba y los inyectaba de forma automática (o automágica). Los ficheros de servicios adelgazaron bastante al no tener que definir de manera explícita cada uno de los servicios que habíamos creado.

Sin embargo, cuando estamos trabajando en un proyecto PHP (y en la mayoría de lenguajes de programación), llega el momento en el que hay que importar y utilizar un módulo, librería, biblioteca o componente de otro desarrollador. Con composer es tremendamente fácil importar librerías, pero, cuando la vas a usar en tu proyecto, ¿un momento? Algo no está funcionando. Efectivamente, el autowire de Symfony no funciona con librerías externas, solo con las que estén dentro de tu propio proyecto.

Cannot autowire service "XXX\MyService": argument "$xx" of method "__construct()" references interface "XXX\YYY" but no such service exists.

Inicializar librerías externas en Symfony

Que no funcione de primeras, no quiere decir que no podamos usar librerías externas en nuestro proyecto. Es más, con declarar estas librerías en el fichero services.yml bastará para poder utilizarlas. Symfony necesita que declaremos la librería de terceros que queremos emplear de forma expresa en el fichero de servicios.

Un ejemplo de definición de librería es el que sigue:

# Definición usando alias
alias.library:
    class: ExternalVendor\Library

# Defnición usando la clase
ExternalVendor\Library:
    class: ExternalVendor\Library
    autowire: true

# Definición de Interfaces
App\Util\TransformerInterface: '@App\Util\Rot13Transformer'

Podemos definir la librería externa de dos formas diferentes: mediante un alias y mediante la declaración de clase.

La primera es útil cuando es un servicio público (aunque deberías plantearte si esto es realmente necesario). Un servicio público es aquel que es global y puede llamarse en casi cualquier parte del código desde el contenedor de dependencias. No se considera una buena práctica de programación porque las dependencias deberían estar inyectadas desde el constructor, pero Symfony te da la opción de inyectarla de cualquier forma.

$library = $container->get('alias.library');
$library->run();

La mayoría de las veces, usarás un servicio privado. Un servicio privado es aquel que no puede inyectarse en mitad del código, ya que debe ser inyectado a través de la función que construye la clase:

class MyClass {
    private Library $library;

    public function __construct(Library $library) {
      $this->library = $library;
    }
}

Por último, es posible que necesites inyectar una interfaz. Un momento, ¿eso es posible? Rotundamente no, pero podemos hacer que Symfony inyecte una clase en concreto cada vez que como dependencia declaremos una interfaz. Esto quiere decir que cada vez que en un constructor declaremos como parámetro una interfaz, Symfony buscará la clase que nosotros hayamos indicado como preferente para ese caso.

class S3Storage implements StorageInterface{};

class MyStorage {
    public function __construct(StorageInterface $storage)
    {
      //Hacer algo super cool !
    }
}

# the ``App\Util\S3Storage`` service will be injected when
# an ``App\Util\StorageInterface`` type-hint is detected
App\Util\StorageInterface: '@App\Util\S3Storage'

Con esto, podrás utilizar en tus proyectos cualquier librería externa que requieras. Bastará con que la definas explícitamente en el fichero services.yml de Symfony para que este la importe.

Pasando parámetros al inicializar la librería

Hasta hora hemos visto cómo declarar una librería externa para que Symfony haga el autowire en nuestro proyecto. Pero, ¿qué pasa si esta librería necesita algún parámetro antes de ser inicializada? Un ejemplo bastante recurrente es la librería de AWS S3 para proyectos en PHP, una librería que permite subir y descargar archivos de un bucket de Amazon (o MinIO, como hemos visto en este blog).

El código de demostración que proporciona Amazon para inicializar la librería requiere que le pasemos algunos parámetros como usuario y clave.

$s3Client = new S3Client([
    'profile' => 'default',
    'region' => 'us-west-2',
    'version' => '2006-03-01',
    'credentials' => [
        'key'      => AWS_KEY,
        'secret'   => AWS_SECRET_KEY,
    ]
]);

//Listing all S3 Bucket
$buckets = $s3Client->listBuckets();

Sin embargo, el Inyector de Dependencias de Symfony es increíblemente flexible y nos evita tener que escribir este código cada vez que inicializamos la librería. El DI (Dependency Injector en inglés) nos permite pasar parámetros y configuraciones a la hora de inicializar la librería, para que cada vez que la usemos en nuestro código, ya venga inicializada con dichos parámetros. Además, nos da la ventaja de no tener que escribir las claves y demás credenciales sensibles en código puro: nos permite hacer referencia a variables de entorno (que no se adjuntan en el código).

Siguiendo con el ejemplo, para inicializar el AWS de S3, bastaría con pasarle los parámetros en el formato que espera, pero en un fichero YML en vez de hacerlo directamente en el código:

Aws\S3\S3Client:
  arguments:
    - version: 'latest'
      region: '%env(AWS_S3_BUCKET_REGION)%'
      credentials:
        key: '%env(AWS_S3_ACCESS_ID)%'
        secret: '%env(AWS_S3_ACCESS_SECRET)%'
class MyStorage {
    public function __construct(S3Client $storage) {
       //Aquí el S3Client ya está inicializado con los
       // parámetros que le hemos pasado.

       // Por ejemplo... siguiendo ejemplo de AWS
       $buckets = $storage->listBuckets();
    }
}

Conclusión

El inyector de dependencias de Symfony es un componente tremendamente útil para PHP, ya sea para usarlo en un proyecto Symfony o en lenguaje PHP puro (ya que al ser un componente, se puede importar sin tener el núcleo de Symfony).

Permite abstraer al desarrollador de usar require, importar ficheros externos, y afear el código (y esas cosas que hacían de PHP un lenguaje bastante feo). Gracias a este componente, podemos estandarizar y centralizar la forma en la que inyectamos clases, dejando un código más profesional y legible.

Otra de las ventajas del componente Dependency Injector de Symfony es que nos permite respetar los principios SOLID de los que ya hablamos en este blog.

En cualquier caso, espero haberte enseñado una forma realmente útil de cómo inicializar dependencias externas en tu proyecto Symfony PHP o, si no usas Symfony, cómo inyectar dependencias con este componente.

¡Qué tengas un feliz coding!