Структура на директорията на приложението
Как да проектирате ясна и мащабируема структура на директориите за проекти в 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/ ← уебсайт frontend │ ├── 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 ← компоненти за 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/ ← всичко за доставката
В модела обикновено се срещат тези типове класове:
Фасади: представляват основната входна точка към определена област в приложението. Те действат като оркестратор, който координира сътрудничеството между различни услуги за изпълнение на цялостни случаи на употреба (като „създаване на поръчка“ или „обработка на плащане“). Под техния оркестрационен слой фасадата скрива подробностите за изпълнение от останалата част на приложението, като по този начин осигурява чист интерфейс за работа с дадения домейн.
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
Картографиране на водещия
Съпоставянето дефинира правила за извеждане на имена на класове от
имена на водещи. Посочваме ги в конфигурацията под
ключа 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]