Как работают приложения?

Сейчас вы читаете основополагающую главу документации Nette. Вы узнаете полные принципы работы веб-приложений от А до Я, с момента рождения запроса до завершения выполнения PHP-скрипта. После прочтения вы будете понимать:

  • как всё это работает
  • что такое Bootstrap, презентер и DI-контейнер
  • как выглядит структура каталогов

Структура каталогов

Откройте пример скелета веб-приложения под названием WebProject. По ходу чтения вы можете заглядывать в обсуждаемые файлы.

Структура каталогов выглядит примерно так:

web-project/
├── app/                      ← каталог приложения
│   ├── Core/                 ← базовые классы, необходимые для работы
│   │   └── RouterFactory.php ← настройка URL-адресов
│   ├── Presentation/         ← презентеры, шаблоны и прочее
│   │   ├── @layout.latte     ← шаблон макета
│   │   └── Home/             ← каталог презентера Home
│   │       ├── HomePresenter.php ← класс презентера Home
│   │       └── default.latte ← шаблон для действия default
│   └── Bootstrap.php         ← стартовый класс Bootstrap
├── assets/                   ← ресурсы (SCSS, TypeScript, исходные изображения)
├── bin/                      ← скрипты, запускаемые из командной строки
├── config/                   ← конфигурационные файлы
│   ├── common.neon
│   └── services.neon
├── log/                      ← записанные ошибки
├── temp/                     ← временные файлы, кеш, …
├── vendor/                   ← библиотеки, установленные Composer
│   ├── ...
│   └── autoload.php          ← автозагрузка всех установленных пакетов
├── www/                      ← публичный каталог, document-root проекта
│   ├── assets/               ← скомпилированные статические файлы (CSS, JS, изображения, ...)
│   ├── .htaccess             ← правила для mod_rewrite
│   └── index.php             ← начальный файл, запускающий приложение
└── .htaccess                 ← запрещает доступ ко всем каталогам, кроме www

Структуру каталогов можно изменять как угодно, переименовывать или переносить папки, она полностью гибкая. У Nette есть и умное автоопределение, которое само распознаёт расположение приложения, включая базовый URL.

Для чуть более крупных приложений мы можем разложить каталоги презентеров и шаблонов по подкаталогам и сгруппировать классы в пространства имён, которые мы называем модулями.

Каталог www/ представляет собой публичный каталог, или document-root проекта. Вы можете его переименовать, ничего больше на стороне приложения настраивать не придётся. Нужно лишь настроить хостинг так, чтобы document-root указывал на этот каталог.

WebProject можно скачать сразу вместе с Nette через Composer:

composer create-project nette/web-project

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

Приложение WebProject готово к запуску, настраивать вообще ничего не нужно, и вы можете открыть его прямо в браузере, обратившись к папке www/.

HTTP-запрос

Всё начинается с того, что пользователь открывает страницу в браузере. Браузер отправляет на сервер HTTP-запрос. Этот запрос нацелен на единственный PHP-файл в публичном каталоге www/, а именно на index.php. Допустим, запрос идёт по адресу https://example.com/product/123. Благодаря подходящей настройке сервера даже такой URL сопоставляется файлу index.php, который и выполняется.

Его задача:

  1. инициализировать окружение
  2. получить фабрику
  3. запустить приложение Nette, которое обработает запрос

Какую фабрику? Мы же не тракторы производим, а сайты делаем! Погодите, сейчас всё объяснится.

Под “инициализацией окружения” мы понимаем, например, включение Tracy – потрясающего инструмента для записи в лог и наглядного показа ошибок. На производственном сервере она записывает ошибки в лог, а в среде разработки показывает их прямо на экране. Поэтому инициализация включает и определение того, работает сайт в производственном режиме или в режиме разработки. Nette использует для этого умное автоопределение: если вы запускаете сайт на localhost, он работает в режиме разработки. Настраивать ничего не нужно, и приложение сразу готово и к разработке, и к боевому развёртыванию. Эти шаги выполняются и подробно описываются в главе о классе Bootstrap.

Третий пункт (да, второй мы пропустили, но вернёмся к нему) – запуск приложения. Обработка HTTP-запросов в Nette лежит на классе Nette\Application\Application (далее Application). Так что, когда мы говорим “запустить приложение”, мы имеем в виду вызов метода с говорящим именем run() у объекта этого класса.

Nette выступает наставником, который направляет вас писать чистые приложения по проверенным методикам. Одна из самых устоявшихся – внедрение зависимостей, сокращённо DI. Мы не хотим сейчас нагружать вас объяснением DI, для этого есть отдельная глава. Важное следствие в том, что ключевые объекты обычно создаёт фабрика объектов, известная как DI-контейнер (или DIC). Да, это та самая фабрика, о которой шла речь. Она порождает нам и объект Application, поэтому сначала нам нужен контейнер. Мы получаем его через класс Configurator, даём ему создать объект Application, вызываем у него метод run(), и приложение Nette запускается. Именно это и происходит в файле index.php.

Nette Application

У класса Application единственная задача: ответить на HTTP-запрос.

Приложения, написанные на Nette, делятся на множество так называемых презентеров (в других фреймворках вы можете встретить термин “контроллер”, это по сути одно и то же). Это классы, каждый из которых представляет определённую страницу сайта: например, главную страницу, товар в интернет-магазине, форму входа, ленту карты сайта и так далее. У приложения может быть от одного до тысяч презентеров.

Application начинает с того, что спрашивает так называемый маршрутизатор, какой презентер должен обработать текущий запрос. Маршрутизатор определяет ответственного. Он изучает входной URL https://example.com/product/123 и по своей настройке решает, что эта задача принадлежит, например, презентеру Product, который должен выполнить действие show для товара с id: 123. Пару “презентер + действие” принято записывать через двоеточие: Product:show.

Итак, маршрутизатор превратил URL в пару Презентер:действие с параметрами, в нашем случае Product:show и id: 123. Как выглядит такой маршрутизатор, вы можете увидеть в файле app/Core/RouterFactory.php, а подробно мы описываем его в главе Маршрутизация.

Пойдём дальше. Application теперь знает имя презентера и может действовать. Он создаёт экземпляр класса ProductPresenter, содержащего код презентера Product. Точнее, он просит DI-контейнер создать презентер, потому что создание объектов – его обязанность.

Презентер может выглядеть так:

class ProductPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ProductRepository $repository,
	) {
	}

	public function renderShow(int $id): void
	{
		// получаем данные из модели и передаём их в шаблон
		$this->template->product = $this->repository->getProduct($id);
	}
}

Презентер берёт обработку запроса на себя. Задача ясна: выполнить действие show с id: 123. В терминологии презентеров это значит, что вызывается метод renderShow(), получающий 123 в параметре $id.

Презентер может обрабатывать несколько действий, то есть у него может быть несколько методов render<Action>(). Однако мы рекомендуем проектировать презентеры с одним действием или с как можно меньшим их числом.

Итак, был вызван метод renderShow(123). Его код – выдуманный пример, но он показывает, как данные передаются в шаблон, а именно записью в $this->template.

Затем презентер возвращает ответ. Это может быть HTML-страница, изображение, XML-документ, отправка файла с диска, JSON или, скажем, перенаправление на другую страницу. Важно, что, если мы явно не указываем, как отвечать (а именно так и обстоит дело с ProductPresenter), ответом будет отрисовка шаблона в HTML-страницу. Почему? Потому что в 99 % случаев мы хотим отрисовать шаблон. Поэтому презентер принимает такое поведение как поведение по умолчанию, чтобы упростить нам работу. В этом суть Nette.

Нам даже не нужно указывать, какой шаблон отрисовать: фреймворк выведет путь автоматически. В случае действия show он просто попробует загрузить шаблон show.latte, лежащий в том же каталоге, что и класс ProductPresenter. Он также попробует найти макет в файле @layout.latte (подробнее о поиске шаблонов).

Затем шаблоны отрисовываются. На этом задача презентера и всего приложения завершена. Если шаблона не существует, возвращается страница с ошибкой 404. Подробнее о презентерах можно узнать на странице Презентеры.

Для верности повторим весь ход событий с чуть другим URL:

  1. URL – https://example.com
  2. Приложение стартует, создаётся DI-контейнер и выполняется Application::run().
  3. Маршрутизатор расшифровывает URL в пару Home:default.
  4. Создаётся экземпляр класса HomePresenter.
  5. Вызывается метод renderDefault() (если он существует).
  6. Отрисовывается шаблон, например default.latte, вместе с макетом, например @layout.latte.

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

Шаблоны

Раз уж речь зашла о шаблонах: Nette использует систему шаблонов Latte. Поэтому файлы шаблонов имеют расширение .latte. Latte используется прежде всего потому, что это самая безопасная система шаблонов для PHP, а ещё самая интуитивная. Учить много нового не нужно: достаточно знания PHP и нескольких тегов. Всё нужное вы найдёте в документации.

В шаблоне вы создаёте ссылки на другие презентеры и действия вот так:

<a n:href="Product:show $productId">product detail</a>

Просто напишите привычную пару Презентер:действие вместо настоящего URL и добавьте нужные параметры. Хитрость в n:href, которая говорит Nette обработать этот атрибут. Он затем породит:

<a href="/product/456">product detail</a>

Порождением URL занимается упомянутый маршрутизатор. Маршрутизаторы в Nette исключительны тем, что умеют не только преобразовывать URL в пару Презентер:действие, но и наоборот: порождать URL из имени презентера, действия и параметров. Благодаря этому вы можете полностью изменить формат URL во всём готовом приложении на Nette, не меняя ни одного символа в шаблонах или презентерах, – достаточно поправить маршрутизатор. Это же обеспечивает так называемую канонизацию, ещё одну уникальную возможность Nette, которая улучшает SEO, автоматически не давая одному и тому же содержимому существовать под разными URL. Многих программистов эта возможность поражает.

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

Нам нужно рассказать о презентерах ещё одну вещь: в них встроена система компонентов. Те, у кого больше опыта, могут вспомнить нечто похожее из Delphi или ASP.NET Web Forms; React или Vue.js построены на в чём-то родственных идеях. В мире PHP-фреймворков это совершенно уникальная возможность.

Компоненты – самостоятельные переиспользуемые единицы, которые мы встраиваем в страницы (то есть в презентеры). Это могут быть формы, таблицы данных, меню, опросы – в общем, всё, что имеет смысл использовать повторно. Мы можем создавать собственные компоненты или воспользоваться какими-то из огромного выбора компонентов с открытым кодом.

Компоненты принципиально меняют подход к разработке приложений. Они открывают новые возможности собирать страницы из заранее подготовленных единиц. И у них есть кое-что общее с Голливудом.

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

DI-контейнер, то есть фабрика объектов, – сердце всего приложения.

Не переживайте, это не какой-то волшебный чёрный ящик, как могли бы навести на мысль предыдущие строки. На деле это довольно обыденный PHP-класс, порождённый Nette и сохранённый в каталоге кеша. В нём много методов с именами вроде createServiceAbcd(), каждый из которых умеет создать и вернуть определённый объект. Да, там есть и метод createServiceApplication__application(), порождающий экземпляр Nette\Application\Application, который понадобился нам в index.php для запуска приложения. Есть и методы для создания отдельных презентеров и так далее.

Объекты, создаваемые DI-контейнером, по некоторым причинам называют сервисами.

По-настоящему особенное в этом классе то, что вы его не программируете – это делает фреймворк. Он действительно порождает PHP-код и сохраняет его на диск. Вы лишь даёте указания, какие объекты контейнер должен уметь создавать и как именно. Эти указания записываются в конфигурационных файлах, которые используют формат NEON и потому имеют расширение .neon.

Конфигурационные файлы служат исключительно для указаний DI-контейнеру. Так что, если вы, например, зададите параметр expiration: 14 days в секции session, DI-контейнер при создании объекта Nette\Http\Session, представляющего сессию, вызовет его метод setExpiration('14 days') и тем самым воплотит конфигурацию в жизнь.

Для вас подготовлена целая глава о том, что можно настраивать и как определять собственные сервисы.

Как только вы немного погрузитесь в создание сервисов, вы столкнётесь с термином autowiring. Это возможность, которая невероятно упростит вам жизнь. Она умеет автоматически передавать объекты туда, где они вам нужны (например, в конструкторы ваших классов), без каких-либо действий с вашей стороны. Вы обнаружите, что DI-контейнер в Nette – маленькое чудо.

Что дальше?

Мы разобрали основополагающие принципы приложений Nette. Пока это был поверхностный обзор, но вскоре вы погрузитесь глубже и со временем начнёте создавать замечательные веб-приложения. Куда двигаться дальше? Вы уже пробовали руководство Создайте своё первое приложение?

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

Пусть фреймворк принесёт вам много радости 💙

версия: 4.x