Nette Documentation Preview

syntax
Co to jest kontener DI?
***********************

.[perex]
Dependency injection container (DIC) to klasa, która może instancjonować i konfigurować obiekty.

Może cię to zaskoczyć, ale w wielu przypadkach nie potrzebujesz kontenera wtrysku zależności, aby skorzystać z wtrysku zależności (w skrócie DI). Przecież nawet w [poprzednim rozdziale |introduction] pokazywaliśmy konkretne przykłady DI i żaden kontener nie był potrzebny.

Jeśli jednak musisz zarządzać dużą liczbą różnych obiektów z wieloma zależnościami, kontener wtrysku zależności będzie naprawdę przydatny. Co ma miejsce np. w przypadku aplikacji internetowych zbudowanych na frameworku.

W poprzednim rozdziale przedstawiliśmy klasy `Article` i `UserController`. Obie mają pewne zależności, a mianowicie bazę danych i fabrykę `ArticleFactory`. I dla tych klas stworzymy teraz kontener. Oczywiście dla tak prostego przykładu posiadanie kontenera nie ma sensu. Ale stworzymy jeden, aby pokazać jak to wygląda i działa.

Oto prosty kontener hardcoded dla tego przykładu:

```php
class Container
{
	public function createDatabase(): Nette\Database\Connection
	{
		return new Nette\Database\Connection('mysql:', 'root', '***');
	}

	public function createArticleFactory(): ArticleFactory
	{
		return new ArticleFactory($this->createDatabase());
	}

	public function createUserController(): UserController
	{
		return new UserController($this->createArticleFactory());
	}
}
```

Użycie wyglądałoby tak:

```php
$container = new Container;
$controller = $container->createUserController();
```

Po prostu pytamy kontener o obiekt i nie musimy nic wiedzieć o tym, jak go stworzyć i jakie są jego zależności; kontener wie to wszystko. Zależności są wstrzykiwane automatycznie przez kontener. To jest jego moc.

Do tej pory kontener zapisywał wszystkie dane na górze. Robimy więc kolejny krok i dodajemy parametry, aby kontener był naprawdę użyteczny:

```php
class Container
{
	public function __construct(
		private array $parameters,
	) {
	}

	public function createDatabase(): Nette\Database\Connection
	{
		return new Nette\Database\Connection(
			$this->parameters['db.dsn'],
			$this->parameters['db.user'],
			$this->parameters['db.password'],
		);
	}

	// ...
}

$container = new Container([
	'db.dsn' => 'mysql:',
	'db.user' => 'root',
	'db.password' => '***',
]);
```

Uważni czytelnicy mogli zauważyć pewien problem. Za każdym razem, gdy otrzymuję obiekt `UserController`, tworzona jest również nowa instancja `ArticleFactory` i baza danych. Z pewnością tego nie chcemy.

Dodamy więc metodę `getService()`, która będzie zwracała w kółko te same instancje:

```php
class Container
{
	private array $services = [];

	public function __construct(
		private array $parameters,
	) {
	}

	public function getService(string $name): object
	{
		if (!isset($this->services[$name])) {
			// getService('Database') bude volat createDatabase()
			$method = 'create' . $name;
			$this->services[$name] = $this->$method();
		}
		return $this->services[$name];
	}

	// ...
}
```

Przy pierwszym wywołaniu np. `$container->getService('Database')` zleci `createDatabase()` stworzenie obiektu bazy danych, który zapisze w tablicy `$services` i zwróci go bezpośrednio przy kolejnym wywołaniu.

Modyfikujemy też resztę kontenera, aby korzystał z `getService()`:

```php
class Container
{
	// ...

	public function createArticleFactory(): ArticleFactory
	{
		return new ArticleFactory($this->getService('Database'));
	}

	public function createUserController(): UserController
	{
		return new UserController($this->getService('ArticleFactory'));
	}
}
```

Przy okazji, termin usługa odnosi się do dowolnego obiektu zarządzanego przez kontener. Stąd nazwa metody `getService()`.

Zrobione. Mamy w pełni funkcjonalny kontener DI! I możemy z niego korzystać:

```php
$container = new Container([
	'db.dsn' => 'mysql:',
	'db.user' => 'root',
	'db.password' => '***',
]);

$controller = $container->getService('UserController');
$database = $container->getService('Database');
```

Jak widać, napisanie DIC nie jest trudne. Warto pamiętać, że same obiekty nie wiedzą, że tworzy je kontener. Można więc w ten sposób stworzyć dowolny obiekt w PHP bez ingerencji w jego kod źródłowy.

Ręczne tworzenie i utrzymywanie klasy kontenera może dość szybko stać się koszmarem. Dlatego w następnym rozdziale opowiemy o [kontenerze Nette DI Container |nette-container], który może generować i aktualizować się niemal samodzielnie.

Co to jest kontener DI?

Dependency injection container (DIC) to klasa, która może instancjonować i konfigurować obiekty.

Może cię to zaskoczyć, ale w wielu przypadkach nie potrzebujesz kontenera wtrysku zależności, aby skorzystać z wtrysku zależności (w skrócie DI). Przecież nawet w poprzednim rozdziale pokazywaliśmy konkretne przykłady DI i żaden kontener nie był potrzebny.

Jeśli jednak musisz zarządzać dużą liczbą różnych obiektów z wieloma zależnościami, kontener wtrysku zależności będzie naprawdę przydatny. Co ma miejsce np. w przypadku aplikacji internetowych zbudowanych na frameworku.

W poprzednim rozdziale przedstawiliśmy klasy Article i UserController. Obie mają pewne zależności, a mianowicie bazę danych i fabrykę ArticleFactory. I dla tych klas stworzymy teraz kontener. Oczywiście dla tak prostego przykładu posiadanie kontenera nie ma sensu. Ale stworzymy jeden, aby pokazać jak to wygląda i działa.

Oto prosty kontener hardcoded dla tego przykładu:

class Container
{
	public function createDatabase(): Nette\Database\Connection
	{
		return new Nette\Database\Connection('mysql:', 'root', '***');
	}

	public function createArticleFactory(): ArticleFactory
	{
		return new ArticleFactory($this->createDatabase());
	}

	public function createUserController(): UserController
	{
		return new UserController($this->createArticleFactory());
	}
}

Użycie wyglądałoby tak:

$container = new Container;
$controller = $container->createUserController();

Po prostu pytamy kontener o obiekt i nie musimy nic wiedzieć o tym, jak go stworzyć i jakie są jego zależności; kontener wie to wszystko. Zależności są wstrzykiwane automatycznie przez kontener. To jest jego moc.

Do tej pory kontener zapisywał wszystkie dane na górze. Robimy więc kolejny krok i dodajemy parametry, aby kontener był naprawdę użyteczny:

class Container
{
	public function __construct(
		private array $parameters,
	) {
	}

	public function createDatabase(): Nette\Database\Connection
	{
		return new Nette\Database\Connection(
			$this->parameters['db.dsn'],
			$this->parameters['db.user'],
			$this->parameters['db.password'],
		);
	}

	// ...
}

$container = new Container([
	'db.dsn' => 'mysql:',
	'db.user' => 'root',
	'db.password' => '***',
]);

Uważni czytelnicy mogli zauważyć pewien problem. Za każdym razem, gdy otrzymuję obiekt UserController, tworzona jest również nowa instancja ArticleFactory i baza danych. Z pewnością tego nie chcemy.

Dodamy więc metodę getService(), która będzie zwracała w kółko te same instancje:

class Container
{
	private array $services = [];

	public function __construct(
		private array $parameters,
	) {
	}

	public function getService(string $name): object
	{
		if (!isset($this->services[$name])) {
			// getService('Database') bude volat createDatabase()
			$method = 'create' . $name;
			$this->services[$name] = $this->$method();
		}
		return $this->services[$name];
	}

	// ...
}

Przy pierwszym wywołaniu np. $container->getService('Database') zleci createDatabase() stworzenie obiektu bazy danych, który zapisze w tablicy $services i zwróci go bezpośrednio przy kolejnym wywołaniu.

Modyfikujemy też resztę kontenera, aby korzystał z getService():

class Container
{
	// ...

	public function createArticleFactory(): ArticleFactory
	{
		return new ArticleFactory($this->getService('Database'));
	}

	public function createUserController(): UserController
	{
		return new UserController($this->getService('ArticleFactory'));
	}
}

Przy okazji, termin usługa odnosi się do dowolnego obiektu zarządzanego przez kontener. Stąd nazwa metody getService().

Zrobione. Mamy w pełni funkcjonalny kontener DI! I możemy z niego korzystać:

$container = new Container([
	'db.dsn' => 'mysql:',
	'db.user' => 'root',
	'db.password' => '***',
]);

$controller = $container->getService('UserController');
$database = $container->getService('Database');

Jak widać, napisanie DIC nie jest trudne. Warto pamiętać, że same obiekty nie wiedzą, że tworzy je kontener. Można więc w ten sposób stworzyć dowolny obiekt w PHP bez ingerencji w jego kod źródłowy.

Ręczne tworzenie i utrzymywanie klasy kontenera może dość szybko stać się koszmarem. Dlatego w następnym rozdziale opowiemy o kontenerze Nette DI Container, który może generować i aktualizować się niemal samodzielnie.