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

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

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

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

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

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

web-project/
├── app/                      ← каталог с приложением
│   ├── Presenters/           ← классы презентеров
│   │   ├── HomepagePresenter.php  ← Класс презентера главной страницы
│   │   └── templates/        ← директория шаблонов
│   │       ├── @layout.latte ← шаблон общего макета
│   │       └── Homepage/     ← шаблоны презентера главной страницы
│   │           └── default.latte  ← шаблон действия `default`
│   ├── Router/               ← конфигурация URL-адресов
│   └── Bootstrap.php         ← загрузочный класс Bootstrap
├── bin/                      ← скрипты командной строки
├── config/                   ← файлы конфигурации
│   ├── common.neon
│   └── local.neon
├── log/                      ← журналы ошибок
├── temp/                     ← временные файлы, кэш, …
├── vendor/                   ← библиотеки, установленные через Composer
│   ├── ...
│   └── autoload.php          ← автозагрузчик библиотек, установленных через Composer
├── www/                      ← публичный корневой каталог проекта
│   ├── .htaccess             ← правила mod_rewrite и т. д.
│   └── index.php             ← начальный файл, запускающий приложение
└── .htaccess                 ← запрещает доступ ко всем каталогам, кроме www

Вы можете изменить структуру каталогов любым способом, переименовать или переместить папки, а затем просто отредактировать пути к log/ и temp/ в файле Bootstrap.php и путь к этому файлу в composer.json в секции autoload. Ничего больше, никакой сложной перенастройки, никаких постоянных изменений. Nette имеет интеллектуальное автоопределение.

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

Публичный каталог www/ может быть изменен без необходимости устанавливать что-либо ещё. На самом деле, часто бывает, что из-за специфики вашего хостинга вам придется переименовать его или, наоборот, установить так называемый 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. Благодаря соответствующим настройкам server settings, этот 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 имеет единственную задачу: для ответа на HTTP-запрос.

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

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

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

Давайте двигаться дальше. Приложение уже знает имя презентера и может продолжить работу. Путем создания объекта ProductPresenter, который является кодом презентера Product. Точнее, он просит контейнер DI создать презентера, потому что создание объектов — это его работа.

Презентер может выглядеть следующим образом:

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

	public function renderShow(int $id): void
	{
		// we obtain data from the model and pass it to the template
		$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.

Нам даже не нужно указывать, какой шаблон нужно вывести, он сам выводит путь к нему в соответствии с простой логикой. В случае с презентером Product и действием show, он пытается проверить, существует ли один из этих файлов шаблонов относительно каталога, в котором находится класс ProductPresenter:

  • templates/Product/show.latte
  • templates/Product.show.latte

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

Чтобы убедиться в этом, давайте попробуем повторить весь процесс, используя немного другой URL:

  1. URL будет https://example.com
  2. мы загружаем приложение, создаем контейнер и запускаем Application::run()
  3. маршрутизатор декодирует URL как пару Homepage:default
  4. создается объект HomepagePresenter
  5. вызывается метод renderDefault() (если существует)
  6. шаблон templates/Homepage/default.latte с макетом templates/@layout.latte отрисован

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

Шаблоны

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

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

<a n:href="Product:show $productId">страница товара</a>

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

<a href="/product/456">страница товара</a>

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

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

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

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

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

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

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

Не волнуйтесь, это не магический черный ящик, как может показаться из предыдущих слов. На самом деле, это один довольно скучный PHP-класс, сгенерированный Nette и хранящийся в каталоге кэша. Он имеет множество методов, названных createServiceAbcd(), и каждый из них создает и возвращает объект. Да, есть также метод createServiceApplication(), который создаёт 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 имеет целый арсенал полезных классов, слой базы данных и т. д. Попробуйте целенаправленно просто просмотреть документацию. Или посетите блог. Вы откроете для себя много интересного.

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