A free and open-source book on ZF3 for beginners

Translation into this language is not yet finished. You can help this project by translating the chapters and contributing your changes.

3.10. Administrador de Servicios

Podemos imaginar a la aplicación web como un conjunto de servicios. Por ejemplo, podemos tener un servicio de autenticación responsable del inicio de sesión para usuarios, un servicio administrador de entidades responsable del acceso a la base de datos, un servicio para la administración de eventos responsable de lanzar eventos y entregarlos al listeners de eventos, etc.

En Zend Framework la clase ServiceManager es un contenedor centralizado para todos los servicios de aplicación. El administrador de servicios se implementa en el componente Zend\ServiceManager, con la clase ServiceManager. El diagrama de herencia de la clase se muestra en la figura 3.5:

Figura 3.5. Diagrama de herencia de la clase administradora de servicios Figura 3.5. Diagrama de herencia de la clase administradora de servicios

El administrador de servicios es creado luego de arrancar la aplicación (dentro del método estático init() de la clase Zend\Mvc\Application). Los servicios estándares disponibles a través del administrador de servicios se presentan en la tabla 3.1. Esta tabla está incompleta, porque el número actual de servicios registrados en el administrador de servicios puede ser muy grande.

Tabla 3.1. Servicios Estándares
Nombre del Servicio Descripción
Application Permite recuperar la clase singleton Zend\Mvc\Application.
ApplicationConfig Arreglo de configuración extraído desde el archivo application.config.php.
Config Arreglo de configuración mezclado, extraído del archivo module.config.php junto con autoload/global.php y el autoload/local.php.
EventManager Permite recuperar una nueva instancia de la clase Zend\EventManager\EventManager. El administrador de eventos permite enviar (lanzar) eventos y asociar listeners de evento.
SharedEventManager Permite recuperar una instancia singleton de la clase Zend\EventManager\SharedEventManager. El administrador de eventos compartidos permite escuchar eventos definidos por otras clases y componentes.
ModuleManager Permite recuperar una clase singleton de Zend\ModuleManager\ModuleManager. El administrador de módulos es responsable de cargar los módulos de la aplicación.
Request La clase singleton de Zend\Http\Request. Representa una petición HTTP recibida desde el client.
Response La clase singleton de Zend\Http\Response. Representa la respuesta HTTP que será enviada al cliente.
Router La clase singleton de Zend\Router\Http\TreeRouteStack. Ejecuta el direccionamiento URL.
ServiceManager El administrador de servicios.
ViewManager La clase singleton de Zend\Mvc\View\Http\ViewManager. Responsable de preparar la capa de vista para pintar la página.

Un servicio es típicamente una clase PHP arbitraria, pero no siempre. Por ejemplo, cuando ZF3 carga los archivos de configuración y mezcla los datos dentro de un arreglo anidado, se guarda el arreglo en dos servicios del administrador de servicios: ApplicationConfig y Config. El primero es una arreglo cargado desde el nivel de aplicación con el archivo application.config.php y el segundo servicio es la mezcla de los archivos de configuración a nivel de módulo y los archivos de configuración autocargados del nivel de aplicación. Luego, en el administrador de eventos podemos guardar cualquier cosa que queramos: una clase PHP, una simple variable o un arreglo.

En la tabla 3.1 podemos ver que en ZF3 casi todo se puede considerar un servicio. El administrador de servicios mismo se registra como un servicio. Además, la clase Application se registra también como un servicio.

Una cosa importante que deberías saber sobre los servicios es que ellos se guardan normalmente en una única instancia (este es un patrón llamado singleton). Obviamente, no necesitamos otra instancia de la clase Application (o podríamos tener una pesadilla).

Pero existe una importante excepción para la regla de arriba. Aunque puede ser confuso al principio el EventManager no es un singleton. Cada vez que recuperamos el administrador de eventos desde el administrador de servicios recibimos un nuevo objeto. Esto se hace por razones de rendimiento y para evitar conflictos entre eventos de distintos componentes. Discutiremos esto luego en la sección Sobre los Administradores de Eventos de este capítulo.

El administrador de servicios define varios métodos necesarios para localizar y recuperar un servicio desde el administrador de servicios (ver la tabla 3.2).

Tabla 3.2. Métodos del ServiceManager
Nombre del Método Descripción
has($name) Revisa si el servicio está registrado.
get($name) Recupera una instancia del servicio registrado.
build($name, $options) Siempre regresa una nueva instancia del servicio solicitado.

Podemos probar si un servicio está registrado pasando su nombre al método has() del administrador de servicios. Si regresa el valor booleano true el servicio está registrado, si regresa false el servicio con el nombre dado no está registrado.

Luego podemos recuperar un servicio a partir de su nombre con la ayuda del método get() del administrador de servicios. Este método toma un único parámetro que representa el nombre del servicio. Veamos el siguiente ejemplo:

<?php

// Retrieve the application config array.
$appConfig = $serviceManager->get('ApplicationConfig');

// Use it (for example, retrieve the module list).
$modules = $appConfig['modules'];

Cuando se llama al método build() siempre se crea una nueva instancia del servicio (comparado con get() que normalmente crea una instancia del servicio una sola vez y la regresa luego en cada nueva petición).

Normalmente no recuperamos servicios con el administrador de servicios desde cualquier lugar de nuestro código sino dentro de una fábrica. Una fábrica es un código responsable de crear un objeto. Cuando se crea el objeto podemos traer desde administrador de servicios a los servicios de que depende el objeto que estamos creando y pasar estos servicios (dependencias) al constructor del objeto. A esto se le llama inyección de dependencias.

Si tenemos alguna experiencia con Zend Framework 2 podemos ver que las cosas ahora son un poco diferentes. En ZF2, había un patrón ServiceLocator que permitía traer dependencias desde administrador de servicios en cualquier parte de la aplicación (controladores, servicios, etc). En ZF3, tenemos que pasar las dependencias explícitamente. Esto es un poco aburrido pero remueve las dependencias «ocultas» y hace que nuestro código sea más fácil y claro de entender.

3.10.1. Registrar un Servicio

Cuando escribimos un sitio web a menudo necesitamos registrar nuestro propio servicio en el administrador de servicios. Una de las maneras de registrar un servicio es usando el método setService() del administrador de servicios. Por ejemplo, vamos a crear y registrar la clase de servicio que convierte monedas y que se usa, por ejemplo, en una página con carrito de compras para convertir EUR a USD:

<?php
// Define a namespace where our custom service lives.
namespace Application\Service;

// Define a currency converter service class.
class CurrencyConverter
{
    // Converts euros to US dollars.
    public function convertEURtoUSD($amount)
    {
        return $amount*1.25;
    }

    //...
}

Arriba entre las líneas 6 y 15 definimos una clase CurrencyConverter de ejemplo (por simplicidad, implementamos solo un método convertEURtoUSD() que es capaz de convertir euros a dolares norte americanos).

// Create an instance of the class.
$service = new CurrencyConverter();
// Save the instance to service manager.
$serviceManager->setService(CurrencyConverter::class, $service);

En este ejemplo se crea una instancia de la clase con el operador new y la registramos en el administrador de servicios usando el método setService() (asumimos que la variable $serviceManager es de tipo class correspondiente a Zend\ServiceManager\ServiceManager y que ha sido declarada en algún lugar).

El método setService() toma dos parámetros: una cadena de caracteres que es el nombre del servicio y la instancia del servicio. El nombre del servicio debe ser único entre todos los otros servicios posibles.

Una vez que el servicio se almacena en el administrador de servicios podemos recuperarlo por su nombre en cualquier lugar de la aplicación con la ayuda del método get() del administrador de servicios. Miremos el siguiente ejemplo:

<?php
// Retrieve the currency converter service.
$service = $serviceManager->get(CurrencyConverter::class);

// Use it (convert money amount).
$convertedAmount = $service->convertEURtoUSD(50);

3.10.2. Nombres de Servicio

Diferentes servicios pueden usar diferentes convenciones de nomenclatura. Por ejemplo, el mismo servicio que convierte monedas puede ser registrado con diferentes nombres: CurrencyConverter, currency_converter, etc. Podemos introducir una convención para hacer uniforme los nombres, recomendamos registrar un servicio por su nombre de clase completo, fully qualified name, de la siguiente manera:

$serviceManager->setService(CurrencyConverter::class);

En el ejemplo de arriba usamos la palabra clave class. Esta se encuentra disponible desde PHP 5.5 y se usa para la resolución de nombres de clase. CurrencyConverter::class se expande al nombre completo de la clase, es decir \Application\Service\CurrencyConverter.

3.10.3. Sobrescribir un Servicio Existente

Si estamos intentando registrar un nombre de servicio que ya está registrado el método setService() lanzará una excepción. Sin embargo, en ocasiones queremos sobrescribir el servicio con el mismo nombre (reemplazarlo por uno nuevo). Con este propósito, podemos usar el método del administrador de servicios setAllowOverride().

<?php
// Allow to replace services
$serviceManager->setAllowOverride(true);

// Save the instance to service manager. There will be no exception
// even if there is another service with such a name.
$serviceManager->setService(CurrencyConverter::class, $service);

El método setAllowOverride() toma un único parámetro booleano que define si se permite reemplazar el servicio CurrencyConverter tanto si ya está registrado como si no.

3.10.4. Registrar Clases Invocables

Hay algo que está mal con el método setService(), si se usa tenemos que crear la instancia del servicio antes de que realmente lo necesitemos. Si nunca usamos el servicio la instanciación del servicio solo derrochará tiempo y memoria. Para resolver esta cuestión el administrador de servicios nos provee del método setInvokableClass().

<?php
// Register an invokable class
$serviceManager->setInvokableClass(CurrencyConverter::class);

En el ejemplo de arriba pasamos al administrador de servicios el nombre completo de la clase (fully qualified) que implementa el servicio en lugar de pasar su instancia. Con esta técnica, el servicio será instanciado por el administrador de servicios solo cuando se llama al método get(CurrencyConverter::class). A esta técnica también se le llama lazy loading.

Los servicios a menudo dependen de otro. Por ejemplo, el servicio que convierte monedas puede usar el servicio de administración de entidades para leer la tasa de cambio desde la base de datos. La desventaja del método setInvokableClass() es que no permite pasar parámetros (dependencias) al servicio en la instanciación del objeto. Para resolver esta cuestión podemos usar fábricas (factories) como se describe más adelante.

3.10.5. Registrar una Fábrica

Una fábrica es una clase que solo puede hacer una cosa, crear otros objetos.

Registramos una fábrica para un servicio con el método setFactory() del administrador de servicios.

La fábrica más simple es InvokableFactory, esta es análoga al método setInvokableClass() de la sección anterior.

<?php
use Zend\ServiceManager\Factory\InvokableFactory;

// This is equivalent to the setInvokableClass() method from previous section.
$serviceManager->setFactory(CurrencyConverter::class, InvokableFactory::class);

Después de haber registrado la fábrica podemos recuperar el servicio desde el administrador de servicios como es usual usando el método get(). El servicio se instanciará solo cuando lo recuperemos desde el administrador de servicios (lazy loading).

En ocasiones la instanciación de un servicio es más compleja que solo crear la instancia del servicio con el operador new (como lo hace la clase InvokableFactory). Podemos necesitar pasar algunos parámetros al constructor del servicio o invocar algunos métodos del servicio justo después de la construcción. Esta lógica de instanciación compleja se puede encapsular dentro de nuestra propia clase fábrica escrita a la medida. La clase fábrica normalmente implementa a la interfaz FactoryInterface:

<?php
namespace Zend\ServiceManager\Factory;

use Interop\Container\ContainerInterface;

interface FactoryInterface
{
    public function __invoke(ContainerInterface $container,
                        $requestedName, array $options = null);
}

Como podemos ver a partir de la definición de la interfaz FactoryInterface, la clase fábrica debe proveer el método mágico __invoke que retorna una instancia de un solo servicio. El administrador de servicios se pasa al método __invoke en el parámetro $container; este se puede usar durante la construcción del servicio para acceder a otros servicios (inyectar dependencias). El segundo parámetro ($requestedName) es el nombre del servicio. El tercer argumento ($options) se puede usar para pasar algunos parámetros al servicio y se usa solo cuando pedimos el servicio con el método del administrador de servicios build().

Como un ejemplo, vamos a escribir una fábrica para nuestro servicio que convierte monedas (ver el código de abajo). No usamos una lógica de construcción compleja para nuestro servicio CurrencyConverter pero para servicios más complejos necesitaremos hacerlo.

<?php
namespace Application\Service\Factory;

use Zend\ServiceManager\Factory\FactoryInterface;
use Application\Service\CurrencyConverter;

// Factory class
class CurrencyConverterFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container,
                     $requestedName, array $options = null)
    {
        // Create an instance of the class.
        $service = new CurrencyConverter();

        return $service;
    }
}

Técnicamente, con ZF3 podemos usar la misma clase fábrica para instanciar varios servicios que tienen código de instanciación similar (para este propósito podemos usar el argumento $requestedName que se pasa al método fábrica __invoke()). Sin embargo, principalmente crearemos diferentes fábrica una por cada servicio.

3.10.6. Registrar un Fábrica Abstracta

Un escenario más complejo para una fábrica es cuando necesitamos determinar en tiempo de ejecución cuales nombres de servicios deberían ser registrados. En esta situación podemos usar una fábrica abstracta. Una fábrica abstracta deberá implementar la interfaz AbstractFactoryInterface:

<?php
namespace Zend\ServiceManager\Factory;

use Interop\Container\ContainerInterface;

interface AbstractFactoryInterface extends FactoryInterface
{
    public function canCreate(ContainerInterface $container, $requestedName);
}

Una fábrica abstracta tiene dos métodos: canCreate() e __invoke(). La primera es necesaria para revisar si la fábrica puede crear el servicio con determinado nombre y el segundo permite crear el servicio. Los métodos toman dos parámetros: el administrador de servicios ($container) y el nombre del servicio ($requestedName).

En comparación con la clase fábrica normal, la diferencia está en que la clase fábrica normal generalmente crea solo un tipo de servicio pero la clase abstracta puede crear dinámicamente tantos tipos de servicios como se quiera.

Registramos una fábrica abstracta con el método del administrador de servicios setAbstractFactory().

Las fábricas abstractas son una poderosa característica pero solo deberíamos usarlas cuando realmente es necesario, porque ellas impactan negativamente en el rendimiento. Es mejor usar las fábricas usuales (no abstractas).

3.10.7. Registrar Alias de Servicios

A veces podemos querer definir un alias para el servicio. Los alias son como enlaces simbólicos: estos hacen referencia a servicios que ya están registrados. Para crear un alias usamos el método setAlias() del administrador de servicios:

<?php
// Register an alias for the CurrencyConverter service
$serviceManager->setAlias('CurConv', CurrencyConverter::class);

Una vez registrado podemos recuperar el servicio usando el método get() del administrador de servicios tanto con el nombre como con el alias.

3.10.8. Servicios Compartidos y no Compartidos

Por defecto los servicios se guardan en una sola instancia en el administrador de servicios. A esto también se le llama patrón de diseño singleton. Por ejemplo, cuando intentamos recuperar dos veces el servicio CurrencyConverter recibiremos el mismo objeto. A esto lo llamamos un servicio compartido.

Pero en algunas (raras) ocasiones necesitamos crear una nueva instancia de un servicio cada vez que alguien lo pida al administrador de servicios. Un ejemplo de esto es el EventManager, tendremos una nueva instancia de él cada vez que lo pidamos.

Para marcar un servicio como no compartido podemos usar el método del administrador de servicios setShared():

$serviceManager->setShared('EventManager', false);

3.10.9. Configuración del Administrador de Servicios

En nuestro sitio web normalmente usamos la configuración del administrador de servicios para registrar nuestros servicios (en lugar de llamar a los métodos del administrador de servicios como describimos arriba).

Para registrar automáticamente un servicio dentro del administrador de servicios, normalmente usamos la llave service_manager en el archivo de configuración. Podemos colocar esta llave dentro de un archivo de configuración a nivel de aplicación o en un archivo de configuración a nivel de módulo.

Si colocamos esta llave en el archivo de configuración a nivel de módulo debemos ser cuidadosos de no sobrescribir el nombre durante la mezcla de la configuración. No debemos registrar el mismo nombre de servicio en diferentes módulos.

La llave service_manager debería verse así:

<?php
return [
    //...

    // Register the services under this key
    'service_manager' => [
        'services' => [
            // Register service class instances here
            //...
        ],
        'invokables' => [
            // Register invokable classes here
            //...
        ],
        'factories' => [
            // Register factories here
            //...
        ],
        'abstract_factories' => [
            // Register abstract factories here
            //...
        ],
        'aliases' => [
            // Register service aliases here
            //...
        ],
        'shared' => [
            // Specify here which services must be non-shared
        ]
  ],

  //...
];

En el ejemplo de arriba podemos ver que la llave service_manager puede contener varias subllaves para registrar servicios de diferentes maneras:

A manera de ejemplo vamos a registrar nuestro servicio CurrencyConverter y crearemos un alias para él:

<?php
use Zend\ServiceManager\Factory\InvokableFactory;
use Application\Service\CurrencyConverter;

return [
    //...

    // Register the services under this key
    'service_manager' => [
        'factories' => [
            // Register CurrencyConverter service.
            CurrencyConverter::class => InvokableFactory::class
        ],
        'aliases' => [
            // Register an alias for the CurrencyConverter service.
            'CurConv' => CurrencyConverter::class
        ],
  ],

  //...
];

Top