Nette Documentation Preview

syntax
Директорийна структура на приложението
**************************************

<div class=perex>

Как да проектираме ясна и мащабируема директорийна структура за проекти в Nette Framework? Ще покажем доказани практики, които ще ви помогнат с организацията на кода. Ще научите:

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

</div>


Важно е да се спомене, че самият Nette Framework не налага никаква конкретна структура. Той е проектиран така, че да може лесно да се адаптира към всякакви нужди и предпочитания.


Основна структура на проекта
============================

Въпреки че 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 основа.


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

Когато за първи път разглеждате нов проект, трябва бързо да се ориентирате в него. Представете си, че разгръщате директорията `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>
\--

Тук е различно - на пръв поглед е ясно, че става въпрос за електронен магазин. Самите имена на директориите разкриват какво може приложението - работи с плащания, поръчки и продукти.

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


Именни пространства
-------------------

Прието е директорийната структура да съответства на именните пространства в приложението. Това означава, че физическото местоположение на файловете отговаря на техния 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/` - това е само въпрос на конвенция и няма влияние върху функционалността на приложението. Директорията съдържа:
- [Входна точка |bootstrapping#index.php] на приложението `index.php`
- Файл `.htaccess` с правила за mod_rewrite (при Apache)
- Статични файлове (CSS, JavaScript, изображения)
- Качени файлове

За правилното осигуряване на сигурността на приложението е от съществено значение да имате правилно [конфигуриран document-root |nette:troubleshooting#Как да промените или премахнете директорията www от URL адреса].

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


Апликационна директория `app/`
==============================

Това е основната директория с кода на приложението. Основна структура:

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

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

Нека сега разгледаме отделните поддиректории по-подробно.


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

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

Този слой организираме по домейни. В сложен проект, който комбинира електронен магазин, блог и API, структурата би изглеждала така:

/--pre
<b>app/Presentation/</b>
├── <b>Shop/</b>              ← електронен магазин frontend
│   ├── <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 endpoints
	└── <b>V1/</b>
\--

Напротив, при прост блог бихме използвали разделяне:

/--pre
<b>app/Presentation/</b>
├── <b>Front/</b>             ← frontend на уебсайта
│   ├── <b>Home/</b>
│   └── <b>Post/</b>
├── <b>Admin/</b>             ← администрация
│   ├── <b>Dashboard/</b>
│   └── <b>Posts/</b>
├── <b>Error/</b>
└── <b>Export/</b>            ← RSS, sitemaps и т.н.
\--

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

Всяка папка с презентер съдържа едноименен презентер и неговите шаблони. Например папка `Dashboard/` съдържа:

/--pre
<b>Dashboard/</b>
├── <b>DashboardPresenter.php</b>     ← презентер
└── <b>default.latte</b>              ← шаблон
\--

Тази директорийна структура се отразява в именните пространства на класовете. Например `DashboardPresenter` се намира в именното пространство `App\Presentation\Admin\Dashboard` (виж [#Мапиране на презентери]):

```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`.


Гъвкаво развитие на структурата
-------------------------------

Едно от големите предимства на тази структура е колко елегантно се адаптира към растящите нужди на проекта. Като пример да вземем частта, генерираща XML фийдове. В началото имаме проста форма:

/--pre
<b>Export/</b>
├── <b>ExportPresenter.php</b>   ← един презентер за всички експорти
├── <b>sitemap.latte</b>         ← шаблон за sitemap
└── <b>feed.latte</b>            ← шаблон за RSS feed
\--

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

/--pre
<b>Export/</b>
├── <b>Sitemap/</b>
│   ├── <b>SitemapPresenter.php</b>
│   └── <b>sitemap.latte</b>
└── <b>Feed/</b>
	├── <b>FeedPresenter.php</b>
	├── <b>zbozi.latte</b>         ← фийд за Zboží.cz
	└── <b>heureka.latte</b>       ← фийд за Heureka.cz
\--

Тази трансформация е напълно плавна - достатъчно е да се създадат нови подпапки, да се раздели кодът в тях и да се актуализират връзките (напр. от `Export:feed` на `Export:Feed:zbozi`). Благодарение на това можем постепенно да разширяваме структурата според нуждите, нивото на влагане не е никак ограничено.

Ако например в администрацията имате много презентери, свързани с управлението на поръчки, като `OrderDetail`, `OrderEdit`, `OrderDispatch` и т.н., можете за по-добра организираност на това място да създадете модул (папка) `Order`, в който ще бъдат (папки за) презентерите `Detail`, `Edit`, `Dispatch` и други.


Местоположение на шаблоните
---------------------------

В предишните примери видяхме, че шаблоните са разположени директно в папката с презентера:

/--pre
<b>Dashboard/</b>
├── <b>DashboardPresenter.php</b>     ← презентер
├── <b>DashboardTemplate.php</b>      ← незадължителен клас за шаблона
└── <b>default.latte</b>              ← шаблон
\--

Това местоположение на практика се оказва най-удобно - всички свързани файлове са ви веднага под ръка.

Алтернативно можете да поставите шаблоните в подпапка `templates/`. Nette поддържа и двата варианта. Дори можете да поставите шаблоните изцяло извън папката `Presentation/`. Всичко за възможностите за разполагане на шаблони ще намерите в главата [Търсене на шаблони |templates#Търсене на шаблони].


Помощни класове и компоненти
----------------------------

Към презентерите и шаблоните често принадлежат и други помощни файлове. Разполагаме ги логично според тяхната област на действие:

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>    ← компоненти за frontend
│   └── <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`. Изборът зависи от навиците на екипа.


Модел - сърцето на приложението
===============================

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

/--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>                  ← всичко около доставката
\--

В модела типично ще срещнете тези типове класове:

**Фасади**: представляват основната входна точка към конкретен домейн в приложението. Действат като оркестратор, който координира сътрудничеството между различни сървиси с цел имплементиране на пълни use-cases (като "създай поръчка" или "обработи плащане"). Под своя оркестрационен слой фасадата скрива имплементационните детайли от останалата част на приложението, като по този начин предоставя чист интерфейс за работа с дадения домейн.

```php
class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// валидация
		// създаване на поръчка
		// изпращане на имейл
		// записване в статистики
	}
}
```

**Сървиси**: фокусират се върху специфична бизнес операция в рамките на домейна. За разлика от фасадата, която оркестрира цели use-cases, сървисът имплементира конкретна бизнес логика (като изчисления на цени или обработка на плащания). Сървисите са типично безсъстоянийни и могат да бъдат използвани или от фасади като строителни блокове за по-сложни операции, или директно от други части на приложението за по-прости задачи.

```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
// Ентитит, мапнат към таблицата 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/`) е домът на техническата основа на приложението. Инфраструктурният код типично включва:

/--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>
├── <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` за изпращане на имейли за поръчки, знае техните шаблони и кога трябва да се изпратят


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

Приложенията често трябва да извършват дейности извън обичайните 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()`, за да не се заключва сесията.


Други възможни директории
=========================

Освен споменатите основни директории, можете според нуждите на проекта да добавите други специализирани папки. Да разгледаме най-често срещаните от тях и тяхното използване:

/--pre
<b>app/</b>
├── <b>Api/</b>              ← логика за API, независима от презентационния слой
├── <b>Database/</b>         ← миграционни скриптове и seeders за тестови данни
├── <b>Components/</b>       ← споделени визуални компоненти в цялото приложение
├── <b>Event/</b>            ← полезно, ако използвате event-driven архитектура
├── <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>
\--

Тук принадлежат компоненти, които имат по-сложна логика. Ако искате да споделяте компоненти между няколко проекта, е препоръчително да ги изнесете в отделен composer пакет.

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

/--pre
<b>app/Mail/</b>
├── <b>templates/</b>            ← имейл шаблони
│   ├── <b>order-confirmation.latte</b>
│   └── <b>welcome.latte</b>
└── <b>OrderMailer.php</b>
\--


Мапиране на презентери
======================

Мапирането дефинира правила за извеждане на името на класа от името на презентера. Специфицираме ги в [конфигурацията|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
```

Мапирането работи така, че името на презентера `Home` замества звездичката в маската `App\Presentation\*Presenter`, с което получаваме крайния име на класа `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/

Тук е различно – на пръв поглед е ясно, че става въпрос за електронен магазин. Самите имена на директориите разкриват какво може приложението – работи с плащания, поръчки и продукти.

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

Именни пространства

Прието е директорийната структура да съответства на именните пространства в приложението. Това означава, че физическото местоположение на файловете отговаря на техния 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]