Definiowanie usług
W konfiguracji instruujemy kontener DI, jak ma tworzyć poszczególne usługi i jak łączyć je z ich zależnościami. Nette oferuje na to bardzo przejrzysty i elegancki sposób.
Sekcja services w pliku konfiguracyjnym NEON to miejsce, w którym definiujemy własne usługi i ich
konfigurację. Spójrzmy na prosty przykład definiujący usługę o nazwie database, reprezentującą instancję
klasy PDO:
services:
database: PDO('sqlite::memory:')
Powyższa konfiguracja daje w kontenerze DI następującą metodę fabryczną:
public function createServiceDatabase(): PDO
{
return new PDO('sqlite::memory:');
}
Nazwy usług pozwalają odwoływać się do nich w innych częściach pliku konfiguracyjnego, w formacie
@nazwaUsługi. Jeśli nie ma potrzeby nadawania usłudze nazwy, możemy po prostu użyć myślnika
(-):
services:
- PDO('sqlite::memory:')
Aby pobrać usługę z kontenera DI, możemy użyć metody getService() z nazwą usługi jako parametrem albo
metody getByType() z typem usługi:
$database = $container->getService('database');
$database = $container->getByType(PDO::class);
Tworzenie usługi
Zwykle tworzymy usługę po prostu przez utworzenie instancji konkretnej klasy. Na przykład:
services:
database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret)
Jeśli potrzebujemy rozbudować konfigurację o kolejne klucze, definicję można rozbić na wiele wierszy:
services:
database:
create: PDO('sqlite::memory:')
setup: ...
Klucz create ma alias factory; oba warianty są powszechnie używane. Zalecamy jednak
create.
Argumenty konstruktora albo metody fabrycznej można alternatywnie podać kluczem arguments:
services:
database:
create: PDO
arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret]
Usługi nie muszą powstawać przez zwykłe utworzenie instancji klasy; mogą też być wynikiem wywołania metod statycznych albo metod innych usług:
services:
database: DatabaseFactory::create()
router: @routerFactory::create()
Zwróć uwagę, że dla uproszczenia zamiast -> używa się ::, zobacz Język wyrażeń. Wygenerowane zostaną takie metody fabryczne:
public function createServiceDatabase(): PDO
{
return DatabaseFactory::create();
}
public function createServiceRouter(): RouteList
{
return $this->getService('routerFactory')->create();
}
Kontener DI musi znać typ tworzonej usługi. Jeśli tworzymy usługę metodą, która nie ma podanego typu zwracanego, musimy podać ten typ jawnie w konfiguracji:
services:
database:
create: DatabaseFactory::create()
type: PDO
Argumenty
Argumenty do konstruktorów i metod przekazujemy bardzo podobnie jak w samym PHP:
services:
database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret)
Dla lepszej czytelności możemy wypisać argumenty w osobnych wierszach. W takim przypadku używanie przecinków staje się opcjonalne:
services:
database: PDO(
'mysql:host=127.0.0.1;dbname=test'
root
secret
)
Argumenty możesz też nazwać, dzięki czemu nie musisz przejmować się ich kolejnością:
services:
database: PDO(
username: root
password: secret
dsn: 'mysql:host=127.0.0.1;dbname=test'
)
Jeśli chcesz pominąć niektóre argumenty i użyć ich wartości domyślnych albo pozwolić, aby usługa została
wstrzyknięta przez autowiring, użyj podkreślenia
(_):
services:
foo: Foo(_, %appDir%)
Argumenty mogą zawierać usługi, parametry i wiele więcej, zobacz Język wyrażeń.
Setup
W sekcji setup definiujemy metody, które mają zostać wywołane przy tworzeniu usługi.
services:
database:
create: PDO(%dsn%, %user%, %password%)
setup:
- setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)
W PHP wyglądałoby to tak:
public function createServiceDatabase(): PDO
{
$service = new PDO('...', '...', '...');
$service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $service;
}
Poza wywołaniami metod można też przypisywać wartości do właściwości. Obsługiwane jest również dodawanie elementów do tablic, co wymaga ujęcia dostępu do tablicy w cudzysłowy, aby uniknąć konfliktu ze składnią NEON:
services:
foo:
create: Foo
setup:
- $value = 123
- '$onClick[]' = [@bar, clickHandler]
W kodzie PHP wyglądałoby to następująco:
public function createServiceFoo(): Foo
{
$service = new Foo;
$service->value = 123;
$service->onClick[] = [$this->getService('bar'), 'clickHandler'];
return $service;
}
W setupie możesz jednak wywoływać również metody statyczne albo metody innych usług. Jeśli potrzebujesz przekazać jako
argument samą bieżącą usługę, odwołaj się do niej przez @self:
services:
foo:
create: Foo
setup:
- My\Helpers::initializeFoo(@self)
- @anotherService::setFoo(@self)
Zwróć uwagę, że dla uproszczenia zamiast -> używa się ::, zobacz Język wyrażeń. Wygenerowana zostanie taka metoda fabryczna:
public function createServiceFoo(): Foo
{
$service = new Foo;
My\Helpers::initializeFoo($service);
$this->getService('anotherService')->setFoo($service);
return $service;
}
Język wyrażeń
Nette DI udostępnia wyjątkowo bogaty język wyrażeń, którym możemy zdefiniować niemal wszystko. W plikach konfiguracyjnych możemy więc używać parametrów:
# parametr
%wwwDir%
# wartość parametru pod kluczem
%mailer.user%
# parametr wewnątrz stringa
'%wwwDir%/images'
Ponadto tworzyć obiekty, wywoływać metody i funkcje:
# utworzenie obiektu
DateTime()
# wywołanie metody statycznej
Collator::create(%locale%)
# wywołanie funkcji PHP
::getenv(DB_USER)
Odwoływać się do usług po nazwie albo po typie:
# usługa po nazwie
@database
# usługa po typie
@Nette\Database\Connection
Używać składni first-class callable:
# utworzenie callbacku, odpowiednik [@user, logout]
@user::logout(...)
Używać stałych:
# stała klasowa
FilesystemIterator::SKIP_DOTS
# pobranie stałej globalnej funkcją PHP constant()
::constant(\PHP_VERSION)
Sięgać po publiczne właściwości i stałe usługi przez @service::member. O tym, czy nazwa oznacza
właściwość, czy stałą, decyduje jej pierwsza litera – mała oznacza właściwość publiczną, wielka
oznacza stałą:
# właściwość publiczna usługi (zaczyna się małą literą)
@settings::apiUrl
# stała klasowa usługi (zaczyna się wielką literą)
@settings::Version
Wywołania metod można łączyć w łańcuch, tak jak w PHP. Dla uproszczenia zamiast -> używa się
:::
DateTime()::format('Y-m-d')
# PHP: (new DateTime())->format('Y-m-d')
@http.request::getUrl()::getHost()
# PHP: $this->getService('http.request')->getUrl()->getHost()
Wyrażeń tych możesz używać wszędzie: przy tworzeniu usług, w argumentach, w sekcji setup albo w parametrach:
parameters:
ipAddress: @http.request::getRemoteAddress()
services:
database:
create: DatabaseFactory::create( @anotherService::getDsn() )
setup:
- initialize( ::getenv('DB_USER') )
Funkcje specjalne
W plikach konfiguracyjnych możesz używać następujących funkcji specjalnych:
not()neguje wartośćbool(),int(),float(),string()bezstratne rzutowanie na podany typtyped()tworzy tablicę wszystkich usług podanego typutagged()tworzy tablicę wszystkich usług z danym tagiem
services:
- Foo(
id: int(::getenv('ProjectId'))
productionMode: not(%debugMode%)
)
W odróżnieniu od standardowego rzutowania PHP, jak (int), rzutowanie bezstratne zgłasza wyjątek dla wartości
nieliczbowych.
Funkcja typed() tworzy tablicę wszystkich usług podanego typu (klasy albo interfejsu). Pomija usługi
z wyłączonym autowiringiem. Można podać również kilka typów, oddzielonych przecinkami.
services:
- BarsDependent( typed(Bar) )
Tablicę usług określonego typu można też przekazać jako argument automatycznie, za pomocą autowiringu.
Funkcja tagged() tworzy z kolei tablicę wszystkich usług z określonym tagiem. Również tutaj możesz podać
kilka tagów oddzielonych przecinkami.
services:
- LoggersDependent( tagged(logger) )
Autowiring
Klucz autowired pozwala wpłynąć na zachowanie autowiringu dla konkretnej usługi. Szczegóły znajdziesz w rozdziale o autowiringu.
services:
foo:
create: Foo
autowired: false # usługa foo jest wyłączona z autowiringu
Usługi leniwe
Leniwe ładowanie to technika odraczająca utworzenie usługi do chwili, gdy jest ona faktycznie potrzebna. W konfiguracji globalnej możesz włączyć leniwe tworzenie dla wszystkich usług naraz. Dla poszczególnych usług możesz potem to zachowanie nadpisać:
services:
foo:
create: Foo
lazy: false
Gdy usługa jest zdefiniowana jako leniwa, przy prośbie o nią do kontenera DI otrzymujemy specjalny obiekt proxy. Proxy to wygląda i zachowuje się identycznie jak prawdziwa usługa, ale faktyczna inicjalizacja (wywołanie konstruktora i wywołania setupu) następuje dopiero przy pierwszym dostępie do którejkolwiek z jej metod albo właściwości.
Pamiętaj, że skoro usługa tworzona jest później, również błędy w jej konfiguracji ujawniają się później. Na przykład nieprawidłowe poświadczenia bazy danych nie ujawnią się przy starcie aplikacji, lecz dopiero przy pierwszym zapytaniu.
Leniwe tworzenie łagodzi też zależności cykliczne, czyli sytuację, w której usługa A wymaga usługi B, a B
jednocześnie wymaga A. Bez niego kontener zgłasza błąd Circular reference detected. Z leniwym proxy usługa
A otrzymuje tylko proxy usługi B, które inicjalizuje się przy faktycznym użyciu, w momencie, gdy A już istnieje. Mimo to
zależność cykliczna sygnalizuje wadliwy projekt i lepiej się jej pozbyć.
Leniwe ładowanie wymaga PHP 8.4 lub nowszego i działa tylko dla usług tworzonych przez bezpośrednie
utworzenie instancji klasy (np. create: Foo), a nie dla tych tworzonych metodą fabryczną. Nie da się go też
użyć dla klas, które ostatecznie rozszerzają wewnętrzną klasę PHP. Gdy leniwego ładowania nie da się zastosować, flaga
lazy: true jest po cichu ignorowana.
Tagi
Tagi służą do dodawania usługom dodatkowych informacji. Usłudze możesz przypisać jeden albo więcej tagów:
services:
foo:
create: Foo
tags:
- cached
Tagi mogą też przechowywać wartości:
services:
foo:
create: Foo
tags:
logger: monolog.logger.event
Aby pobrać wszystkie usługi powiązane z określonymi tagami, możesz użyć funkcji tagged():
services:
- LoggersDependent( tagged(logger) )
W kontenerze DI nazwy wszystkich usług z określonym tagiem pobierzesz metodą findByTag():
$names = $container->findByTag('logger');
// $names to tablica z nazwami usług jako kluczami i wartościami tagów jako wartościami
// np. ['foo' => 'monolog.logger.event', ...]
Tryb inject
Flaga inject: true włącza wstrzykiwanie zależności przez właściwości publiczne z atrybutem Inject i metody inject*().
services:
articles:
create: App\Model\Articles
inject: true
Domyślnie tryb inject włączony jest tylko dla presenterów.
Modyfikowanie usług
Kontener DI zawiera liczne usługi dodane przez rozszerzenia wbudowane albo użytkownika. Definicje tych istniejących usług możesz
modyfikować bezpośrednio w konfiguracji. Możesz na przykład zmienić klasę usługi application.application,
którą domyślnie jest Nette\Application\Application, na inną:
services:
application.application:
create: MyApplication
alteration: true
Flaga alteration wskazuje, że jedynie modyfikujemy istniejącą usługę. Działa też jako zabezpieczenie:
jeśli modyfikowana usługa nie istnieje, kompilacja kończy się wyjątkiem.
Możemy też uzupełnić setup:
services:
application.application:
create: MyApplication
alteration: true
setup:
- '$onStartup[]' = [@resource, init]
Usługi nie musisz identyfikować jej wewnętrzną nazwą – możesz odwołać się do niej przez typ. Poprzedni przykład można zapisać także tak:
services:
@Nette\Application\Application:
create: MyApplication
Modyfikując usługę, możemy chcieć usunąć pierwotne argumenty, elementy setupu albo tagi, używając klucza
reset:
services:
application.application:
create: MyApplication
alteration: true
reset:
arguments: true
setup: true
tags: true
Jeśli chcesz usunąć usługę dodaną przez rozszerzenie, możesz zrobić to tak:
services:
cache.journal: false