Архитектура Laravel приложения
Архитектура Laravel приложения
Ссылка на репозиторий с кодом примеров.
Прошлая статья с точки зрения промышленного кода оставляет желать лучшего, поэтому решил ее обновить и дописать новое. Прошлая статья подойдет тем, у кого Laravel первый фреймворк, а эта, если хотите совершенствоваться и писать правильно.
Структура папок будет для продукта такой. В этой структуре контроллер лежит сразу в папке Products, как основная точка входа, для работы с продуктами в админке. Папка Entity - содержит DTO(Data Transfer Object) объекты, по сути сущности для удобной работы. Папка DataProvider - содержит провайдеры данных, которые предоставляют результирующие данные, которые были запрошены или отправлены из контроллера в провайдер данных. В провайдер данных сами данные уже попадают из репозитория (папка Repository) - тут содержится логика запросов (модели Eloquent или сырые SQL запросы). При необходимости данные могут быть обработаны сервисом (папка Service). Папка Utils может содержать общие методы и классы полезные для работы. Папка Interfaces - содержит интерфейсы, по сути при такой архитектур вся суть именно в них.
Класс контроллер будет таким. В нем я обозначаю приватную переменную $productDataProvider, которая соответствует интерфейсу IProductDataProvider. Экземпляр класса, который соответствует данному интерфейсу инстанцируется через статический метод сервисного контейнера. ProductServiceContainer этот аналог DI контейнера для локального модуля Products. В Laravel работает внедрение зависимостей DI (dependency injection) через имена классов, но если DI делать через интерфейсы, то необходимо больше изменений. Поэтому для этого примера в контроллер отдается сервисный контейнер ProductServiceContainer.
Далее в контроллере пойдем к методу index(). В нем, через инстанс дата провайдера $this->productDataProvider я обращаюсь к доступным методам этого провайдера.
Доступные для работы с дата провайдером методы содержатся в его интерфейсе. Это можно увидеть в подсказках PHPStorm. Хотя точно такие же подсказки будут и при наличии в классе просто публичных методов, но в данном случае интерфейс определяет контракт того, как можно взаимодействовать с классом, не зная его внутренней реализации. Внутреннюю реализацию в таком случае стоит реализовывать в приватных методах.
Реализация класса ProductDataProvider. В его конструктор передается экземпляр класса ProductRepository, реализующего интерфейс IProductRepository. В этом случае мы можем легко заменить класс ProductRepository любым другим классом, который реализует этот интерфейс, что по сути уменьшает связность классов, в отличие от того, если бы мы инстанцировали репозиторий в классе дата провайдера через new.
Перейдем к методу getLatestProducts в дата провайдере, который вызывается в методе index в контроллере. Этот метод принимает аргумент типа int и отдает массив. В PHPDoc я обозначил, что это массив DTO объектов. В самом методе идет обращение к экземпляру репозитория и к его методу, который также есть в интерфейсе репозитория.
Класс репозитория. В нем идет обращение к модели Products. Через модель Products мы получаем записи из базы данных, обходим эти записи в цикле и передаем каждую отдельную запись в DTO объект, чтобы потом наполнить массив созданными DTO объектами.
DTO объект создается таким образом.
После того, как мы получили массив объектов из репозитория, он попадает в дата провайдер. В случае сложной логики, эти данные в дата провайдере передаются в сервисный слой, который преобразует эти данные. Таким образом легко выносится логика различных преобразований в сервис, и потом этот сервис легко тестировать, так как обычно он на вход принимает массив или объекты и отдает преобразованные данные, и в нем нет никаких зависимостей в идеале. На этом этапе сервис у меня не добавлен, поэтому добавлю и протестирую его чуть ниже.
Объясню суть ProductServiceContainer. Он отвечает за внедрение зависимостей в наш дата провайдер. Сейчас в дата провайдер передается экземпляр репозитория реализующего определенный интерфейс, который определен в сигнатуре конструктора в дата провайдере.
Если проще, то: экземпляр класса ProductRepository удовлетворяет интерфейсу IProductRepository. Этот интерфейс определен в сигнатуре дата провайдера.
В контейнере мы можем создать экземпляры других репозиториев (отвечающих за другие сущности в проекте), создать классы сервисов с какой-то логикой, а уже дата провайдер бы использовал у себя эти экземпляры.
- 687