Может войдёшь?
Черновики Написать статью Профиль

Солидные практики

BestPractics SOLID

В современном искусстве программирования техника SOLID получила широкое распространение благодаря старому доброму принципу «разделяй и властвуй».
В этой статье я хочу осветить некоторые солидные моменты, которые неочевидны новичкам.

Для начала я покажу общую архитектуру приложения, так как без нее будет непонятно использование классов и наследований.

Так как я буду использовать ресур-контроллер, то маршруты описываются примитивным массивом вида

PHP
<?php

Route
::resources([
    
'customers'  => 'CustomersController',
]);

Сейчас он обрабатывает запрос вида http:://localhost/ajax/customers который попадает в совершенно типичный тонкий контроллер из бестпрактикс следующего вида:

PHP
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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)

ukrainsky

Зачем связывать репозиторий с request из веба?

Зачем репозиторий работает с фасадом модели? Почему бы явно не прокинуть билдер?

Странные неймспейсы.

Что такое Ajax? Вы на ajax, веб будете делать новый репозиторий?

РепозиТарии, переменные без доллара, «ресур-контроллеры» — все это очень странно.

Наследуете кучу методов, роутов. Придет момент, когда нужно запретить удаление модели — переопределите destroy в контроллере и кинете exception?

От ифа в CustomersRepository Вы ведь так и не избавились.

nailfor

— Зачем связывать репозиторий с request из веба?

реп может обрабатывать любые запросы, в том числе и запросы с файлами.

— Зачем репозиторий работает с фасадом модели? Почему бы явно не прокинуть билдер?
По той же причине. Реп — универсален, он может работать с любыми моделями, использования билдера привело бы к определению какого-то единого общего интерфейса, который далеко не всегда возможен.

— Что такое Ajax?
Аjax — подход к построению интерактивных пользовательских интерфейсов веб-приложений, заключающийся в «фоновом» обмене данными браузера с веб-сервером(c) вики.

— Вы на ajax, веб будете делать новый репозиторий?
Естественно. SOLID как раз об этом.

— переменные без доллара
очепятка. Бывает.

— Придет момент, когда нужно запретить удаление модели
В контроллере есть
protected $cannt = [
'destroy',
]
в статье не описано для упрощения.

— От ифа в CustomersRepository Вы ведь так и не избавились
на самом деле избавился. Переменная в контроллере содержит имя переменной реквеста, которая подключает разные репозитарии, в зависимости. Опять же здесь для простоты это пропущено.

alexMt

1. CustomersController завязан на конкретной реализации CustomersRepositary а не на абстракции остальные классы тоже — это уже не SOLID
2. Почему репозиторий знает про необходимый формат данных? CustomersAgeCollectionResource — нарушение SOLID

"— Вы на ajax, веб будете делать новый репозиторий?
Естественно. SOLID как раз об этом."
Какой принцип солид это утверждает? Этим вы нарушаете принцип DRY

"на самом деле избавился. Переменная в контроллере содержит имя переменной реквеста, которая подключает разные репозитарии, в зависимости. Опять же здесь для простоты это пропущено.
"
Тот же if только выше

jesse628wallick

Вы демонстрируете тонкие контроллеры и репозитории, где контроллеры делегируют большую часть логики классам репозитория. Такое разделение задач повышает удобство сопровождения и тестируемости.
<a href="http://www.maxim-timeclock.com">MaximTimeClock</a>

Написать комментарий

Разметка: ? ?

Авторизуйся, чтобы прокомментировать.