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

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

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, но я думаю общий принцип этого метода уже понятен.

Как вы считаете, полезен ли этот материал? Да Нет

Комментарии (2)

ukrainsky

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

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

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

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

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

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

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

nailfor

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

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

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

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

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

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

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

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

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

Разметка: ? ?

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