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 автоматично завантажувати класи з модулів.
composer.json
{
  "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% випадків.

Тож публікуймо стаби:

cli
php artisan vendor:publish --provider="Nwidart\Modules\LaravelModulesServiceProvider" --tag="stubs"

І в конфігурації вказуємо директорію з опублікованими шаблонами.

config/modules.php
[
    'stubs' => [
        'path' => base_path('stubs/nwidart-stubs')
    ]
]

Окрім коду - раджу одразу налаштувати stub під Code Style. Щоб команда могла згенерувати що завгодно автоматично. Найлегший спосіб - створити команду, що згенерує модуль з усіма найпоширенішими файлами та запустити команду код стайлеру. Повторювати, доки кодстайл модуля не буде ідеальним.

cli
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

Та команда перевірки.

cli
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'и обов'язкові для роботи. У випадках, де ні, завжди можна видалити.

Просто створюю команду як звичайну ларавель. Для мінімізації коду наслідуюсь від вже існуючої команди. Так додаю в

config/modules.php
[
'commands' => ConsoleServiceProvider::defaultCommands()
    ->merge([
        \App\Console\Command\ModelMakeCommand::class
    ])->toArray(),
]

Міграції та seed

За замовчуванням міграції в модулях працюють як і звичайні. Просто зберігаються в директоріях модуля. Тут є проблеми з оптимізацією, про які ви повинні знати.

І мій особистий "закид". Не те щоб я його придумав, так працювали перші версії пакета модулів. Міграції після мержу редагувати не можна. Вони вже пройшли на стейджі, проді та у кожного розробника на локальній машині. Використовуючи модулі ми можемо ізолювати міграції в модулі. І публікувати їх лише як останній етап роботи.

Цей підхід мені подобається, він дозволяє спокійно ізольовано працювати над модулем. Мержитись, робити помилки, і виправляти їх.

stubs/nwidart-stubs/scaffold/provider.stub
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.xml
<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 це не критично, але для інших рішень - може впливати на оптимізацію. Тому, по-перше, не реєструйте те, що не використовуєте, а по-друге, використовуйте кеш.

.env.example
MODULES_CACHE_ENABLED=true
MODULES_CACHE_DRIVER=file

Telescope, Octane, PHPstan

Додайте modules.* до telescope EventWatcher. Це звільнить телескоп від спаму подіями реєстрації laravel modules пакету

config/telescope.php
[
  'watchers' => [
     'ignore' => [
        'modules.*'
     ],
  ]
]

Якщо використовуєте Octane для локальної роботи додайте директорію модулів до watch конфігурації

config/octane.php
[
 'watch' => [
        ...
        'Modules'
    ],
]

І у випадку використання PHPstan не забудьте додати модулі для сканування

parameters:
    paths:
        - Modules/