brezgalov / yii2-api-helpers
api utils making you code better
Installs: 170
Dependents: 1
Suggesters: 0
Security: 0
Stars: 3
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/brezgalov/yii2-api-helpers
Requires
- php: >=7.2.0
 - yiisoft/yii2: >=2.0.6
 
README
Этот пакет - результат работы по стандартизации, ускорению разработки команды и увеличению качества кода.
Он ориентирован на разработку в первую очередь серверных API, однако, работа с web формами так же поддерживается. (Апи часто требует "админку" для существования в виде продукта)
Ключевые особенности
Стандартизация подключения логики.
Никакой логики в контроллере, обеспечиваем подключение "Controller -> Action -> Service"
Отдельный интерфейс для пользовательского ввода.
Если сервису не нужен пользовательский ввод - мы не будем пытаться его наполнять.
Отсутствие необходимости контроля за транзакциями.
Переворачиваем игру: теперь транзакции есть везде и "стажер" не забудет их применить, а отказ от использования транзакций представлен в явном виде
Последовательное выполнение action при одновременном вызове с одного и того же IP.
Избегаем случаев, когда много одновременных запросов путают друг другу "карты"
Использование отложенных событий.
Списываем деньги с карты пользователя, только если action гарантированно выполнился без ошибок
Разделение форматирования ответа и логики.
Один сервис, много вариантов форматирования в разных action
Разделение логики и отображения для web-форм.
Вывод html - отдельный вариант форматирования. Для передачи данных на страницу используем "интерфейс DTO".
Установка
composer require brezgalov/yii2-api-helpers --prefer-dist
Подключение сервиса
Используем метод Controller::actions() для подключения action:
public function actions()
{
    return [
        'list' => [
            'class' => ApiGetAction::class,
            'service' => MyExampleRepositoryService::class,
            'methodName' => MyExampleRepositoryService::METHOD_LIST,
        ],
        'cities' => [
            'class' => ApiActiveGetAction::class,
            'service' => KladrCitiesSearch::class,
        ],
        'regions' => [
            'class' => ApiActiveGetAction::class,
            'service' => KladrRegionsSearch::class,
            'dataProviderSetup' => [
                'pagination' => false,
            ],
            'afterDataProviderInit' => function(ActiveDataProvider $dataProvider) {
                $dataProvider->sort->defaultOrder = ['name' => SORT_DESC];
                return $dataProvider;
            },
        ],
    ];
}
Код сервиса MyExampleRepositoryService:
class MyExampleRepositoryService extends Model
{
    const METHOD_LIST = 'listData';
    protected function getExampleData()
    {
        return [
            [
                'id' => 1,
                'name' => 'Barbara',
                'sex' => 'female',
            ],
            [
                'id' => 2,
                'name' => 'Mike',
                'sex' => 'male',
            ],
        ];
    }
    public function listData()
    {
        return $this->getExampleData();
    }
}
Для разработки API я предлагаю использовать такие action:
- ApiGetAction - для вывода любой информации
 - ApiPostAction - для работы логики
 - ApiGetActiveAction - для вывода списков с помощью ActiveDataProvider
 
_ApiGetAction и ApiPostAction отличаются набором поведений внутри. Cм. секцию "Поведения для action"
При подключении ApiGetAction/ApiPostAction action используется 2 параметра:
- service - Конфигурация класса сервиса
 - methodName - Строка, обозначающая метод сервиса, который необходимо вызвать
 
Для упрощения реализации вывода списка объектов можно использовать ApiGetActiveAction. Основное отличие от \yii\rest\IndexAction заключается в использовании ApiGetAction и механизма форматирования ответа. Благодаря этому возможна гибкая настройка DataProvider.
При подключении ApiGetActiveAction можно указать:
- service - Модель, возвращающая ActiveQuery
 - methodName - Предполагается, что модель наследует ISearch, поэтому по-умолчанию указано "getQuery"
 - dataProviderSetup - Массив настроек DataProviderInterface
 - afterDataProviderInit - Callback для редактирования инстанцированного DataProviderInterface
 
Пользовательский ввод
Обрабатываем пользовательский ввод напрямую
Пользовательский ввод представлен в виде полей сервиса, которые заполняются через IRegisterInputInterface:
class MyExampleRepositoryService extends Model implements IRegisterInputInterface
{
    public $idFilter;
  
    public $nameFilter;
    public function registerInput(array $data = [])
    {
        $this->nameFilter = $data['filter_name'] ?? $this->nameFilter;
        $this->idFilter = $data['filter_ID'] ?? $this->idFilter;
    }
    ... // other code
Благодаря такому интерфейсу:
! Не нужно реализовывать/вызывать наполнение пользовательскими данными, когда это не требуется
! Интерфейс web API и сервиса могут различаться, это бывает полезно для рефакторинга
! Разрывает связь между rules и load для сервисов на основе Model, теперь проще указать правило валидации в rules для переменной которую не хотим заполнять данными запроса
Используем Model::load для пользовательского ввода
Так же мы можем поддерживать работу с web-формами или сервисами старой версии этой библиотеки, обернув метод load
class MyExampleRepositoryService extends Model implements IRegisterInputInterface
{
    public $id;
    public $name;
    public function rules()
    {
        return [
            [['id'], 'integer'],
            [['name'], 'string'],
        ];
    }
    public function registerInput(array $data = [])
    {
        $this->load($data, '');
    }
    ... // other code
Форматирование ответа
Для форматирования ответа сервиса необходимо использовать объект интерфейса IFormatter
interface IFormatter
{
    public function format($service, $result);
}
Метод format($service, $result) позволяет иметь доступ к состоянию сервиса через переменную $service, для формирования более комплексной логики форматирования.
По-умолчанию для форматирования ответа API используется объект класса ModelResultFormatter. Он отвечает за стандартную обработку ошибок.
Для примера реализуем Formatter скрывающий поле "sex" из набора данных:
class MyExampleFormatter extends ModelResultFormatter
{
    public function format($service, $result)
    {
        if (is_array($result)) {
            foreach ($result as &$item) {
                unset($item['sex']);
            }
            return $result;
        }
        return parent::format($service, $result);
    }
}
Подключим Formatter в контроллере:
public function actions()
{
    return [
        'index' => [
            'class' => ApiGetAction::class,
            'service' => MyExampleRepositoryService::class,
            'methodName' => MyExampleRepositoryService::METHOD_LIST,
            'formatter' => MyExampleFormatter::class
        ],
    ];
}
Валидация и обработка ошибок
Обработка ошибок API происходит, когда метод сервиса возвращает false.
Вывести ошибку можно с применением объекта Brezgalov\ApiHelpers\v2\ErrorException или использовать метод Model::addError($attribute, $error)
ErrorException позволяет самостоятельно выбрать формат ошибки и код ответа.
Использование ошибок класса Model приведет к стандартному отображению ошибок Yii2 с кодом ответа 422
Поведения для action
Библиотека позволяет подключить поведения к action.
Стандартный набор поведений action задается через метод BaseAction::getDefaultBehaviors()
class ApiPostAction extends BaseAction
{
    const BEHAVIOR_KEY_TRANSACTION = 'transaction';
    const BEHAVIOR_KEY_MUTEX = 'mutex';
    const BEHAVIOR_KEY_DELAYED_EVENTS = 'delayedEvents';
    public $formatter = ModelResultFormatter::class;
    protected function getDefaultBehaviors()
    {
        return [
            static::BEHAVIOR_KEY_TRANSACTION => TransactionBehavior::class,
            static::BEHAVIOR_KEY_MUTEX  => MutexBehavior::class,
            static::BEHAVIOR_KEY_DELAYED_EVENTS  => DelayedEventsBehavior::class,
        ];
    }
}
Поле BaseAction::$behavior позволяет управлять поведениями при подключении к контроллеру. Если по ключу поведения передать значение false - поведение не будет подключено к action
public function actions()
{
    return [
        'my-post-action' => [
            'class' => ApiPostAction::class,
            ...
            'behaviors' => [
                ApiPostAction::BEHAVIOR_KEY_TRANSACTION => false,
                MyCustomBehavior::class,
            ],
        ],
    ];
}
Работа с web-формами
RenderAction отвечает за отрисовку страниц и обладает настройками для подключения/отключения layout, заголовка страницы, указания view и режима отрисовки (по-умолчанию / отрисовка файла / ajax-отрисовка)
class RenderAction extends BaseAction
{
    /**
    * @var bool
    */
    public $layout = true;
    /**
     * @var string
     */
    public $title;
    /**
     * @var string
     */
    public $view;
    /**
     * @var string
     */
    public $mode = ViewResultFormatter::RENDER_MODE_DEFAULT;
    /**
     * @var ViewContextInterface|string|array
     */
    public $viewContext;
    /**
     * @var IFormatter
     */
    public $formatter = ViewResultFormatter::class;
    ...
}
Пример реализации ViewContext
class ViewContext implements ViewContextInterface
{
    /**
     * @return string the view path that may be prefixed to a relative view name.
     */
    public function getViewPath()
    {
        return __DIR__;
    }
}
Для передачи данных на view используется интерфейс IRenderFormatterDTO.
Можно использовать вариант реализации при котором сервис возвращает объект DTO наследующий этот интерфейс. Или, что проще, можно создать сервис *Page, который наследовать этот интерфейс и возвращать сам себя
Пример сервиса *Page:
class RightsTablePage extends Model implements IRenderFormatterDTO, IRegisterInputInterface
{
    const PAGE_PREPARE_METHOD = 'preparePageData';
    /**
     * @var RightsTableDto
     */
    protected $tableDto;
    /**
     * @var RightsTableFactory
     */
    public $rightsTableFactory;
    /**
     * RightsTablePage constructor.
     * @param array $config
     */
    public function __construct($config = [])
    {
        parent::__construct($config);
        if (empty($this->rightsTableFactory)) {
            $this->rightsTableFactory = new RightsTableFactory();
        }
    }
    /**
     * @return RightsTableDto[]
     */
    public function getViewParams()
    {
        return [
            'tableDto' => $this->tableDto,
            'tableErrors' => $this->submitRightsService->getErrorSummary(true),
        ];
    }
    /**
     * @param array $data
     * @return bool
     */
    public function registerInput(array $data = [])
    {
        $this->submitRightsService->registerInput($data);
        return true;
    }
    /**
     * @return $this
     */
    public function preparePageData()
    {
        $this->tableDto = $this->rightsTableFactory->buildTableDto();
        return $this;
    }
}
::getViewParams() - отвечает за передачу данных на view
::registerInput() - отвечает за регистрацию пользовательского ввода
::preparePageData() - отвечает за подготовку данных
Пример использования:
public function actions()
{
    return [
        'get-table' => [
            'class' => RenderAction::class,
            'service' => RightsTablePage::class,
            'methodName' => RightsTablePage::PAGE_PREPARE_METHOD,
            'title' => 'Таблица ролей и разрешений',
            'view' => 'RightsTable/View',
            'viewContext' => ViewContext::class,
        ],
    ];
}
Для обработки submit'a формы я предлагаю использовать SubmitRenderAction. Он работает аналогично RenderAction, это позволяет отрисовать форму снова с отображением ошибок, если такие произошли. Параметр successRedirectRoute позволяет указать маршрут для перехода при успешном submit.
Пример подключения SubmitRenderAction:
[
    'class' => SubmitRenderAction::class,
    'service' => RightsTablePage::class,
    'methodName' => RightsTablePage::SUBMIT_TABLE_METHOD,
    'title' => 'Таблица ролей и разрешений',
    'successRedirectRoute' => 'rights-table/index',
    'view' => 'RightsTable/View',
    'viewContext' => ViewContext::class,
],
Более подробно можно посмотреть пример использования RenderAction в репозитории brezgalov/yii2-rights-manager