Definición de servicios

En la configuración le indicamos al contenedor DI cómo debe crear cada servicio y cómo conectarlo con sus dependencias. Nette ofrece para ello una forma muy clara y elegante.

La sección services del archivo de configuración NEON es donde definimos nuestros propios servicios y su configuración. Veamos un ejemplo sencillo que define un servicio llamado database, que representa una instancia de la clase PDO:

services:
	database: PDO('sqlite::memory:')

La configuración anterior da como resultado el siguiente método factory en el contenedor DI:

public function createServiceDatabase(): PDO
{
	return new PDO('sqlite::memory:');
}

Los nombres de los servicios permiten referirse a ellos en otras partes del archivo de configuración con el formato @nombreDelServicio. Si no hace falta darle nombre al servicio, podemos usar simplemente un guion (-):

services:
	- PDO('sqlite::memory:')

Para obtener un servicio del contenedor DI podemos usar el método getService() con el nombre del servicio como parámetro, o el método getByType() con el tipo del servicio:

$database = $container->getService('database');
$database = $container->getByType(PDO::class);

Creación de servicios

Normalmente creamos un servicio simplemente instanciando una clase concreta. Por ejemplo:

services:
	database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret)

Si necesitamos ampliar la configuración con más claves, la definición se puede repartir en varias líneas:

services:
	database:
		create: PDO('sqlite::memory:')
		setup: ...

La clave create tiene el alias factory; ambas variantes se usan habitualmente. Recomendamos, sin embargo, usar create.

Los argumentos del constructor o del método factory se pueden indicar alternativamente con la clave arguments:

services:
	database:
		create: PDO
		arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret]

Los servicios no tienen por qué crearse necesariamente instanciando una clase; también pueden ser el resultado de llamar a métodos estáticos o a métodos de otros servicios:

services:
	database: DatabaseFactory::create()
	router: @routerFactory::create()

Fíjese en que, por simplicidad, se usa :: en lugar de ->, véase Lenguaje de expresiones. Se generarán estos métodos factory:

public function createServiceDatabase(): PDO
{
	return DatabaseFactory::create();
}

public function createServiceRouter(): RouteList
{
	return $this->getService('routerFactory')->create();
}

El contenedor DI necesita conocer el tipo del servicio que se crea. Si creamos un servicio con un método que no tiene indicado el tipo de retorno, debemos declarar ese tipo explícitamente en la configuración:

services:
	database:
		create: DatabaseFactory::create()
		type: PDO

Argumentos

Los argumentos se pasan a los constructores y a los métodos de forma muy parecida a como se hace en el propio PHP:

services:
	database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret)

Para mejorar la legibilidad podemos poner los argumentos en líneas separadas. En ese caso, las comas son opcionales:

services:
	database: PDO(
		'mysql:host=127.0.0.1;dbname=test'
		root
		secret
	)

También puede dar nombre a los argumentos, con lo que no tendrá que preocuparse por su orden:

services:
	database: PDO(
		username: root
		password: secret
		dsn: 'mysql:host=127.0.0.1;dbname=test'
	)

Si quiere omitir algunos argumentos y usar sus valores por defecto, o que se le inyecte un servicio mediante autowiring, use un guion bajo (_):

services:
	foo: Foo(_, %appDir%)

Los argumentos pueden incluir servicios, parámetros y mucho más, véase Lenguaje de expresiones.

Setup

En la sección setup definimos los métodos que deben llamarse al crear el servicio.

services:
	database:
		create: PDO(%dsn%, %user%, %password%)
		setup:
			- setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)

En PHP esto tendría este aspecto:

public function createServiceDatabase(): PDO
{
	$service = new PDO('...', '...', '...');
	$service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
	return $service;
}

Además de llamar a métodos, también se pueden asignar valores a propiedades. También se admite añadir elementos a arrays, para lo que hay que encerrar el acceso al array entre comillas para evitar conflictos con la sintaxis de NEON:

services:
	foo:
		create: Foo
		setup:
			- $value = 123
			- '$onClick[]' = [@bar, clickHandler]

Lo que en código PHP tendría este aspecto:

public function createServiceFoo(): Foo
{
	$service = new Foo;
	$service->value = 123;
	$service->onClick[] = [$this->getService('bar'), 'clickHandler'];
	return $service;
}

En el setup puede, sin embargo, llamar también a métodos estáticos o a métodos de otros servicios. Si necesita pasar como argumento el propio servicio actual, refiérase a él con @self:

services:
	foo:
		create: Foo
		setup:
			- My\Helpers::initializeFoo(@self)
			- @anotherService::setFoo(@self)

Fíjese en que, por simplicidad, se usa :: en lugar de ->, véase Lenguaje de expresiones. Se generará este método factory:

public function createServiceFoo(): Foo
{
	$service = new Foo;
	My\Helpers::initializeFoo($service);
	$this->getService('anotherService')->setFoo($service);
	return $service;
}

Lenguaje de expresiones

Nette DI ofrece un lenguaje de expresiones excepcionalmente rico con el que podemos definir casi cualquier cosa. En los archivos de configuración podemos usar así parámetros:

# parámetro
%wwwDir%

# valor de un parámetro bajo una clave
%mailer.user%

# parámetro dentro de una cadena
'%wwwDir%/images'

Además, crear objetos y llamar a métodos y funciones:

# crear un objeto
DateTime()

# llamar a un método estático
Collator::create(%locale%)

# llamar a una función de PHP
::getenv(DB_USER)

Referirse a los servicios por su nombre o por su tipo:

# servicio por nombre
@database

# servicio por tipo
@Nette\Database\Connection

Usar la sintaxis first-class callable:

# crear un callback, equivalente a [@user, logout]
@user::logout(...)

Usar constantes:

# constante de clase
FilesystemIterator::SKIP_DOTS

# obtener una constante global con la función constant() de PHP
::constant(\PHP_VERSION)

Acceder a las propiedades públicas y a las constantes de un servicio con @servicio::miembro. Si el nombre se resuelve como propiedad o como constante lo decide su primera letra: una inicial minúscula significa propiedad pública, una mayúscula significa constante:

# propiedad pública de un servicio (empieza por minúscula)
@settings::apiUrl

# constante de clase de un servicio (empieza por mayúscula)
@settings::Version

Las llamadas a métodos se pueden encadenar igual que en PHP. Por simplicidad se usa :: en lugar de ->:

DateTime()::format('Y-m-d')
# PHP: (new DateTime())->format('Y-m-d')

@http.request::getUrl()::getHost()
# PHP: $this->getService('http.request')->getUrl()->getHost()

Puede usar estas expresiones en cualquier sitio: al crear servicios, en los Argumentos, en la sección Setup o en los parámetros:

parameters:
	ipAddress: @http.request::getRemoteAddress()

services:
	database:
		create: DatabaseFactory::create( @anotherService::getDsn() )
		setup:
			- initialize( ::getenv('DB_USER') )

Funciones especiales

En los archivos de configuración puede usar las siguientes funciones especiales:

  • not() niega un valor
  • bool(), int(), float(), string() conversión sin pérdida al tipo indicado
  • typed() crea un array de todos los servicios del tipo indicado
  • tagged() crea un array de todos los servicios con la etiqueta dada
services:
	- Foo(
		id: int(::getenv('ProjectId'))
		productionMode: not(%debugMode%)
	)

A diferencia de la conversión estándar de PHP, como (int), la conversión sin pérdida lanza una excepción para valores no numéricos.

La función typed() crea un array de todos los servicios del tipo indicado (clase o interfaz). Excluye los servicios que tienen el autowiring desactivado. También se pueden indicar varios tipos separados por comas.

services:
	- BarsDependent( typed(Bar) )

Un array de servicios de un determinado tipo también se puede pasar como argumento automáticamente mediante autowiring.

La función tagged() crea un array de todos los servicios con una etiqueta concreta. También aquí puede indicar varias etiquetas separadas por comas.

services:
	- LoggersDependent( tagged(logger) )

Autowiring

La clave autowired le permite influir en el comportamiento del autowiring para un servicio concreto. Los detalles están en el capítulo sobre autowiring.

services:
	foo:
		create: Foo
		autowired: false     # el servicio foo queda excluido del autowiring

Servicios lazy

La carga lazy es una técnica que aplaza la creación de un servicio hasta que realmente se necesita. En la configuración global puede activar la creación lazy para todos los servicios de una vez. Para los servicios concretos puede después modificar ese comportamiento:

services:
	foo:
		create: Foo
		lazy: false

Cuando un servicio se define como lazy, al pedirlo al contenedor DI recibimos un objeto proxy especial. Ese proxy tiene el mismo aspecto y se comporta igual que el servicio real, pero la inicialización real (la llamada al constructor y las llamadas del setup) ocurre solo en el primer acceso a alguno de sus métodos o propiedades.

Tenga en cuenta que, como el servicio se crea más tarde, los errores de su configuración también se manifiestan más tarde. Por ejemplo, unas credenciales de base de datos incorrectas no se revelarán al arrancar la aplicación, sino en la primera consulta.

La creación lazy también mitiga las dependencias circulares, es decir, la situación en la que el servicio A necesita el servicio B y B necesita a la vez A. Sin ella, el contenedor informa del error Circular reference detected. Con un proxy lazy, el servicio A recibe solo un proxy del servicio B, que se inicializa cuando realmente se usa, en un momento en el que A ya existe. Aun así, una dependencia circular señala un diseño defectuoso y es mejor deshacerse de ella.

La carga lazy requiere PHP 8.4 o superior y funciona solo para servicios creados instanciando directamente una clase (p. ej. create: Foo), no para los creados por un método factory. Tampoco se puede usar para clases que en última instancia extienden una clase interna de PHP. Cuando la carga lazy no se puede aplicar, la bandera lazy: true se ignora en silencio.

Etiquetas

Las etiquetas sirven para añadir información complementaria a los servicios. Puede asignar una o varias etiquetas a un servicio:

services:
	foo:
		create: Foo
		tags:
			- cached

Las etiquetas también pueden llevar valores:

services:
	foo:
		create: Foo
		tags:
			logger: monolog.logger.event

Para obtener todos los servicios asociados a determinadas etiquetas puede usar la función tagged():

services:
	- LoggersDependent( tagged(logger) )

Dentro del contenedor DI puede obtener los nombres de todos los servicios con una etiqueta concreta con el método findByTag():

$names = $container->findByTag('logger');
// $names es un array con los nombres de los servicios como claves y los valores de la etiqueta como valores
// p. ej. ['foo' => 'monolog.logger.event', ...]

Modo inject

La bandera inject: true activa la inyección de dependencias mediante propiedades públicas con el atributo Inject y mediante los métodos inject*().

services:
	articles:
		create: App\Model\Articles
		inject: true

De forma predeterminada, el modo inject está activado solo para los presenters.

Modificación de servicios

El contenedor DI contiene numerosos servicios añadidos por las extensiones integradas o propias. Puede modificar las definiciones de esos servicios existentes directamente en la configuración. Por ejemplo, puede cambiar la clase del servicio application.application, que por defecto es Nette\Application\Application, por otra:

services:
	application.application:
		create: MyApplication
		alteration: true

La bandera alteration indica que solo estamos modificando un servicio existente. También actúa como salvaguarda: si el servicio que se modifica no existe, la compilación falla con una excepción.

También podemos completar el setup:

services:
	application.application:
		create: MyApplication
		alteration: true
		setup:
			- '$onStartup[]' = [@resource, init]

No tiene que identificar el servicio por su nombre interno: puede referirse a él por su tipo. El ejemplo anterior también se puede escribir así:

services:
	@Nette\Application\Application:
		create: MyApplication

Al modificar un servicio quizá queramos eliminar los argumentos, los elementos del setup o las etiquetas originales, con la clave reset:

services:
	application.application:
		create: MyApplication
		alteration: true
		reset:
			arguments: true
			setup: true
			tags: true

Si quiere eliminar un servicio añadido por una extensión, puede hacerlo así:

services:
	cache.journal: false
versión: 3.x