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:
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.
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).
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.
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);
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
.
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.
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.
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.
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).
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.
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);
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:
services
(línea 7) permite registrar instancias de clases.invokables
(línea 11) permite registrar un nombre completo de clase;
el servicio será instanciado usando lazy loading.factories
(línea 15) permite registrar una fábrica, que es capaz
de crear instancias de un solo servicio.abstract_factories
(línea 9) se usa para registrar fábricas abstractas,
que son capaces de registrar varios servicios por nombre.aliases
(línea 23) provee la capacidad de registrar un alias para un
servicio.shared
(línea 27) permite especificar cuales servicios no deben ser
compartidos.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
],
],
//...
];