В современном искусстве программирования техника SOLID получила широкое распространение благодаря старому доброму принципу «разделяй и властвуй».
В этой статье я хочу осветить некоторые солидные моменты, которые неочевидны новичкам.
Для начала я покажу общую архитектуру приложения, так как без нее будет непонятно использование классов и наследований.
Так как я буду использовать ресур-контроллер, то маршруты описываются примитивным массивом вида
<?php
Route::resources([
'customers' => 'CustomersController',
]);
Сейчас он обрабатывает запрос вида http:://localhost/ajax/customers который попадает в совершенно типичный тонкий контроллер из бестпрактикс следующего вида:
<?php
namespace App\Http\Controllers\Ajax;
use App\Repositories\Ajax\CustomersRepositary;
class CustomersController extends AjaxController
{
/**
* Конструктор
*
* @param CustomersRepositary $model
*/
public function __construct(CustomersRepositary $model)
{
$this->model = $model;
parent::__construct();
}
}
он наследуется от AjaxController в котором описаны общие для всех ресурсных контроллеров методы index, show, store и destroy, которые, в свою очередь, работают с моделью-репозитарием содержащим всю логику работы.
Для примера рассмотрим метод index:
<?php
namespace App\Http\Controllers\Ajax;
use Illuminate\Routing\Controller;
use Illuminate\Http\Request;
class AjaxController extends Controller
{
/**
* хранит модель-репозитарий
*/
protected $model;
...
public function index(Request $request)
{
$model = $this->getModel($request);
$res = $model->getCollection($request);
return $res;
}
Как видно, после получения модели-репозитария вызывается метод getCollection, в модели он описан следующим образом
<?php
namespace App\Repositories\Ajax;
class AjaxRepositary
{
/**
* хранит класс таблицы
*/
protected $model;
/**
* хранит ресурс-коллекцию для отображения результата
*/
protected $collection;
/**
* хранит количество записей на одной странице
*/
protected perPage = 100;
/**
* Возвращает таблицу с данными
* @param Request $request
* @return model
*/
public function getModels($request)
{
$class = $this->model;
return $class::paginate($this->perPage);
}
/**
* Возвращает класс ресурс-коллекцию
* @param Request $request
* @return string
*/
public function getResourceCollection($request) : string
{
return $this->collection;
}
/**
* Отображает коллекцию моделей в ее представлении
* @param Request $request
* @return resource
*/
public function getCollection($request)
{
$models = $this->getModels($request);
$collection = $this->getResourceCollection($request);
if ($collection) {
return new $collection($models);
}
return new ResourceCollection($models);
}
}
Тогда конечная модель-репозитарий CustomersRepositary примет такой вид:
<?php
namespace App\Repositories\Ajax;
use App\Http\Resources\Ajax\CustomersCollectionResource;
use App\Models\db\storage\dbCustomer;
class CustomersRepositary extends AjaxRepositary
{
protected $model = dbCustomer::class;
protected $collection = CustomersCollectionResource::class;
}
Как видно репозитарий так же получился тонким, он подключает рабочую таблицу и связывает ее с ресурсом для отображения
Осталось описать CustomersCollectionResource — это ресурс(вьюха) для представления ответа в виде json-объекта
<?php
namespace App\Http\Resources\Ajax;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CustomersCollectionResource extends ResourceCollection
{
/**
* Возвращает представление объекта
* @param $object
*/
protected function getResource($object)
{
//не буду здесь слишком заморачиваться, пусть это будет простой массив значений
return [
'id' => $object->id,
'name' => $object->name,
];
}
/**
* {@inheritdoc}
*/
public function toArray($request)
{
$res = [];
foreach($this->collection as $object){
$res[] = $this->getResource($object);
}
return $res;
}
}
Итак... после всех проделанных манипуляций при запросе к адресу /ajax/customers у нас должно получиться что то вроде
{"data": [ {"id": 1, "name": "Иванов"}, {"id": 2, "name": "Петров"}, {"id": 3, "name": "Сидоров"} ]}
Теперь предположим, что нам необходимо как-то усложнить результат.
Пусть, к примеру, при некотором запросе мы хотим получить какое-то дополнительное поле, например возраст покупателей, тогда запрос будет /ajax/customers?withAge=1
Что сделает юный падаван увидив такую задачу? Правильно, полезет менять CustomersCollectionResource на что то вида
<?php
namespace App\Http\Resources\Ajax;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CustomersCollectionResource extends ResourceCollection
{
/**
* Возвращает представление объекта
* @param $object
*/
protected function getResource($object)
{
//не буду здесь слишком заморачиваться, пусть это будет простой массив значений
return [
'id' => $object->id,
'name' => $object->name,
];
}
protected function getResourceWithAge($object)
{
return [
'id' => $object->id,
'name' => $object->name,
'age' => $object->age,
];
}
/**
* {@inheritdoc}
*/
public function toArray($request)
{
$res = [];
$withAge = $request->get('withAge', false);
foreach($this->collection as $object){
if ($withAge) {
$res[] = $this->getResourceWithAge($object);
}
else {
$res[] = $this->getResource($object);
}
}
return $res;
}
}
Ну оно же работает? Конечно.
Но со временем такое программирование приведет к нагромождению if/switch и усложнит понимание того, что здесь вообще происходит.
SOLID, а именно SRP говорит о принципе единственности ответственности, поэтому сделаем класс-наследник
<?php
namespace App\Http\Resources\Ajax;
class CustomersAgeCollectionResource extends CustomersCollectionResource
{
/**
* {@inheritdoc}
*/
protected function getResource($object)
{
$res = parent::getResource($object);
return array_merge($res, [
'age' => $object->age,
]);
}
}
А подключать его будем в CustomersRepositary:
<?php
namespace App\Repositories\Ajax;
use App\Http\Resources\Ajax\CustomersCollectionResource;
use App\Http\Resources\Ajax\CustomersAgeCollectionResource;
use App\Models\db\storage\dbCustomer;
class CustomersRepositary extends AjaxRepositary
{
protected $model = dbCustomer::class;
protected $collection = CustomersCollectionResource::class;
/**
* {@inheritdoc}
*/
public function getResourceCollection($request) : string
{
if ($request->get('withAge', false))
{
return CustomersAgeCollectionResource::class;
}
return $this->collection;
}
}
Что мы этим добились? Представление не должно заниматься проверками входящих данных, само представление-наследник получился тонким, изменяется один метод getResource который наследует данные родителя и обогащает их дополнением в виде возраста.
Конечно, можно пойти еще дальше и вынести проверку withAge в контроллер, где и должны происходить такого рода проверки. Для этого нужно создать новый репозитарий CustomersAgeRepositary и перегрузить метод getModel в CustomersController, но я думаю общий принцип этого метода уже понятен.
Комментарии (4)
Зачем связывать репозиторий с request из веба?
Зачем репозиторий работает с фасадом модели? Почему бы явно не прокинуть билдер?
Странные неймспейсы.
Что такое Ajax? Вы на ajax, веб будете делать новый репозиторий?
РепозиТарии, переменные без доллара, «ресур-контроллеры» — все это очень странно.
Наследуете кучу методов, роутов. Придет момент, когда нужно запретить удаление модели — переопределите destroy в контроллере и кинете exception?
От ифа в CustomersRepository Вы ведь так и не избавились.
— Зачем связывать репозиторий с request из веба?
реп может обрабатывать любые запросы, в том числе и запросы с файлами.
— Зачем репозиторий работает с фасадом модели? Почему бы явно не прокинуть билдер?
По той же причине. Реп — универсален, он может работать с любыми моделями, использования билдера привело бы к определению какого-то единого общего интерфейса, который далеко не всегда возможен.
— Что такое Ajax?
Аjax — подход к построению интерактивных пользовательских интерфейсов веб-приложений, заключающийся в «фоновом» обмене данными браузера с веб-сервером(c) вики.
— Вы на ajax, веб будете делать новый репозиторий?
Естественно. SOLID как раз об этом.
— переменные без доллара
очепятка. Бывает.
— Придет момент, когда нужно запретить удаление модели
В контроллере есть
protected $cannt = [
'destroy',
]
в статье не описано для упрощения.
— От ифа в CustomersRepository Вы ведь так и не избавились
на самом деле избавился. Переменная в контроллере содержит имя переменной реквеста, которая подключает разные репозитарии, в зависимости. Опять же здесь для простоты это пропущено.
1. CustomersController завязан на конкретной реализации CustomersRepositary а не на абстракции остальные классы тоже — это уже не SOLID
2. Почему репозиторий знает про необходимый формат данных? CustomersAgeCollectionResource — нарушение SOLID
"— Вы на ajax, веб будете делать новый репозиторий?
Естественно. SOLID как раз об этом."
Какой принцип солид это утверждает? Этим вы нарушаете принцип DRY
"на самом деле избавился. Переменная в контроллере содержит имя переменной реквеста, которая подключает разные репозитарии, в зависимости. Опять же здесь для простоты это пропущено.
"
Тот же if только выше
Вы демонстрируете тонкие контроллеры и репозитории, где контроллеры делегируют большую часть логики классам репозитория. Такое разделение задач повышает удобство сопровождения и тестируемости.
<a href="http://www.maxim-timeclock.com">MaximTimeClock</a>