Директорийна структура на приложението
Как да проектираме ясна и мащабируема директорийна структура за проекти в 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/
Тук е различно – на пръв поглед е ясно, че става въпрос за електронен магазин. Самите имена на директориите разкриват какво може приложението – работи с плащания, поръчки и продукти.
Първият подход (организация по тип класове) носи на практика редица проблеми: код, който логически е свързан, е разпръснат в различни папки и трябва да прескачате между тях. Затова ще организираме по домейни.
Именни пространства
Прието е директорийната структура да съответства на именните
пространства в приложението. Това означава, че физическото
местоположение на файловете отговаря на техния namespace. Например клас,
разположен в app/Model/Product/ProductRepository.php
, трябва да има namespace
App\Model\Product
. Този принцип помага за ориентацията в кода и
опростява autoloading-а.
Единствено срещу множествено число в имената
Забележете, че при основните директории на приложението използваме
единствено число: app
, config
, log
, temp
, www
.
Също така и вътре в приложението: Model
, Core
, Presentation
.
Това е така, защото всяка от тях представлява една цялостна
концепция.
Подобно, например app/Model/Product
представлява всичко около
продуктите. Няма да го наречем Products
, защото не става въпрос за
папка, пълна с продукти (тогава там биха били файлове nokia.php
,
samsung.php
). Това е namespace, съдържащ класове за работа с продукти –
ProductRepository.php
, ProductService.php
.
Папката app/Tasks
е в множествено число, защото съдържа набор от
самостоятелни изпълними скриптове – CleanupTask.php
, ImportTask.php
.
Всеки от тях е самостоятелна единица.
За консистентност препоръчваме да използвате:
- Единствено число за namespace, представляващ функционална цялост (макар и работещ с множество ентитита)
- Множествено число за колекции от самостоятелни единици
- В случай на несигурност или ако не искате да мислите за това, изберете единствено число
Публична директория 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/ ← електронен магазин frontend │ ├── Product/ │ ├── Cart/ │ └── Order/ ├── Blog/ ← блог │ ├── Home/ │ └── Post/ ├── Admin/ ← администрация │ ├── Dashboard/ │ └── Products/ └── Api/ ← API endpoints └── V1/
Напротив, при прост блог бихме използвали разделяне:
app/Presentation/ ├── Front/ ← frontend на уебсайта │ ├── Home/ │ └── Post/ ├── Admin/ ← администрация │ ├── Dashboard/ │ └── Posts/ ├── Error/ └── Export/ ← RSS, sitemaps и т.н.
Папки като 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 ← шаблон за sitemap └── feed.latte ← шаблон за RSS feed
С времето се добавят други типове фийдове и се нуждаем от повече
логика за тях… Няма проблем! Папката 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 ← компоненти за frontend │ └── 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/ ← всичко около доставката
В модела типично ще срещнете тези типове класове:
Фасади: представляват основната входна точка към конкретен домейн в приложението. Действат като оркестратор, който координира сътрудничеството между различни сървиси с цел имплементиране на пълни use-cases (като “създай поръчка” или “обработи плащане”). Под своя оркестрационен слой фасадата скрива имплементационните детайли от останалата част на приложението, като по този начин предоставя чист интерфейс за работа с дадения домейн.
class OrderFacade
{
public function createOrder(Cart $cart): Order
{
// валидация
// създаване на поръчка
// изпращане на имейл
// записване в статистики
}
}
Сървиси: фокусират се върху специфична бизнес операция в рамките на домейна. За разлика от фасадата, която оркестрира цели use-cases, сървисът имплементира конкретна бизнес логика (като изчисления на цени или обработка на плащания). Сървисите са типично безсъстоянийни и могат да бъдат използвани или от фасади като строителни блокове за по-сложни операции, или директно от други части на приложението за по-прости задачи.
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,
]);
}
}
Value обекти: неизменни обекти, представляващи стойности без собствена идентичност – например парична сума или имейл адрес. Две инстанции на value обект със същите стойности се считат за идентични.
Инфраструктурен код
Папката 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/ ← миграционни скриптове и seeders за тестови данни ├── Components/ ← споделени визуални компоненти в цялото приложение ├── Event/ ← полезно, ако използвате event-driven архитектура ├── 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
.
Това постигаме с удвояване на двоеточието (изисква 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]