Магия RESTful API в Yii2

Май 22nd, 2015
Метки: ,

Кому и зачем нужно REST API

В современном мире, мире где доступ к Интернету имеют сотни видов самых разнообразных устройств, веб-приложения вышли далеко за пределы привычных браузеров. И не смотря на то, что большинство устройств имеют встроенные браузеры, все чаще в качестве клиентского приложения используются приложения, разработанные с учетом специфики конкретного устройства. В итоге для одного приложения может быть десяток типов клиентских приложений. Но суть самого приложения не меняется. REST API — это способ сделать универсальное бекенд-приложение, приложение, которое не придется переделывать при добавлении нового типа клиента. REST — это клиент-серверная распределенная архитектура со всеми вытекающими отсюда плюсами. А как же web? Веб-приложение — это еще один клиент. А с учетом того, как семимильными шагами развивается популярность JS-MV* фреимворков (Angular, Backbone, CanJS, Ember и тп) веб скоро окончательно перейдет на одно-страничные приложения, взаимодействующие с сервером посредством асинхронных HTTP-запросов.

RESTful API в Yii2 — проще не бывает

Yii2 позволяет написать полноценное RESTful API за пару минут. Причем написать такое API может даже фронтенщик, мало знакомый с yii и php в принципе (если REST API используется для управления ресурсами, а логика всего приложения содержится в JS-фреимворке).

  • Устанавливаем и настраиваем базовое приложение yii (для тех, кто не знает как, читаем тут)

  • Создаем таблицу в БД и генерируем модель с помощью gii
  • Создаем класс контроллера и наследуем его от yii\rest\ActiveController
  • Прописываем в контроллере модель
    Весь код контроллера:

    namespace app\controllers;
    
    use yii\rest\ActiveController;
    
    class UserController extends ActiveController
    {
        public $modelClass = 'app\models\User';
    }
    
  • Настраиваем роутинг
    'urlManager' => [
                'enablePrettyUrl' => true,
                'enableStrictParsing' => true,
                'showScriptName' => false,
                'rules' => [
                    ['class' => 'yii\rest\UrlRule', 'controller' => 'user'],
                ],
            ],
            'request' => [
                'parsers' => [
                    'application/json' => 'yii\web\JsonParser',
                ]
            ],
    

RESTful API готов :) Больше ничего делать не нужно.

Что получилось?

У вас теперь есть следующие url для доступа к API (пояснения о том, как это работает можно не читать).

  • GET-запрос /users — постраничный вывод данных модели
    Обратите внимание имя модели переведено во множественное число (это можно отключить в настройках класса yii\rest\UrlRule установив pluralize в false).

    new ActiveDataProvider([
                'query' => $modelClass::find(),
            ]);
    
  • POST-запрос /users — добавление модели.
    $model = new $this->modelClass([
                'scenario' => $this->scenario,
            ]);
    
            $model->load(Yii::$app->getRequest()->getBodyParams(), '');
    if ($model->save()) {
                $response = Yii::$app->getResponse();
                $response->setStatusCode(201);
                $id = implode(',', array_values($model->getPrimaryKey(true)));
                $response->getHeaders()->set('Location', Url::toRoute([$this->viewAction, 'id' => $id], true));
            } elseif (!$model->hasErrors()) {
                throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
            }
    
            return $model;
    
  • GET-запрос /users/1 — выбор данных для 1 экземпляра модели.
     $model = $modelClass::findOne($id);
    
  • PUT-запрос /users/1 — обновление данных для 1 экземпляра модели. Находит модель, загружает данные, сохраняет и возвращает модель в случаи успеха
    $model = $modelClass::findOne($id);
    $model->load(Yii::$app->getRequest()->getBodyParams(), '');
    if ($model->save() === false && !$model->hasErrors()) {
                throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
            }
    
            return $model;
    
  • DELETE-запрос /users/1 — удаление модели
     $model = $this->findModel($id);
     if ($model->delete() === false) {
                throw new ServerErrorHttpException('Failed to delete the object for unknown reason.');
            }
    
            Yii::$app->getResponse()->setStatusCode(204);
    

Как работает магия RESTfulAPI в Yii2

Откуда столько action, мы еще ничего не написали?
В ActiveController от которого мы отнаследовали наш контроллер уже существуют все описанные выше actions. Вот так, к примеру, разработчики Yii за нас уже написали actionIndex

 'index' => [
                'class' => 'yii\rest\IndexAction',
                'modelClass' => $this->modelClass,
                'checkAccess' => [$this, 'checkAccess'],
            ],

Как так получается, что REST контроллеры отдают то ActiveRecord модель, то и вовсе ActiveDataProvider, а в результате отдает xml или json-массив?
ActiveController отнаследован от yii\rest\Controller, в котором прописан метод afterAction. Метод сериализует полученный результат по умолчанию с помощью класса yii\rest\Serializer

public function serialize($data)
    {
        if ($data instanceof Model && $data->hasErrors()) {
            return $this->serializeModelErrors($data);
        } elseif ($data instanceof Arrayable) {
            return $this->serializeModel($data);
        } elseif ($data instanceof DataProviderInterface) {
            return $this->serializeDataProvider($data);
        } else {
            return $data;
        }
    }

Т.е в своем REST контроллере отнаследованном от yii\rest\ActiveController или yii\rest\Controller можно не думать об конвертировании данных. Можно смело возвращать ActiveRecord модель, любой класс, реализующий интерфейс Arrayable или DataProviderInterface или готовый массив.
За формат ответа (конвертацию массива в JSON или XML) отвечает yii\filters\ContentNegotiator в зависимости от того, какой формат ответа был запрошен, т.е вам, как разработчику об этом думать тоже не нужно.

Небольшой тюнинг REST API в Yii2

Так как эта статья вводная, приведу лишь небольшую часть простых «настроек» для только что созданного за пару минут RESTful API:
Как запретить удаление модели через REST API?
В настройках yii\rest\UrlRule можно указать какие методы поддерживает ваше API через only или исключить отдельные методы через except

'urlManager' => [
            'enablePrettyUrl' => true,
            'enableStrictParsing' => true,
            'showScriptName' => false,
            'rules' => [
                ['class' => 'yii\rest\UrlRule', 
                 'controller' => 'user'
                 'except' => ['delete'],
                ],
            ],
        ],

Как запретить доступ к определенным полям модель через REST API?
Для этого в модели нужно переопределить методы fields() и extraFields()

public function fields()
    {
        return ['id', 'email', 'username'];
    }
 public function extraFields()
    {
        return ['status'];
    }

Теперь при запросе GET /users будут переданы только поля id, email и username. А при запросе GET /users?expand=status вернет данные id, email, username и status.
Как добавить вычисляемые поля модели для REST API?
Все так-же определив их в методе fields() в виде анонимной функции

public function fields()
    {
        return [
'id', 
'email', 
'username' => function ($model) {
            return $model->first_name . ' ' . $model->last_name;
        }
];
    }
 public function extraFields()
    {
        return ['status'];
    }

Конечно возможности создания REST API в Yii2 не ограничиваются описанными в данной статье примерами. За пределами осталось много интересного — авторизация пользователя, проверка прав, сценарии при работе с моделью, лимитирование запросов и многое другое. Надеюсь позже удастся написать вторую часть и заполнить эти пробелы. Ну а пока, если есть вопросы — пишите в комментах, а я постараюсь ответить.


Метки: ,

Оставить комментарий

31 комментариев »

    Валера

    Как раз хотел попробовать совместить Ember.js и Yii2….
    Только вот вопрос, я правильно понимаю, что для этого нужен будет один контроллер RestController который будет отдавать всё-всё ? Так как для Одностраничников нужна сразу вся инфа.

      Developer

      Нет. Контроллеров может быть сколько угодно. Один контроллер = 1 сущность, которой вы собираетесь управлять. Одностраничник — это не значит, что запрос на сервер всегда только один. Для посетителя сайт одностраничный, без перезагрузки страницы. Но ajax запросы идут на разные урлы. Но если не воспринимать REST как догму, а взять как основу архитектуры, то вполне может быть контроллер, который объединит нужные данные из нескольких объектов и вернет их одним запросом.


    Husniddin

    Недавно начал изучать Yii 2. С REST API есть недопонимание. Например если класс унаследован от yii\rest\ActiveController то можем запросто использовать GET, POST, PUT и DELETE запросы. GET запрос возвращает все записи пользователя (например статьи пользователя) в базе данных. Теперь если нам нужно чтобы при GET запросе отображались не все записи, а записи залогиненного пользователя. Что нужно делать в таком случае? B остальных POST, PUT и DELETE запросах тоже самое. Переопределить унаследованные методы (например actionIndex) или есть другой способ? Заранее спасибо.

      Developer

      Для этого в IndexAction есть переменная $prepareDataProvider, к ней нужно присвоить колбек функцию, которая будет возвращать ActiveDataProvider со всеми нужными вам условиями. В контроллере переопределить метод actions() и дописать туда свой колбек

       public function actions()
          {
              return [
                  'index' => [
                      'class' => 'yii\rest\IndexAction',
                      'modelClass' => $this->modelClass,
                      'checkAccess' => [$this, 'checkAccess'],
                      'prepareDataProvider' =>  function ($action) {
                         return new ActiveDataProvider([
                               'query' => Pages::find()->where(...),
                         ]);
                       }
                  ],
      ...
      

      Для PUT, POST и DELETE обрабатывается конкретная запись с неким ID. Так что в таких запросах нужно проверять имеет ли право пользователь менять что-либо в записи с этим ID. Для этого можно использовать checkAccess. Переопределите этот метод в своем контроллере. Добавьте туда нужную проверку и бросайте эксепшен (ForbiddenHttpException) если у пользователя нет прав на данную запись.


    Ravend

    Хорошая статья, спасибо! Вопрос… Как делать лимитирование запросов?

    neiron

    с переоределением разобрался, но вот если нужно переопределение только для GET, а PUT должен работать по стандартной схеме? нигде не найду решение(

      Developer

      Вы наследуетесь от ActiveController. Все методы, которые вы переопределили в своем классе работают так как у вас написано. Те методы, что вы не переопределили, работают так, как указано в родительском классе.


    Михаил

    Спасибо, очень помог Ваш материал, особенно раздел про fields(). Полдня искал, почему API не цепляет атрибуты, созданные геттерами, и как автоматически раскрывать связи. Рад что нашел в такой человекопонятной подаче и с примерами :)


    Роман

    У меня advanced приложение — я хотел бы что бы сайт работал как обычно но + rest api было, такое возможно? Попробовал перестроить на rest api и все встало (( получается что надо rest api отдельно поднимать? Спасибо

      Developer

      Нет, отдельное приложение поднимать не надо. Но отдельные rest контроллеры создать придется.
      Я делала так:
      1) создала контроллер отнаследованный от yii\rest\Controller в папке controllers/rest, со всеми нужными мне action
      2) прописала роутинг

       'rules' => [
                      [
                          'class' => 'yii\rest\UrlRule',
                          'controller' => \app\controllers\rest\NewsController::class
                      ],
                 ... //прочие правила для обычного приложения
      

      В итоге по адресу /rest/news у меня rest api новостей, а остальное приложение работает как обычно.


    Сергей

    ‘controller’ => \app\controllers\rest\NewsController::class — контроллер у вас создан в папке frontend или backend?
    создала контроллер отнаследованный от yii\rest\Controller — это случайно не опечтка, может yii\rest\ActiveController ?
    Спасибо

      Developer

      У меня в основном все примеры для base app. Куда положить зависит от того, откуда будет доступ. В advanced — это как два разных приложения в одном.
      ActiveController если вам нужны готовые Rest action, yii\rest\Controller — если готовыми пользоваться не собираетесь и будите писать свои.


    Павел

    Подскажите пожалуйста: пытаюсь отправить из формы данные POST через api controller метод create — запись создается, возвращает id, но в базу пишутся нули, переопределил свой метод как create (спасибо вашей подсказочке) — то же самое, не могу понять где собака зарыта может быть…

      Developer

      Скорее всего поля у модели не safe, а вы присваиваете их через массовое присвоение

      $model->load(Yii::$app->request->post())
      

      Поэтому часть полей у вас не присваивается и пишется в базу с дефолтным значением. Напомню, что safe считаются поля, упомянутые в правилах валидации, хотя бы так: [[‘name’, ‘age’], ‘safe’]


    Умид

    Есть форма с загрузкой файла который сделано с ActiveRecord. Как отправить запрос в restful api и обработать ошибки ??? Как правильно это сделать ?

      Developer

      Фаил отправляется вместе с остальными данными, т.е ничего особенного делать не нужно. Ошибки возвращать в формате json, обычно возвращают код ошибки и текстовое сообщение.


    Сергей

    Подскажите, пожалуйста, как сделать чтобы при вызове обычного контроллера, этот контроллер взял данные по REST у другого, уже RESTfull контроллера, обработал их и вывел их максимально похоже как бы это было в стандартной реализации без REST’a? Спасибо!

      Developer

      Я думаю нужно делать запрос через curl к REST, а потом обрабатывать эти данные как надо. REST придуман как сервер для разных клиентов, теоретически веб-клиент и рест бывают на разных серверах, поэтому только через веб-запросы. Но когда это в одном приложении, я делаю общие методы для рест и не рест контроллеров и в веб-контроллере пользуюсь этим-же методом, без вызова rest. Нормального готового веб-клиента для Yii я не нашла. Если найдете — дайте знать.


    User

    А как проверить это на клиенте?

      Developer

      Сделать запрос в браузере или в консоли через curl.
      Как отправить POST запрос в консоли через curl

      curl 'url' -d 'data'
      

      data — ваши данные, которые нужно передать. Если параметров несколько, то объединяем через &: ‘id=2&type=3&status=1′
      Как отправить DELETE запрос в консоли через curl

      curl -X 'DELETE' 'url'
      

      Надеюсь это поможет.


    Yii developer

    I have followed all actions needed. However, i got following error message:
    Response content must not be array
    I am a new yii developer. Please, help me?

      Developer

      My guess is that you use wrong parent Controller. It should be yii\rest\Controller instead of yii\web\Controller.
      Rest controller can handle a response which is Arrayable, Model or DataProvider. Any other data should be converted to string, for example with json_encode()


    Сергей

    Здравствуйте, подскажите как организовать выборку with relation? Например есть модели user and profile. Я так понял использование rest не дает возможности выбора связанных моделей.

      Developer

      Все правильно, true REST подразумевает работу с одной сущностью. Т.е user и profile — это разные сущности и должны быть разные запросы. Но имхо не всегда это разумно и иногда не стоит воротить новые REST запросы ради связаных данных. Поэтому можно отдавать связанные модели в том-же запросе

      return json_encode(['user' => $user, 'profile' => $user->profile]);
      

      Но это уже не канонический REST, не REST в чистом виде. Если честно, чистого REST я еще на практике нигде не встречала.


    Tualatin

    Подскажите, возможно ли в Yii2 десериализовать приходящие данные и замапить их на свои модели, при этом не наследуясь от Active Record (Model)?

      Developer

      Можно:

       $model->attributes = unserialize($data);
      

    Tualatin

    Так это как раз с моделью (Model) или ее наследником работает, а мне надо для своих классов, которые от Model никак не наследуются. Есть ли общий механизм сериализации/десеарилизации данных (жел. разных форматов), как это реализовано, например, в jms serializer?

      Developer

      Не знаю как это реальзовано, но я бы получала аттрибуты класса, в цикле проходилась по ним и заполняла значения если они есть в переданном массиве. Если вы точно уверены что ключи массива точно соответствуют аттрибутам класса то можно так:

      //$entity - объект вашего класса   $data - массив данных
      function setEntityData($entity, $data)
      {
        foreach ($data as $k => $v) {
          $entity->{$k} = $v;
       }
       return $entity;
      }
      

      Ничего готового ни в одном фреимворке не встречала. Но только ради этого наследоваться от Active Record точно не стоит.


    Александр

    Здравствуйте, нигде не нашёл ваши контакты — как можно с вами связаться по вопросам сотрудничества?

      Developer

      В настоящий момент я не ищу работу и не беру никакие проекты на разработку, если ваше сотрудничество относится к моим Open Source проектам, то связаться со мной можно через Github


Оставить комментарий:









Копирование материалов разрешено при наличии активной ссылки на источник
Яндекс.Метрика