Définition des services

La configuration est l'endroit où nous apprenons au conteneur DI comment créer chaque service et comment le relier à ses dépendances. Nette propose pour cela une manière très claire et élégante.

La section services du fichier de configuration NEON est l'endroit où nous définissons nos propres services et leur configuration. Regardons un exemple simple qui définit un service nommé database, représentant une instance de la classe PDO :

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

La configuration ci-dessus produit la méthode fabrique suivante dans le conteneur DI :

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

Les noms des services permettent de les référencer dans d'autres parties du fichier de configuration, sous la forme @nomDuService. S'il n'est pas nécessaire de donner un nom au service, nous pouvons simplement utiliser un tiret (-) :

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

Pour récupérer un service depuis le conteneur DI, nous pouvons utiliser la méthode getService() avec le nom du service en paramètre, ou la méthode getByType() avec le type du service :

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

Création de services

Habituellement, nous créons un service simplement en instanciant une classe précise. Par exemple :

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

Si nous avons besoin d'étoffer la configuration avec d'autres clés, la définition peut s'étaler sur plusieurs lignes :

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

La clé create a un alias factory ; les deux variantes sont couramment utilisées. Nous recommandons cependant create.

Les arguments du constructeur ou de la méthode fabrique peuvent également être indiqués à l'aide de la clé arguments :

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

Les services ne doivent pas nécessairement être créés par simple instanciation d'une classe ; ils peuvent aussi être le résultat de l'appel de méthodes statiques ou de méthodes d'autres services :

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

Notez que, par souci de simplicité, on écrit :: au lieu de ->, voir Langage d'expressions. Ces méthodes fabriques seront générées :

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

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

Le conteneur DI a besoin de connaître le type du service créé. Si nous créons un service à l'aide d'une méthode qui n'a pas de type de retour déclaré, nous devons indiquer ce type explicitement dans la configuration :

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

Arguments

Nous passons les arguments aux constructeurs et aux méthodes d'une manière très proche de celle de PHP lui-même :

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

Pour une meilleure lisibilité, nous pouvons énumérer les arguments sur des lignes distinctes. Dans ce cas, les virgules deviennent facultatives :

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

Vous pouvez aussi nommer les arguments, ce qui vous dispense de vous soucier de leur ordre :

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

Si vous voulez omettre certains arguments et utiliser leur valeur par défaut, ou faire injecter un service par autowiring, utilisez un tiret bas (_) :

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

Les arguments peuvent contenir des services, des paramètres et bien plus encore, voir Langage d'expressions.

Setup

Dans la section setup, nous définissons les méthodes qui doivent être appelées lors de la création du service.

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

En PHP, cela donnerait ceci :

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

Outre les appels de méthodes, il est aussi possible d'affecter des valeurs à des propriétés. L'ajout d'éléments à des tableaux est également pris en charge ; il faut alors mettre l'accès au tableau entre guillemets pour éviter tout conflit avec la syntaxe NEON :

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

Ce qui, en code PHP, donnerait ceci :

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

Dans le setup, vous pouvez cependant aussi appeler des méthodes statiques ou des méthodes d'autres services. Si vous avez besoin de passer le service courant lui-même en argument, référencez-le par @self :

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

Notez que, par souci de simplicité, on écrit :: au lieu de ->, voir Langage d'expressions. La méthode fabrique suivante sera générée :

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

Langage d'expressions

Nette DI propose un langage d'expressions exceptionnellement riche, qui nous permet de définir presque n'importe quoi. Dans les fichiers de configuration, nous pouvons ainsi utiliser des paramètres :

# paramètre
%wwwDir%

# valeur d'un paramètre sous une clé
%mailer.user%

# paramètre à l'intérieur d'une chaîne
'%wwwDir%/images'

Nous pouvons également créer des objets, appeler des méthodes et des fonctions :

# créer un objet
DateTime()

# appeler une méthode statique
Collator::create(%locale%)

# appeler une fonction PHP
::getenv(DB_USER)

Référencer les services par leur nom ou par leur type :

# service par son nom
@database

# service par son type
@Nette\Database\Connection

Utiliser la syntaxe first-class callable :

# créer un callback, équivalent à [@user, logout]
@user::logout(...)

Utiliser des constantes :

# constante de classe
FilesystemIterator::SKIP_DOTS

# obtenir une constante globale à l'aide de la fonction PHP constant()
::constant(\PHP_VERSION)

Accéder aux propriétés publiques et aux constantes d'un service via @service::membre. C'est la première lettre du nom qui décide s'il s'agit d'une propriété ou d'une constante : une minuscule initiale signifie une propriété publique, une majuscule une constante :

# propriété publique d'un service (commence par une minuscule)
@settings::apiUrl

# constante de classe d'un service (commence par une majuscule)
@settings::Version

Les appels de méthodes peuvent être chaînés comme en PHP. Par souci de simplicité, on écrit :: au lieu de -> :

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

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

Vous pouvez utiliser ces expressions partout : lors de la création des services, dans les Arguments, dans la section Setup ou dans les paramètres :

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

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

Fonctions spéciales

Dans les fichiers de configuration, vous pouvez utiliser les fonctions spéciales suivantes :

  • not() inverse une valeur
  • bool(), int(), float(), string() conversion sans perte vers le type indiqué
  • typed() crée un tableau de tous les services du type indiqué
  • tagged() crée un tableau de tous les services portant le tag donné
services:
	- Foo(
		id: int(::getenv('ProjectId'))
		productionMode: not(%debugMode%)
	)

Contrairement à la conversion standard de PHP, comme (int), la conversion sans perte lève une exception pour les valeurs non numériques.

La fonction typed() crée un tableau de tous les services du type indiqué (classe ou interface). Elle exclut les services dont l'autowiring est désactivé. Il est aussi possible d'indiquer plusieurs types, séparés par des virgules.

services:
	- BarsDependent( typed(Bar) )

Un tableau de services d'un certain type peut aussi être passé automatiquement en argument grâce à l'autowiring.

La fonction tagged() crée quant à elle un tableau de tous les services portant un tag précis. Là encore, vous pouvez indiquer plusieurs tags séparés par des virgules.

services:
	- LoggersDependent( tagged(logger) )

Autowiring

La clé autowired vous permet d'influencer le comportement de l'autowiring pour un service donné. Pour les détails, voir le chapitre sur l'autowiring.

services:
	foo:
		create: Foo
		autowired: false     # le service foo est exclu de l'autowiring

Services lazy

Le chargement lazy est une technique qui diffère la création d'un service jusqu'à ce qu'il soit réellement nécessaire. Dans la configuration globale, vous pouvez activer la création lazy pour tous les services d'un coup. Pour chaque service, vous pouvez ensuite redéfinir ce comportement :

services:
	foo:
		create: Foo
		lazy: false

Lorsqu'un service est défini comme lazy, nous recevons, en le demandant au conteneur DI, un objet proxy particulier. Ce proxy a l'apparence et le comportement du service réel, mais l'initialisation effective (l'appel du constructeur et des appels de setup) n'a lieu qu'au premier accès à l'une de ses méthodes ou propriétés.

Gardez à l'esprit que, le service étant créé plus tard, les erreurs de sa configuration se manifestent elles aussi plus tard. Par exemple, des identifiants de base de données incorrects ne se révéleront pas au démarrage de l'application, mais seulement à la première requête.

La création lazy atténue également les dépendances circulaires, c'est-à-dire la situation où le service A a besoin du service B et où B a en même temps besoin de A. Sans elle, le conteneur signale l'erreur Circular reference detected. Avec un proxy lazy, le service A ne reçoit qu'un proxy du service B, qui s'initialise au moment où il est réellement utilisé, alors que A existe déjà. Une dépendance circulaire signale néanmoins une conception défaillante et il vaut mieux s'en débarrasser.

Le chargement lazy nécessite PHP 8.4 ou plus récent et ne fonctionne que pour les services créés par instanciation directe d'une classe (par exemple create: Foo), pas pour ceux créés par une méthode fabrique. Il ne peut pas non plus être utilisé pour les classes qui, en fin de compte, étendent une classe interne de PHP. Lorsque le chargement lazy ne peut pas s'appliquer, le drapeau lazy: true est ignoré silencieusement.

Tags

Les tags servent à ajouter des informations complémentaires aux services. Vous pouvez attribuer un ou plusieurs tags à un service :

services:
	foo:
		create: Foo
		tags:
			- cached

Les tags peuvent aussi porter des valeurs :

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

Pour récupérer tous les services associés à des tags précis, vous pouvez utiliser la fonction tagged() :

services:
	- LoggersDependent( tagged(logger) )

Au sein du conteneur DI, vous pouvez obtenir les noms de tous les services portant un tag précis à l'aide de la méthode findByTag() :

$names = $container->findByTag('logger');
// $names est un tableau dont les clés sont les noms des services et les valeurs celles des tags
// par ex. ['foo' => 'monolog.logger.event', ...]

Mode inject

Le drapeau inject: true active l'injection de dépendances par des propriétés publiques portant l'attribut Inject et par les méthodes inject*().

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

Par défaut, le mode inject n'est activé que pour les presenters.

Modification des services

Le conteneur DI contient de nombreux services ajoutés par des extensions intégrées ou écrites par l'utilisateur. Vous pouvez modifier les définitions de ces services existants directement dans la configuration. Vous pouvez par exemple remplacer la classe du service application.application, qui est par défaut Nette\Application\Application, par une autre :

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

Le drapeau alteration indique que nous ne faisons que modifier un service existant. Il joue aussi le rôle de garde-fou : si le service modifié n'existe pas, la compilation échoue avec une exception.

Nous pouvons aussi compléter le setup :

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

Vous n'êtes pas obligé d'identifier un service par son nom interne – vous pouvez le désigner par son type. L'exemple précédent peut aussi s'écrire ainsi :

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

Lors de la modification d'un service, nous pouvons vouloir supprimer les arguments, les éléments de setup ou les tags d'origine, à l'aide de la clé reset :

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

Si vous voulez supprimer un service ajouté par une extension, vous pouvez procéder ainsi :

services:
	cache.journal: false
version: 3.x