Взаимодействие flash с сервером на php с помощью swx

January 1, 2007

Дорога из желтого кирпича: Flash 8 & SWX & PHP & MYSQL



Я продолжаю знакомить вас с различными технологиями в мире flash позволяющими быстрее и проще (без глупых ошибок) создавать сложные приложения. Тема сегодняшнего материала – это организация взаимодействия между flash и серверными скриптами написанными на php.

Самое сложное в написании статьи доказать читателю зачем я выношу на его суд ту или иную технологию. Почему я решил рассказать о библиотеке X, а не об Y. Большей частью этот выбор субъективен (объективно плохие продукты живут на моем компьютере очень не долго). Библиотек для организации удаленных вызовов из flash логики реализованной на серверных языках (php, java, .net) не так уж и много (хотя десяток можно легко насчитать). Некоторые из них делают серьезные коммерческие компании, некоторые ваяют (как говорится на коленке) компьютерные энтузиасты. Используются различные форматы для передачи данных, стандартизированные (читай, рекомендуемые) и не очень. Но в одном они сходятся. С тем как реализован механизм вызова серверной логики и передача сложных структур данных из flash на сервер и обратно, мириться просто нельзя. Если у вас flash8 в стандартной поставке, то вам придется обойтись только XML, LoadVars. И если передать данные на сервер еще кое-как можно, то принять в ответ сложную структуру данных достаточно “неудобно”. Неудобство проявляется в повышенном числе потенциальных ошибок (выявлению которых не очень способствует среда разработки flash) возникающих в ходе операций “принять, разобрать, и переместить” информацию из некоторого входного формата в ваши переменные или визуальные элементы управления. Например, что вы будете делать, если php скрипт должен вернуть массив объектов Department (отдел), в каждом из отделов есть множество объектов Human (сотрудник), а у каждого из сотрудников в свою очередь есть массив из объектов Salary(Зарплата)? Вообще-то, такие сложные структуры данных принято кодировать в виде xml документов. Но с другой стороны в actionscript2 нет встроенной поддержки xpath, а выполнять разбор данных с помощью бесконечно вложенных firstChild, childrens (методов класса XML), откровенно говоря, не удобно. Так что вам остается либо использовать посторонние библиотеки (с нормальной поддержкой xpath) для разбора xml документов. Либо перейти на actionscript3, где появилась наконец-то поддержка E4X. А что делать, если вы не можете перейти на новый язык? Скажем, если вы поддерживаете унаследованную программную систему, к которой периодически нужно добавлять новые функции. Когда вышел flash 9 с поддержкой actionscript 3 (flex builder использующий туже библиотеку классов и язык был доступен еще год назад), то выбор стал гораздо шире. Я сам предпочитаю использовать для своих проектов AMFPHP и SOAP-сервисы. Их можно использовать как в flash8, так и flash9 основанных проектах (в случае flash8 вам потребуется еще установить дополнительное расширение к flash – “Flash remoting components”). С другой стороны я признаю, что для такого подхода требуются достаточно серьезные знания в области программирования и большинство начинающих flash-еров это только отпугнет. В общем, знакомьтесь, SWX – простая, дружелюбная и хорошо документированная библиотека для организации удаленных вызовов основанная на php из основанных на actionscript2 проектов.

Домашний сайт проекта http://swxformat.org/. На момент написания статьи последняя доступная версия была 1.0 beta. Но библиотека развивается достаточно быстро, новые версии выходят буквально каждые два-три месяца. Так в перспективе разработчики хотят добавить поддержку и actionscript3-основанных проектов. После того как вы скачали и распаковали архив с библиотекой мы выполним настройку ее серверной части. Так как обычно серверные скрипты используются, очевидно, не для тривиальных расчетов (сколько будет a+b), а для выборки данных из некоторой СУБД. То конечной целью этой статьи будет показать, как можно разработать приложение, соединяющееся с СУБД mysql, делающее из нее некоторую выборку данных, отображающее эти данные в DataGrid. А также мы реализуем операции добавления, и удаления записей из этой таблицы.

Начнем мы с краткого описания идеи лежащей в основе swx. Вы все умеете загружать в flash-ролик некоторый внешний ресурс с помощью LoadMovie. А что если загружаемый файл будет не статическим заранее созданным роликом, а динамически генерируемым с помощью php клипом, содержащим объявления некоторых переменных? Очевидно, что вы сможете обращаться к ним. Так вот, на стороне сервера вы создаете некоторое количество сервисов – это обычные php классы, с методами. Все эти классы/методы следует зарегистрировать с помощью менеджера сервисов. После чего вы из flash-ролика можете делать удаленные вызовы, просто указав имя класса сервиса и имя метода который нужно вызвать в его составе. Забудьте об тех ужасных строках “index.php?fio=bill&age=12&action=save”, которые вам приходилось анализировать в вызываемом php файле, когда использовался объект XML или LoadVars. Теперь все – отправка запроса на сервер, разбор запроса, вызов php-метода и кодирование результатов его работы будет выполнено прозрачно. Вам будет казаться, что вы вызываете функции находящиеся не на сервере, а в самом ролике.

Давайте займемся настройкой вызываемого сервиса. Для этого скопируйте содержимое под-каталога “php” из архива внутрь каталога swxserver на вашем веб-сервере. Если вы мечтаете о собственном веб-сервере, но не знаете, как его установить и настроить, то рекомендую выкачать из интернет упаковку с джентльменским набором “apache+php+mysql”. Например, denver, xampp, wamp. Все что вам нужно сделать, так это запустить инсталлятор, который выполнит все настройки самостоятельно. Приятная особенность xampp в том, что вы можете легко переключаться между используемыми версиями php (4 или 5-ая версия) с помощью одного нажатия мыши. Настоятельно прошу всегда, когда вы создаете некоторое веб-приложение выполнять его тест под разными версиями php, если не уверены в том какой у вас будет коммерческий хостинг. Теперь по адресу http://localhost/swxserver/swx.php находится менеджер сервисов. Откройте файл swx_config.php – это конфигурационный файл с настройками swx. Их не так уж и много, посмотрите значение переменной $servicesPath, она указывает путь к каталогу, где будут размещены файлы/классы/сервисы. По-умолчанию это подкаталог http://localhost/swxserver/services. В том случае если объем пересылаемых данных от сервера клиенту достаточно велик, то имеет смысл включить архивирование “на-лету” сгенерированного swf файла, измените для этого значение переменной $compressionLevel (возможные значения от 0 до 9, соответственно, “нет сжатия” – “максимальное сжатие”).

Теперь я создам базу данных, в которой будет находиться одна таблица Users со следующими полям (id_user, fio, birthdate, sex). Для этого я запустил консоль-панель управления mysql (она находится в каталоге mysql/bin/mysql.exe) и создал в ней базу данных и таблицу как показано на рис. 1.



Следующий шаг – это написание класса сервиса. В его составе будут три метода. Первый из них - listUsers – этот метод не будет получать никаких параметров. Результатом его работы будет массив содержащий список ассоциативных массивов с кодирующими каждую запись, в последующем эти данные будут загружены внутрь DataGrid с помощью flash. Второй метод – dropUser (id_user) – получив на вход номер человека, этот метод выполнит его удаление из базы данных. И наконец, третий метод – addUser (fio, birthdate, sex) – будет выполнять добавление нового пользователя в таблицу базы данных. В случае если запрошенную операцию выполнить не удастся, то все три функции будут возвращать специальное значение false. Я специально не использую никаких специальных (нестандартных и очень удобных) библиотек для доступа к данным в php. Возможно, в будущем я напишу специальную статью посвященную adodb, pdo, propel. Пока же я выполню подсоединение к серверу с помощью метода mysql_connect, затем выберу базу данных с помощью функции mysql_select_db. Перед тем как делать запросы на выборку или внесение данных мне необходимо будет выполнить переход сервера mysql в режим работы с нужной мне кодировкой. Для этого я применил метод mysql_query (‘SET NAMES кодировка’);. Так как swf файлы хранят текст в формат utf8, то и кодировка подключения к базе данных должна быть таковой. Все эти действия по подключению к БД я делаю в конструкторе класса.

Теперь нам нужно подумать о методах отладки кода. Хорошая новость в том, что в состав SWX входит написанное на flex приложение играющее роль отладчика, каждый раз когда вы делаете вызов то сведения о возвращенных данных помещаются в этот отладчик. Приложение-отладчик, оно находится тут "php\analyzer\analyzer.swf". Внешний вид его окна показан на рис. 2.



Но это еще не все. Мы договорились, что если вызов некоторого метода привел к тому что была сгенерирована ошибка, то этот метод возвращает значение false. А как теперь узнать какая именно это была ошибка. Для примера я решил показать, что SWX поддерживает работу с сессиями, так что когда возникает ошибка, я помещаю текст сообщения об этой ошибке внутрь специальной переменной сессии “ lasterrormsg ”. Также в состав класса UserService я ввел особый метод getLastError, который возвращает текст сообщения последней произошедшей ошибки.

Вот полный код примера файле userservice.php (не забудьте его поместить внутрь каталога http://localhost/swxserver/services):
  1. <?php
  2. // создаем класс сервиса
  3.  class UserService {
  4.   var $connection;
  5.   // в этой переменной будет храниться ссылка на подключение к базе данных
  6.   function UserService (){
  7.    if (! session_id()){session_start();}
  8.    $this->connection = mysql_connect('localhost', 'root', '');
  9.    if ($this->connection){
  10.     if ( !mysql_select_db('swx_users', $this->connection)){
  11.       $_SESSION ['lasterrormsg'] = 'Contructor: Cannot Select DB';
  12.     }
  13.     else{
  14.      mysql_query('SET NAMES utf8', $this->connection);
  15.     }  
  16.    }
  17.    else { 
  18.      $_SESSION ['lasterrormsg'] = 'Contructor: No Connection To DB Server'; 
  19.    } 
  20.   } // --- end foo
  21.  
  22.   function getLastError (){
  23.    return isset($_SESSION['lasterrormsg'])?$_SESSION['lasterrormsg']:'NO ERRORS';
  24.   }// --- end foo
  25.  
  26.   function listUsers (){
  27.    if (! $this->connection){
  28.       return false;
  29.    }
  30.    $list = mysql_query('select * from users',  $this->connection);
  31.    // если запрос не удался то возвращаем специальное значение false
  32.    if (! $list) 
  33.        return false;
  34.  
  35.    $rez = array ();
  36.    while (($row = mysql_fetch_assoc($list)) !== false){
  37.      $rez [] = $row;
  38.    }
  39.    mysql_free_result($list);
  40.    return $rez;
  41.   }// --- end foo
  42.  
  43.   function dropUser ($user_id){
  44.     if (! $this->connection){
  45.       return false; 
  46.     }
  47.     if (! is_numeric($user_id)){
  48.       $_SESSION ['lasterrormsg'] = 'dropUser: User ID IsNot Integer:' . $user_id;
  49.       return false;  
  50.     }
  51.    if (! mysql_query('delete from users where id_user = ' . $user_id , $this->connection)){
  52.      $_SESSION ['lasterrormsg'] = 'addUser: Cannot Add User : ' . mysql_error($this->connection);
  53.      return false;  
  54.    }
  55.    return true;  
  56.   }// --- end foo
  57.  
  58.   function addUser ($fio, $birthdate , $sex){
  59.     if (! $this->connection){
  60.       $_SESSION ['lasterrormsg'] = 'listUsers: No Connection To DB Server';
  61.       return false; 
  62.     }
  63.     $sex = ($sex != 0 && $sex != 1)?'0':$sex;
  64.     if (! mysql_query('insert into users set fio = "'.mysql_escape_string($fio).'",
  65.        birthdate="'.mysql_escape_string($birthdate).'", sex =  ' . $sex, $this->connection))
  66.     {
  67.       $_SESSION ['lasterrormsg'] = 'addUser: Cannot Add User : ' . mysql_error($this->connection);
  68.       return false;
  69.     }
  70.    return mysql_insert_id($this->connection); 
  71.    } // --- end foo
  72.  
  73. }// class
  74. ?>
Теперь мы вернемся к flash, создадим новый документ в стиле actionscript2 и добавим в на stage компонент DataGrid, дайте ему имя gridUsers. Также необходимо добавить две кнопки с именами btnLoadUsers и btnDropUser – это соответственно кнопка загрузки списка пользователей и удаления одного из них. Ниже разместите три текстовых поля для ввода ФИО, даты рождения и пола, дайте им имена txtFIO, txtBirthDate, txtSex. И, наконец, разместите кнопку, выполняющую отправку запроса на добавление данных в базу: btnAddUser.

Затем в actions для первого кадра выполните инициализацию подсистемы SWX. Прежде всего необходимо импортировать пакет “org.swxformat”, для того чтобы flash знал где его искать не забудьте добавить с помощью “Edit->Preferences->ActionScript” ссылку classpath на каталог куда вы распаковали архив swx (исходники библиотеки находятся в подкаталоге flash, там же вы можете найти еще примеры работы с swx). После импорта библиотеки следует создать объект типа SWX, и указать для него два основных параметра gateway и encoding. Второй из них должен содержать не название кодировки для обмена данными (не самое удачное название переменной), а способ HTTP вызова, т.е. метод GET или метод POST. Первый же параметр хитрее, он должен указывать полный “http://” путь к файлу php играющему роль менеджера зарегистрированных на веб-сервере классов-сервисов. Я рекомендую задавать данный параметр не в виде константы, а получать через param из внешнего html-файла, в который внедрен ваш ролик-приложение. Это повышает гибкость проекта и дает возможность легко переносить код с одного хостинга на другой. На этом все, подготовка клиента к удаленному вызову завершена.

Остается только написать код, который присоединяет к кнопкам обработчики событий, которые в свою очередь делают удаленные вызовы. Обратите внимание, что после обработки запросов addUser и dropUser (если конечно они были успешны) выполняется запрос listUser для получения обновленного списка пользователей.
  1. // создаем обработчики событий для каждой из кнопок
  2. btnLoadUsers.addEventListener("click", LoadUsersHandler);
  3. btnDropUser.addEventListener("click", DropUserHandler);
  4. btnAddUser.addEventListener("click", AddUserHandler);
  5.  
  6. import org.swxformat.*;
  7.  
  8. var swx:SWX = new SWX (); 
  9. swx.gateway = "http://localhost/swxserver/swx.php";
  10. swx.encoding = "POST";
  11. swx.debug = true;
  12.  
  13. // функцию срабатывающая при нажатии на кнопку "Загрузить список людей"
  14. function DropUserHandler(evt_obj:Object){
  15.  var callDetails:Object =  {
  16.     serviceClass: "UserService",
  17.     // имя вызываемого класса
  18.     method: "dropUser",
  19.     // имя вызываемого метода
  20.     args: [_root.gridUsers.dataProvider[_root.gridUsers.focusedCell.itemIndex].id_user],
  21.     result: [this, resultHandlerOnDrop],
  22.     timeout: [this, timeoutHandler],
  23.     debug: true 
  24.  }
  25.  // теперь делаем удаленный вызов
  26.  swx.call(callDetails);
  27. }
  28.  
  29. // функцию срабатывающая при нажатии на кнопку "Загрузить список людей"
  30. function AddUserHandler(evt_obj:Object){
  31.   var callDetails:Object =  {
  32.     serviceClass: "UserService",
  33.     // имя вызываемого класса
  34.     method: "addUser",
  35.     // имя вызываемого метода
  36.     args: [_root.txtFIO.text , _root.txtBirthDate.text , _root.txtSex.text],
  37.     result: [this, resultHandlerOnAdd],
  38.     timeout: [this, timeoutHandler],
  39.     debug: true 
  40.   }
  41.  // теперь делаем удаленный вызов
  42.  swx.call(callDetails);
  43. }
  44.  
  45.  
  46. // функцию срабатывающая при нажатии на кнопку "Загрузить список людей"
  47. function LoadUsersHandler(evt_obj:Object){
  48.   var callDetails:Object =  {
  49.     serviceClass: "UserService",
  50.     // имя вызываемого класса
  51.     method: "listUsers",
  52.     args: [],
  53.     result: [this, resultHandlerOnLoad],
  54.     timeout: [this, timeoutHandler],
  55.     debug: true 
  56.   }
  57.   swx.call(callDetails);
  58. }
  59.  
  60. // функцию срабатывающая при нажатии на кнопку "Загрузить список людей"
  61. function getLastError(){
  62.  var callDetails:Object =  {
  63.     serviceClass: "UserService",
  64.     method: "getLastError",
  65.     args: [],
  66.     result: [this, resultHandlerOnGetLastError],
  67.     timeout: [this, timeoutHandler],
  68.     debug: true 
  69.  }
  70.  swx.call(callDetails);
  71. }
  72.  
  73. // функция вызываемая при получении ответа на запрос getLastError
  74. function resultHandlerOnGetLastError(event:Object){
  75.  trace ('LastError: ' + event.result);
  76. }
  77.  
  78. // функция вызываемая при получении ответа на запрос addUser
  79. function resultHandlerOnAdd(event:Object){
  80.  if ((''+event.result) == 'false')
  81.    getLastError ();
  82.  else{ 
  83.    LoadUsersHandler (event); 
  84.  }
  85. }
  86.  
  87. // а эта функция вызывается тогда когда приходит ответ на запрос dropUser
  88. function resultHandlerOnDrop(event:Object){
  89.  if ((''+event.result) == 'false')
  90.    getLastError ();
  91.  else{  
  92.    LoadUsersHandler (event);  
  93.  }
  94. }
  95.  
  96. // обработчик события загрузка списка всех людей завершена 
  97. function resultHandlerOnLoad(event:Object){
  98.  if ((''+event.result) == 'false')
  99.    getLastError ();
  100.  else{ 
  101.   var x = new Array ();
  102.   for (var i = 0; i < event.result.length; i++)
  103.      x.push (event.result [i]);
  104.   _root.gridUsers.dataProvider = x;
  105.  }
  106. }
  107.  
  108. // функция обработчик события время ожидания исчерпано
  109. function timeoutHandler(event:Object){
  110.  trace ("TimeOut");
  111. }
Результат работы скрипта показан на рис.3.



Вы можете доработать данный пример, добавив в него новые функции, например, редактирование данных, поиск, фильтры, сортировку.

Categories: Flash & Flex