2013-03-30

REST и разделение сервера и клиента

Intro/Trends

В IT-моду входит адаптивный дизайн. Ведь это раньше (давным-давно) у нас, разработчиков сайтов, был только один PC и IE5-6, под которые в основном и готовились сайты. А что сейчас? Куча платформ и разрешений/размеров экранов. Появились и средства для верстки под разные устройства - например, CSS-фича "@media-queries", по сути, ничего сложного, просто теперь необходимо писать отдельные стили под разные разрешения/размеры и ориентации экранов, рутина, одним словом.
В связи со всем этим назрела необходимость бОльшего разделения клиентской и серверной части. Лично я считаю, что имея кучу различных устройств, достаточно под каждое адаптировать клиентское приложение, а серверная часть должна для всех оставаться независимой, то есть возвращать сырые данные без отображения. REST нам в помощь.
Есть у меня старая CMS, ведь большинство разработчиков пробовали в начале карьеры писать свойские CMS и лишь у единиц получилось вывести их на рынок и начать зарабатывать или, хотя бы, сделать известными в мире Open Source. На этой самой морально устаревшей CMS я и решил попрактиковаться. Задача: реализовать REST API и простенькую админку.


Начнем с менеджера PHP-библиотек

Менеджер, управляющий библиотеками и зависимостями - это всегда удобно, что в OC, что в проекте на любом языкае. Нашел популярный Composer.
На оффициальном сайте (ссылка выше) можно ознакомиться с инструкцией по установке и
использованию. Лично мне он показался не таким удобным как Bundler для Ruby или NPM для NodeJS. Но все же, им можно пользоваться.
Готовим файл "composer.js" с библиотеками для автоматической установки:
{"require":{
"doctrine/dbal": "2.3.*",
"silex/silex": "1.0.*@dev"
}}
Для проекта я выбрал работу с базой с помощью Doctrine DBAL и Silex (от разработчиков Symfony) в качестве REST-контроллера.

Серверная часть административного интерфейса

Как уже упоминалось выше, используем Doctrine DBAL и Silex.
// подключение библиотек, загруженных установленных с помощью Composer
require_once(__DIR__.'/../vendor/autoload.php');
// инициализация Silex-приложения с провайдерами
$app = new Silex\Application();
$app['debug'] = $env['debug'];
$app->register(new Silex\Provider\ServiceControllerServiceProvider());
$app->register(new Silex\Provider\DoctrineServiceProvider(), array(
    'db.options' => array(
'driver' => $env['db']['driver'],
'user' =>$env['db']['user'],
'password' => $env['db']['pass'],
'dbname' => $env['db']['name'],
    ),
));
Создадим простенький контроллер, управляющий разделами сайта:
$ctrl = array();
$ctrl['chapters'] = $app['controllers_factory'];
$ctrl['chapters']->get('/', function () use ($app) {
$chapters = $app['db']->fetchAll('SELECT * FROM chapters ORDER BY title');
return $app->json($chapters);
});
$ctrl['chapters']->put('/', function () use ($app) {
$app['db']->insert('chapters', array('title' => $app['request']->get('title'), 'label' => md5($app['request']->get('title'))));
return $app->json(array());
});
$ctrl['chapters']->get('/{id}', function ($id) use ($app) {
$chapter = $app['db']->fetchAssoc('SELECT * FROM chapters WHERE id = ?', array($id));
return $app->json($chapter);
});
$ctrl['chapters']->put('/{id}', function ($id) use ($app) {
$app['db']->update('chapters', array('title' => $app['request']->get('title')), array('id' => $id));
return $app->json(array());
});
$ctrl['chapters']->delete('/{id}', function ($id) use ($app) {
$app['db']->delete('chapters', array('id' => $id));
return $app->json(array());
});
$app->mount('/chapters', $ctrl['chapters']);
Symfony\Component\HttpFoundation\Request::enableHttpMethodParameterOverride();
$app->run();
Сия часть приложения позволяет осуществлять полный цикл CRUD-управления разделами сайта. Но это только малая часть. Необходимо еще реализовать авторизацию и управление всеми остальными сущностями сайта, такими как пользователи и группы, домены, сайты, области данных и различные компоненты и модули.

Клиентская сторона административного интерфейса

Рассмотрел несколько популярных JS-фреймворков для создания клиентской части системы (AngularJS, Knockout, Backbone). Выбор пал на Angular JS, просто потому, что он с поддержкой Google, но это не важно, так как, теперь имея REST API, можно в любой момент переписать клиентскую часть на любом другом фреймворке, например, на Backbone.
Для начала просто реализуем на этом чудо-фреймворке просмотр списка разделов и отдельного раздела.

index.html:
<!doctype html>
<html ng-app="admin">
<head>
<meta charset="utf-8">
<script src="vendor/angular/angular.min.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers.js"></script>
<script src="js/services.js"></script>
<script src="vendor/angular/angular-resource.min.js"></script>
</head>
<body>
<div ng-view></div>
</body>
</html>
app.js:
angular.module('admin', ['adminServices']).config(
[ '$routeProvider', function($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'tpl/dashboard.php',
controller: DashboardCtrl
}).when('/chapters', {
templateUrl : 'tpl/chapters/list.php',
controller : ChaptersListCtrl
}).when('/chapters/:id', {
templateUrl : 'tpl/chapters/view.php',
controller : ChaptersViewCtrl
}).otherwise({
redirectTo : '/'
});
} ]);
controllers.js:
function DashboardCtrl($scope, $http) {
//
}
function ChaptersListCtrl($scope, Chapter) {
$scope.chapters = Chapter.query();
}
function ChaptersViewCtrl($scope, $routeParams, Chapter) {
$scope.chapter = Chapter.get({id: $routeParams.id}, function(chapter) {
//
});
}
services.js:
angular.module('adminServices', ['ngResource']).
factory('Chapter', function($resource){
return $resource('/_cms/rest/index.php/chapters/:id', {}, {
query: {method:'GET', params:{id:''}, isArray:true}
});
});
В дальнейшем можно разработать нативные приложения под мобильные устройства или адаптивные HTML5-приложения.

P.S. Для браузерной части админки я выбрал популярный и удобный фреймворк Twitter Bootstrap. Начать его использовать очень просто, в официальной документации все понятно расписано.