Структура каталогов приложения
Как спроектировать понятную и масштабируемую структуру каталогов для проектов на 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
. Этот принцип помогает ориентироваться в коде и
упрощает автозагрузку.
Единственное vs множественное число в названиях
Обратите внимание, что для основных каталогов приложения мы
используем единственное число: app
, config
, log
,
temp
, www
. Точно так же и внутри приложения: Model
,
Core
, Presentation
. Это потому, что каждый из них представляет
собой единую целостную концепцию.
Аналогично, например, app/Model/Product
представляет все, что связано
с продуктами. Мы не назовем это Products
, потому что это не папка,
полная продуктов (там были бы файлы nokia.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 ├── zbozi.latte ← фид для Zboží.cz └── heureka.latte ← фид для Heureka.cz
Эта трансформация абсолютно плавная – достаточно создать новые
подпапки, распределить в них код и обновить ссылки (например, с
Export:feed
на Export:Feed:zbozi
). Благодаря этому мы можем постепенно
расширять структуру по мере необходимости, уровень вложенности никак
не ограничен.
Если, например, в администрировании у вас много презентеров,
связанных с управлением заказами, таких как 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). Сущности могут содержать бизнес-правила, касающиеся их данных, и логику валидации.
// Сущность, отображаемая на таблицу базы данных orders
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
– создает экземпляры класса для отправки электронной почты, решает настройки SMTPApp\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
Сюда относятся компоненты, имеющие более сложную логику. Если вы хотите делиться компонентами между несколькими проектами, рекомендуется выделить их в отдельный composer-пакет.
В каталог app/Mail
можно поместить управление электронной
почтой:
app/Mail/ ├── templates/ ← шаблоны электронной почты │ ├── order-confirmation.latte │ └── welcome.latte └── OrderMailer.php
Маппинг презентеров
Маппинг определяет правила для вывода имени класса из имени
презентера. Мы указываем их в конфигурации под ключом
application › mapping
.
На этой странице мы показали, что презентеры размещаем в папке
app/Presentation
(или app/UI
). Эту конвенцию мы должны сообщить Nette в
конфигурационном файле. Достаточно одной строки:
application:
mapping: App\Presentation\*\**Presenter
Как работает маппинг? Для лучшего понимания сначала представим
приложение без модулей. Мы хотим, чтобы классы презентеров попадали в
пространство имен App\Presentation
, чтобы презентер Home
отображался на класс App\Presentation\HomePresenter
. Этого мы достигнем с
помощью следующей конфигурации:
application:
mapping: App\Presentation\*Presenter
Маппинг работает так, что имя презентера Home
заменяет
звездочку в маске App\Presentation\*Presenter
, тем самым мы получаем
итоговое имя класса App\Presentation\HomePresenter
. Просто!
Однако, как вы видите в примерах в этой и других главах, классы
презентеров мы размещаем в одноименных подкаталогах, например,
презентер Home
отображается на класс App\Presentation\Home\HomePresenter
.
Этого мы достигнем удвоением звездочки:
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]