Nette Documentation Preview

syntax
Структура каталогу програми
***************************

<div class=perex>

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

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

</div>


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


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

Хоча Nette Framework не диктує фіксованої структури каталогів, існує перевірена структура за замовчуванням у вигляді [веб-проекту |https://github.com/nette/web-project]:

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

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


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

Коли ви вперше вивчаєте новий проект, ви повинні мати можливість швидко зорієнтуватися. Уявіть, що ви натискаєте на каталог `app/Model/` і бачите таку структуру:

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

З неї ви дізнаєтеся лише, що проект використовує деякі сервіси, сховища та сутності. Ви нічого не дізнаєтесь про справжню мету програми.

Давайте розглянемо інший підхід - **організація за доменами**:

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

Тут все інакше - з першого погляду зрозуміло, що це сайт електронної комерції. Самі назви каталогів показують, що може робити додаток - він працює з платежами, замовленнями і товарами.

Перший підхід (організація за типами класів) на практиці створює кілька проблем: логічно пов'язаний код розкиданий по різних папках і доводиться перестрибувати між ними. Тому ми будемо організовувати за доменами.


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

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


Однина та множина в іменах .[#toc-singular-vs-plural-in-names]
--------------------------------------------------------------

Зверніть увагу, що ми використовуємо однину для основних каталогів програми: `app`, `config`, `log`, `temp`, `www`. Те ж саме стосується і всередині програми: `Model`, `Core`, `Presentation`. Це тому, що кожна з них представляє одну уніфіковану концепцію.

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

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

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


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

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

Для належної безпеки додатку дуже важливо мати правильно [налаштований корінь документа |nette:troubleshooting#how-to-change-or-remove-www-directory-from-url].

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


Каталог додатків `app/` .[#toc-application-directory-app]
=========================================================

Це основний каталог з кодом програми. Базова структура:

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

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

Тепер давайте детально розглянемо окремі підкаталоги.


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

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

Ми організовуємо цей рівень за доменами. У складному проекті, який поєднує в собі електронну комерцію, блог і API, структура буде виглядати так:

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

І навпаки, для простого блогу ми б використовували таку структуру:

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

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

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

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

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

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

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

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


Гнучка розробка структури .[#toc-flexible-structure-development]
----------------------------------------------------------------

Однією з найбільших переваг цієї структури є те, наскільки елегантно вона адаптується до зростаючих потреб проекту. Для прикладу візьмемо частину, що генерує XML-стрічки. Спочатку у нас є проста форма:

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

Згодом додається більше типів фідів, і нам потрібно більше логіки для них... Ніяких проблем! Папка `Export/` просто стає модулем:

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

Ця трансформація є абсолютно плавною - просто створіть нові підпапки, розділіть в них код і оновіть посилання (наприклад, з `Export:feed` на `Export:Feed:amazon`). Завдяки цьому ми можемо поступово розширювати структуру в міру необхідності, рівень вкладеності ніяк не обмежений.

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


Розташування шаблону .[#toc-template-location]
----------------------------------------------

У попередніх прикладах ми бачили, що шаблони знаходяться безпосередньо в папці з доповідачем:

/--pre
<b>Dashboard/</b>
├── <b>DashboardPresenter.php</b>     ← ведучий
├── <b>DashboardTemplate.php</b>      ← необов'язковий клас шаблону
└── <b>default.latte</b>              ← шаблон
\--

Таке розташування виявляється найзручнішим на практиці - всі пов'язані файли у вас під рукою.

Крім того, ви можете розмістити шаблони в підпапці `templates/`. Nette підтримує обидва варіанти. Ви навіть можете розмістити шаблони поза текою `Presentation/`. Все про варіанти розташування шаблонів ви можете знайти в розділі [Пошук шаблонів |templates#Template Lookup].


Допоміжні класи та компоненти .[#toc-helper-classes-and-components]
-------------------------------------------------------------------

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

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

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

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

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

3. **Для всього додатку** - в `Presentation/Accessory/`:
/--pre
<b>app/Presentation/</b>
├── <b>Accessory/</b>
│   ├── <b>LatteExtension.php</b>
│   └── <b>TemplateFilters.php</b>
├── <b>Front/</b>
└── <b>Admin/</b>
\--

Або ви можете розмістити допоміжні класи типу `LatteExtension.php` або `TemplateFilters.php` в теці інфраструктури `app/Core/Latte/`. А компоненти в `app/Components`. Вибір залежить від конвенцій команди.


Модель - серце програми .[#toc-model-heart-of-the-application]
==============================================================

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

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

У моделі зазвичай зустрічаються такі типи класів:

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

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

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

```php
class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// розрахунок ціни
	}
}
```

**Сховища даних: відповідають за всю взаємодію зі сховищем даних, як правило, базою даних. Їхнє завдання - завантажувати та зберігати об'єкти, а також реалізовувати методи їхнього пошуку. Репозиторій захищає решту програми від деталей реалізації бази даних і надає об'єктно-орієнтований інтерфейс для роботи з даними.

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

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

**Сутності - об'єкти, що представляють основні бізнес-концепції в додатку, які мають свою ідентичність і змінюються з часом. Зазвичай це класи, що відображаються на таблиці бази даних за допомогою ORM (наприклад, Nette Database Explorer або Doctrine). Сутності можуть містити бізнес-правила, що стосуються їхніх даних та логіки перевірки.

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

**Об'єкти-значення**: незмінні об'єкти, що представляють значення без власної ідентичності - наприклад, сума грошей або адреса електронної пошти. Два екземпляри об'єкта-значення з однаковими значеннями вважаються ідентичними.


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

Папка `Core/` (або `Infrastructure/`) містить технічну основу програми. Інфраструктурний код зазвичай включає:

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

Для невеликих проектів, звичайно, достатньо пласкої структури:

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

Це код що:

- Керує технічною інфраструктурою (маршрутизація, логування, кешування)
- Інтегрує зовнішні сервіси (Sentry, Elasticsearch, Redis)
- Надає базові послуги для всього додатку (пошта, база даних)
- Здебільшого не залежить від конкретного домену - кеш або логгер працює однаково для електронної комерції або блогу.

Цікаво, де знаходиться певний клас - тут чи в моделі? Ключова відмінність полягає в тому, що код в `Core/`:

- Нічого не знає про домен (товари, замовлення, статті)
- Зазвичай може бути перенесений в інший проект
- Вирішує "як це працює" (як відправляти пошту), а не "що це робить" (яку пошту відправляти)

Приклад для кращого розуміння:

- `App\Core\MailerFactory` - створює екземпляри класу надсилання електронної пошти, обробляє налаштування SMTP
- `App\Model\OrderMailer` - використовує `MailerFactory` для відправки листів про замовлення, знає їхні шаблони та час відправки


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

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

Приклад:

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

Що належить до моделі, а що до командних скриптів? Наприклад, логіка відправки одного листа є частиною моделі, а масова відправка тисяч листів належить до `Tasks/`.

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


Інші можливі директорії .[#toc-other-possible-directories]
==========================================================

На додаток до згаданих базових каталогів, ви можете додати інші спеціалізовані папки відповідно до потреб проекту. Розглянемо найпоширеніші з них та їх використання:

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

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

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

Це місце для компонентів зі складнішою логікою. Якщо ви хочете спільно використовувати компоненти між кількома проектами, краще виокремити їх в окремий пакунок композитора.

У директорії `app/Mail` ви можете розмістити управління комунікацією електронною поштою:

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


Мапування ведучого .[#toc-presenter-mapping]
============================================

Відображення визначає правила отримання імен класів з імен доповідачів. Ми вказуємо їх у [конфігурації |configuration] під ключем `application › mapping`.

На цій сторінці ми показали, що розміщуємо презентери у теці `app/Presentation` (або `app/UI`). Нам потрібно повідомити Nette про цю домовленість у файлі конфігурації. Достатньо одного рядка:

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

Як працює відображення? Щоб краще зрозуміти, давайте спочатку уявимо додаток без модулів. Ми хочемо, щоб класи доповідача потрапляли до простору імен `App\Presentation`, щоб доповідач `Home` відображався у клас `App\Presentation\HomePresenter`. Це досягається за допомогою такої конфігурації:

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

Відображення працює шляхом заміни зірочки у масці `App\Presentation\*Presenter` на ім'я доповідача `Home`, у результаті чого буде отримано кінцеве ім'я класу `App\Presentation\HomePresenter`. Все просто!

Однак, як ви бачите у прикладах у цій та інших главах, ми розміщуємо класи доповідачів у однойменних підкаталогах, наприклад, доповідач `Home` зіставляється з класом `App\Presentation\Home\HomePresenter`. Ми досягаємо цього, подвоюючи двокрапку (вимагається Nette Application 3.2):

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

Тепер перейдемо до зіставлення ведучих з модулями. Ми можемо визначити специфічне відображення для кожного модуля:

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

Відповідно до цієї конфігурації, доповідач `Front:Home` зіставляється з класом `App\Presentation\Front\Home\HomePresenter`, тоді як доповідач `Api:OAuth` зіставляється з класом `App\Api\OAuthPresenter`.

Оскільки модулі `Front` та `Admin` мають подібний метод зіставлення і, ймовірно, таких модулів буде більше, можна створити загальне правило, яке замінить їх. До маски класу буде додано нову зірочку для модуля:

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

Це також працює для глибше вкладених структур каталогів, таких як 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/
├── app/              ← каталог програми
├── assets/           ← SCSS, JS файли, зображення..., або ресурси/
├── bin/              ← скрипти командного рядка
├── config/           ← конфігурація
├── log/              ← зареєстровані помилки
├── temp/             ← тимчасові файли, кеш
├── tests/            ← тести
├── vendor/           ← бібліотеки, встановлені Composer
└── www/              ← загальнодоступний каталог (корінь документа)

Ви можете вільно змінювати цю структуру відповідно до ваших потреб – перейменовувати або переміщувати теки. Потім вам потрібно лише відкоригувати відносні шляхи до каталогів в 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/

Це єдиний каталог, доступний з Інтернету (так званий корінь документа). Ви можете часто зустріти назву public/ замість www/ – це лише питання умовності і не впливає на функціональність. Каталог містить:

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

Для належної безпеки додатку дуже важливо мати правильно налаштований корінь документа.

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

Каталог додатків app/

Це основний каталог з кодом програми. Базова структура:

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

Bootstrap.php клас запуску програми, який ініціалізує середовище, завантажує конфігурацію та створює контейнер DI.

Тепер давайте детально розглянемо окремі підкаталоги.

Презентації та шаблони

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

Ми організовуємо цей рівень за доменами. У складному проекті, який поєднує в собі електронну комерцію, блог і API, структура буде виглядати так:

app/Presentation/
├── Shop/              ← Фронтенд електронної комерції
│   ├── Product/
│   ├── Cart/
│   └── Order/
├── Blog/              ← блог
│   ├── Home/
│   └── Post/
├── Admin/             ← адміністрування
│   ├── Dashboard/
│   └── Products/
└── Api/               ← Кінцеві точки API
	└── V1/

І навпаки, для простого блогу ми б використовували таку структуру:

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

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

Кожна папка з доповідачем містить однойменний доповідач і його шаблони. Наприклад, папка Dashboard/ містить:

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

Ця структура каталогів відображається у просторах імен класів. Наприклад, DashboardPresenter знаходиться у просторі імен App\Presentation\Admin\Dashboard (див. відображення доповідача):

namespace App\Presentation\Admin\Dashboard;

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

Ми посилаємося на доповідача Dashboard всередині модуля Admin у додатку, використовуючи двокрапку, як Admin:Dashboard. До його дії default – як Admin:Dashboard:default. Для вкладених модулів ми використовуємо більше двокрапок, наприклад Shop:Order:Detail:default.

Гнучка розробка структури

Однією з найбільших переваг цієї структури є те, наскільки елегантно вона адаптується до зростаючих потреб проекту. Для прикладу візьмемо частину, що генерує XML-стрічки. Спочатку у нас є проста форма:

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

Згодом додається більше типів фідів, і нам потрібно більше логіки для них… Ніяких проблем! Папка Export/ просто стає модулем:

Export/
├── Sitemap/
│   ├── SitemapPresenter.php
│   └── sitemap.latte
└── Feed/
	├── FeedPresenter.php
	├── amazon.latte         ← корм для Amazon
	└── ebay.latte           ← feed для eBay

Ця трансформація є абсолютно плавною – просто створіть нові підпапки, розділіть в них код і оновіть посилання (наприклад, з Export:feed на Export:Feed:amazon). Завдяки цьому ми можемо поступово розширювати структуру в міру необхідності, рівень вкладеності ніяк не обмежений.

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

Розташування шаблону

У попередніх прикладах ми бачили, що шаблони знаходяться безпосередньо в папці з доповідачем:

Dashboard/
├── DashboardPresenter.php     ← ведучий
├── DashboardTemplate.php      ← необов'язковий клас шаблону
└── default.latte              ← шаблон

Таке розташування виявляється найзручнішим на практиці – всі пов'язані файли у вас під рукою.

Крім того, ви можете розмістити шаблони в підпапці templates/. Nette підтримує обидва варіанти. Ви навіть можете розмістити шаблони поза текою Presentation/. Все про варіанти розташування шаблонів ви можете знайти в розділі Пошук шаблонів.

Допоміжні класи та компоненти

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

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

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

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

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

3. Для всього додатку – в Presentation/Accessory/:

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

Або ви можете розмістити допоміжні класи типу LatteExtension.php або TemplateFilters.php в теці інфраструктури app/Core/Latte/. А компоненти в app/Components. Вибір залежить від конвенцій команди.

Модель – серце програми

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

app/Model/
├── Payment/                   ← все про платежі
│   ├── PaymentFacade.php      ← основна точка входу
│   ├── PaymentRepository.php
│   ├── Payment.php            ← суб'єкт
├── Order/                     ← все про замовлення
│   ├── OrderFacade.php
│   ├── OrderRepository.php
│   ├── Order.php
└── Shipping/                  ← все про доставку

У моделі зазвичай зустрічаються такі типи класів:

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

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

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

class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// розрахунок ціни
	}
}

**Сховища даних: відповідають за всю взаємодію зі сховищем даних, як правило, базою даних. Їхнє завдання – завантажувати та зберігати об'єкти, а також реалізовувати методи їхнього пошуку. Репозиторій захищає решту програми від деталей реалізації бази даних і надає об'єктно-орієнтований інтерфейс для роботи з даними.

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

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

**Сутності – об'єкти, що представляють основні бізнес-концепції в додатку, які мають свою ідентичність і змінюються з часом. Зазвичай це класи, що відображаються на таблиці бази даних за допомогою ORM (наприклад, Nette Database Explorer або Doctrine). Сутності можуть містити бізнес-правила, що стосуються їхніх даних та логіки перевірки.

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

Об'єкти-значення: незмінні об'єкти, що представляють значення без власної ідентичності – наприклад, сума грошей або адреса електронної пошти. Два екземпляри об'єкта-значення з однаковими значеннями вважаються ідентичними.

Інфраструктурний код

Папка Core/ (або Infrastructure/) містить технічну основу програми. Інфраструктурний код зазвичай включає:

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

Для невеликих проектів, звичайно, достатньо пласкої структури:

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

Це код що:

  • Керує технічною інфраструктурою (маршрутизація, логування, кешування)
  • Інтегрує зовнішні сервіси (Sentry, Elasticsearch, Redis)
  • Надає базові послуги для всього додатку (пошта, база даних)
  • Здебільшого не залежить від конкретного домену – кеш або логгер працює однаково для електронної комерції або блогу.

Цікаво, де знаходиться певний клас – тут чи в моделі? Ключова відмінність полягає в тому, що код в Core/:

  • Нічого не знає про домен (товари, замовлення, статті)
  • Зазвичай може бути перенесений в інший проект
  • Вирішує „як це працює“ (як відправляти пошту), а не „що це робить“ (яку пошту відправляти)

Приклад для кращого розуміння:

  • App\Core\MailerFactory – створює екземпляри класу надсилання електронної пошти, обробляє налаштування SMTP
  • App\Model\OrderMailer – використовує MailerFactory для відправки листів про замовлення, знає їхні шаблони та час відправки

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

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

Приклад:

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

Що належить до моделі, а що до командних скриптів? Наприклад, логіка відправки одного листа є частиною моделі, а масова відправка тисяч листів належить до Tasks/.

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

Інші можливі директорії

На додаток до згаданих базових каталогів, ви можете додати інші спеціалізовані папки відповідно до потреб проекту. Розглянемо найпоширеніші з них та їх використання:

app/
├── Api/              ← Логіка API не залежить від рівня представлення
├── Database/         ← скрипти міграції та сівалки для тестових даних
├── Components/       ← спільні візуальні компоненти у всьому додатку
├── Event/            ← корисно, якщо використовується архітектура, керована подіями
├── Mail/             ← шаблони листів та пов'язана з ними логіка
└── Utils/            ← допоміжні класи

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

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

Це місце для компонентів зі складнішою логікою. Якщо ви хочете спільно використовувати компоненти між кількома проектами, краще виокремити їх в окремий пакунок композитора.

У директорії app/Mail ви можете розмістити управління комунікацією електронною поштою:

app/Mail/
├── templates/            ← шаблони листів
│   ├── order-confirmation.latte
│   └── welcome.latte
└── OrderMailer.php

Мапування ведучого

Відображення визначає правила отримання імен класів з імен доповідачів. Ми вказуємо їх у конфігурації під ключем 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

Це також працює для глибше вкладених структур каталогів, таких як presenter Admin:User:Edit, де сегмент із зірочкою повторюється для кожного рівня і призводить до класу App\Presentation\Admin\User\Edit\EditPresenter.

Альтернативним варіантом запису є використання масиву, що складається з трьох сегментів, замість рядка. Цей запис еквівалентний попередньому:

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