 
									Приветствую, это очередная статья о разработке web приложения с использованием под названием Personal maps. В прошлый раз мы закончили разработку клиентской части приложения, а сегодня займемся созданием REST интерфейса. Ссылки на все предыдущие части вы найдёте внизу этой страницы.
Форматы запросов клиентской части к серверу и его ответов у нас уже определены. Фактически мы это сделали когда тестировали сервис Places. Теперь нам нужно реализовать поддержку этих запросов серверной частью приложения.
Создание REST сервисов с помощью Yii фреймворка
Примечание. Если вы не знакомы с общими принципами создания REST сервисов, то рекомендую прочитать статью Create a REST API with PHP.
Также напоминаю, что исходный код приложения размещён на GitHub и доступна демоверсия приложения.
Source
Обычные web приложения работают с двумя типами HTTP запросов – GET и POST, т.к. вы не можете отправить другие типы запросов с помощью стандартной HTML формы. Но при использовании JavaScript ситуация изменяется. AngularJS, как и большинство других фреймворков, позволяет использовать дополнительные типы запросов, в нашем случае это PUT и DELETE.
Таким образом, для создания REST сервиса нам нужно:
- написать контроллер, который будет обрабатывать запросы;
- создать правила для роутера.
Контроллер REST сервиса
Прежде всего, давайте вспомним, какие запросы мы можем получить от клиентской части.
- GET api/places– в ответ мы должны вернуть массив, содержащий все объекты данного пользователя;
- GET api/places/id_объекта– ищем объект с данным- idи возвращаем только его;
- POST api/places– этот запрос указывает, что необходимо создать новый объект, информация о нём передаётся в параметрах запроса;
- PUT api/places/id_объекта– изменение объекта с указанным- id, как и в случае с- POSTинформация об объекте будет отправлена в параметрах запроса;
- DELETE api/places/id_объекта– этот запрос говорит о том, что необходимо удалить объект с указанным- id.
Т.е. мы можем создать обычный контроллер с методами, которые будут соответствовать этим запросам. Но, т.к. наш REST сервис должен отправлять ответы в json формате с правильными заголовками, то будет удобнее создать абстрактный базовый класс RestController и на его основе – контроллер сервиса.
В документации Yii есть хорошая статья о создании REST сервисов. Я использовал приведённый в ней пример в качестве основы для RestController, но немного его упростил с учётом особенностей данного приложения.
abstract class RestController extends Controller
{
    // Members
    /**
     * Default response format
     * either 'json' or 'xml'
     */
    private $format = 'json';
    /**
     * @return array action filters
     */
    public function filters()
    {
        return array();
    }
    // Actions
    abstract public function actionList();
    abstract public function actionView($id);
    abstract public function actionCreate();
    abstract public function actionUpdate($id);
    abstract public function actionDelete($id);
    protected function _sendResponse($status = 200, $body = '', $content_type = 'text/html')
    {
        // set the status
        $status_header = 'HTTP/1.1 ' . $status . ' ' . $this->_getStatusCodeMessage($status);
        header($status_header);
        // and the content type
        header('Content-type: ' . $content_type);
        // pages with body are easy
        if($body != '')
        {
            // send the body
            echo $body;
        }
        // we need to create the body if none is passed
        else
        {
            // create some body messages
            $message = '';
            // this is purely optional, but makes the pages a little nicer to read
            // for your users.  Since you won't likely send a lot of different status codes,
            // this also shouldn't be too ponderous to maintain
            switch($status)
            {
                case 401:
                    $message = 'You must be authorized to view this page.';
                    break;
                case 404:
                    $message = 'The requested URL ' . $_SERVER['REQUEST_URI'] . ' was not found.';
                    break;
                case 500:
                    $message = 'The server encountered an error processing your request.';
                    break;
                case 501:
                    $message = 'The requested method is not implemented.';
                    break;
            }
            // servers don't always have a signature turned on
            // (this is an apache directive "ServerSignature On")
            $signature = ($_SERVER['SERVER_SIGNATURE'] == '') ? $_SERVER['SERVER_SOFTWARE'] . ' Server at ' . $_SERVER['SERVER_NAME'] . ' Port ' . $_SERVER['SERVER_PORT'] : $_SERVER['SERVER_SIGNATURE'];
            // this should be templated in a real-world solution
            $body = '
<!doctype html>
<html lang="en-US">
<head>
    <meta charset="UTF-8">
    <title>' . $status . ' ' . $this->_getStatusCodeMessage($status) . '</title>
</head>
<body>
    <h1>' . $this->_getStatusCodeMessage($status) . '</h1>
    <p>' . $message . '</p>
    <hr />
    <address>' . $signature . '</address>
</body>
</html>';
            echo $body;
        }
        Yii::app()->end();
    }
    protected function _getStatusCodeMessage($status)
    {
        // these could be stored in a .ini file and loaded
        // via parse_ini_file()... however, this will suffice
        // for an example
        $codes = Array(
            200 => 'OK',
            400 => 'Bad Request',
            401 => 'Unauthorized',
            402 => 'Payment Required',
            403 => 'Forbidden',
            404 => 'Not Found',
            500 => 'Internal Server Error',
            501 => 'Not Implemented',
        );
        return (isset($codes[$status])) ? $codes[$status] : '';
    }
}
В строках 18-26 мы объявляем пять абстрактных методов, которые соответствуют всем нашим запросам. Т.е. класс, который будет наследовать RestController должен будет определить эти методы.
Также у нас есть два вспомогательных метода.
_getStatusCodeMessage – в нём просто определены описания HTTP статусов, которые будут отправляться в заголовках ответов.
_sendResponse – формирует ответ сервера, т.е. устанавливает HTTP заголовки и отправляет данные.
Теперь создадим PlacesController
class PlacesController extends RestController
{
    public function actionIndex() {
        if (Yii::app()->user->checkAccess('user')) {
            $this->render('index');
        }
        else {
            $this->redirect(array('site/login'));
        }
    }
    public function actionList()
    {
        if (!Yii::app()->user->checkAccess('user')) {
            $this->_sendResponse(403);
            return;
        }
        //searching only for current users places (defaultScope returns appropriate condition)
        $places = Places::model()->findAll();
        echo CJSON::encode($places);
    }
    public function actionView($id)
    {
    }
    public function actionCreate()
    {
        if (!Yii::app()->user->checkAccess('user')) {
            $this->_sendResponse(403);
            return;
        }
        $data = CJSON::decode(file_get_contents('php://input'));
        $place = new Places();
        $place->attributes = $data;
        if ($place->save()) {
            $this->_sendResponse(200, CJSON::encode($place));
        }
        else {
            $this->_sendResponse(500, CJSON::encode(array(
                'message'=>'Could not save place',
                'errors'=>$place->getErrors(),
            )));
        }
    }
    public function actionUpdate($id)
    {
        $data = CJSON::decode(file_get_contents('php://input'));
        $place = Places::model()->findByPk($id);
        if (!Yii::app()->user->checkAccess('user', array('place'=>$place))) {
            $this->_sendResponse(403);
            return;
        }
        if (null === $place) {
            $this->_sendResponse(404, CJSON::encode(array('message'=>'Could not find place with id = '.$id)));
        }
        $place->attributes = $data;
        if ($place->save()) {
            $this->_sendResponse(200, CJSON::encode($place));
        }
        else {
            $this->_sendResponse(500, CJSON::encode(array(
                'message'=>'Could not save place',
                'errors'=>$place->getErrors(),
            )));
        }
    }
    public function actionDelete($id)
    {
        $place = Places::model()->findByPk($id);
        if (null === $place) {
            $this->_sendResponse(404, CJSON::encode(array('message'=>'Could not find place with id = '.$id)));
            return;
        }
        if (!Yii::app()->user->checkAccess('user', array('place'=>$place))) {
            $this->_sendResponse(403);
            return;
        }
        if ($place->delete()) {
            $this->_sendResponse(200, CJSON::encode($place));
        }
        else {
            $this->_sendResponse(500, CJSON::encode(array(
                'message'=>'Could not delete place',
                'errors'=>$place->getErrors(),
            )));
        }
    }
}
Этот класс наследует RestController, т.е. в нём мы должны определить все абстрактные методы (actionList, actionView и т.д.). Во всех этих методах мы в первую очередь с помощью Yii::app()->user->checkAccess проверяем права текущего пользователя и если проверка не пройдена, отправляем 403 ошибку.
Примечание. Подробно систему разграничения доступа мы рассмотрим в следующий раз.
После этого, мы выполняем соответствующую операцию. Т.е. либо возвращаем список объектов (actionList), либо создаём, изменяем или удаляем указанную модель. Во всех методах id модели передаётся в первом параметре, т.к. его определяет роутер при разборе URL. Для получения остальных данных запроса используется функция file_get_contents.
$data = CJSON::decode(file_get_contents('php://input'));
Т.к. данные приходят в JSON формате, мы их декодируем с помощью класса CJSON.
Ещё одни момент касается обработки ошибок. Если у пользователя не достаточно прав для выполнения операции – отправляем 403-ю со стандартным сообщением (задано в методе _getStatusCodeMessage класса RestController).
Если модель не найдена (методы actionUpdate и actionDelete) – возвращаем 404-ю с собственным сообщением, т.к. по стандартному сложно понять, что именно не было найдено.
Наконец, если операцию выполнить не удалось, возвращаем 500-ую. В описание этих ошибок входят два параметра:
message – содержит название операции, которую не удалось выполнить;
errors – содержит массив с ошибками.
Проверить отправку этих ошибок вы можете следующим образом. Запустите приложение и дождитесь загрузки списка объектов. После этого остановите сервер базы данных и попробуйте изменить какой-нибудь объект. В форме вы увидите соответствующие ответы. И обратите внимание, что после того как вы снова запустите сервер базы, пользователь сможет продолжить работу без необходимости перезагружать страницу и не потеряет введённые данные.
Правила для роутера
Формат запросов к API отличается от стандартного формата запросов Yii (имя_контроллера/имя_метода), поэтому добавим в файл в файл config/main.php правила для каждого из наших запросов.
Обратите внимание, что для каждого правила, мы указываем параметр verb. Это необходимо, т.к. шаблоны для запросов на просмотр, удаление и обновление объекта не отличаются ничем кроме типа (GET, PUT, DELETE).
'components'=>array(
	...
	'urlManager'=>array(
		'urlFormat'=>'path',
		'rules'=>array(
			// REST patterns
			array('places/list', 'pattern'=>'api/places', 'verb'=>'GET'),
			array('places/view', 'pattern'=>'api/places/<id:\d+>', 'verb'=>'GET'),
			array('places/update', 'pattern'=>'api/places/<id:\d+>', 'verb'=>'PUT'),
			array('places/delete', 'pattern'=>'api/places/<id:\d+>', 'verb'=>'DELETE'),
			array('places/create', 'pattern'=>'api/places', 'verb'=>'POST'),
			// regular patterns
			'<controller:\w+>/<id:\d+>'=>'<controller>/view',
			'<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>',
			'<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
		),
		'showScriptName'=>false,
	),
	...
),
Важно помнить, что роутер разбирает правила сверху вниз и использует первое же правило, которое соответствует данному запросу. В данном случае если мы добавим новые правила в конец списка, то запросы вроде api/list будут обрабатываться стандартными правилами и Yii попытается найти класс ApiController и вызвать его метод actionList.
Как видите, создание REST API особой сложности не представляет. Естественно, этот пример очень простой и в реальном приложении у вас будет гораздо больше методов. Кроме того, может возникнуть необходимость одновременно поддерживать несколько версий API. В таких случаях контроллер и правила роутера станут сложнее, но общий принцип останется тем же.
В следующей части мы рассмотрим разграничение прав пользователей с помощью RBAC.
Если есть вопросы или замечания, пишите. Успехов!
Содержание
- Personal Maps: используем Yii и AngularJS для разработки web приложения. Часть 1.
- Personal Maps: Устанавливаем и настраиваем Yii, проектируем структуру базы данных. Часть 2.
- Personal Maps: главная страница и структура клиентской части приложения. Часть 3.
- Personal Maps: создаём сервис AngularJS. Часть 4.
- Personal Maps: тестирование AngularJS сервиса с помощью Jasmine и Karma. Часть 5.
- Personal Maps: контроллеры и представления в AngularJS. Часть 6.
- Personal maps: создаём директиву для подключения Google Maps. Часть 7.
- Personal maps: REST интерфейс. Часть 8.
- Personal Maps: авторизация и аутентификация (с использованием Yii RBAC). Часть 9.
- Personal Maps: локализация и интернационализация. Часть 10


