Приветствую, это очередная статья о разработке 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