Bootstrapping

Bootstrapping – это процесс инициализации окружения приложения, создания контейнера внедрения зависимостей (DI) и запуска приложения. Мы обсудим:

  • как класс Bootstrap инициализирует окружение
  • как приложения настраиваются файлами NEON
  • как различать производственный режим и режим разработки
  • как создать и настроить DI-контейнер

Приложения, будь то веб-приложения или скрипты, запускаемые из командной строки, начинают своё выполнение с той или иной инициализации окружения. В былые времена за это отвечал файл с именем вроде include.inc.php, подключаемый начальным файлом. В современных приложениях Nette его заменил класс Bootstrap, который как часть приложения находится в файле app/Bootstrap.php. Он может выглядеть, например, так:

namespace App;

use Nette;
use Nette\Bootstrap\Configurator;

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

	public function __construct()
	{
		$this->rootDir = dirname(__DIR__);
		// Configurator отвечает за настройку окружения приложения и сервисов.
		$this->configurator = new Configurator;
		// Задаём каталог для временных файлов, порождаемых Nette (например, скомпилированных шаблонов)
		$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 умён, и режим разработки включается автоматически,
		// либо вы можете включить его для конкретного IP-адреса, раскомментировав следующую строку:
		// $this->configurator->setDebugMode('secret@23.75.345.200');

		// Включает Tracy - лучший "швейцарский нож" для отладки.
		$this->configurator->enableTracy($this->rootDir . '/log');

		// RobotLoader: автоматически загружает все классы в выбранном каталоге
		$this->configurator->createRobotLoader()
			->addDirectory(__DIR__)
			->register();
	}

	private function setupContainer(): void
	{
		// Загружаем конфигурационные файлы
		$this->configurator->addConfig($this->rootDir . '/config/common.neon');
	}
}

index.php

У веб-приложений начальным файлом служит index.php, лежащий в публичном каталоге www/. Он поручает классу Bootstrap инициализировать окружение и создать DI-контейнер. Затем он получает из контейнера сервис Application, который и запускает веб-приложение:

$bootstrap = new App\Bootstrap;
// Инициализируем окружение и создаём DI-контейнер
$container = $bootstrap->bootWebApplication();
// DI-контейнер создаёт объект Nette\Application\Application
$application = $container->getByType(Nette\Application\Application::class);
// Запускаем приложение Nette и обрабатываем входящий запрос
$application->run();

Объект $application в ходе обработки запроса испускает события: onStartup, onRequest, onPresenter, onResponse, onShutdown и onError (при необработанном исключении). Вы можете привязать к ним обработчики, что удобно для ведения лога или мониторинга всего приложения.

Как видите, настроить окружение и создать контейнер внедрения зависимостей (DI) помогает класс Nette\Bootstrap\Configurator. Сейчас мы познакомим вас с ним подробнее.

Режим разработки и производственный режим

Nette ведёт себя по-разному в зависимости от того, работает он на сервере разработки или на производственном:

🛠️ Режим разработки
Показывает панель отладки Tracy с полезными сведениями (SQL-запросы, время выполнения, использованная память)
При ошибке показывает подробную страницу ошибки с вызовами функций и содержимым переменных
Автоматически обновляет кеш при изменении шаблонов Latte, конфигурационных файлов и прочего
🚀 Производственный режим
Не показывает никаких отладочных сведений, все ошибки записываются в лог
При ошибке показывает ErrorPresenter или общую страницу “Server Error”
Кеш никогда не обновляется автоматически!
Оптимизирован ради скорости и безопасности

Режим выбирается автоопределением, поэтому обычно ничего настраивать и переключать вручную не нужно:

  • режим разработки: на localhost (IP-адрес 127.0.0.1 или ::1), если нет прокси (то есть его HTTP-заголовок не обнаружен)
  • производственный режим: везде остальном

Если мы хотим включить режим разработки и в других случаях, например для программистов, заходящих с определённого IP-адреса, мы используем setDebugMode():

$this->configurator->setDebugMode('23.75.345.200'); // можно передать и массив IP-адресов

Мы настоятельно рекомендуем сочетать IP-адрес с cookie. Сохраните в cookie nette-debug секретный токен, например secret1234, и тем самым включите режим разработки для программистов, заходящих с определённого IP-адреса и имеющих в cookie этот токен:

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

Мы можем и полностью отключить режим разработки, даже на localhost:

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

Учтите, что значение true принудительно включает режим разработки, чего на производственном сервере быть никогда не должно.

Автоопределением внутренне занимается статический метод Configurator::detectDebugMode(), который вы можете вызвать и сами, например чтобы определить режим разработки вне конфигуратора. Он принимает необязательный белый список IP-адресов или имён компьютеров и возвращает, должен ли текущий запрос выполняться в режиме разработки:

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

Инструмент отладки Tracy

Ради удобной отладки мы включим прекрасный инструмент Tracy. В режиме разработки он наглядно показывает ошибки, а в производственном записывает их в указанный каталог:

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

Временные файлы

Nette использует кеш для DI-контейнера, RobotLoader, шаблонов и прочего. Поэтому нужно задать путь к каталогу, где будет храниться кеш:

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

В Linux или macOS задайте каталогам log/ и temp/ права на запись.

RobotLoader

Обычно мы хотим автоматически загружать классы с помощью RobotLoader, поэтому нам нужно его запустить и дать ему загружать классы из каталога, где лежит Bootstrap.php (то есть __DIR__), и из всех его подкаталогов:

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

Альтернативный подход – загружать классы исключительно через Composer по PSR-4.

Часовой пояс

Часовой пояс по умолчанию можно задать через конфигуратор.

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

Конфигурация DI-контейнера

Частью процесса запуска является создание DI-контейнера, то есть фабрики объектов, которая служит сердцем всего приложения. На деле это PHP-класс, порождённый Nette и сохранённый в каталоге кеша. Фабрика создаёт ключевые объекты приложения, а конфигурационными файлами мы указываем ей, как их создавать и настраивать, и тем самым влияем на поведение всего приложения.

Конфигурационные файлы обычно пишутся в формате NEON. В отдельной главе вы можете прочитать, что можно настраивать.

В режиме разработки контейнер автоматически обновляется при изменении кода или конфигурационных файлов. В производственном режиме он порождается только один раз, а изменения не проверяются ради максимальной производительности.

Метод createContainer() собирает контейнер и возвращает его экземпляр, а метод loadContainer() возвращает только имя порождённого класса контейнера, который вы затем можете создать сами. Это полезно в продвинутых сценариях.

Конфигурационные файлы загружаются методом addConfig():

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

Если мы хотим добавить больше конфигурационных файлов, мы можем вызвать функцию addConfig() несколько раз.

$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');
}

Имя cli.php – не опечатка: конфигурацию можно записать и в PHP-файле, который возвращает её массивом.

Другие конфигурационные файлы мы можем добавить и в секции includes.

Если в конфигурационных файлах встречаются элементы с одинаковыми ключами, они будут перезаписаны или, в случае массивов, объединены. Файл, подключённый позже, имеет более высокий приоритет, чем предыдущий. Файл, в котором указана секция includes, имеет более высокий приоритет, чем подключённые в нём файлы.

Статические параметры

Параметры, используемые в конфигурационных файлах, можно определить в секции parameters, а также передать (или переопределить) методом addStaticParameters() (его более старый, ныне устаревший псевдоним – addParameters()). Важно, что разные значения параметров вызовут порождение дополнительных DI-контейнеров, то есть дополнительных классов.

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

На параметр projectId можно сослаться в конфигурации обычной записью %projectId%.

Динамические параметры

Мы можем добавить в контейнер и динамические параметры, разные значения которых, в отличие от статических, не вызовут порождения новых DI-контейнеров.

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

Так мы легко добавим, например, переменные окружения, на которые затем можно сослаться в конфигурации записью %env.variable%.

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

Параметры по умолчанию

В конфигурационных файлах вы можете использовать эти параметры:

  • %appDir% – абсолютный путь к каталогу с файлом Bootstrap.php
  • %wwwDir% – абсолютный путь к каталогу с начальным файлом index.php
  • %tempDir% – абсолютный путь к каталогу временных файлов
  • %vendorDir% – абсолютный путь к каталогу, куда Composer устанавливает библиотеки
  • %rootDir% – абсолютный путь к корневому каталогу проекта
  • %baseUrl% – абсолютный URL корневого каталога (динамический параметр, вычисляемый во время выполнения)
  • %debugMode% – находится ли приложение в режиме отладки
  • %consoleMode% – пришёл ли запрос из командной строки

Импортированные сервисы

Теперь копнём глубже. Хотя назначение DI-контейнера – создавать объекты, изредка может понадобиться вставить в контейнер уже существующий объект. Мы делаем это, определив сервис с флагом imported: true.

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

А в bootstrap вставляем объект в контейнер:

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

Разные окружения

Смело меняйте класс Bootstrap под свои нужды. Вы можете добавить в метод bootWebApplication() параметры, чтобы различать веб-проекты. Или можно добавить другие методы, например bootTestEnvironment(), инициализирующий окружение для модульных тестов, bootConsoleApplication() для скриптов, вызываемых из командной строки, и так далее.

public function bootTestEnvironment(): Nette\DI\Container
{
	Tester\Environment::setup(); // инициализация Nette Tester
	$this->setupContainer();
	return $this->configurator->createContainer();
}

public function bootConsoleApplication(): Nette\DI\Container
{
	$this->configurator->setDebugMode(false);
	$this->initializeEnvironment();
	$this->setupContainer();
	return $this->configurator->createContainer();
}
версия: 4.x