Разрабатывая онлайн-чат для сайта под MODX Revolution, необходимо было как-то отслеживать события на сервере. Чтобы избежать "велосипедов", как любят выражаться мои московские коллеги по фрилансу, стал копаться в доках ExtJS. И наткнулся на Ext.Direct .К своему стыду, признаюсь, что и не слышал о нем ранее. Прочитав описание, как часто бывает с такими сложными вещами, ничего не понял. Полазив пол-дня по форумам и русским ресурсам, поматерив "наших" авторов и очередной раз зарекшись не тратить больше попусту время, вернулся к родной англоязычной документации и терпеливо прочитал спецификацию. Спецификацию нашел для версии 6.0.2, а в моей MODX - 3.5. И стала постепенно вырисовываться очень крутая штука.

Вкратце:

  • Основная идея:
    • Прозрачный вызов методов классов(процедур) на сервере из броузера.
    • Напр. в броузере: на JS пишем Sotrudniki.uvolity("Вася Петухов") и все...
    • ExtJS сам посылает запрос на сервер в определенном формате(json) с указанием имени класса, метода, аргументов и проч...
    • На сервере Router обрабатывает запрос. Создает экземпляр класса Sotrudniki и вызывает его метод uvolity с параметрами "Вася Петухов" и прочими. И увольняет Васю.
Это так называемый RPC. По нашему-Удаленный Вызов Процедур. Т.е. в итоге происходит отображение(mapping) объектов JS в объекты PHP(можно и на С#, Ruby и прочие). Мы реально работаем с классами на сервере через объекты JS. Для этого объекты JS должны повторять структуру классов PHP.

Классная идея!

Вообще это вещь из разряда - "долго запрягать, но быстро ехать". Требует некоторых затрат на запуск. Но "в итоге получаем минимум кода, минимум обслуживания, минимум багов", как написано в одной статье. Меня интересовала обработка событий на сервере. А именно - отслеживание добавления записей посетителями сайта. Но захотелось изучить детальнее RPC. Особенность документации по Ext.Direct: подробно описаны все методы и свойства. Почитаешь-почитаешь и не знаешь как этим всем распорядиться. Нет даже короткого примера по реализации. Много раз слышал о высоком пороге вхождения в ExtJS. Думается, это во многом благодаря такой документации. Здесь доки - это больше справочник для тех, кто уже хорошо знает предмет.

Наконец нашел статью на английском, где очень подробно расписан пример реализации. И сразу все стало понятно.

Структура реализации Ext.Direct для RPC

  • config.php - представление массива API, из которого скрипт api.php сгенерит js-объект - провайдер Ext.app.REMOTING_API для Ext.Direct. Т.о. при добавлении новых методов класса, нужно лишь добавить метод в этот файл и в сам класс.
            
    <?php
    $API = array(
        'Rabotniky'=>array(
            'methods'=>array(
                'uvolity'=>array(
                    'len'=>1
                )
            ,'prinyaty'=>array(
                    'len'=>1
                )
            )
        )
    );
              
  • api.php - генератор js-объекта Ext.app.REMOTING_API из config.php . Этот скрипт остается неизменным при добавлении классов и методов. Включается в <head>: <script src="/..../api.php"></script>
            
    <?php
    require('config.php');
    header('Content-Type: text/javascript');
    $actions = array();
    foreach($API as $aname=>&$a){
        $methods = array();
        foreach($a['methods'] as $mname=>&$m){
            $md = array(
                'name'=>$mname,
                'len'=>$m['len']
            );
            if(isset($m['formHandler']) && $m['formHandler']){
                $md['formHandler'] = true;
            }
            $methods[] = $md;
        }
        $actions[$aname] = $methods;
    }
    $cfg = array(
        'url'=>'/mdx/direct_test/router.php',
        'type'=>'remoting',
        'actions'=>$actions
    );
    echo 'Ext.app.REMOTING_API = ';
    echo json_encode($cfg);
    echo ';';
    
              
  • router.php - роутер. Так же неизменяемая часть структуры. Его задача принять запрос от броузера,найти соответствующий класс и вызвать его метод с заданными параметрами. Получить результат, упаковать в json и отправить броузеру. Я его немного упростил. Убрал некоторые функции, а то трудно было б въехать в суть.
            
    <?php
    require('config.php');
    $isUpload = false;
    if(isset($HTTP_RAW_POST_DATA)) {
        header('Content-Type: text/javascript');
        $data = json_decode($HTTP_RAW_POST_DATA);
    }else {
        die('Invalid request.');
    }
    
    function doRpc($cdata){
        global $API;
        try {
            if(!isset($API[$cdata->action])) {
                throw new Exception('Вызван неизвестный класс: ' . $cdata->action);
            }
            $action = $cdata->action;
            $a = $API[$action];
            $method = $cdata->method;
            $mdef = $a['methods'][$method];
            if(!$mdef){
                throw new Exception("Вызван неизвестный метод: $method в классе $action");
            }
            $r = array(
                'type'=>'rpc',
                'tid'=>$cdata->tid,
                'action'=>$action,
                'method'=>$method
            );
            require_once("classes/$action.php"); 
            $o = new $action();
            $params = isset($cdata->data) && is_array($cdata->data) ? $cdata->data : array();
    
            $r['result'] = call_user_func_array(array($o, $method), $params); add_test_str(' $r["result"]='.$r['result']);
    
        } catch(Exception $e) {
            $r['type'] = 'exception';
            $r['message'] = $e->getMessage();
            $r['where'] = $e->getTraceAsString();
        }
        return $r;
    }
    
    $response = null;     
    if(is_array($data)) {
        $response = array();                          
        foreach($data as $d) {
            $response[] = doRpc($d);
        }
    } else {
        $response = doRpc($data);
    }
    echo json_encode($response);  
    
              
  • classes - каталог в котором находятся классы. Имена файлов классов, самих классов и методов должны строго соответсвовать массиву API(файл 'config.php'). Я для примера создал файл Rabotniky.php . Он предельно прост, что б не размыть идею.
            
    <?php
    class Rabotniky{
        function uvolity($name) {
            return $name." - уволен!";
        }
    	function prinyaty($name) {                                                 
    
            return '"'.$name." - принят на работу.\"";
    	}
    }          

test_direct_Rabotniky.html - броузерная часть. Написана на ExtJS, что логично).

        
<>!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
    <title>Ext.Direct Form Example</title>
    <link rel="stylesheet" type="text/css" href="/...../assets/ext3/resources/css/ext-all.css"/>
    <script src="/...../assets/ext3/adapter/ext/ext-base.js" type="text/javascript">
    <script src="/...../assets/ext3/ext-all.js" type="text/javascript">
    <script src="/...../direct_test/api.php">
</head>
<body>
<div id="b" style="width:500px;margin: 10px auto; padding-top: 200px;">
<h1>Ext.Direct Test</h1>
<script>
    Ext.onReady(function(){
        Ext.Direct.addProvider(Ext.app.REMOTING_API);
        var formExample = new Ext.form.FormPanel({
            title: 'Прием, увольнение работников.',
            padding: 10,
            buttons:[{
                text: 'Принять на работу',
                handler: function(){                    
                    var name=document.getElementById('cb_fio').value;
                    if(name=='....') return;
                    Rabotniky.prinyaty(name,function(provider, response){
                    var html_="<h4 style='color:red;'>"+response.result+"</h4>";
                    document.getElementById('b').innerHTML=html_;                   
                    });
                }
            }
            ,{
                text: 'Уволить без выходного пособия',
                handler: function(){
                    var name=document.getElementById('cb_fio').value;
                    if(name=='....') return;
                    Rabotniky.uvolity(name,function(provider, response){
                    var html_="<h4 style='color:red;'>"+response.result+"</h4>";
                    document.getElementById('b').innerHTML=html_;                   
                    });
                }
            }
            ],
            renderTo: 'b',
            items: [{
                xtype:'combo',
                id:'cb_fio',
                emptyText:'....',
                fieldLabel: 'Работник',
                labelStyle: 'font-weight:bold; font-size:8;',
                name: 'rab',
                msgTarget: 'side',
                mode: 'local',
                store: new Ext.data.ArrayStore({
                    id: 0,
                    fields: ['name'],
                    data: [['Вася Ухов'], ['Петя Безухов'],['Коля Носов']]
                    }),
                    valueField: 'name',
                    displayField:'name'
            }]
        });
    });
</script>
</div>
</body>
</html>          
Ну тут, вроде, все понятно. Если нет - готов пояснить. При загрузке api.php имеем -
  Ext.app.REMOTING_API = {
      "url":"/..../direct_test/router.php",
      "type":"remoting",
      "actions":{
              "Rabotniky":[
                  {"name":"uvolity","len":1},
                  {"name":"prinyaty","len":1}
              ]}
  };
  
Т.е. получаем динамически сгенерированный провайдер, со всеми нужными параметрами.

Здесь - рабочий пример. Когда все сделаешь, кажется что все так просто и логично, странно - что так долго не мог въехать в тему.

PS

При всей видимой сложности - очень продуманная вещь. Если учесть что большая часть файлов неизменная, устанавливается один раз и надолго. Напомню, при добавлении(изменении) классов и методов - меняется лишь простой файл config.php и сами классы. В броузере провайдер генерится автоматом. Вызовы методов классов предельно просты. Напр.:Rabotniky.uvolity(--параметры--,--функция-обработчик результата--);

"Марксизм - не догма, а руководство к действию"

Ф.Энгельс
Поэтому, можно сильно упростить структуру, обойтись роутером и классами. А провайдер описать вручную. Но это не наш путь... Ну а использование Ext.Direct в качестве обработчика событий на сервере - вообще реализуется просто. Но это постараюсь описать попозже, когда сделаю нормальную реализацию.