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

Все мы тут собрались умные, образованные, красивые и опытные. И на сегодняшний день, мне кажется, почти все используют тот, или иной вид 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 — пожалуйста ссылайтесь на автора, если вам взбредет в голову где-то это использовать. Спасибо.

Хорошего, красивого и стабильного кода вам!
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 28
  • +2
    >Для подобных систем век MVC знаконец-то закончился, и слава, как говорится, тебе господи!

    По-моему, ничем не противоречит ваша концепция концепции MVC, вы просто выносите часть VC и часть интерфейса к M на клиента. По крайней мере, если приложить некоторые усилия, то JavaScript код можно будет разделить на V и C, а также интерфейс к M
    • 0
      Я с вами не согласен. Так, с большой натяжкой, так можно под MVC подтянуть любую конструкцию.
      • 0
        Выделение слоя модели у вас налицо, функцию log вполне можно считать вьюшкой (она абстрагирует вывод от остальной программы), всё остальное — контроллер (взаимодействие между моделью и вьюшкой) ;)
        • +1
          Хорошо, я объясню, почему я с вами не согласен. Основная разница в терминах и сущностях при помощи которых можно описать и вести разработку. В MVC вы будете строго разделять модель данных от внешнего вида и от обработки данных, что вполне естественно. В нашем же случае мы работаем на уровне взаимодействия интерфейсов. JavaScript программист вполне может быть не в курсе реализации того, что он использует, да и сами RPC вызовы могут возвращать как данные, так и готовое отображение (HTML генерируем и показываем в клиенте — тоже ок!).

          В частном случае, конечно, можно подвести эту систему к MVC, но на практике ее возможности гораздо шире. Разрабатывая проект таким способом вы, по сути, сразу же получаете готовое RPC API для его внешнего использования другими приложениями\проектами, чего очень трудно добиться при других схемах разработки.
          • 0
            Почему это трудно?
            И чем вам REST не угодил?
            • +1
              Так в идеальном MVC взаимодействие осуществляется по интерфейсам, о внутреннем устройстве модели или вьюхи контроллер не знает, а они друг о друге даже не подозревают. А данные всегда хранятся, передаются и возвращаются в каком-то формате, не могут быть «просто данные» в информационной системе, какой это формат JSON, HTML или какой-то бинарный — не суть, особенно при наличии средств инкапсуляции (например нативная поддержка каких-то типов данных в ЯП).

              MVC, имхо, не ограничивает возможности никак, она лишь описывает возможную (и во многих отношениях удобную) архитектуру реализаций этих возможностей. Нормальный фреймворк позволяет реализовать классические протоколы RPC с минимальными затратами, а уж вопрос формата представления возвращаемых данных (XML, HTML, JSON, plain tetx, ...) вообще не должен стоять
              • 0
                Будь по вашему — пусть это будет MVC. Тем не менее, речь в статье не о дизайне конечного приложения, а о способе взаимодействия кода внутри него.
                • 0
                  Вот, видимо, поэтому мне фраза слух взгляд и резанула. Внутри MVC могут быть различные способы взаимодействия, равно как и какими-то конкретными способами можно реализовать не только MVC. А вы как-то противопоставили… Наверное, что-то конкретное имели в виду.
      • +3
        >Таким образом, я даже не знаю что за модель получилась, но это скорее толстый клиент уже, а не MVC

        По-моему эти термины [толстый клиент, MVC] являются вполне непротиворечивыми.
        • 0
          Сорри за небольшое дублирование первого коммента…
          • +1
            Я выше ответил, почему я так считаю.
            • +1
              > В нашем же случае мы работаем на уровне взаимодействия интерфейсов.
              Противоречия нет

              > JavaScript программист вполне может быть не в курсе реализации того, что он использует,
              Противоречия нет

              > да и сами RPC вызовы могут возвращать как данные, так и готовое отображение (HTML генерируем и показываем в клиенте — тоже ок!).
              Тут уже даже не совсем классический толстый клиент получается. Но противоречивости терминов MVC и Thick Client я все равно не вижу.

              Так что конкретно мой комментарий вы ничуть не опровергли. Или у меня с логикой не все впорядке, что вполне возможно в день рождения :)
              • 0
                Чем дальше я отвечаю на комментарии, тем больше понятия «тонкий клиент» и «толстый клиент» становятся похожими на «толстого троля» и «тонкого троля». Как я уже написал выше — это статья не о дизайне приложения, а о способе взаимодействия кода внутри него. Спасибо.
        • +1
          > impl/TestUser.inc
          Не рекомендуется так скрипты называть.
          Лучше *.inc.php
          • 0
            А ещееё лучше — выносить их поверх корня и не нервничать из-за названий. ;)
        • 0
          Возможно, вам будет интересна еще эта технология: habrahabr.ru/blogs/Jaxer/
          • 0
            >>> выбор был не велик
            Выбор был ???

            Даже если и был, то «PHP-быдлокодинг»-а среди вариантов быть не должно!!!

            Он необходим в скоростной разработке (прототипы, презентации, и.т.д.) а в обычной разработке нет такой буквы!!!
            • 0
              Спасибо, мне лично было интересно, а может и пригодится когда. Да, заметил пару опечаток: "(назовем его для приличия aip/Test.php)" и «систем век MVC знаконец-то закончился, и слава, как говорится, тебе господи!»
              • +2
                В extJS есть классная штука — DataProxy и его расширение HttpProxy, которая, в том числе, позволяет делать удобную обработку действий со Store. А то у вас я так понял какой-то свой объект rpc и ServiceProxy, думаю раз уж extJS решили использовать, то стандартные средства сподручней будут.
                • 0
                  Автор статьи вроде как Qooxdoo решил использовать. А в extjs есть более близкая «технология», имхо, к тому, что в статье описано — Ext Direct.
                  • 0
                    Работают ли эти системы с сессиями и позволяют ли разграничивать права доступа к объектам на уровне вызовов их методов?
                    • 0
                      Что мешает создать объект, наследуемый от HttpProxy или Direct и добавить туда контроль доступа? Последний я правда не использовал, пока не знаю как там что.
                      • 0
                        Попробуйте сделать это и расскажите нам, что получилось… :)
                  • 0
                    Как успехи?

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.