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

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

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

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

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

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

web-project/
├── app/                      ← каталог с приложением
│   ├── Core/                 ← базовые классы, необходимые для работы
│   │   └── RouterFactory.php ← конфигурация URL-адресов
│   ├── Presentation/         ← презентеры, шаблоны и т.п.
│   │   ├── @layout.latte     ← шаблон макета
│   │   └── Home/             ← каталог презентера Home
│   │       ├── HomePresenter.php ← класс презентера Home
│   │       └── default.latte ← шаблон действия default
│   └── Bootstrap.php         ← загрузочный класс Bootstrap
├── bin/                      ← скрипты, запускаемые из командной строки
├── config/                   ← конфигурационные файлы
│   ├── common.neon
│   └── services.neon
├── log/                      ← логируемые ошибки
├── temp/                     ← временные файлы, кеш, …
├── vendor/                   ← библиотеки, установленные Composer
│   ├── ...
│   └── autoload.php          ← автозагрузка всех установленных пакетов
├── www/                      ← публичный каталог или document-root проекта
│   ├── .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, что является замечательным инструментом для логирования или визуализации ошибок. На production-сервере он логирует ошибки, на сервере разработки сразу отображает. Следовательно, к инициализации относится и решение, работает ли веб в режиме production или разработки. Для этого Nette использует умное автоопределение: если вы запускаете веб на localhost, он работает в режиме разработки. Вам не нужно ничего настраивать, и приложение сразу готово как для разработки, так и для реального развертывания. Эти шаги выполняются и подробно описаны в главе о классе Bootstrap.

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

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

Nette Application

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

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

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

Таким образом, маршрутизатор преобразовал URL в пару Presenter:action + параметры, в нашем случае 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(), и в параметре $id он получает 123.

Презентер может обслуживать несколько действий, то есть иметь несколько методов 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. загружаем приложение, создается контейнер и запускается 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">деталь продукта</a>

Просто вместо реального URL вы пишете известную пару Presenter:action и указываете возможные параметры. Хитрость в n:href, которое говорит, что этот атрибут обработает Nette. И сгенерирует:

<a href="/product/456">деталь продукта</a>

Генерацией URL занимается уже упомянутый маршрутизатор. Дело в том, что маршрутизаторы в Nette уникальны тем, что они могут выполнять не только преобразование из URL в пару presenter:action, но и наоборот, то есть из имени презентера + действия + параметров генерировать URL. Благодаря этому в Nette вы можете полностью изменить формы URL во всем готовом приложении, не изменяя ни одного символа в шаблоне или презентере. Просто изменив маршрутизатор. Также благодаря этому работает так называемая канонизация, что является еще одной уникальной особенностью Nette, которая способствует лучшему SEO (оптимизации для поисковых систем), автоматически предотвращая существование дублирующегося контента на разных URL. Многие программисты считают это потрясающим.

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

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

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

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

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

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

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

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

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

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

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

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

Куда дальше?

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

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

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

версия: 4.0