В современном искусстве программирования техника 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, но я думаю общий принцип этого метода уже понятен.
Комментарии (2)
Зачем связывать репозиторий с request из веба?
Зачем репозиторий работает с фасадом модели? Почему бы явно не прокинуть билдер?
Странные неймспейсы.
Что такое Ajax? Вы на ajax, веб будете делать новый репозиторий?
РепозиТарии, переменные без доллара, «ресур-контроллеры» — все это очень странно.
Наследуете кучу методов, роутов. Придет момент, когда нужно запретить удаление модели — переопределите destroy в контроллере и кинете exception?
От ифа в CustomersRepository Вы ведь так и не избавились.
— Зачем связывать репозиторий с request из веба?
реп может обрабатывать любые запросы, в том числе и запросы с файлами.
— Зачем репозиторий работает с фасадом модели? Почему бы явно не прокинуть билдер?
По той же причине. Реп — универсален, он может работать с любыми моделями, использования билдера привело бы к определению какого-то единого общего интерфейса, который далеко не всегда возможен.
— Что такое Ajax?
Аjax — подход к построению интерактивных пользовательских интерфейсов веб-приложений, заключающийся в «фоновом» обмене данными браузера с веб-сервером(c) вики.
— Вы на ajax, веб будете делать новый репозиторий?
Естественно. SOLID как раз об этом.
— переменные без доллара
очепятка. Бывает.
— Придет момент, когда нужно запретить удаление модели
В контроллере есть
protected $cannt = [
'destroy',
]
в статье не описано для упрощения.
— От ифа в CustomersRepository Вы ведь так и не избавились
на самом деле избавился. Переменная в контроллере содержит имя переменной реквеста, которая подключает разные репозитарии, в зависимости. Опять же здесь для простоты это пропущено.