Как работают приложения?
Сейчас вы читаете основополагающую главу документации 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, который и
выполняется.
Его задача:
- инициализировать окружение
- получить фабрику
- запустить приложение 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:
- URL –
https://example.com - Приложение стартует, создаётся DI-контейнер и выполняется
Application::run(). - Маршрутизатор расшифровывает URL в пару
Home:default. - Создаётся экземпляр класса
HomePresenter. - Вызывается метод
renderDefault()(если он существует). - Отрисовывается шаблон, например
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 предлагает целый арсенал полезных классов, слой работы с базой данных и многое другое. Попробуйте походить по документации. Или загляните в блог. Вы обнаружите много интересного.
Пусть фреймворк принесёт вам много радости 💙