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


