Как создать API на Yii-Framework?

Хотелось бы поделится опытом написания небольшого и удобного api-сервера на Yii-Framework. У нас появилась задача написать небольшой backend к приложению на android и api сервер к нему. Было принято решение реализовать все в одном проекте, не разделяя api и backend. Но как же реализовать данную задачу “красиво” и удобно?

Наша команда путём проб и ошибок нашла эффективный и достаточно быстрый путь его написания, и мы рады поделиться с вами этим небольшим открытием, надеемся вам это пригодится.

Пошаговый алгоритм создания API-сервера на Yii-Framework

Итак, мы будем использовать Yii-Framework 1.1.14, так как на момент написания статьи это свежайшая стабильная версия (с нетерпением ждем 2.0 stable), и базу данных MySQL.

Пропустим подробности по созданию пустого проекта и его настройку, я думаю каждый, кому интересна данная статья, умеет это делать. Для примера создадим две модели – country и category с помощью миграции:

$this->createTable(‘{{country}}’, array(
              ‘id’=>’pk’,
              ‘name’=>’text NOT NULL’,
), ‘ENGINE=MyISAM  DEFAULT CHARSET=utf8′);
$this->createTable(‘{{category}}’, array(
              ‘id’=>’pk’,
              ‘name’=>’text NOT NULL’,
              ‘country_id’=>’int(11)’,
), ‘ENGINE=MyISAM  DEFAULT CHARSET=utf8′);

В каждую таблицу добавим по две записи. Для примера в таблицу country внесем две страны Russia, India. В таблицу category внесем две категории category1, category2 и присвоим им соответственно разные страны. Поставим задачу – написать два api-метода данного вида:

  1. http://anysite.com/api/getCountryList/ возвращает список стран
  2. http://anysite.com/api/getCategoryList/countryId/1 возвращает список категорий по стране, если страна не указана, возвращает все категории.

Для начала напишем класс Response, который будет формировать ответ сервера в формате JSON. Объявим три приватных свойства класса:

private $data;  //Будем хранить данные ответа.
private $code;  //HTTP код ответа сервера .
private $message;  //Сообщение сервера.

Объявим константы для кодов и сообщений:

const MSG_OK = 'ok';
const MSG_NO_DATA = 'no data';
const MSG_WRONG_INPUT = 'wrong input data';
const MSG_UNKNOWN_ERROR = 'Unknown error';
const MSG_METHOD_NOT_FOUND = 'unknown method';
const CODE_OK = 200;
const CODE_ERROR = 500;
const CODE_NOT_FOUND = 404;

Переопределим сеттеры и геттеры:

public function __set($name, $value) {
              $this->data[$name] = $value;
}
public function __get($name) {
              return isset($this->data[$name]) ? $this->data[$name] : ”;
}
public function __isset($name) {
              return isset($this->data[$name]);
}
public function __unset($name) {
              if(isset($this->data[$name])){
                               unset($this->data[$name]);
              }
}

Объявим метод, который будет устанавливать код и сообщение сервера.

public function setCode($code, $message){
              $this->code = $code;
              $this->message = $message;
}

И, наконец, определим непосредственно сам метод, который отправляет ответ:

public function send(){
              switch($this->code){
                               case self::CODE_NOT_FOUND:
                                                 header(‘HTTP/1.0 404 Not Found’);
                                                 break;
                               case self::CODE_ERROR:
                                                 header(‘HTTP/1.0 500 Internal Error’);
                                                 break;
                               case self::CODE_OK:
                                                 header(‘HTTP/1.0 200 OK’);
                                                 break;
              }
              $array = array(
              ‘code’ => $this->code,
              ‘message’ => $this->message,
              );
              if(!empty($this->data)){
                               $array = array_merge($array, array(‘data’ => $this->data));
              }
              echo json_encode($array);
}

Далее нам необходимо создать контроллер controllerApi, в котором будет формироваться список api-методов и происходить весь логический процесс. Объявим приватное свойство response, в котором будем хранить объект, созданного нами класса Response.

private $response;
public function init() {
              $this->response = new Response;
}

Напишем метод actionCall с аргументов $method. Данный метод – обработчик запросов к api-серверу. Все запросы будут проходить через него. Если api-метод существует и аргументы все верны, actionCall вызовет запрошенный api-метод, в противном случае вернет ошибку.

public function actionCall($method = null) {
              if (method_exists($this, $method)) {
                               $f = new ReflectionMethod($this, $method);
                               $args = array();
                               foreach ($f->getParameters() as $param) {
                                                 if (Yii::app()->request->getParam($param->name)) {
                                                                  $args += array($param->name => Yii::app()->request->getParam($param->name));
                                                 } else if (!$param->isDefaultValueAvailable()) {
                                                                  $this->response->setCode(Response::CODE_ERROR, Response::MSG_WRONG_INPUT);
                                                                  $this->response->send();
                                                                  Yii::app()->end();
                                                 } else {
                                                                  $args += array($param->name => $param->getDefaultValue());
                                                 }
                               }
                               call_user_func_array(array($this, $method), $args);
              } else {
                               $this->response->setCode(Response::CODE_NOT_FOUND, Response::MSG_METHOD_NOT_FOUND);
                               $this->response->send();
              }
}

В данном методе мы используем так называемый Reflection (совокупность классов, которые служат для получения информации об объектах языка непосредственно во время выполнения скрипта, который появился в php5). Мы использовали ReflectionMethod, так как работаем с классом.

С помощью метода getParameters получаем аргументы запрошенного api-метода, далее сверяем каждый аргумент с полученными GET данными. В случае если имя GET параметра соответствует аргументу (if (Yii::app()->request->getParam($param->name))), устанавливается значение данного аргумента из GET параметра. В случае, когда GET параметр не установлен для данного аргумента, мы проверяем этот аргумент на наличие значения по умолчанию. Если значение по умолчанию есть (то есть данный аргумент не обязателен для вызова данного api-метода), то устанавливаем данное значение ($param->getDefaultValue()). В случае, когда значение по умолчанию отсутствует (if (!$param->isDefaultValueAvailable())), то возвращаем ошибку о неправильном вызове данного api-метода.

Настроим routing таким образом, чтобы запросы вида http://anysite.com/api/<method>/<param>/<value>/……/<param>/<value> перенаправлялись на метод actionCall. Для этого в отконфигурируем UrlManager:

‘urlManager’ => array(
‘urlFormat’ => ‘path’,
‘showScriptName’ => false,
‘rules’ => array(
              ‘/api/<method:\w+>/*’ => ‘api/call’,
),
),

Далее можем приступить к написанию самих api-методов, о которых мы говорили в начале статьи.

private function getCountryList() {
              if ($countries = Country::model()->findAll()) {
                               $data = array();
                               foreach ($countries as $country) {
                                                 $array = array(‘id’ => $country->id, ‘name’ => $country->name);
                                                 $data[] = $array;
                               }
                               $this->response->countries = $data;
                               $this->response->setCode(Response::CODE_OK, Response::MSG_OK);
              } else {
                               $this->response->setCode(Response::CODE_OK, Response::MSG_NO_DATA);
              }
              $this->response->send();
}
private function getCategoryList($countryId = null) {
              $criteria = new CDbCriteria;
              if (!empty($countryId)) {
                               $criteria->addCondition(‘country_id = :country_id’);
                               $criteria->params['country_id'] = (int) $countryId;
              }
              if ($categories = Category::model()->findAll($criteria)) {
                               $data = array();
                               foreach ($categories as $category) {
                                                 $array = array(‘id’ => $category->id, ‘name’ => $category->name);
                                                 $data[] = $array;
                               }
                               $this->response->categories = $data;
                               $this->response->setCode(Response::CODE_OK, Response::MSG_OK);
              } else {
                               $this->response->setCode(Response::CODE_OK, Response::MSG_NO_DATA);
              }
              $this->response->send();
}

Примеры запросов и ответов:

  1. Запрос: http://anysite.com/api/getCountryList/

    Ответ:

    {"code":200,"message":"ok","data":{"countries":[{"id":"1","name":"Russia"},
    "id":"2","name":"India"}]}}
  2. Запрос: http://anysite.com/api/getCategoryList/

    Ответ:

    {"code":200,"message":"ok","data":{"categories":[{"id":"1","name":"category1"},
    "id":"2","name":"category2"}]}}

    Если бы данный api-метод был бы объявлен таким образом: private function getCategoryList($countryId),
    ответ был бы таким:

    {"code":500,"message":"wrong input data"}

    потому что не был отправлен обязательный атрибут API метода.

  3. Запрос: http://anysite.com/api/getCategoryList/countryId/1

    Ответ:

    {"code":200,"message":"ok","data":{"categories":[{"id":"1","name":"category1"}]}}
  4. Запрос несуществующего метода.

    Ответ:

    {"code":404,"message":"unknown method"}

Заключение

Таким образом, мы получили очень удобный инструмент для реализации api-сервера. Его мы использовали в Андроид приложении, в котором пользователь указывает все нужные параметры для печати фотографий, оформляет заказ и потом уже оплачивает через банк или использует мобильный платеж. Наша статья пригодится всем разработчикам, которым необходимо решение 2-в-1 – Back-end и API.