Я обычно разделяю большие приложения на 4-5 пространств имён, которые привязаны к глобальному app пространству имён. Например, я хочу создать приложение ToDo List, так что базовое пространство имён будет ToDo.
Затем у меня есть 3 пространства имён внутри этого:
App
— laravel-специфичная функциональность — классы валидаторы, сервис-провайдеры базовой модели и тд;
Domain
— вся моя бизнес-логика, такая как сущности, интерфейсы репозиториев, сервисы домена;
Infrastructure
— вся базовая логика. Это включает в себя реализации репозиториев, декораторы кэша и тд;
В дополнение к этому, у меня есть по крайней мере одно пространство имён для взаимодействия с внешним миром. Для типичного веб-приложения это Http, для REST API это Api, для команд Artisan это CLI. Итоговая структура приложения может быть примерно такой:
app/
----ToDo/
--------App/
------------Providers/
----------------ToDoServiceProvider.php
----------------ConfigServiceProvider.php
------------Validators/
----------------LaravelValidator.php
------------ValueObject.php
------------BaseModel.php
--------Domain/
------------List/
----------------EloquentList.php
----------------ListRepository.php
----------------ListService.php
----------------ListValidator.php
----------------Priority.php
------------Task/
----------------EloquentTask.php
----------------TaskRepository.php
----------------TaskService.php
----------------TaskValidator.php
--------Http/
------------Lists/
----------------ListController.php
----------------ListPresenter.php
----------------ListViewComposer.php
------------Tasks/
----------------TaskController.php
--------Infrastructure/
------------Lists/
----------------ListRepositoryCacheDecorator.php
----------------EloquentListRepository.php
------------EloquentTaskRepository.php
Пространство имён App
Это то что я рассматриваю как основную точку взаимодействия между моей бизнес-логикой, логикой инфраструктуры, взаимодействием с внешним миром (web, REST API, CLI), и Laravel. Это то место где я настраиваю все мои зависимости и храню классы которые расширяются потом кодом в домене, и не содержат никакой логики домена в себе. Основное правило здесь — большинство классов будут абстрактными.
- Связывание
TaskRepository
и EloquentTaskRepository
через сервис-провайдер
- Связывание конфигурации из одного из конфигов Laravel и класса, который требует её как часть конструктора[1]
- Создание абстрактного класса
LaravelValidator
, который содержит в себе интерфейс без реальных правил валидации бизнес-логики
- Создание класса
BaseModel
который предоставляет общую функциональность моим сущностям
- Создание специфичных сервисов уровня приложения(не сервисов домена), которые зависимы от Laravel, например утилиты для перемещения файлов[2]
- Создание абстрактных классов которые используются различными портами(web, REST API и тд), например
ApiController
который имеет вспомогательные методы для трансформации ответов сервера, возвращения API-специфичных кодов ошибок, внедрения meta-информации и тд.
[1] Например, у меня может быть Validator
, который зависит от массива правильныхTask Categories
, которые определены в файле конфигурации. Конструктор принимает массив категорий и я использую сервис-провайдер для для связи:
//ToDo/App/Providers/ConfigServiceProvider.php
public function register()
{
$categories = $this->app['config']->get('todo.categories');
$this->app->bind('ToDo\Domain\Tasks\TaskValidator.php', function($app)
{
$categories = $this->app['config']->get('todo.categories');
$factory = $this->app->make('Illuminate\Validation\Factory');
return new TaskValidator($factory, $categories);
}
[2] Здесь возможно расхождение во мнениях, но я верю что моя логика домена не должна заботиться о том в какой директории файл, до тех пор пока она не получит доступ к этому файлу. Поэтому сервис для определения расположения файлов это слой инфраструктуры или приложения.
Пространство имён Domain
Тут просто, это место где живёт вся моя бизнес-логика. Сущности, Объекты-значения, Валидаторы, Спецификации, вся функциональность которая доставляет результат клиенту находится здесь. В идеале мои сущности должны быть POPO(Plain Old PHP Objects) и не зависеть от Eloquent, но пока это сложно заставить работать хорошо, я имею склонность использовать Eloquent для моих сущностей — это единственное место где я сильно нарушаю принцип разделения ответственности SRP. Но пространство имёнToDo\Domain\List
будет содержать следующее:
EloquentList
— сущность List
, и я обычно использую мутаторы для объектов-значений. Если нам нужно загрузить в сущность List
все задания, это будет сделано автоматически.
Priority
— самовалидирующийся объект-значение, который содержит приоритет сущности List
. Он может принимать цифровое значение от 1 до 10, или что-то простое типа «низкий», «средний», «высокий».
ListRepository
— интерфейс репозитория с методами такими как напримерfindById($id)
, store($list)
, setPerPage($perPage = 0)
, sortBy($field, $direction = 'DESC')
, existsByName($name)
и тд.
ListService
— это главный класс с которым будет взаимодействовать мой порт. Если функциональность достаточно сложная, я разобью его на несколько классов.
ListValidator
— это валидатор который ответственный за реализацию валидации и ему нужны внешние данные о том что мы можем делать с сущностями и объектами-значениями, а что нет. Например, мы должны удостовериться что название для List
уникальное среди остальных сущностей List
(что требует репозиторий как зависимость так как это нельзя сделать в рамках только одной сущности List
).
Все эти вещи должны меняться только тогда когда мне скажет клиент. Любые технические изменения должны уходить в абстрактные классы из которых расширяются классы домена или должны идти в реализации интерфейсов в пространстве имён домена.
Пространство имён Http
Это место где находится вся логика которая связывает моё приложение с внешним миром(например браузером). Всё что отвечает за HTTP запросы должно жить здесь. Это может включать в себя следующее:
ListController
— достаточно самообъясняюще. Он может расширяться из абстрактного контроллера в пространстве имён App
если нужно, и может иметьListService
внедрённым в себя. Он может быть ответственным за ловлю исключений валидации которые приходят из класса ListService
, их обработку путём редиректа и отправки ошибок во вьюху.
ListPresenter
— если нам нужно добавить html-специфичное форматирование для списков, например форматирование дат или преобразование цифр приоритета в более читаемый label, эта функциональность должна быть здесь. Контроллер должен быть ответственным за создание экземпляра объекта, его преобразования и доставки его во вьюху.
ListViewComposer
— если есть какая либо дополнительная информация которую мы должны прикрепить к одной или нескольким вьюхам списков, это будет жить здесь. Здесь может находиться всё и даже могут быть внедрены различные сервисы приложения или домена.
Это только один «порт» моего приложения. Если у меня есть команды Artisan или REST API, то для них будет своё пространство имён с соответствующими классами.
Пространство имён Infrastructure
Всё что относится к базовому состоянию находится здесь. Самая очевидная вещь которая должна быть здесь это реализации репозиториев. Например:
EloquentListRepository
— Eloquent-специфичная реализация интерфейсаListRepository
в пространстве имён Domain
.
ListRepositoryCacheDecorator
— это реализация того же интерфейса что и у репозитория и она является декоратором который добавляет функциональность кеширования.
Другие вещи которые могут быть здесь: класс ListDatabaseMapper
, если используется паттерн Data Mapper вместо Eloquent.