11. Laravel Modules. Stubs в Laravel проєктах. Структура Laravel проєкту
Відео версія
(YouTube - для зворотнього зв'язку)
Про модульну структуру
Модульна структура проєкту в порівнянні зі стандартною структурою Laravel має кілька значних переваг:
- Організація коду:
У модульній структурі кожен модуль відповідає за конкретний функціонал або доменну область програми, що дуже і дуже спрощує навігацію та підтримку коду. - Розділ зон відповідальності членів команди:
Модульна структура дозволяє різним командам або розробникам працювати над окремими модулями незалежно один від одного, сильно мінімізуючи git конфлікти. Це також зменшує час на планування та оцінку - менеджери знають, хто за який модуль відповідальний, і "чії" помилки виникли. - Можливість виділити модуль в окремий сервіс:
Це основна причина, чому раджу модулі при роботі з монолітною архітектурою. В ситуаціях, коли якась частина програми потребує більше ресурсів, з модульної структури винести її в сервіс набагато легше, ніж в стандартній структурі Laravel. Це не буде дуже легко й швидко, але це реальна задача, яку можна оцінити. У випадку, коли все лежить в одній купі, оцінити таку задачу неможливо. - Зменшення залежностей:
Модульна структура змушує розробників думати про взаємодію між модулями. Це пасивно зменшить кількість непотрібних зв'язків. І так чи інакше стандартизує підходи по взаємодії, що дуже спрощує розуміння коду та перехід до "мікросервісів". - Тестування:
На CI/CD ви проганяєте всі тести. У випадку з модулями є перевага в навантаженні, test coverage. Ви перестанете запускати першу ж сотню тестів, бо це займає дуже багато часу. А запустити для окремого модуля - проблем не буде. Перевикористання модулів:
В усіх "маркетингових" текстах про модульність перевикористання модулів стоїть першим. Створіть модуль, оформіть в окремий проєкт і використовуйте в усіх проєктах компанії. Теоретично я можу уявити, як би модулі я міг так винести. І тут є два підходи. Перший - використовувати модуль як composer залежність. Цей підхід взагалі не життєздатний, бо ви будете боятися оновлювати пакет, створите під кожен проєкт окрему гілку, бо страшно щось зламати для інших проєктів. І будете мати мільйон одночасно "живих" гілок, і підтримувати кожну окремо. Другий варіант - публікувати (копіювати з залежностей в код) ваш модуль один раз і працювати з ним локально. Це життєздатно, але перетворить розробку модуля з нуля на підтримку написаного кимось. Що завжди довше, ніж робити з нуля. Бо з великою ймовірністю ваші підходи, потреби та навички відрізняються від задачі, для якої писався модуль. Тому використовуйте на свій страх і ризик.
Laravel Modules package
Для модульності рекомендую пакет nWidart/laravel-modules
Обов'язково налаштовуйте пакет під себе. Генеруйте лише ті файли та директорії, які ви використовуєте. Мінімізуйте кількість автоматично згенерованого коду. Пам'ятайте, що підключення/реєстрація пустого файлу конфігурації або сервіс провайдера, який ви не використовуєте, впливає на продуктивність системи в цілому.
Для мого проєкту я роблю такі зміни:
- Виношу всі залежності з директорії
/app
. Це просто зручно. В рамках модуля глибока структура директорій не потрібна. ДиректоріяControllers
абоProviders
легко сприймаються в кореневій директорії модуля. - Змінюю всі назви директорій на Uppercase. Це зручно для PSR-4 стандарту, namespaces будуть збігатися з директоріями, і нам не треба ніяких псевдонімів для composer правил.
- Відмовляюсь від composer залежностей всередині модуля. Тут причина проста - цим важко керувати. Якщо виносимо в окремий сервіс модуль - нам все одно доведеться перебрати купу пакетів. І тут важливо. Треба дозволити composer автоматично завантажувати класи з модулів.
{
"autoload": {
"psr-4": {
"Modules\\": "Modules/"
}
}
}
І також - не забувайте, що в autoload на продакшн не повинні потрапити тести (і seed, migration, factory в ідеальному світі). Для цього треба додати їх в autoload-dev
. Я ж не додаю, бо в .dockerignore.production
файлі не завантажую тести взагалі - **/**/Tests
.
- Відмовляюсь від усіх фронтенд ресурсів
/route/web.php
,/resources
,vite.config.js
. Бо у мене лише API методи. Фронтенду нема. - Не генерую
config
- конфігурація потрібна далеко не всім модулям. Коли знадобиться - створю для конкретного модуля. - Не генерую
EventServiceProvider
- якщо знадобиться, легше по навантаженню генерувати прямо вModuleServiceProvider
. - Файли локалізації - вони взагалі потрібні дуже рідко, для умовних текстів email.
Laravel stub
У Laravel "stub" (шаблон) - шаблон коду, який використовується для генерації різних класів та файлів. Будь-яка artisan команда, яка створює файл, створює його з файлу .stub
. Stub можна опублікувати. Для nWidart/laravel-modules це обов'язково, бо налаштування під себе модулів буде в 99% випадків.
Тож публікуймо стаби:
php artisan vendor:publish --provider="Nwidart\Modules\LaravelModulesServiceProvider" --tag="stubs"
І в конфігурації вказуємо директорію з опублікованими шаблонами.
[
'stubs' => [
'path' => base_path('stubs/nwidart-stubs')
]
]
Окрім коду - раджу одразу налаштувати stub під Code Style. Щоб команда могла згенерувати що завгодно автоматично. Найлегший спосіб - створити команду, що згенерує модуль з усіма найпоширенішими файлами та запустити команду код стайлеру. Повторювати, доки кодстайл модуля не буде ідеальним.
rm -rf Modules/Blog &&
docker compose exec app php artisan module:make Blog &&
docker compose exec app php artisan module:make-model Post Blog &&
docker compose exec app php artisan module:make-request PostRequest Blog &&
docker compose exec app php artisan module:make-request PostResource Blog &&
docker compose exec app php artisan module:make-migration some_migration Blog &&
docker compose exec app php artisan module:make-test SomeTest Blog &&
docker compose exec app php artisan module:make-test SomeTest Blog --feature
Та команда перевірки.
docker compose exec app php artisan insights --no-interaction
Змінюйте
stub
при зміні code style правил.
Порада з власного досвіду - видаляйте коментарі зі
stub
файлів. Генерувати файли команда буде постійно, а дивитися на одні й ті ж тексти при кожному рев'ю не ок (так мозок їх ігнорує, але навіщо витрачати його ресурс).
Власні module команди
Widart/laravel-modules](https://laravelmodules.com/docs/v11/custom-module-generator) має багато власних команд для роботи з модулем. Але іноді виникає необхідність власних рішень або змінити поведінку команди. Для прикладу, мені дуже зручно генерувати модель одразу з міграцією, seed'ами та фабрикою. Це нагадує команді, що фабрика та seed'и обов'язкові для роботи. У випадках, де ні, завжди можна видалити.
Просто створюю команду як звичайну ларавель. Для мінімізації коду наслідуюсь від вже існуючої команди. Так додаю в
[
'commands' => ConsoleServiceProvider::defaultCommands()
->merge([
\App\Console\Command\ModelMakeCommand::class
])->toArray(),
]
Міграції та seed
За замовчуванням міграції в модулях працюють як і звичайні. Просто зберігаються в директоріях модуля. Тут є проблеми з оптимізацією, про які ви повинні знати.
І мій особистий "закид". Не те щоб я його придумав, так працювали перші версії пакета модулів. Міграції після мержу редагувати не можна. Вони вже пройшли на стейджі, проді та у кожного розробника на локальній машині. Використовуючи модулі ми можемо ізолювати міграції в модулі. І публікувати їх лише як останній етап роботи.
Цей підхід мені подобається, він дозволяє спокійно ізольовано працювати над модулем. Мержитись, робити помилки, і виправляти їх.
private function loadMigrations(): void
{
$isTestEnv = App::environment('testing');
if ($isTestEnv) {
$this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations'));
}
}
Тут я завантажую міграції лише в testing середовищі для тестів.
Але є й великий мінус:
Всі учасники розробки будуть забувати опублікувати міграції.
Тут, звісно, можна запаритися і створити команду, яку закинути на pre-push. Але іншим разом.
І про seeders. Laravel за замовчуванням не бачить seed з модулів. Для комфортної роботи їх треба додавати до глобального database/seeders/DatabaseSeeder.php
.
Я раджу збирати seed в SomeModelDatabaseSeeder
і вже його викликати в DatabaseSeeder
.
Tests setup
Laravel за замовчуванням не бачить тестів з директорій модулів. Дуже легко виправити: додайте 2 блоки до phpunit.xml
: ModuleFeature
та ModuleUnit
.
<phpunit>
<testsuites>
...
<testsuite name="ModuleFeature">
<directory>./Modules/**/Tests/Feature</directory>
</testsuite>
<testsuite name="ModuleUnit">
<directory>./Modules/**/Tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>
Оптимізація та Cache
Модулі, як і всі сервіс провайдери, завантажуються при кожній ініціалізації проєкту. Для Laravel Octane це не критично, але для інших рішень - може впливати на оптимізацію. Тому, по-перше, не реєструйте те, що не використовуєте, а по-друге, використовуйте кеш.
MODULES_CACHE_ENABLED=true
MODULES_CACHE_DRIVER=file
Telescope, Octane, PHPstan
Додайте modules.*
до telescope EventWatcher.
Це звільнить телескоп від спаму подіями реєстрації laravel modules пакету
[
'watchers' => [
'ignore' => [
'modules.*'
],
]
]
Якщо використовуєте Octane для локальної роботи додайте директорію модулів до watch
конфігурації
[
'watch' => [
...
'Modules'
],
]
І у випадку використання PHPstan не забудьте додати модулі для сканування
parameters:
paths:
- Modules/