Bootstrapping

Bootstrapping to proces inicjalizacji środowiska aplikacji, tworzenia kontenera wstrzykiwania zależności (DI) i uruchamiania aplikacji. Omówimy:

  • jak klasa Bootstrap inicjalizuje środowisko
  • jak konfiguruje się aplikacje za pomocą plików NEON
  • jak odróżnić tryb produkcyjny od deweloperskiego
  • jak utworzyć i skonfigurować kontener DI

Aplikacje, zarówno webowe, jak i skrypty uruchamiane z wiersza poleceń, zaczynają swoje działanie od jakiejś formy inicjalizacji środowiska. Dawniej odpowiadał za to plik nazywany może include.inc.php, dołączany przez plik początkowy. W nowoczesnych aplikacjach Nette zastąpiła go klasa Bootstrap, którą jako część aplikacji znajdziesz w pliku app/Bootstrap.php. Może wyglądać na przykład tak:

namespace App;

use Nette;
use Nette\Bootstrap\Configurator;

class Bootstrap
{
	private Configurator $configurator;
	private string $rootDir;

	public function __construct()
	{
		$this->rootDir = dirname(__DIR__);
		// konfigurator odpowiada za ustawienie środowiska aplikacji i usług
		$this->configurator = new Configurator;
		// ustawiamy katalog na pliki tymczasowe generowane przez Nette (np. skompilowane szablony)
		$this->configurator->setTempDirectory($this->rootDir . '/temp');
	}

	public function bootWebApplication(): Nette\DI\Container
	{
		$this->initializeEnvironment();
		$this->setupContainer();
		return $this->configurator->createContainer();
	}

	private function initializeEnvironment(): void
	{
		// Nette jest sprytne i tryb deweloperski włącza się automatycznie,
		// albo możesz włączyć go dla konkretnego adresu IP, odkomentowując poniższy wiersz:
		// $this->configurator->setDebugMode('secret@23.75.345.200');

		// włącza Tracy: najlepszy "scyzoryk szwajcarski" do debugowania
		$this->configurator->enableTracy($this->rootDir . '/log');

		// RobotLoader: automatycznie wczytuje wszystkie klasy z wybranego katalogu
		$this->configurator->createRobotLoader()
			->addDirectory(__DIR__)
			->register();
	}

	private function setupContainer(): void
	{
		// wczytujemy pliki konfiguracyjne
		$this->configurator->addConfig($this->rootDir . '/config/common.neon');
	}
}

index.php

W przypadku aplikacji webowych plikiem początkowym jest index.php, leżący w katalogu publicznym www/. Zleca on klasie Bootstrap zainicjowanie środowiska i utworzenie kontenera DI. Następnie pobiera z kontenera usługę Application, która uruchamia aplikację webową:

$bootstrap = new App\Bootstrap;
// inicjalizacja środowiska + utworzenie kontenera DI
$container = $bootstrap->bootWebApplication();
// kontener DI tworzy obiekt Nette\Application\Application
$application = $container->getByType(Nette\Application\Application::class);
// uruchamiamy aplikację Nette i obsługujemy przychodzące żądanie
$application->run();

Obiekt $application podczas obsługi żądania emituje zdarzenia: onStartup, onRequest, onPresenter, onResponse, onShutdown i onError (przy nieobsłużonym wyjątku). Możesz podpiąć do nich handlery, co przydaje się do logowania albo monitorowania całej aplikacji.

Jak widać, w ustawieniu środowiska i utworzeniu kontenera wstrzykiwania zależności (DI) pomaga klasa Nette\Bootstrap\Configurator. Przedstawimy ją teraz szczegółowo.

Tryb deweloperski kontra produkcyjny

Nette zachowuje się różnie w zależności od tego, czy działa na serwerze deweloperskim, czy produkcyjnym:

🛠️ Tryb deweloperski
wyświetla pasek debugowania Tracy z przydatnymi informacjami (zapytania SQL, czas wykonania, zużyta pamięć)
przy błędzie wyświetla szczegółową stronę błędu z wywołaniami funkcji i zawartością zmiennych
automatycznie odświeża cache przy zmianie szablonów Latte, plików konfiguracyjnych itd.
🚀 Tryb produkcyjny
nie wyświetla żadnych informacji debugowych, wszystkie błędy zapisuje do logu
przy błędzie wyświetla ErrorPresenter albo ogólną stronę “Server Error”
cache nigdy nie odświeża się automatycznie!
zoptymalizowany pod kątem szybkości i bezpieczeństwa

Tryb wybierany jest przez autodetekcję, więc zwykle nie trzeba niczego konfigurować ani przełączać ręcznie:

  • tryb deweloperski: na localhoście (adres IP 127.0.0.1 albo ::1), o ile nie ma proxy (czyli nie wykryto jego nagłówka HTTP)
  • tryb produkcyjny: wszędzie indziej

Jeśli chcemy włączyć tryb deweloperski również w innych przypadkach, na przykład dla programistów łączących się z konkretnego adresu IP, użyjemy setDebugMode():

$this->configurator->setDebugMode('23.75.345.200'); // można podać także tablicę adresów IP

Zdecydowanie zalecamy połączenie adresu IP z ciasteczkiem. W ciasteczku nette-debug zapisz tajny token, np. secret1234, i w ten sposób aktywuj tryb deweloperski dla programistów łączących się z konkretnego adresu IP, którzy mają w ciasteczku również wspomniany token:

$this->configurator->setDebugMode('secret1234@23.75.345.200');

Tryb deweloperski możemy też całkowicie wyłączyć, nawet dla localhosta:

$this->configurator->setDebugMode(false);

Zwróć uwagę, że wartość true wymusza włączenie trybu deweloperskiego, co nigdy nie powinno wydarzyć się na serwerze produkcyjnym.

Autodetekcją zajmuje się wewnętrznie statyczna metoda Configurator::detectDebugMode(), którą możesz wywołać także samodzielnie, na przykład aby wykryć tryb deweloperski poza konfiguratorem. Przyjmuje opcjonalną białą listę adresów IP albo nazw komputerów i zwraca, czy bieżące żądanie ma działać w trybie deweloperskim:

$debug = Nette\Bootstrap\Configurator::detectDebugMode('23.75.345.200');

Narzędzie do debugowania Tracy

Dla łatwego debugowania włączymy znakomite narzędzie Tracy. W trybie deweloperskim wizualizuje błędy, a w trybie produkcyjnym loguje je do wskazanego katalogu:

$this->configurator->enableTracy($this->rootDir . '/log');

Pliki tymczasowe

Nette używa cache dla kontenera DI, RobotLoadera, szablonów itd. Dlatego trzeba ustawić ścieżkę do katalogu, w którym cache będzie przechowywany:

$this->configurator->setTempDirectory($this->rootDir . '/temp');

Na Linuksie albo macOS ustaw katalogom log/ i temp/ prawa do zapisu.

RobotLoader

Zwykle będziemy chcieli automatycznie wczytywać klasy za pomocą RobotLoadera, musimy więc go uruchomić i pozwolić mu wczytywać klasy z katalogu, w którym leży Bootstrap.php (czyli __DIR__), oraz ze wszystkich jego podkatalogów:

$this->configurator->createRobotLoader()
	->addDirectory(__DIR__)
	->register();

Alternatywnym podejściem jest wczytywanie klas wyłącznie przez Composera zgodnie z PSR-4.

Strefa czasowa

Za pomocą konfiguratora możesz ustawić domyślną strefę czasową.

$this->configurator->setTimeZone('Europe/Prague');

Konfiguracja kontenera DI

Częścią procesu startowego jest utworzenie kontenera DI, czyli fabryki obiektów, która jest sercem całej aplikacji. To w rzeczywistości klasa PHP generowana przez Nette i zapisywana w katalogu cache. Fabryka produkuje kluczowe obiekty aplikacji, a za pomocą plików konfiguracyjnych instruujemy ją, jak ma je tworzyć i ustawiać, wpływając tym samym na zachowanie całej aplikacji.

Pliki konfiguracyjne zapisywane są zwykle w formacie NEON. W osobnym rozdziale przeczytasz, co można skonfigurować.

W trybie deweloperskim kontener aktualizuje się automatycznie przy każdej zmianie kodu albo plików konfiguracyjnych. W trybie produkcyjnym generowany jest tylko raz, a zmiany nie są sprawdzane, aby zmaksymalizować wydajność.

Podczas gdy createContainer() buduje kontener i zwraca jego instancję, metoda loadContainer() zwraca tylko nazwę wygenerowanej klasy kontenera, której instancję możesz potem utworzyć samodzielnie. Przydaje się to w zaawansowanych scenariuszach.

Pliki konfiguracyjne wczytuje się metodą addConfig():

$this->configurator->addConfig($this->rootDir . '/config/common.neon');

Jeśli chcemy dodać więcej plików konfiguracyjnych, możemy wywołać funkcję addConfig() wielokrotnie.

$configDir = $this->rootDir . '/config';
$this->configurator->addConfig($configDir . '/common.neon');
$this->configurator->addConfig($configDir . '/services.neon');
if (PHP_SAPI === 'cli') {
	$this->configurator->addConfig($configDir . '/cli.php');
}

Nazwa cli.php nie jest literówką; konfigurację można zapisać także w pliku PHP, który zwraca ją jako tablicę.

Kolejne pliki konfiguracyjne możemy dodać również w sekcji includes.

Jeśli w plikach konfiguracyjnych pojawią się elementy o tych samych kluczach, zostaną nadpisane albo, w przypadku tablic, scalone. Plik dołączony później ma wyższy priorytet niż poprzedni. Plik, w którym wymieniona jest sekcja includes, ma wyższy priorytet niż pliki w nim dołączone.

Parametry statyczne

Parametry używane w plikach konfiguracyjnych można definiować w sekcji parameters, a także przekazywać (albo nadpisywać) metodą addStaticParameters() (której starszy, obecnie przestarzały alias to addParameters()). Ważne jest, że różne wartości parametrów spowodują wygenerowanie kolejnych kontenerów DI, czyli kolejnych klas.

$this->configurator->addStaticParameters([
	'projectId' => 23,
]);

Do parametru projectId można odwołać się w konfiguracji standardowym zapisem %projectId%.

Parametry dynamiczne

Do kontenera możemy dodać również parametry dynamiczne, których różne wartości, w odróżnieniu od parametrów statycznych, nie spowodują wygenerowania nowych kontenerów DI.

$this->configurator->addDynamicParameters([
	'remoteIp' => $_SERVER['REMOTE_ADDR'],
]);

W ten sposób łatwo dodamy na przykład zmienne środowiskowe, do których w konfiguracji można potem odwołać się zapisem %env.zmienna%.

$this->configurator->addDynamicParameters([
	'env' => getenv(),
]);

Parametry domyślne

W plikach konfiguracyjnych możesz używać tych parametrów:

  • %appDir% to bezwzględna ścieżka do katalogu zawierającego plik Bootstrap.php
  • %wwwDir% to bezwzględna ścieżka do katalogu zawierającego plik wejściowy index.php
  • %tempDir% to bezwzględna ścieżka do katalogu na pliki tymczasowe
  • %vendorDir% to bezwzględna ścieżka do katalogu, w którym Composer instaluje biblioteki
  • %rootDir% to bezwzględna ścieżka do katalogu głównego projektu
  • %baseUrl% to bezwzględny URL katalogu głównego (parametr dynamiczny wyliczany w czasie działania)
  • %debugMode% wskazuje, czy aplikacja działa w trybie debug
  • %consoleMode% wskazuje, czy żądanie przyszło z wiersza poleceń

Usługi importowane

Teraz zejdziemy głębiej. Choć zadaniem kontenera DI jest tworzenie obiektów, czasem może pojawić się potrzeba wstawienia do kontenera istniejącego obiektu. Robimy to, definiując usługę z flagą imported: true.

services:
	myservice:
		type: App\Model\MyCustomService
		imported: true

A w bootstrapie wstawiamy obiekt do kontenera:

$this->configurator->addServices([
	'myservice' => new App\Model\MyCustomService('foobar'),
]);

Różne środowiska

Śmiało modyfikuj klasę Bootstrap według swoich potrzeb. Do metody bootWebApplication() możesz dodać parametry odróżniające projekty webowe. Albo możemy dodać inne metody, na przykład bootTestEnvironment(), która inicjalizuje środowisko dla testów jednostkowych, bootConsoleApplication() dla skryptów wywoływanych z wiersza poleceń itd.

public function bootTestEnvironment(): Nette\DI\Container
{
	Tester\Environment::setup(); // inicjalizacja Nette Testera
	$this->setupContainer();
	return $this->configurator->createContainer();
}

public function bootConsoleApplication(): Nette\DI\Container
{
	$this->configurator->setDebugMode(false);
	$this->initializeEnvironment();
	$this->setupContainer();
	return $this->configurator->createContainer();
}
wersja: 4.x