Service-Definitionen
In der Konfiguration sagen wir dem DI-Container, wie er die einzelnen Services erzeugen und wie er sie mit ihren Abhängigkeiten verbinden soll. Nette bietet dafür einen sehr übersichtlichen und eleganten Weg.
Der Abschnitt services in der NEON-Konfigurationsdatei ist der Ort, an dem wir eigene Services und ihre
Konfiguration definieren. Sehen wir uns ein einfaches Beispiel an, das einen Service namens database definiert, der
eine Instanz der Klasse PDO darstellt:
services:
database: PDO('sqlite::memory:')
Aus der obigen Konfiguration entsteht im DI-Container diese Factory-Methode:
public function createServiceDatabase(): PDO
{
return new PDO('sqlite::memory:');
}
Über die Namen der Services lassen sich diese in anderen Teilen der Konfigurationsdatei referenzieren, und zwar in der Form
@serviceName. Wenn es nicht nötig ist, dem Service einen Namen zu geben, können wir einfach einen Aufzählungspunkt
(-) verwenden:
services:
- PDO('sqlite::memory:')
Um einen Service aus dem DI-Container zu holen, können wir die Methode getService() mit dem Namen des Services
als Parameter verwenden oder die Methode getByType() mit dem Typ des Services:
$database = $container->getService('database');
$database = $container->getByType(PDO::class);
Erstellung eines Services
Üblicherweise erzeugen wir einen Service einfach, indem wir eine bestimmte Klasse instanziieren. Zum Beispiel:
services:
database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret)
Wenn wir die Konfiguration um weitere Schlüssel erweitern müssen, lässt sich die Definition auf mehrere Zeilen aufteilen:
services:
database:
create: PDO('sqlite::memory:')
setup: ...
Der Schlüssel create hat den Alias factory; beide Varianten sind gebräuchlich. Wir empfehlen
jedoch, create zu verwenden.
Die Argumente für den Konstruktor oder die Factory-Methode lassen sich alternativ über den Schlüssel arguments
angeben:
services:
database:
create: PDO
arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret]
Services müssen nicht zwingend durch einfaches Instanziieren einer Klasse entstehen; sie können auch das Ergebnis des Aufrufs statischer Methoden oder von Methoden anderer Services sein:
services:
database: DatabaseFactory::create()
router: @routerFactory::create()
Beachten Sie, dass der Einfachheit halber :: statt -> verwendet wird, siehe Ausdruckssprache. Es entstehen diese Factory-Methoden:
public function createServiceDatabase(): PDO
{
return DatabaseFactory::create();
}
public function createServiceRouter(): RouteList
{
return $this->getService('routerFactory')->create();
}
Der DI-Container muss den Typ des erzeugten Services kennen. Erzeugen wir einen Service über eine Methode ohne angegebenen Rückgabetyp, müssen wir diesen Typ in der Konfiguration ausdrücklich angeben:
services:
database:
create: DatabaseFactory::create()
type: PDO
Argumente
Argumente übergeben wir Konstruktoren und Methoden sehr ähnlich, wie es in PHP selbst geschieht:
services:
database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret)
Für bessere Lesbarkeit lassen sich die Argumente auf einzelne Zeilen schreiben. In diesem Fall sind die Kommas optional:
services:
database: PDO(
'mysql:host=127.0.0.1;dbname=test'
root
secret
)
Sie können die Argumente auch benennen und müssen sich dann nicht um ihre Reihenfolge kümmern:
services:
database: PDO(
username: root
password: secret
dsn: 'mysql:host=127.0.0.1;dbname=test'
)
Wenn Sie bestimmte Argumente weglassen und ihre Standardwerte verwenden oder einen Service über Autowiring einsetzen lassen wollen, verwenden Sie einen
Unterstrich (_):
services:
foo: Foo(_, %appDir%)
Argumente können Services, Parameter und vieles mehr enthalten, siehe Ausdruckssprache.
Setup
Im Abschnitt setup legen wir die Methoden fest, die beim Erzeugen des Services aufgerufen werden sollen.
services:
database:
create: PDO(%dsn%, %user%, %password%)
setup:
- setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)
In PHP sähe das so aus:
public function createServiceDatabase(): PDO
{
$service = new PDO('...', '...', '...');
$service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $service;
}
Neben Methodenaufrufen lassen sich auch Werte an Properties zuweisen. Ebenso wird das Hinzufügen von Elementen zu Arrays unterstützt, wobei der Array-Zugriff in Anführungszeichen stehen muss, um Konflikte mit der NEON-Syntax zu vermeiden:
services:
foo:
create: Foo
setup:
- $value = 123
- '$onClick[]' = [@bar, clickHandler]
Im PHP-Code sähe das so aus:
public function createServiceFoo(): Foo
{
$service = new Foo;
$service->value = 123;
$service->onClick[] = [$this->getService('bar'), 'clickHandler'];
return $service;
}
Im Setup lassen sich aber auch statische Methoden oder Methoden anderer Services aufrufen. Wenn Sie den aktuellen Service
selbst als Argument übergeben müssen, verweisen Sie mit @self auf ihn:
services:
foo:
create: Foo
setup:
- My\Helpers::initializeFoo(@self)
- @anotherService::setFoo(@self)
Beachten Sie, dass der Einfachheit halber :: statt -> verwendet wird, siehe Ausdruckssprache. Es entsteht diese Factory-Methode:
public function createServiceFoo(): Foo
{
$service = new Foo;
My\Helpers::initializeFoo($service);
$this->getService('anotherService')->setFoo($service);
return $service;
}
Ausdruckssprache
Nette DI bietet eine außerordentlich reiche Ausdruckssprache, mit der sich fast alles definieren lässt. In den Konfigurationsdateien können wir also Parameter verwenden:
# Parameter
%wwwDir%
# Wert eines Parameters unter einem Schlüssel
%mailer.user%
# Parameter innerhalb eines Strings
'%wwwDir%/images'
Weiter Objekte erzeugen sowie Methoden und Funktionen aufrufen:
# Objekt erzeugen
DateTime()
# statische Methode aufrufen
Collator::create(%locale%)
# PHP-Funktion aufrufen
::getenv(DB_USER)
Auf Services entweder über ihren Namen oder über ihren Typ verweisen:
# Service nach Namen
@database
# Service nach Typ
@Nette\Database\Connection
Die First-Class-Callable-Syntax verwenden:
# Callback erzeugen, gleichbedeutend mit [@user, logout]
@user::logout(...)
Konstanten verwenden:
# Klassenkonstante
FilesystemIterator::SKIP_DOTS
# globale Konstante über die PHP-Funktion constant() holen
::constant(\PHP_VERSION)
Über @service::member greifen Sie auf öffentliche Properties und Konstanten eines Services zu. Ob der Name eine
Property oder eine Konstante meint, entscheidet sein erster Buchstabe – ein kleiner Anfangsbuchstabe bedeutet eine öffentliche
Property, ein großer eine Konstante:
# öffentliche Property eines Services (beginnt mit einem Kleinbuchstaben)
@settings::apiUrl
# Klassenkonstante eines Services (beginnt mit einem Großbuchstaben)
@settings::Version
Methodenaufrufe lassen sich genau wie in PHP verketten. Der Einfachheit halber wird :: statt ->
verwendet:
DateTime()::format('Y-m-d')
# PHP: (new DateTime())->format('Y-m-d')
@http.request::getUrl()::getHost()
# PHP: $this->getService('http.request')->getUrl()->getHost()
Diese Ausdrücke können Sie überall verwenden, beim Erstellen von Services, in den Argumente, im Abschnitt Setup oder in den Parametern:
parameters:
ipAddress: @http.request::getRemoteAddress()
services:
database:
create: DatabaseFactory::create( @anotherService::getDsn() )
setup:
- initialize( ::getenv('DB_USER') )
Spezielle Funktionen
In den Konfigurationsdateien können Sie die folgenden speziellen Funktionen verwenden:
not()negiert einen Wertbool(),int(),float(),string()verlustfreie Umwandlung in den angegebenen Typtyped()erzeugt ein Array aller Services des angegebenen Typstagged()erzeugt ein Array aller Services mit dem angegebenen Tag
services:
- Foo(
id: int(::getenv('ProjectId'))
productionMode: not(%debugMode%)
)
Anders als die übliche Umwandlung in PHP, etwa (int), wirft die verlustfreie Umwandlung bei nicht numerischen
Werten eine Exception.
Die Funktion typed() erzeugt ein Array aller Services des angegebenen Typs (Klasse oder Interface). Services mit
abgeschaltetem Autowiring lässt sie aus. Es lassen sich auch mehrere Typen angeben, getrennt durch Kommas.
services:
- BarsDependent( typed(Bar) )
Ein Array von Services eines bestimmten Typs lässt sich auch automatisch über Autowiring als Argument übergeben.
Die Funktion tagged() erzeugt dann ein Array aller Services mit einem bestimmten Tag. Auch hier lassen sich
mehrere Tags durch Kommas getrennt angeben.
services:
- LoggersDependent( tagged(logger) )
Autowiring
Über den Schlüssel autowired können Sie das Verhalten des Autowirings für einen bestimmten Service
beeinflussen. Einzelheiten finden Sie im Kapitel über
Autowiring.
services:
foo:
create: Foo
autowired: false # der Service foo ist vom Autowiring ausgenommen
Lazy Services
Lazy Loading ist eine Technik, die das Erzeugen eines Services aufschiebt, bis er tatsächlich gebraucht wird. In der globalen Konfiguration können Sie das lazy Erzeugen für alle Services auf einmal einschalten. Für einzelne Services lässt sich dieses Verhalten dann überschreiben:
services:
foo:
create: Foo
lazy: false
Ist ein Service als lazy definiert, bekommen wir beim Anfordern aus dem DI-Container ein besonderes Proxy-Objekt. Dieses Proxy sieht aus und verhält sich genau wie der tatsächliche Service, die eigentliche Initialisierung (Aufruf des Konstruktors und der Setup-Aufrufe) geschieht aber erst beim ersten Zugriff auf eine seiner Methoden oder Properties.
Denken Sie daran: Weil der Service später erzeugt wird, zeigen sich auch Fehler in seiner Konfiguration später. Falsche Zugangsdaten zur Datenbank verraten sich zum Beispiel nicht beim Start der Anwendung, sondern erst bei der ersten Query.
Das lazy Erzeugen entschärft auch zirkuläre Abhängigkeiten, also die Situation, dass Service A den Service B braucht und B
zugleich A. Ohne es meldet der Container den Fehler Circular reference detected. Mit einem lazy Proxy bekommt Service
A nur ein Proxy von Service B, das sich erst initialisiert, wenn es tatsächlich verwendet wird, also zu einem Zeitpunkt, an dem
A bereits existiert. Trotzdem ist eine zirkuläre Abhängigkeit ein Zeichen für einen fehlerhaften Entwurf, und es ist besser,
sie loszuwerden.
Lazy Loading erfordert PHP 8.4 oder neuer und funktioniert nur für Services, die durch direktes Instanziieren
einer Klasse entstehen (etwa create: Foo), nicht für solche, die eine Factory-Methode erzeugt. Ebenso lässt es sich
nicht für Klassen verwenden, die letztlich von einer internen PHP-Klasse erben. Wenn sich Lazy Loading nicht anwenden lässt,
wird das Flag lazy: true stillschweigend ignoriert.
Tags
Tags dienen dazu, Services um ergänzende Informationen zu erweitern. Sie können einem Service einen oder mehrere Tags zuweisen:
services:
foo:
create: Foo
tags:
- cached
Tags können auch Werte tragen:
services:
foo:
create: Foo
tags:
logger: monolog.logger.event
Um alle Services mit bestimmten Tags zu holen, können Sie die Funktion tagged() verwenden:
services:
- LoggersDependent( tagged(logger) )
Innerhalb des DI-Containers holen Sie die Namen aller Services mit einem bestimmten Tag über die Methode
findByTag():
$names = $container->findByTag('logger');
// $names ist ein Array mit den Namen der Services als Schlüsseln und den Werten der Tags als Werten
// z. B. ['foo' => 'monolog.logger.event', ...]
Inject-Modus
Das Flag inject: true schaltet die Übergabe von Abhängigkeiten über öffentliche Properties mit dem Attribut Inject und über Methoden inject*() ein.
services:
articles:
create: App\Model\Articles
inject: true
Standardmäßig ist der Modus inject nur für Presenter eingeschaltet.
Änderungen an Services
Der DI-Container enthält zahlreiche Services, die über eingebaute oder eigene Extensions hinzugekommen sind. Die Definitionen dieser
bestehenden Services können Sie direkt in der Konfiguration ändern. So können Sie zum Beispiel für den Service
application.application die Klasse, die standardmäßig Nette\Application\Application ist, gegen eine
andere austauschen:
services:
application.application:
create: MyApplication
alteration: true
Das Flag alteration zeigt an, dass wir einen bestehenden Service lediglich ändern. Es dient zugleich als
Absicherung: Existiert der geänderte Service nicht, scheitert die Kompilierung mit einer Exception.
Wir können auch das Setup ergänzen:
services:
application.application:
create: MyApplication
alteration: true
setup:
- '$onStartup[]' = [@resource, init]
Sie müssen einen Service nicht über seinen internen Namen ansprechen – Sie können stattdessen auf seinen Typ verweisen. Das vorige Beispiel lässt sich auch so schreiben:
services:
@Nette\Application\Application:
create: MyApplication
Beim Ändern eines Services möchten wir vielleicht die ursprünglichen Argumente, Setup-Einträge oder Tags entfernen; dafür
gibt es den Schlüssel reset:
services:
application.application:
create: MyApplication
alteration: true
reset:
arguments: true
setup: true
tags: true
Wenn Sie einen Service entfernen wollen, den eine Extension hinzugefügt hat, geht das so:
services:
cache.journal: false