26 December 2010

PHP, JavaScript, RPC и другие страшные слова

PHP
Sandbox
Все мы тут собрались умные, образованные, красивые и опытные. И на сегодняшний день, мне кажется, почти все используют тот, или иной вид RPC между JavaScript и PHP, который работает на API из того, или иного фреймворка. Кое кто даже разрабатывает какие-то свои костыли и подпорки. Я не исключение, конечно же. Правда пошел я по пути наименьшего сопротивления и, собственно, речь в этой статье пойдет как раз об этом — об очередной реализации XML\JSON RPC для JavaScript и PHP.

Предыстория такова, что мне необходимо было разработать некую систему управления данными. Естественно, что данные хранятся в СУБД, а управлять ими надо через веб. Привязываться к фреймворкам готовым очень не хотелось, потому выбор был не велик — PHP-быдлокодинг, или MVC с рендерингом на основе готовых разработок вроде smarty. Однако, примерно в то же время, я обратил внимание на такие проекты как extJS (он же Sencha теперь) и qooxdoo, которые позволяют создавать полноценные веб-приложения минуя утомительную HTML-верстку, генерацию HTML/XML, XSLT преобразования и многие другие «страшные» вещи, характерные для MVC и PHP-быдлокода. Потому созрел следующий план действий.:
  • пользовательский интерфейс на extJS или qooxdoo
  • модели на PHP
  • связь моежду ними XML\JSON RPC
Таким образом, я даже не знаю что за модель получилась, но это скорее толстый клиент уже, а не MVC. Вообще, то что вышло, в итоге мне очень нравится. Для интерфейса выбор пал на qooxoo — это GNU лицензия и, по моему мнению, его код все таки больше направлен на ООП и ФП чем на декларативное JSON-style программирование интерфейса как в extJS, и так уже сложилось, что мне это ближе и понятнее. А вот с RPC пришлось немного повозится.

Хотелось найти что-то легковесное, независимое, понятное, простое и работающее с пол пинка. Такой проект был найден по адресу code.google.com/p/json-xml-rpc — это достаточно гибкая и простая реализация RPC с сервером на PHP и клиентом на JavaScript, работающая как с XML, так и с JSON и поддерживающая как синхронные, так и асинхронные вызовы, что оказалось не маловажным в последствии. Следующим шагом являлось расширение RPC, а так же система распределеня ролей и прав доступа. Об этом хочется рассказать подробнее. Итак.

Начну с того, что я являюсь адептом школы ООП и вообще в оригинале пишу на С++, в итоге, как таковой сам PHP и JavaScript меня иногда безумно бесят в плане того что они позволяют, или не позволяют делать в коде. Не смотря на это я считаю что писать код, который будет многократно использоваться на ООП не только можно но и нужно. И совершенно не имеет значения на чем писать. Потому, для данной системы был разработан набор интерфейсов и их реализаций, в котором отражены такие аспекты работы, как сессии, авторизация, пользователь и его роль, а так же система RPC-проксирования, позволяющая не только полностью автоматически переносить классы PHP в JavaScript но и распределять права доступа к этим классам с точностью до вызова метода.

Для начала я покажу как это выглядит с точки зрения постановки задачи и кода на PHP и JavaScript. Допустим у нас есть простой класс:
class Test {
 public function mul ($param1, $param2) {
  return $param1 * $param2;
 }
}


* This source code was highlighted with Source Code Highlighter.
Мы хотим иметь такой же точно класс с точно таким же функционалом в нашем клиентском JavaScript-е и написать что-то вроде:
var test = new Test();
var result = test.foo(1,2);

* This source code was highlighted with Source Code Highlighter.
Для этого, в текущем варианте, мне необходимо создать файл (назовем его для приличия aip/Test.php) со следующим содержимым:
include '../base/IRPCProxy.inc';

class Test extends IRPCProxy {
 public function mul ($param1, $param2) {
  return $param1 * $param2;
 }
}

IRPCProxy::populate(new Test());

* This source code was highlighted with Source Code Highlighter.
И создадим тестовый JavaScript в другом файле (не мудрствуя лукаво назовем его test.html) такого плана:
 var test = new rpc.ServiceProxy('api/Test.php', { asynchronous:false });
 var result = test.mul (19, 7);

* This source code was highlighted with Source Code Highlighter.
Как вы можете видеть, ничего особенно не изменилось ни с точки зрения PHP ни с точки зрения JavaScripta. Нужно просто наследовать необходимый нам класс в PHP от IRPCProxy и «особенным образом» инициализировать переменную test в JavaScript-е. Естественно, что код JavaScript-а не полный, но в итоге пользование такими удаленными объектами выглядит в коде именно так.

Теперь то, что касается прав доступа. Сама по себе система RPC не имеет понятия ни о сессиях, ни о пользователях, ни, естесственно об их ролях. Этим всем незаметно и просто управляют классы и интерфейсы разработанные мною. Основные сущности:
  • пользователь, его роль и авторизация
  • сессия, ее данные и авторизация
  • RPC-прокси и доступ к методам порожденных классов
Следут отметить, что при запросе к PHP код для RPC не генерируется и не парсится каждый раз, а кешируется, в зависимости от параметров сессии. При чем в текущей реализации код кешируется в виде данных сессии, но возможно их выгоднее было бы хранить в отдельных файлах или еще как-то, я не тестировал такой вариант. Итак, как итог, для того, чтобы получить полноценную систему нам нужно реализовать код пользователя и код сессии. Это не обязательно, так как базовые классы вполне выполняют свою работу без конкретных уточнений в реализации. То есть код, написанный выше будет работать именно так, как вы ожидаете.

Начнем с пользователя. Он у нас будет ленивый и его авторизация будет состоять толькоиз проверки на правильный логин и изменение роли (файл impl/TestUser.inc):
/**
* RPC user implementation
* @author alexander.voronin@gmail.com
*/
class TestUser extends IRPCUser {
  /**
   * @see IRPCUser::authorize()
   */
  public function authorize ( $login, $password ) {
    if ( $login == "test" ) {
      $this->login = $login;
      $this->role = "admin";
      return true;
    } else {
      IRPCProxy::logText("Authorization failed for user '$login' - invalid login");
      return false;
    }
  }
};

* This source code was highlighted with Source Code Highlighter.
Далее нам нужна сессия. Для того, чтобы не путать данные с другими проектами наша сессия будет инициализироваться со специальным префиксом «test» (файл impl/TestSession.inc):
require_once '../base/IRPCSession.inc';

/**
* RPC session implementation
* @author alexander.voronin@gmail.com
*/
class TestSession extends IRPCSession {
  /**
   * Reload CTOR and create own session namespace
   */
  function __construct () {
    parent::__construct("test");
  }
};

* This source code was highlighted with Source Code Highlighter.
Ну и наконец сама реализация прокси должна «знать», о том, что у нас используются не стандартные пользователи и сессии, а наши собственные (файл impl/RPCTestProxy.inc):
// base
require_once '../base/IRPCProxy.inc';
// implementations
require_once 'TestSession.inc';
require_once 'TestUser.inc';

/**
* RPC proxy implementation
* @author alexander.voronin@gmail.com
*/
class TestProxy extends IRPCProxy {
  /**
   * @see IRPCProxy::createSession()
   * @return TestSession
   */
  public function createSession() {
    return new TestSession();
  }
  /**
   * @see IRPCProxy::createUser()
   * @return TestUser
   */
  public function createUser() {
    return new TestUser();
  }
}

* This source code was highlighted with Source Code Highlighter.
Теперь мы готовы распределять права по ролям в нашем RPC коде на PHP. С точки зрения текущей реализации это делается единожды при запросе, а полученный код, кешируется в данных сессии. Ключ к закешированному коду сложный и состоит из префикса сессии и роли пользователя, так что, если в процессе работы у пользователя поменяется его роль, то отсутствующий код будет сгенерирован автоматически. Следует правда учесть, что если код уже существует, то сбросить кеш можно только уничтожив сессию или воздействовать на закешированный код «напрямую», чего, конечно же, лучше не делать, чтобы код оставался понятным и читаемым. Итак, распределяем права (файл api/Test.php):
<?php

include '../impl/RPCTestProxy.inc';

class Test extends RPCTestProxy {
function haveAccess ( $method ) {
switch ( $this->getUser()->getRole()) {
case "anonymous":
switch ( $method ) {
case "mul":
case "getRole":
case "sessionLogin":
return true;
default:
return false;
}
case "admin":
return true;
default:
return false;
}
}

public function mul ($param1, $param2) {
return $param1 * $param2;
}

public function getRole () {
return $this->getUser()->getRole();
}

public function sessionLogin ($login) {
if ( $this->authorize($login, "")) {
return true;
} else {
return false;
}
}

public function sessionLogout () {
$this->logout();
}

public function forAdmins () {
return "hello admin";
}
}

IRPCProxy::populate(new Test());

?>


* This source code was highlighted with Source Code Highlighter.
Как видно из кода, мы просто определяем доступ к методам класса по их имени и исходя из текущей роли пользователя. Так же мы добавили метод sessionLogin, который позволяет нам авторизоваться без пароля и sessionLogout, который очевидно заканчивает нашу сессию. Не стоит беспокоится о том, что происходит внутри — там просто все работает и, после успешной авторизации, у пользователя, как мы и договаривались выше, поменяется роль. Это все, что нам необходимо для дальнейшей работы. Теперь расширим наш тест на JavaScript так чтобы показать как все это работает:
 // simple log
 function log ( msg ) {
  document.write('LOG: ' + msg + '\n' );
 }
 var start = new Date();

 // RPC init
 var test = new rpc.ServiceProxy('api/Test.php', { asynchronous:false });

 log('Test calls: ' + test.system.listMethods());
 log( 'Role: ' + test.getRole());

 var result = test.mul (19, 7);
 log ( 'Test result: ' + result );

 // try to login with invalid credentials
 if ( !test.sessionLogin ('vasya')) {
  log ( 'Login failed!' );
 } else {
  log ( 'Login success!' );
 }
 // check role after login
 log( 'Role: ' + test.getRole());

 // now try to login with valid credentials
 if ( !test.sessionLogin ('test')) {
  log ( 'Login failed!' );
 } else {
  log ( 'Login success!' );
 }
 // check role after login
 log( 'Role: ' + test.getRole());

 // must reinit RPC to get access to new methods
 test = new rpc.ServiceProxy('api/Test.php', { asynchronous:false });
 log('Test calls: ' + test.system.listMethods());
 log('Admin test: ' + test.forAdmins());

 // logout now
 log ( 'Logout now...' );
 test.sessionLogout();
 // check role after login
 log( 'Role: ' + test.getRole());

 // check timing
 var stop = new Date();
 var testTime = stop.getTime() - start.getTime ();
 log ( 'Test time: ' + testTime + 'ms' );

* This source code was highlighted with Source Code Highlighter.
Как видно из кода, в процессе его выполнения, пользователь два раза меняет свою роль. При этом после авторизации необходимо заново создать RPC объект, для того чтобы обновить список его методов. После выполнения логаута сессия со всеми данными уничтожается и роль пользователя меняется на анонимного. Вот типичный результат исполнения такого кода в броузере:

LOG: Test calls: mul,getRole,sessionLogin,system.setJSONDateFormat,system.setDBResultIndexType
LOG: Role: anonymous
LOG: Test result: 133
LOG: Login failed!
LOG: Role: anonymous
LOG: Login success!
LOG: Role: admin
LOG: Test calls: mul,getRole,sessionLogin,sessionLogout,forAdmins,system.setJSONDateFormat,system.setDBResultIndexType
LOG: Admin test: hello admin
LOG: Logout now...
LOG: Role: anonymous
LOG: Test time: 212ms


Как результат я получил рабочую веб-систему без HTML верстки вообще. Я пишу код моделей данных и их обработки на чистом, ничем не замутненном PHP и разрешаю доступ к ним через описанную выше RPC систему, а клиент — это обычное qooxdoo-приложение, со своими окошечками, рюшечками и оборочками. Для подобных систем век MVC знаконец-то закончился, и слава, как говорится, тебе господи!

В заключении я бы хотел несколько слов сказать о производительности. Несомненно — RPC это медленно. Однако используя асинхронные RPC запросы мы можем избавится от подвисаний броузера в момент RPC вызовов, ну и конечно же, следует обращать внимание на то ка кнаписан сам код. Возможно десяток RPC вызовов стоит объединить в один, или подумать над тем что ик ак вы возвращаете в результате работы метода.

Пример, описанный выше целиком, вместе с кодом RPC обертки можно загрузить по адресу depositfiles.com/files/xsnid5wg9 — пожалуйста ссылайтесь на автора, если вам взбредет в голову где-то это использовать. Спасибо.

Хорошего, красивого и стабильного кода вам!
Tags:PHPjavascriptRPCqooxdoo
Hubs: PHP
+8
7.4k 58
Comments 28
Popular right now
Full stack developer (PHP, JavaScript) — B2B
from 200,000 ₽SkyengRemote job
Middle Developer PHP/JS (SolusIO)
from 90,000 to 150,000 ₽PleskНовосибирскRemote job
Fullstack-разработчик (PHP, JS, Yii)
from 90,000 ₽ВсеИнструменты.руRemote job
PHP разработчик
from 100,000 ₽Simtech DevelopmentСанкт-Петербург
PHP-разработчик
from 80,000 to 150,000 ₽CallProfiRemote job
Top of the last 24 hours