Nette Documentation Preview

syntax
Структура каталогов приложения
******************************

<div class=perex>

Как разработать четкую и масштабируемую структуру каталогов для проектов на Nette Framework? Мы покажем вам проверенные практики, которые помогут вам организовать ваш код. Вы узнаете:

- как **логически структурировать** приложение по каталогам
- как спроектировать структуру так, чтобы она **масштабировалась** по мере роста проекта
- каковы **возможные альтернативы** и их преимущества и недостатки

</div>


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


Базовая структура проекта .[#toc-basic-project-structure]
=========================================================

Хотя Nette Framework не диктует никакой фиксированной структуры каталогов, существует проверенная структура по умолчанию в виде [Web Project |https://github.com/nette/web-project]:

/--pre
<b>web-project/</b>
├── <b>app/</b>              ← каталог приложений
├── <b>assets/</b>           ← SCSS, JS файлы, изображения..., альтернативный вариант resources/
├── <b>bin/</b>              ← скрипты командной строки
├── <b>config/</b>           ← конфигурация
├── <b>log/</b>              ← зарегистрированные ошибки
├── <b>temp/</b>             ← временные файлы, кэш
├── <b>tests/</b>            ← тесты
├── <b>vendor/</b>           ← библиотеки, установленные Composer
└── <b>www/</b>              ← публичный каталог (document-root)
\--

Вы можете свободно изменять эту структуру в соответствии со своими потребностями - переименовывать или перемещать папки. Тогда вам просто нужно будет скорректировать относительные пути к каталогам в `Bootstrap.php` и, возможно, `composer.json`. Больше ничего не нужно, никакой сложной переконфигурации, никаких постоянных изменений. Nette обладает интеллектуальным автоопределением и автоматически распознает местоположение приложения, включая его базу URL.


Принципы организации кода .[#toc-code-organization-principles]
==============================================================

Когда вы впервые изучаете новый проект, вы должны быть в состоянии быстро сориентироваться. Представьте, что вы перешли в каталог `app/Model/` и увидели такую структуру:

/--pre
<b>app/Model/</b>
├── <b>Services/</b>
├── <b>Repositories/</b>
└── <b>Entities/</b>
\--

Из нее вы узнаете только то, что проект использует некоторые сервисы, репозитории и сущности. Вы ничего не узнаете о реальном назначении приложения.

Давайте рассмотрим другой подход - **организацию по доменам**:

/--pre
<b>app/Model/</b>
├── <b>Cart/</b>
├── <b>Payment/</b>
├── <b>Order/</b>
└── <b>Product/</b>
\--

Это совсем другое дело - с первого взгляда понятно, что это сайт электронной коммерции. Сами названия каталогов говорят о том, что умеет делать приложение - оно работает с платежами, заказами и товарами.

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


Пространства имен .[#toc-namespaces]
------------------------------------

Принято считать, что структура каталогов соответствует пространствам имен в приложении. Это означает, что физическое расположение файлов соответствует их пространству имен. Например, класс, расположенный в каталоге `app/Model/Product/ProductRepository.php`, должен иметь пространство имен `App\Model\Product`. Этот принцип помогает ориентироваться в коде и упрощает автозагрузку.


Единственное и множественное число в именах .[#toc-singular-vs-plural-in-names]
-------------------------------------------------------------------------------

Обратите внимание, что мы используем единственное число для основных каталогов приложения: `app`, `config`, `log`, `temp`, `www`. То же самое относится и к внутренним каталогам приложения: `Model`, `Core`, `Presentation`. Это связано с тем, что каждый из них представляет собой единую концепцию.

Аналогично, `app/Model/Product` представляет все, что связано с продуктами. Мы не называем его `Products`, потому что это не папка с продуктами (в ней были бы такие файлы, как `iphone.php`, `samsung.php`). Это пространство имен, содержащее классы для работы с продуктами - `ProductRepository.php`, `ProductService.php`.

Папка `app/Tasks` имеет множественное число, потому что содержит набор отдельных исполняемых скриптов - `CleanupTask.php`, `ImportTask.php`. Каждый из них является самостоятельной единицей.

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


Публичный каталог `www/` .[#toc-public-directory-www]
=====================================================

Эта директория является единственной, доступной из Интернета (так называемый document-root). Вы можете часто встретить название `public/` вместо `www/` - это просто условность и не влияет на функциональность. Каталог содержит:
- [Точка входа в |bootstrap#index.php] приложение `index.php`
- Файл `.htaccess` с правилами mod_rewrite (для Apache)
- Статические файлы (CSS, JavaScript, изображения)
- Загружаемые файлы

Для обеспечения безопасности приложения очень важно правильно [настроить document-root |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url].

.[note]
Никогда не размещайте папку `node_modules/` в этом каталоге - она содержит тысячи файлов, которые могут быть исполняемыми и не должны находиться в открытом доступе.


Каталог приложений `app/` .[#toc-application-directory-app]
===========================================================

Это основной каталог с кодом приложения. Основная структура:

/--pre
<b>app/</b>
├── <b>Core/</b>               ← инфраструктурные вопросы
├── <b>Model/</b>              ← бизнес-логика
├── <b>Presentation/</b>       ← презентаторы и шаблоны
├── <b>Tasks/</b>              ← командные скрипты
└── <b>Bootstrap.php</b>       ← класс загрузки приложения
\--

`Bootstrap.php` [класс запуска приложения |bootstrap], который инициализирует окружение, загружает конфигурацию и создает DI-контейнер.

Теперь рассмотрим отдельные подкаталоги подробнее.


Презентаторы и шаблоны .[#toc-presenters-and-templates]
=======================================================

Презентационная часть приложения находится в каталоге `app/Presentation`. Альтернативой может служить короткий `app/UI`. Это место для всех презентаторов, их шаблонов и любых вспомогательных классов.

Мы организуем этот слой по доменам. В сложном проекте, объединяющем электронную коммерцию, блог и API, структура будет выглядеть следующим образом:

/--pre
<b>app/Presentation/</b>
├── <b>Shop/</b>              ← фронтенд электронной коммерции
│   ├── <b>Product/</b>
│   ├── <b>Cart/</b>
│   └── <b>Order/</b>
├── <b>Blog/</b>              ← блог
│   ├── <b>Home/</b>
│   └── <b>Post/</b>
├── <b>Admin/</b>             ← администрирование
│   ├── <b>Dashboard/</b>
│   └── <b>Products/</b>
└── <b>Api/</b>               ← конечные точки API
	└── <b>V1/</b>
\--

И наоборот, для простого блога мы бы использовали такую структуру:

/--pre
<b>app/Presentation/</b>
├── <b>Front/</b>             ← фронтенд сайта
│   ├── <b>Home/</b>
│   └── <b>Post/</b>
├── <b>Admin/</b>             ← администрирование
│   ├── <b>Dashboard/</b>
│   └── <b>Posts/</b>
├── <b>Error/</b>
└── <b>Export/</b>            ← RSS, карты сайта и т.д.
\--

Папки типа `Home/` или `Dashboard/` содержат ведущие и шаблоны. Папки типа `Front/`, `Admin/` или `Api/` называются **модулями**. Технически это обычные каталоги, которые служат для логической организации приложения.

Каждая папка с ведущим содержит аналогичный ведущий и его шаблоны. Например, папка `Dashboard/` содержит:

/--pre
<b>Dashboard/</b>
├── <b>DashboardPresenter.php</b>     ← ведущий
└── <b>default.latte</b>              ← шаблон
\--

Эта структура каталогов отражается в пространствах имен классов. Например, `DashboardPresenter` находится в пространстве имен `App\Presentation\Admin\Dashboard` (см. [сопоставление с ведущим |#presenter mapping]):

```php
namespace App\Presentation\Admin\Dashboard;

class DashboardPresenter extends Nette\Application\UI\Presenter
{
	//...
}
```

Мы ссылаемся на ведущего `Dashboard` внутри модуля `Admin` в приложении, используя обозначение двоеточия, как `Admin:Dashboard`. К его действию `default` тогда как `Admin:Dashboard:default`. Для вложенных модулей мы используем больше двоеточий, например, `Shop:Order:Detail:default`.


Разработка гибкой структуры .[#toc-flexible-structure-development]
------------------------------------------------------------------

Одно из главных преимуществ этой структуры - то, как элегантно она адаптируется к растущим потребностям проекта. В качестве примера возьмем часть, генерирующую XML-ленты. Изначально у нас есть простая форма:

/--pre
<b>Export/</b>
├── <b>ExportPresenter.php</b>   ← один ведущий для всех экспортов
├── <b>sitemap.latte</b>         ← шаблон для карты сайта
└── <b>feed.latte</b>            ← шаблон для RSS-ленты
\--

Со временем добавляются новые типы фидов, и нам нужна дополнительная логика для них... Нет проблем! Папка `Export/` просто становится модулем:

/--pre
<b>Export/</b>
├── <b>Sitemap/</b>
│   ├── <b>SitemapPresenter.php</b>
│   └── <b>sitemap.latte</b>
└── <b>Feed/</b>
	├── <b>FeedPresenter.php</b>
	├── <b>amazon.latte</b>         ← фид для Amazon
	└── <b>ebay.latte</b>           ← фид для eBay
\--

Эта трансформация совершенно гладкая - просто создайте новые подпапки, разделите на них код и обновите ссылки (например, с `Export:feed` на `Export:Feed:amazon`). Благодаря этому мы можем постепенно расширять структуру по мере необходимости, уровень вложенности никак не ограничен.

Например, если в администрировании у вас много презентеров, связанных с управлением заказами, таких как `OrderDetail`, `OrderEdit`, `OrderDispatch` и т. д., для лучшей организации можно создать модуль (папку) `Order`, который будет содержать (папки для) презентеров `Detail`, `Edit`, `Dispatch` и других.


Расположение шаблона .[#toc-template-location]
----------------------------------------------

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

/--pre
<b>Dashboard/</b>
├── <b>DashboardPresenter.php</b>     ← ведущий
├── <b>DashboardTemplate.php</b>      ← дополнительный класс шаблона
└── <b>default.latte</b>              ← шаблон
\--

Такое расположение оказывается наиболее удобным на практике - все связанные с ними файлы оказываются под рукой.

В качестве альтернативы вы можете поместить шаблоны в подпапку `templates/`. Nette поддерживает оба варианта. Можно даже разместить шаблоны совсем вне папки `Presentation/`. Все, что касается вариантов размещения шаблонов, вы найдете в главе ["Поиск шаблонов" |templates#Template Lookup].


Классы-помощники и компоненты .[#toc-helper-classes-and-components]
-------------------------------------------------------------------

Презентаторы и шаблоны часто поставляются с другими вспомогательными файлами. Мы размещаем их логически в соответствии с их областью применения:

1. **Непосредственно с презентатором** в случае специфических компонентов для данного презентатора:

/--pre
<b>Product/</b>
├── <b>ProductPresenter.php</b>
├── <b>ProductGrid.php</b>        ← компонент для вывода списка товаров
└── <b>FilterForm.php</b>         ← форма для фильтрации
\--

2. **Для модуля** - рекомендуем использовать папку `Accessory`, которая располагается аккурат в начале алфавита:

/--pre
<b>Front/</b>
├── <b>Accessory/</b>
│   ├── <b>NavbarControl.php</b>    ← компоненты для фронтенда
│   └── <b>TemplateFilters.php</b>
├── <b>Product/</b>
└── <b>Cart/</b>
\--

3. **Для всего приложения** - в `Presentation/Accessory/`:
/--pre
<b>app/Presentation/</b>
├── <b>Accessory/</b>
│   ├── <b>LatteExtension.php</b>
│   └── <b>TemplateFilters.php</b>
├── <b>Front/</b>
└── <b>Admin/</b>
\--

Или вы можете поместить вспомогательные классы, такие как `LatteExtension.php` или `TemplateFilters.php`, в папку инфраструктуры `app/Core/Latte/`. А компоненты - в `app/Components`. Выбор зависит от командных соглашений.


Модель - сердце приложения .[#toc-model-heart-of-the-application]
=================================================================

Модель содержит всю бизнес-логику приложения. Для ее организации действует то же правило - мы структурируем по доменам:

/--pre
<b>app/Model/</b>
├── <b>Payment/</b>                   ← все о платежах
│   ├── <b>PaymentFacade.php</b>      ← основная точка входа
│   ├── <b>PaymentRepository.php</b>
│   ├── <b>Payment.php</b>            ← сущность
├── <b>Order/</b>                     ← всё о заказах
│   ├── <b>OrderFacade.php</b>
│   ├── <b>OrderRepository.php</b>
│   ├── <b>Order.php</b>
└── <b>Shipping/</b>                  ← всё о доставке
\--

В модели обычно встречаются такие типы классов:

**Фасады**: представляют собой основную точку входа в определенный домен приложения. Они выступают в роли оркестратора, который координирует взаимодействие между различными сервисами для реализации законченных сценариев использования (например, "создать заказ" или "обработать платеж"). Под своим оркестровым слоем фасад скрывает детали реализации от остальной части приложения, обеспечивая тем самым чистый интерфейс для работы с данным доменом.

```php
class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// Проверка
		// создание заказа
		// отправка писем
		// запись в статистику
	}
}
```

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

```php
class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// расчёт цены
	}
}
```

**Хранилища**: осуществляют все коммуникации с хранилищем данных, обычно базой данных. Их задача - загружать и сохранять сущности и реализовывать методы для их поиска. Хранилище ограждает остальную часть приложения от деталей реализации базы данных и предоставляет объектно-ориентированный интерфейс для работы с данными.

```php
class OrderRepository
{
	public function find(int $id): ?Order
	{
	}

	public function findByCustomer(int $customerId): array
	{
	}
}
```

**Сущности**: объекты, представляющие основные бизнес-концепции в приложении, которые имеют свою идентичность и изменяются со временем. Обычно это классы, отображенные на таблицы базы данных с помощью ORM (например, Nette Database Explorer или Doctrine). Сущности могут содержать бизнес-правила, касающиеся их данных и логики проверки.

```php
// Сущность, сопоставленная с таблицей базы данных "Заказы
class Order extends Nette\Database\Table\ActiveRow
{
	public function addItem(Product $product, int $quantity): void
	{
		$this->related('order_items')->insert([
			'product_id' => $product->id,
			'quantity' => $quantity,
			'unit_price' => $product->price,
		]);
	}
}
```

**Объекты значений**: неизменяемые объекты, представляющие значения без собственной идентичности - например, денежную сумму или адрес электронной почты. Два экземпляра объекта-значения с одинаковыми значениями считаются идентичными.


Код инфраструктуры .[#toc-infrastructure-code]
==============================================

Папка `Core/` (или также `Infrastructure/`) является домом для технической основы приложения. Инфраструктурный код обычно включает:

/--pre
<b>app/Core/</b>
├── <b>Router/</b>               ← маршрутизация и управление URL-адресами
│   └── <b>RouterFactory.php</b>
├── <b>Security/</b>             ← аутентификация и авторизация
│   ├── <b>Authenticator.php</b>
│   └── <b>Authorizator.php</b>
├── <b>Logging/</b>              ← протоколирование и мониторинг
│   ├── <b>SentryLogger.php</b>
│   └── <b>FileLogger.php
├── <b>Cache/</b>                ← слой кэширования
│   └── <b>FullPageCache.php</b>
└── <b>Integration/</b>          ← интеграция с внешними сервисами
	├── <b>Slack/</b>
	└── <b>Stripe/</b>
\--

Для небольших проектов, естественно, достаточно плоской структуры:

/--pre
<b>Core/</b>
├── <b>RouterFactory.php</b>
├── <b>Authenticator.php</b>
└── <b>QueueMailer.php</b>
\--

Это код, который:

- Управляет технической инфраструктурой (маршрутизация, логирование, кэширование)
- Интегрирует внешние сервисы (Sentry, Elasticsearch, Redis)
- Обеспечивает базовые сервисы для всего приложения (почта, база данных)
- В основном не зависит от конкретного домена - кэш или логгер работает одинаково для электронной коммерции или блога.

Задаетесь вопросом, где находится определенный класс - здесь или в модели? Ключевое различие заключается в том, что код в `Core/`:

- Ничего не знает о домене (товары, заказы, статьи).
- Обычно может быть перенесен в другой проект
- Решает "как это работает" (как отправить почту), а не "что это делает" (какую почту отправить)

Пример для лучшего понимания:

- `App\Core\MailerFactory` - создает экземпляры класса отправки электронной почты, обрабатывает настройки SMTP
- `App\Model\OrderMailer` - использует `MailerFactory` для отправки писем о заказах, знает их шаблоны и время отправки


Командные скрипты .[#toc-command-scripts]
=========================================

Приложениям часто требуется выполнять задачи, выходящие за рамки обычных HTTP-запросов - будь то фоновая обработка данных, обслуживание или периодические задачи. Для выполнения используются простые скрипты в каталоге `bin/`, в то время как реальная логика реализации размещается в каталоге `app/Tasks/` (или `app/Commands/`).

Пример:

/--pre
<b>app/Tasks/</b>
├── <b>Maintenance/</b>               ← сценарии обслуживания
│   ├── <b>CleanupCommand.php</b>     ← удаление старых данных
│   └── <b>DbOptimizeCommand.php</b>  ← оптимизация базы данных
├── <b>Integration/</b>               ← интеграция с внешними системами
│   ├── <b>ImportProducts.php</b>     ← импорт из системы поставщика
│   └── <b>SyncOrders.php</b>         ← синхронизация заказов
└── <b>Scheduled/</b>                 ← регулярные задачи
	├── <b>NewsletterCommand.php</b>  ← рассылка новостей
	└── <b>ReminderCommand.php</b>    ← уведомления клиентов
\--

Что относится к модели, а что к командным скриптам? Например, логика отправки одного письма является частью модели, а массовая отправка тысяч писем - `Tasks/`.

Задачи обычно [запускаются из командной строки |https://blog.nette.org/en/cli-scripts-in-nette-application] или через cron. Их также можно запускать через HTTP-запрос, но при этом необходимо учитывать безопасность. Ведущий, запускающий задачу, должен быть защищен, например, только для вошедших в систему пользователей или с помощью надежного токена и доступа с разрешенных IP-адресов. Для длительных заданий необходимо увеличить лимит времени выполнения сценария и использовать `session_write_close()`, чтобы избежать блокировки сессии.


Другие возможные каталоги .[#toc-other-possible-directories]
============================================================

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

/--pre
<b>app/</b>
├── <b>Api/</b>              ← Логика API не зависит от уровня представления
├── <b>Database/</b>         ← скрипты миграции и семплы для тестовых данных
├── <b>Components/</b>       ← общие визуальные компоненты для всего приложения
├── <b>Event/</b>            ← полезно при использовании событийно-управляемой архитектуры
├── <b>Mail/</b>             ← шаблоны электронной почты и связанная с ними логика
└── <b>Utils/</b>            ← вспомогательные классы
\--

Для общих визуальных компонентов, используемых в презентаторах всего приложения, вы можете использовать папку `app/Components` или `app/Controls`:

/--pre
<b>app/Components/</b>
├── <b>Form/</b>                 ← общие компоненты формы
│   ├── <b>SignInForm.php</b>
│   └── <b>UserForm.php</b>
├── <b>Grid/</b>                 ← компоненты для списков данных
│   └── <b>DataGrid.php</b>
└── <b>Navigation/</b>           ← элементы навигации
	├── <b>Breadcrumbs.php</b>
	└── <b>Menu.php</b>
\--

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

В директории `app/Mail` вы можете разместить управление почтовыми коммуникациями:

/--pre
<b>app/Mail/</b>
├── <b>templates/</b>            ← шаблоны электронной почты
│   ├── <b>order-confirmation.latte</b>
│   └── <b>welcome.latte</b>
└── <b>OrderMailer.php</b>
\--


Составление карты ведущего .[#toc-presenter-mapping]
====================================================

Mapping определяет правила получения имен классов из имен ведущих. Мы указываем их в [конфигурации |configuration] под ключом `application › mapping`.

На этой странице мы показали, что размещаем ведущих в папке `app/Presentation` (или `app/UI`). Нам нужно сообщить Nette об этом соглашении в файле конфигурации. Достаточно одной строки:

```neon
application:
	mapping: App\Presentation\*\**Presenter
```

Как работает отображение? Чтобы лучше понять, давайте сначала представим себе приложение без модулей. Мы хотим, чтобы классы ведущих относились к пространству имен `App\Presentation`, чтобы ведущий `Home` отображался на класс `App\Presentation\HomePresenter`. Это достигается с помощью такой конфигурации:

```neon
application:
	mapping: App\Presentation\*Presenter
```

Сопоставление происходит путем замены звездочки в маске `App\Presentation\*Presenter` на имя ведущего `Home`, в результате чего получается конечное имя класса `App\Presentation\HomePresenter`. Просто!

Однако, как вы увидите в примерах в этой и других главах, мы помещаем классы ведущих в одноименные подкаталоги, например, ведущий `Home` отображается на класс `App\Presentation\Home\HomePresenter`. Для этого мы удваиваем двоеточие (требуется Nette Application 3.2):

```neon
application:
	mapping: App\Presentation\**Presenter
```

Теперь мы перейдем к отображению ведущих в модулях. Для каждого модуля мы можем определить конкретное отображение:

```neon
application:
	mapping:
		Front: App\Presentation\Front\**Presenter
		Admin: App\Presentation\Admin\**Presenter
		Api: App\Api\*Presenter
```

Согласно этой конфигурации, ведущий `Front:Home` относится к классу `App\Presentation\Front\Home\HomePresenter`, а ведущий `Api:OAuth` - к классу `App\Api\OAuthPresenter`.

Поскольку модули `Front` и `Admin` имеют схожий метод отображения и, вероятно, таких модулей будет больше, можно создать общее правило, которое заменит их. В маску класса будет добавлена новая звездочка для модуля:

```neon
application:
	mapping:
		*: App\Presentation\*\**Presenter
		Api: App\Api\*Presenter
```

Это также работает для более глубоких вложенных структур каталогов, таких как ведущий `Admin:User:Edit`, где сегмент со звездочкой повторяется для каждого уровня и приводит к классу `App\Presentation\Admin\User\Edit\EditPresenter`.

Альтернативной нотацией является использование массива, состоящего из трех сегментов, вместо строки. Эта нотация эквивалентна предыдущей:

```neon
application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]
```

Структура каталогов приложения

Как разработать четкую и масштабируемую структуру каталогов для проектов на Nette Framework? Мы покажем вам проверенные практики, которые помогут вам организовать ваш код. Вы узнаете:

  • как логически структурировать приложение по каталогам
  • как спроектировать структуру так, чтобы она масштабировалась по мере роста проекта
  • каковы возможные альтернативы и их преимущества и недостатки

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

Базовая структура проекта

Хотя Nette Framework не диктует никакой фиксированной структуры каталогов, существует проверенная структура по умолчанию в виде Web Project:

web-project/
├── app/              ← каталог приложений
├── assets/           ← SCSS, JS файлы, изображения..., альтернативный вариант resources/
├── bin/              ← скрипты командной строки
├── config/           ← конфигурация
├── log/              ← зарегистрированные ошибки
├── temp/             ← временные файлы, кэш
├── tests/            ← тесты
├── vendor/           ← библиотеки, установленные Composer
└── www/              ← публичный каталог (document-root)

Вы можете свободно изменять эту структуру в соответствии со своими потребностями – переименовывать или перемещать папки. Тогда вам просто нужно будет скорректировать относительные пути к каталогам в Bootstrap.php и, возможно, composer.json. Больше ничего не нужно, никакой сложной переконфигурации, никаких постоянных изменений. Nette обладает интеллектуальным автоопределением и автоматически распознает местоположение приложения, включая его базу URL.

Принципы организации кода

Когда вы впервые изучаете новый проект, вы должны быть в состоянии быстро сориентироваться. Представьте, что вы перешли в каталог app/Model/ и увидели такую структуру:

app/Model/
├── Services/
├── Repositories/
└── Entities/

Из нее вы узнаете только то, что проект использует некоторые сервисы, репозитории и сущности. Вы ничего не узнаете о реальном назначении приложения.

Давайте рассмотрим другой подход – организацию по доменам:

app/Model/
├── Cart/
├── Payment/
├── Order/
└── Product/

Это совсем другое дело – с первого взгляда понятно, что это сайт электронной коммерции. Сами названия каталогов говорят о том, что умеет делать приложение – оно работает с платежами, заказами и товарами.

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

Пространства имен

Принято считать, что структура каталогов соответствует пространствам имен в приложении. Это означает, что физическое расположение файлов соответствует их пространству имен. Например, класс, расположенный в каталоге app/Model/Product/ProductRepository.php, должен иметь пространство имен App\Model\Product. Этот принцип помогает ориентироваться в коде и упрощает автозагрузку.

Единственное и множественное число в именах

Обратите внимание, что мы используем единственное число для основных каталогов приложения: app, config, log, temp, www. То же самое относится и к внутренним каталогам приложения: Model, Core, Presentation. Это связано с тем, что каждый из них представляет собой единую концепцию.

Аналогично, app/Model/Product представляет все, что связано с продуктами. Мы не называем его Products, потому что это не папка с продуктами (в ней были бы такие файлы, как iphone.php, samsung.php). Это пространство имен, содержащее классы для работы с продуктами – ProductRepository.php, ProductService.php.

Папка app/Tasks имеет множественное число, потому что содержит набор отдельных исполняемых скриптов – CleanupTask.php, ImportTask.php. Каждый из них является самостоятельной единицей.

Для согласованности мы рекомендуем использовать:

  • Единственное число для пространств имен, представляющих функциональную единицу (даже если вы работаете с несколькими сущностями).
  • Множественное число для коллекций независимых единиц.
  • В случае неопределенности или если вы не хотите об этом думать, выбирайте единственное число

Публичный каталог www/

Эта директория является единственной, доступной из Интернета (так называемый document-root). Вы можете часто встретить название public/ вместо www/ – это просто условность и не влияет на функциональность. Каталог содержит:

  • Точка входа в приложение index.php
  • Файл .htaccess с правилами mod_rewrite (для Apache)
  • Статические файлы (CSS, JavaScript, изображения)
  • Загружаемые файлы

Для обеспечения безопасности приложения очень важно правильно настроить document-root.

Никогда не размещайте папку node_modules/ в этом каталоге – она содержит тысячи файлов, которые могут быть исполняемыми и не должны находиться в открытом доступе.

Каталог приложений app/

Это основной каталог с кодом приложения. Основная структура:

app/
├── Core/               ← инфраструктурные вопросы
├── Model/              ← бизнес-логика
├── Presentation/       ← презентаторы и шаблоны
├── Tasks/              ← командные скрипты
└── Bootstrap.php       ← класс загрузки приложения

Bootstrap.php класс запуска приложения, который инициализирует окружение, загружает конфигурацию и создает DI-контейнер.

Теперь рассмотрим отдельные подкаталоги подробнее.

Презентаторы и шаблоны

Презентационная часть приложения находится в каталоге app/Presentation. Альтернативой может служить короткий app/UI. Это место для всех презентаторов, их шаблонов и любых вспомогательных классов.

Мы организуем этот слой по доменам. В сложном проекте, объединяющем электронную коммерцию, блог и API, структура будет выглядеть следующим образом:

app/Presentation/
├── Shop/              ← фронтенд электронной коммерции
│   ├── Product/
│   ├── Cart/
│   └── Order/
├── Blog/              ← блог
│   ├── Home/
│   └── Post/
├── Admin/             ← администрирование
│   ├── Dashboard/
│   └── Products/
└── Api/               ← конечные точки API
	└── V1/

И наоборот, для простого блога мы бы использовали такую структуру:

app/Presentation/
├── Front/             ← фронтенд сайта
│   ├── Home/
│   └── Post/
├── Admin/             ← администрирование
│   ├── Dashboard/
│   └── Posts/
├── Error/
└── Export/            ← RSS, карты сайта и т.д.

Папки типа Home/ или Dashboard/ содержат ведущие и шаблоны. Папки типа Front/, Admin/ или Api/ называются модулями. Технически это обычные каталоги, которые служат для логической организации приложения.

Каждая папка с ведущим содержит аналогичный ведущий и его шаблоны. Например, папка Dashboard/ содержит:

Dashboard/
├── DashboardPresenter.php     ← ведущий
└── default.latte              ← шаблон

Эта структура каталогов отражается в пространствах имен классов. Например, DashboardPresenter находится в пространстве имен App\Presentation\Admin\Dashboard (см. сопоставление с ведущим):

namespace App\Presentation\Admin\Dashboard;

class DashboardPresenter extends Nette\Application\UI\Presenter
{
	//...
}

Мы ссылаемся на ведущего Dashboard внутри модуля Admin в приложении, используя обозначение двоеточия, как Admin:Dashboard. К его действию default тогда как Admin:Dashboard:default. Для вложенных модулей мы используем больше двоеточий, например, Shop:Order:Detail:default.

Разработка гибкой структуры

Одно из главных преимуществ этой структуры – то, как элегантно она адаптируется к растущим потребностям проекта. В качестве примера возьмем часть, генерирующую XML-ленты. Изначально у нас есть простая форма:

Export/
├── ExportPresenter.php   ← один ведущий для всех экспортов
├── sitemap.latte         ← шаблон для карты сайта
└── feed.latte            ← шаблон для RSS-ленты

Со временем добавляются новые типы фидов, и нам нужна дополнительная логика для них… Нет проблем! Папка Export/ просто становится модулем:

Export/
├── Sitemap/
│   ├── SitemapPresenter.php
│   └── sitemap.latte
└── Feed/
	├── FeedPresenter.php
	├── amazon.latte         ← фид для Amazon
	└── ebay.latte           ← фид для eBay

Эта трансформация совершенно гладкая – просто создайте новые подпапки, разделите на них код и обновите ссылки (например, с Export:feed на Export:Feed:amazon). Благодаря этому мы можем постепенно расширять структуру по мере необходимости, уровень вложенности никак не ограничен.

Например, если в администрировании у вас много презентеров, связанных с управлением заказами, таких как OrderDetail, OrderEdit, OrderDispatch и т. д., для лучшей организации можно создать модуль (папку) Order, который будет содержать (папки для) презентеров Detail, Edit, Dispatch и других.

Расположение шаблона

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

Dashboard/
├── DashboardPresenter.php     ← ведущий
├── DashboardTemplate.php      ← дополнительный класс шаблона
└── default.latte              ← шаблон

Такое расположение оказывается наиболее удобным на практике – все связанные с ними файлы оказываются под рукой.

В качестве альтернативы вы можете поместить шаблоны в подпапку templates/. Nette поддерживает оба варианта. Можно даже разместить шаблоны совсем вне папки Presentation/. Все, что касается вариантов размещения шаблонов, вы найдете в главе „Поиск шаблонов“.

Классы-помощники и компоненты

Презентаторы и шаблоны часто поставляются с другими вспомогательными файлами. Мы размещаем их логически в соответствии с их областью применения:

1. Непосредственно с презентатором в случае специфических компонентов для данного презентатора:

Product/
├── ProductPresenter.php
├── ProductGrid.php        ← компонент для вывода списка товаров
└── FilterForm.php         ← форма для фильтрации

2. Для модуля – рекомендуем использовать папку Accessory, которая располагается аккурат в начале алфавита:

Front/
├── Accessory/
│   ├── NavbarControl.php    ← компоненты для фронтенда
│   └── TemplateFilters.php
├── Product/
└── Cart/

3. Для всего приложения – в Presentation/Accessory/:

app/Presentation/
├── Accessory/
│   ├── LatteExtension.php
│   └── TemplateFilters.php
├── Front/
└── Admin/

Или вы можете поместить вспомогательные классы, такие как LatteExtension.php или TemplateFilters.php, в папку инфраструктуры app/Core/Latte/. А компоненты – в app/Components. Выбор зависит от командных соглашений.

Модель – сердце приложения

Модель содержит всю бизнес-логику приложения. Для ее организации действует то же правило – мы структурируем по доменам:

app/Model/
├── Payment/                   ← все о платежах
│   ├── PaymentFacade.php      ← основная точка входа
│   ├── PaymentRepository.php
│   ├── Payment.php            ← сущность
├── Order/                     ← всё о заказах
│   ├── OrderFacade.php
│   ├── OrderRepository.php
│   ├── Order.php
└── Shipping/                  ← всё о доставке

В модели обычно встречаются такие типы классов:

Фасады: представляют собой основную точку входа в определенный домен приложения. Они выступают в роли оркестратора, который координирует взаимодействие между различными сервисами для реализации законченных сценариев использования (например, „создать заказ“ или „обработать платеж“). Под своим оркестровым слоем фасад скрывает детали реализации от остальной части приложения, обеспечивая тем самым чистый интерфейс для работы с данным доменом.

class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// Проверка
		// создание заказа
		// отправка писем
		// запись в статистику
	}
}

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

class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// расчёт цены
	}
}

Хранилища: осуществляют все коммуникации с хранилищем данных, обычно базой данных. Их задача – загружать и сохранять сущности и реализовывать методы для их поиска. Хранилище ограждает остальную часть приложения от деталей реализации базы данных и предоставляет объектно-ориентированный интерфейс для работы с данными.

class OrderRepository
{
	public function find(int $id): ?Order
	{
	}

	public function findByCustomer(int $customerId): array
	{
	}
}

Сущности: объекты, представляющие основные бизнес-концепции в приложении, которые имеют свою идентичность и изменяются со временем. Обычно это классы, отображенные на таблицы базы данных с помощью ORM (например, Nette Database Explorer или Doctrine). Сущности могут содержать бизнес-правила, касающиеся их данных и логики проверки.

// Сущность, сопоставленная с таблицей базы данных "Заказы
class Order extends Nette\Database\Table\ActiveRow
{
	public function addItem(Product $product, int $quantity): void
	{
		$this->related('order_items')->insert([
			'product_id' => $product->id,
			'quantity' => $quantity,
			'unit_price' => $product->price,
		]);
	}
}

Объекты значений: неизменяемые объекты, представляющие значения без собственной идентичности – например, денежную сумму или адрес электронной почты. Два экземпляра объекта-значения с одинаковыми значениями считаются идентичными.

Код инфраструктуры

Папка Core/ (или также Infrastructure/) является домом для технической основы приложения. Инфраструктурный код обычно включает:

app/Core/
├── Router/               ← маршрутизация и управление URL-адресами
│   └── RouterFactory.php
├── Security/             ← аутентификация и авторизация
│   ├── Authenticator.php
│   └── Authorizator.php
├── Logging/              ← протоколирование и мониторинг
│   ├── SentryLogger.php
│   └── FileLogger.php
├── Cache/                ← слой кэширования
│   └── FullPageCache.php
└── Integration/          ← интеграция с внешними сервисами
	├── Slack/
	└── Stripe/

Для небольших проектов, естественно, достаточно плоской структуры:

Core/
├── RouterFactory.php
├── Authenticator.php
└── QueueMailer.php

Это код, который:

  • Управляет технической инфраструктурой (маршрутизация, логирование, кэширование)
  • Интегрирует внешние сервисы (Sentry, Elasticsearch, Redis)
  • Обеспечивает базовые сервисы для всего приложения (почта, база данных)
  • В основном не зависит от конкретного домена – кэш или логгер работает одинаково для электронной коммерции или блога.

Задаетесь вопросом, где находится определенный класс – здесь или в модели? Ключевое различие заключается в том, что код в Core/:

  • Ничего не знает о домене (товары, заказы, статьи).
  • Обычно может быть перенесен в другой проект
  • Решает „как это работает“ (как отправить почту), а не „что это делает“ (какую почту отправить)

Пример для лучшего понимания:

  • App\Core\MailerFactory – создает экземпляры класса отправки электронной почты, обрабатывает настройки SMTP
  • App\Model\OrderMailer – использует MailerFactory для отправки писем о заказах, знает их шаблоны и время отправки

Командные скрипты

Приложениям часто требуется выполнять задачи, выходящие за рамки обычных HTTP-запросов – будь то фоновая обработка данных, обслуживание или периодические задачи. Для выполнения используются простые скрипты в каталоге bin/, в то время как реальная логика реализации размещается в каталоге app/Tasks/ (или app/Commands/).

Пример:

app/Tasks/
├── Maintenance/               ← сценарии обслуживания
│   ├── CleanupCommand.php     ← удаление старых данных
│   └── DbOptimizeCommand.php  ← оптимизация базы данных
├── Integration/               ← интеграция с внешними системами
│   ├── ImportProducts.php     ← импорт из системы поставщика
│   └── SyncOrders.php         ← синхронизация заказов
└── Scheduled/                 ← регулярные задачи
	├── NewsletterCommand.php  ← рассылка новостей
	└── ReminderCommand.php    ← уведомления клиентов

Что относится к модели, а что к командным скриптам? Например, логика отправки одного письма является частью модели, а массовая отправка тысяч писем – Tasks/.

Задачи обычно запускаются из командной строки или через cron. Их также можно запускать через HTTP-запрос, но при этом необходимо учитывать безопасность. Ведущий, запускающий задачу, должен быть защищен, например, только для вошедших в систему пользователей или с помощью надежного токена и доступа с разрешенных IP-адресов. Для длительных заданий необходимо увеличить лимит времени выполнения сценария и использовать session_write_close(), чтобы избежать блокировки сессии.

Другие возможные каталоги

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

app/
├── Api/              ← Логика API не зависит от уровня представления
├── Database/         ← скрипты миграции и семплы для тестовых данных
├── Components/       ← общие визуальные компоненты для всего приложения
├── Event/            ← полезно при использовании событийно-управляемой архитектуры
├── Mail/             ← шаблоны электронной почты и связанная с ними логика
└── Utils/            ← вспомогательные классы

Для общих визуальных компонентов, используемых в презентаторах всего приложения, вы можете использовать папку app/Components или app/Controls:

app/Components/
├── Form/                 ← общие компоненты формы
│   ├── SignInForm.php
│   └── UserForm.php
├── Grid/                 ← компоненты для списков данных
│   └── DataGrid.php
└── Navigation/           ← элементы навигации
	├── Breadcrumbs.php
	└── Menu.php

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

В директории app/Mail вы можете разместить управление почтовыми коммуникациями:

app/Mail/
├── templates/            ← шаблоны электронной почты
│   ├── order-confirmation.latte
│   └── welcome.latte
└── OrderMailer.php

Составление карты ведущего

Mapping определяет правила получения имен классов из имен ведущих. Мы указываем их в конфигурации под ключом application › mapping.

На этой странице мы показали, что размещаем ведущих в папке app/Presentation (или app/UI). Нам нужно сообщить Nette об этом соглашении в файле конфигурации. Достаточно одной строки:

application:
	mapping: App\Presentation\*\**Presenter

Как работает отображение? Чтобы лучше понять, давайте сначала представим себе приложение без модулей. Мы хотим, чтобы классы ведущих относились к пространству имен App\Presentation, чтобы ведущий Home отображался на класс App\Presentation\HomePresenter. Это достигается с помощью такой конфигурации:

application:
	mapping: App\Presentation\*Presenter

Сопоставление происходит путем замены звездочки в маске App\Presentation\*Presenter на имя ведущего Home, в результате чего получается конечное имя класса App\Presentation\HomePresenter. Просто!

Однако, как вы увидите в примерах в этой и других главах, мы помещаем классы ведущих в одноименные подкаталоги, например, ведущий Home отображается на класс App\Presentation\Home\HomePresenter. Для этого мы удваиваем двоеточие (требуется Nette Application 3.2):

application:
	mapping: App\Presentation\**Presenter

Теперь мы перейдем к отображению ведущих в модулях. Для каждого модуля мы можем определить конкретное отображение:

application:
	mapping:
		Front: App\Presentation\Front\**Presenter
		Admin: App\Presentation\Admin\**Presenter
		Api: App\Api\*Presenter

Согласно этой конфигурации, ведущий Front:Home относится к классу App\Presentation\Front\Home\HomePresenter, а ведущий Api:OAuth – к классу App\Api\OAuthPresenter.

Поскольку модули Front и Admin имеют схожий метод отображения и, вероятно, таких модулей будет больше, можно создать общее правило, которое заменит их. В маску класса будет добавлена новая звездочка для модуля:

application:
	mapping:
		*: App\Presentation\*\**Presenter
		Api: App\Api\*Presenter

Это также работает для более глубоких вложенных структур каталогов, таких как ведущий Admin:User:Edit, где сегмент со звездочкой повторяется для каждого уровня и приводит к классу App\Presentation\Admin\User\Edit\EditPresenter.

Альтернативной нотацией является использование массива, состоящего из трех сегментов, вместо строки. Эта нотация эквивалентна предыдущей:

application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]