Я обычно разделяю большие приложения на 4-5 пространств имён, которые привязаны к глобальному app пространству имён. Например, я хочу создать приложение ToDo List, так что базовое пространство имён будет ToDo.
Затем у меня есть 3 пространства имён внутри этого:
App
— laravel-специфичная функциональность — классы валидаторы, сервис-провайдеры базовой модели и тд;Domain
— вся моя бизнес-логика, такая как сущности, интерфейсы репозиториев, сервисы домена;Infrastructure
— вся базовая логика. Это включает в себя реализации репозиториев, декораторы кэша и тд;
В дополнение к этому, у меня есть по крайней мере одно пространство имён для взаимодействия с внешним миром. Для типичного веб-приложения это Http, для REST API это Api, для команд Artisan это CLI. Итоговая структура приложения может быть примерно такой:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
, которые определены в файле конфигурации. Конструктор принимает массив категорий и я использую сервис-провайдер для для связи:
1
2
3
4
5
6
7
8
9
10
//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.