Flex and PHP: remoting with Zend AMF
The latest PHP library to add support for AMF and remoting is Zend Framework. The preview prelease version 1.7 offers a new component Zend_AMF that lets you create Flex applications that talk to PHP backends using remoting. Since I am a big fan of remoting as a way to get data to your Flex/AIR clients, I wanted to add a short post explaining how to use it. Here is another post I wrote on remoting with AMFPHP. Actually this post is a part of a larger article I did for Adobe Developer Connection. I want to keep it more focused, so I wrote this one.
You can download a Flex Builder project that contains the code I explain in this article from here. Inside of the archive you will find a readme.txt file explaining what to do with it.
Installing the Zend Framework
After downloading the Zend Framework 1.7 archive, extract the files. Next, you have to add the library folder to your PHP include path. Open the php.ini file and add the path to the library folder to the include_path; on my machine looks like:
include_path = “c:\htdocs\zend_framework\library”
Next, save the file and restart your web server. You can read more about installing Zend Framework here. With this, you’ve completed the “installation” of Zend Framework.
What is AMF and remoting and why should you use it?
If you already know these answers, you may want to skip to the next section. Let’s start by understanding remote procedure calls. Remote procedure calls let Flex applications make direct calls on the methods of your server side classes. Using BlazeDS or LiveCycle Data Services you can expose your Java and ColdFusion classes to the Flex application. However, if you use PHP you need a third party library on the server to expose PHP classes directly. Existing solutions include Zend AMF, WebORB, and AMFPHP. This article focuses on remoting with Zend AMF. AMF is a binary protocol for serializing the messages. Because it is binary, it is more efficient in terms of bandwidth and server processing load than JSON or XML methods. If you want to see for yourself how much more efficient it is, James Ward has put together a nice benchmark.
Zend AMF is a PHP library that knows how to serialize and deserialize the AMF protocol (it is part of the Zend Framework starting with version 1.7), and thus lets you expose PHP classes to Flex applications. Another compelling reason for using remoting is code reuse. Because you can call methods on PHP classes and these methods can return PHP objects, you don’t have to modify your existing code to output JSON or XML.
As I noted earlier, Zend AMF remoting uses AMF to serialize messages between the server and Flex client. It also offers the ability to map an ActionScript class to a PHP class. For example, suppose you want to display in Flex the information from a table with the following structure:
contacts
-------------------------------
id primary key int
name varchar(255)
email varchar(255)
When using remoting, you create an ActionScript class to model this data in the client and a PHP class to model the same data on the server. When you create the PHP class that you want to call from Flex, you add a method that, for example, retrieves all the contacts from the table. This method will return an array of PHP VO classes, and in Flex you will get an array of ActionScript objects. All the conversions from PHP objects to AMF to ActionScript objects are done automatically for you by Flex and Zend AMF.
When you use XML or JSON for remoting, you’ll tipically need extra steps in Flex to process the data in order to display or store it.
Let’s look at a working example.
Create the Flex PHP project
Usually, when I work with Flex and PHP projects, I prefer to use Flex Builder and Zend Studio installed together. It is possible, however, to work with Flex Builder and a PHP plugin to help you with the PHP code. Either way, you should create a Flex project that uses PHP on the server side (if you plan to use Zend Studio and Flex Builder, first create a Zend PHP Project, then use the Add Flex Nature wizard to add Flex PHP nature on the project). This way you streamline the deployment of the SWF file (the compiled result of the Flex project) to the PHP server. I chose to create a new project called “flex_php”.
Next, create a folder inside the PHP server root named “zendamf_remote”, and add this folder to the project. Choose New > Folder, and then click on the Advanced button. If you want to have the source files for the Zend Framework available to your project, and you use Zend Studio too, then open the properties page for the project, go to the PHP Include Path > Libraries tab, and add an External Folder pointing to the place where the Zend Framework is installed.
Create the PHP code
In the “zendamf_remote” folder, create three PHP files: MyService.php, VOAuthor.php, and index.php. Open the MyService.php page and paste the following code (you need to update the connection information for your specific database setup; to do this, look for the four constants at the top of the class):
<?php require_once('VOAuthor.php'); //connection info define("DATABASE_SERVER", "localhost"); define("DATABASE_USERNAME", "mihai"); define("DATABASE_PASSWORD", "mihai"); define("DATABASE_NAME", "flex360"); class MyService { /** * Retrieve all the records from the table * @return an array of VOAuthor */ public function getData() { //connect to the database. //we could have used an abstracting layer for connecting to the database. //for the sake of simplicity, I choose not to. $mysql = mysql_connect(DATABASE_SERVER, DATABASE_USERNAME, DATABASE_PASSWORD); mysql_select_db(DATABASE_NAME); //retrieve all rows $query = "SELECT id_aut, fname_aut, lname_aut FROM authors_aut ORDER BY fname_aut"; $result = mysql_query($query); $ret = array(); while ($row = mysql_fetch_object($result)) { $tmp = new VOAuthor(); $tmp->id_aut = $row->id_aut; $tmp->fname_aut = $row->fname_aut; $tmp->lname_aut = $row->lname_aut; $ret[] = $tmp; } mysql_free_result($result); return $ret; } /** * Update one item in the table * @param VOAuthor to be updated * @return NULL */ public function saveData($author) { if ($author == NULL) return NULL; //connect to the database. $mysql = mysql_connect(DATABASE_SERVER, DATABASE_USERNAME, DATABASE_PASSWORD); mysql_select_db(DATABASE_NAME); //save changes $query = "UPDATE authors_aut SET fname_aut='".$author->fname_aut."', lname_aut='".$author->lname_aut."' WHERE id_aut=". $author->id_aut; $result = mysql_query($query); return NULL; } } ?>
This is the class you will call from Flex. It has two methods: one to get all the records from the table, and another to update the values for one record.
Let’s create the code for the Value Object, the data model. This is used by the MyService class to wrap one row from the table. Thus, the method getData() returns an array of VOAuthor, and the method saveData() receives one argument: the VOAuthor of the row that was changed. Open the file VOAuthor.php and add this code:
<?php class VOAuthor { public $id_aut; public $fname_aut; public $lname_aut; } ?>
As you can see, this class is very simple; it just provides the same members as the fields from the table. Finally let’s create the code for index.php file. This is the plumbing code that expose the MyService class to Flex clients with the help of the Zend AMF. Add the following code:
<?php require_once('Zend/Amf/Server.php'); require_once('MyService.php'); $server = new Zend_Amf_Server(); //adding our class to Zend AMF Server $server->setClass("MyService"); //Mapping the ActionScript VO to the PHP VO //you don't have to add the package name $server->setClassMap("VOAuthor", "VOAuthor"); echo($server -> handle()); ?>
I use an instance of Zend AMF server to create a PHP end point that can be called from Flex. Then I register the MyService class to the server, thus I can call this class from Flex. And finally I map the ActionScript data model (VOAuthor) to the PHP VOAuthor data model.
When you use remoting, you get the casting of the data to the right type for free. For example, MyService.getData() method returns an array of VOAuthor PHP objects. However, as you will see later, in Flex the result is an array of VOAuthor ActionScript objects.
Creating the Flex application
Now that you have the PHP code in place, you are ready to create the Flex code that will call the PHP class. I want the Flex application to have a button that gets the data from the server, uses a data grid to display the data, and enables the user to edit any cell (except ids) within the data grid. Whenever a cell is edited, the update is sent automatically to the server and saved to the database as well.
First, be sure to select the Flex perspective from the top right icons of Eclipse.
The next thing you need to do is to create a configuration file that Flex can use to reach the PHP service. Create the file services-config.xml in the root of the project. Open the file and add this code:
<?xml version="1.0" encoding="UTF-8"?> <services-config> <services> <service id="amfphp-flashremoting-service" class="flex.messaging.services.RemotingService" messageTypes="flex.messaging.messages.RemotingMessage"> <destination id="zend"> <channels> <channel ref="my-zend"/> </channels> <properties> <source>*</source> </properties> </destination> </service> </services> <channels> <channel-definition id="my-zend" class="mx.messaging.channels.AMFChannel"> <endpoint uri="http://localhost/zendamf_remote/" class="flex.messaging.endpoints.AMFEndpoint"/> </channel-definition> </channels> </services-config>
Be sure to check the endpoint node (at the bottom of the file); your URL to the zendamf_remote folder might be different. Set the value appropriately for your setup.
Now you need to tell Flex Builder to use this file when compiling the project. Right click on the project name in the Project Explorer and choose Properties. Select Flex Compiler and add the following to Additional compiler arguments field: -services “absolute_path_to_the_file/services_config.xml”:
You will use a RemoteObject to communicate with the server, so add a mx:RemoteObject tag. You need to set the source attribute to MyService (this is the PHP class name) and the destination to zend – this is the destination created in the services-config.xml file. Also give a name to this object by adding an id attribute and set it to myRemote. Set the attribute showBusyCursor to true (whenever a call is made this will render the mouse icon as a watch, until a response from the server is received). The code should look like this:
<mx:RemoteObject id="myRemote" destination="zend" source="MyService" showBusyCursor="true"> </mx:RemoteObject>
Now you need to declare the methods you want to call on the PHP class, and add the listeners for fault and result events. The code is:
<mx:RemoteObject id="myRemote" destination="zend" source="MyService" showBusyCursor="true" fault="faultListener(event)"> <mx:method name="getData" result="getDataListener(event)"/> <mx:method name="saveData" result="saveDataListener(event)"/> </mx:RemoteObject>
Next you need a UI to make the call to the server and display/edit the data. A button and a data grid will do. Add this code above the RemoteObject code:
<mx:VBox top="30" left="100"> <mx:Button label="Get data" click="{myRemote.getData()}" /> <mx:DataGrid id="myGrid" editable="true" itemEditEnd="save(event)"/> </mx:VBox>
As you can see, the button calls the getData() method on the remoteObject. The data grid has an event listener registered for the itemEditEnd event.
The last step is to create the listeners you declared. For this, add an mx:Script tag to your MXML application and define four functions in it:
<mx:Script>
<![CDATA[
import mx.controls.dataGridClasses.DataGridColumn;
import mx.events.DataGridEvent;
import org.corlan.VOAuthor;
import mx.controls.Alert;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.collections.ArrayCollection;
/**
* listener for the data grid's itemEditEnd event
*/
privatefunction save(event:DataGridEvent):void {
//we don't want to update the id of the item
if (event.dataField == "id_aut") {
event.preventDefault();
return;
}
//retrieve the new value from the item editor instance
var dataGrid:DataGrid = event.target as DataGrid;
var col:DataGridColumn = dataGrid.columns[event.columnIndex];
var newValue:String = dataGrid.itemEditorInstance[col.editorDataField];
//retrieve the data model that was edited
var author:VOAuthor = event.itemRenderer.data as VOAuthor;
// if the value wasn't change, exit
if (newValue == author[event.dataField])
return;
//update the model with the new values
author[event.dataField] = newValue;
//call the remote method passing the data we want to be saved
myRemote.saveData(author);
}
/**
* Result listener for get data operation
*/
privatefunction getDataListener(event:ResultEvent):void {
//set the result array as data provider for the data grid
myGrid.dataProvider = event.result as Array;
}
/**
* Result listener for save data operation
*/
privatefunction saveDataListener(event:ResultEvent):void {
Alert.show("The data was saved!");
}
/**
* Fault listener for RemoteObject
*/
privatefunction faultListener(event:FaultEvent):void {
Alert.show(event.fault.message, "Error");
}
]]>
</mx:Script>
Finally, you need to create the ActionScript Value Object that will act as a data model for the data sent from PHP. Right-click on the src folder from Flex Navigator, and choose New > ActionScript class. For the package type org.corlan, and for the name type VOAuthor. Click OK. Now it is time to add the members and some meta-data:
package org.corlan {
[RemoteClass(alias="VOAuthor")]
[Bindable]
publicclass VOAuthor {
publicvar id_aut:int;
publicvar fname_aut:String;
publicvar lname_aut:String;
}
}
The RemoteClass meta-data is very important. This tells to the ActionScript that the remote class (the one from PHP) that it maps to is called VOAuthor. If you forget this or you misconfigure it, you will get generic objects in ActionScript instead of VOAuthor, and associative arrays in PHP instead of VOAuthor.
You are done. There shouldn’t be any errors.
Now you are ready to test the code. Start the Flex application by clicking Run in the toolbar. When the application opens in your default browser, click the Get data button. You should see the data grid populated with some data:
To edit the items, just double click on any name and change something. When you finish editing, click outside the data grid. The changes will be sent to the server. If you don’t believe me, just go to the database and view the records.
That’s it people!
Do you know live Flex/AIR applications with PHP backend?
I want to create and maintain a list of Flex and AIR applications that use PHP on the server, and are live so any one can see them. Thus, if you build such applications or you know some, please drop a comment on this post.
I really need your help people! Many thanks
Flex and PHP: remoting with AMFPHP
Since I recently received some requests for a simple example on getting up to speed with Flex, PHP, and AMF, I decided to write a tutorial on this topic. I will show you how to do remote procedure calls from Flex to PHP classes using AMFPHP. Soon I will post an article on how to do RPC using the Zend Framework and ZendAMF.
What is AMFPHP and why should you use remoting?
If you already know these answers, you may want to skip to the next section.
Let’s start by understanding of remote procedure calls. Remote procedure calls let Flex applications make direct calls on the methods of your server side classes. Using BlazeDS or LCDS you can expose your Java and ColdFusion classes to the Flex application. However, if you use PHP you need a third party library on the server to expose PHP classes directly. Existing solutions include ZendAMF, WebOrb, and AMFPHP. This article focuses on remoting with AMFPHP, which uses a binary protocol (AMF) to serialize the messages. Because it is binary, it is more efficient in terms of bandwidth and server processing load than JSON or XML methods. If you want to see for yourself, James Ward has put together a nice benchmark.
AMFPHP is a PHP open source library that knows how to serialize and deserialize the AMF protocol, and thus lets you expose PHP classes to Flex applications. Another compelling reason for using remoting is code reuse. Because you can call methods on PHP classes and these methods can return PHP objects, you don’t have to modify your existing code to output JSON or XML.
As I said earlier, AMFPHP remoting uses AMF for serializing messages between the server and Flex client. And it offers a nice feature to map an ActionScript class to a PHP class. For example, suppose you want to display in Flex the information from a table with the following structure:
contacts
-------------------------------
id primary key int
name varchar(255)
email varchar(255)
When using remoting, you create an ActionScript class to model this data in the client and a PHP class to model the same data on the server. When you create the PHP class that you want to call from Flex, you add a method that, for example, retrieves all the contacts from the table. This method will return an array of PHP model classes. This is what you need to do. And in Flex you will get an array of ActionScript objects. All the conversions from PHP to AMF format and from AMF format to ActionScript objects are done automatically for you by Flex and AMFPHP.
When you use XML or JSON for remoting, usually you need extra steps in Flex to process the data in order to display or store it.
Let’s look at a working example.
Step 1: Install the AMFPHP and understand its structure
While it is not hard to create a Flex application that makes RPCs using AMFPHP, I found some possible glitches when you do it for the first time. If you missed something, you will end up with errors such as:
- the array you retrieve in Flex is not of your type, but a generic object
- in PHP you don’t get a PHP VO class as an argument when you call a method, but an associative array
It is my intention to explain all the small things you need to take care of, so you can get it right.
First grab the AMFPHP library archive, unzip it somewhere on your machine and then copy the amfphp folder on your PHP webserver. From now on, I will refer to this folder as the installation folder. On my machine this folder is c:/htdocs/amfphp and the URL is http://localhost/amfphp.
If you open this folder you will notice a folder named “browser”. When you open this folder in your browser (on my machine http://localhost/amfphp/browser) you will get a Flex application that lets you test all the exposed PHP classes:
Beside this folder, there is “services” folder. This is very important. In the “services” folder you need to place all the PHP classes you want to expose to Flex code. Also, all the PHP Value Object classes you want to use for modeling the data must be inside the folder “services/vo/” + <the package name as folders> For example if you have the ActionScript class org.corlan.VOAuthor and you want to map to a PHP class with the same name, then the PHP class should be in “services/vo/org/corlan/VOAuthor.php”.
These are the default configurations for AMFPHP. If you don’t like them you can open the globals.php file from inside of the installation folder and make changes.
Step 2: Create the PHP code
Let’s create a small PHP class that does two things:
- Reads and returns all the records from a table
- Offers a method to update one record
I use MySQL, and the table creation SQL is as follows:
CREATE TABLE `authors_aut` ( `id_aut` int(11) NOT NULL auto_increment, `fname_aut` varchar(255) NOT NULL, `lname_aut` varchar(255) default NULL, PRIMARY KEY (`id_aut`), UNIQUE KEY `fname_aut` (`fname_aut`,`lname_aut`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=5 ; -- -- Dumping data for table `authors_aut` -- INSERT INTO `authors_aut` VALUES (1, 'Dantes', 'Alighierie'); INSERT INTO `authors_aut` VALUES (4, 'Niccolo', 'Machiavelli'); INSERT INTO `authors_aut` VALUES (3, 'Umberto', 'Eco'); INSERT INTO `authors_aut` VALUES (2, 'William', 'Shakespeare');
So I create a PHP file inside the amf/service/ folder called MyService.php. Inside of this file I create a PHP class with two methods: getData() and saveData(). The complete code is here:
<?php require_once ('./vo/org/corlan/VOAuthor.php'); //conection info define( "DATABASE_SERVER", "localhost"); define( "DATABASE_USERNAME", "mihai"); define( "DATABASE_PASSWORD", "mihai"); define( "DATABASE_NAME", "flex360"); class MyService { public function getData() { //connect to the database. $mysql = mysql_connect(DATABASE_SERVER, DATABASE_USERNAME, DATABASE_PASSWORD); mysql_select_db(DATABASE_NAME); //retrieve all rows $query = "SELECT id_aut, fname_aut, lname_aut FROM authors_aut ORDER BY fname_aut"; $result = mysql_query($query); $ret = array(); while ($row = mysql_fetch_object($result)) { $tmp = new VOAuthor(); $tmp->id_aut = $row->id_aut; $tmp->fname_aut = $row->fname_aut; $tmp->lname_aut = $row->lname_aut; $ret[] = $tmp; } mysql_free_result($result); return $ret; } public function saveData($author) { if ($author == NULL) return NULL; //connect to the database. $mysql = mysql_connect(DATABASE_SERVER, DATABASE_USERNAME, DATABASE_PASSWORD); mysql_select_db(DATABASE_NAME); //save changes $query = "UPDATE authors_aut SET fname_aut='".$author->fname_aut."', lname_aut='".$author->lname_aut."' WHERE id_aut=". $author->id_aut; $result = mysql_query($query); return NULL; } } ?>
The code is pretty simple. No abstract database layer, just the simplest PHP code to make something useful.
As you can see in the above code, I used the VOAuthor PHP class. So, now it’s time to create this class (this class models one row from the table). Basically this class will have one field for each row of the table (I will keep the name of the fields similar to the table fields) and an extra field that tells AMFPHP how to serialize the class when sending back the message. Let’s see the code and then I will explain a little more about this field:
<?php class VOAuthor { public $id_aut; public $fname_aut; public $lname_aut; // explicit actionscript class var $_explicitType = "org.corlan.VOAuthor"; } ?>
The extra field is $_explicitType, and its value is the fully qualified ActionScript Value Object I intend to use in the Flex application to model the data. If you don’t configure this field correctly, then in the Flex app you will not get your strongly typed ActionScript class, but a dynamic object.
Important! Make sure you do not add empty spaces or other chars after the PHP closing tag. If you leave extra chars, the output buffer will be flushed and the message that AMFPHP sends to the Flex client will not be correctly formatted.
If you go back to the browser service of AMFPHP, you can try the code — select the getData method and click the Call button. You should get an array of objects.
Step 3: Create the Flex project
We have all the PHP code in place, it is time to create the Flex application. First step is to create a Flex project using the PHP server type (you can read here an article I wrote on how to create Flex and PHP projects if you want to find more tips and tricks). This is the first page of the wizard:
Click “Next” and then “Finish”.
Step 4: Create the ActionScript code
Now let’s create the ActionScript value object class, VOAuthor. Right click on the “src” folder and choose New > ActionScript class (make sure you enter the package name):
The code for this class is:
package org.corlan {
[RemoteClass(alias="org.corlan.VOAuthor")]
[Bindable]
public class VOAuthor {
public var id_aut:int;
public var fname_aut:String;
public var lname_aut:String;
}
}
How does Flex know to serialize the ActionScript VOAuthor class to the PHP VOAuthor? Because of the tag RemoteClass. Here you enter the name of the PHP class you want to use and the path from the “amfphp/vo/” to the class as the package name. Thus I end up with “org.corlan.VOAuthor”.
It is time to put all these together and create the ActionScript code that makes the call to the PHP class and displays the info. For this, open the php_amf.mxlm file if it isn’t already open and add this code:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"> <mx:Script> <![CDATA[ import mx.controls.dataGridClasses.DataGridColumn; import mx.events.DataGridEvent; import org.corlan.VOAuthor; import mx.controls.Alert; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; import mx.collections.ArrayCollection; /** * This function is called when an item was edited in the data grid. * Calls the saveData() method on the PHP server */ private function save(event:DataGridEvent):void { var dataGrid:DataGrid = event.target as DataGrid; var dsColumnIndex:Number = event.columnIndex; var col:DataGridColumn = dataGrid.columns[dsColumnIndex]; var newValue:String = dataGrid.itemEditorInstance[col.editorDataField]; var dsFieldName:String = event.dataField; var author:VOAuthor = event.itemRenderer.data as VOAuthor; if (newValue == author[dsFieldName]) return; //get the new value for the first name or last name author[dsFieldName] = newValue; myRemote.saveData(author); } ]]> </mx:Script> <!-- this is the RemoteObject used to make the RPC calls --> <mx:RemoteObject id="myRemote" destination="MyService" source="MyService" endpoint="http://localhost/amfphp/gateway.php" showBusyCursor="true"/> <mx:VBox top="30" left="100"> <mx:Button label="Get data" click="{myRemote.getData()}" /> <mx:DataGrid id="myGrid" dataProvider="{myRemote.getData.lastResult}" editable="true" itemEditEnd="save(event)"> <mx:columns> <mx:DataGridColumn dataField="id_aut" editable="false"/> <mx:DataGridColumn dataField="fname_aut"/> <mx:DataGridColumn dataField="lname_aut"/> </mx:columns> </mx:DataGrid> </mx:VBox> </mx:Application>
For remoting, Flex uses RemoteObject. As you can see in my code, I create one instance of this object. Then I configure the endpoint to work with AMFPHP and the PHP class (MyService). For this I add the URL to the gateway.php file as the value of the attribute endpoint, and I set MyService as the value of the destination and source attributes.
The UI of the application is very simple: a button to call the getData() method from the server and a data grid for displaying and editing the data. The binding between the data retrieved from the server and the data grid is done directly on the data grid using the property lastResult: dataProvider=”{myRemote.getData.lastResult}”.
There is an event listener registered on the data grid for the event of ending the editing of a cell. Inside this event listener, I call the saveData() method using the instance of the currently edited Value Object.
Final words
That’s it folks! If you are too lazy to set up the project and copy and the code, you can download the project from here. Read the readme.txt after you import the project in Flex Builder using the Import wizard > Flex Builder. I will post another article on ZendAMF soon. See you!
Flex, AMF, PHP and Zend Framework
If you missed the last week announcements coming from Adobe and Zend, then you might want to read this post.
Basically, the two companies announced a join effort to make easier the developing of PHP applications with Flex frontends.The two companies will deliver technologies, content and services to make it easy for enterprise developers to build rich Internet applications (RIAs) using Flex on the client and PHP on the server.
Part of this joined effort, are the support for AMF in the Zend Framework, a better experience for developers when working with Flex Builder and Zend Studio (both these IDEs are build on top of Eclipse).
My recent article from Adobe Developer Connection, Working with Flex and PHP in Eclipse, shows the things and some workarounds you can do today. I expect a much better experience in the near future and I plan to write back and to record some videos on this subject. Here is a link to the original announcement.
Kind of a Data Service implementation for PHP using AMFPHP and RemoteObject
Last week I played a little bit with AMFPHP and Flex and I put together this code. Basically I’ve built a Flex service that mimics (a little bit) the data services from LiveCycle Data Services. From the first time I worked with Data Services, I really liked the simple API you can use to retrieve data from the server and to send it back. In short, the workflow in the client goes like this:
- Create an instance of Data Service, and set the destination for i.
- Call the method fill() on this instance, and provide an ArrayCollection to be filled with what the server sends back to the client.
- Any changes you might make on this ArrayCollection (for example you might add or delete an item) are automatically sent to the server by the Data Service instance if you set the auto-commit property to true. Or, if you set auto-commit to false you can just call the commit() method and you are done.
- If you want to revert all the changes that were not committed yet, just call revertChanges().
Of course, this is not the complete story behind data services. They can do a lot more:
- synchronization (automatically push the changes that one client makes to all the other clients)
- lazy loading
- paging (if the collection is really big, you can retrieve just a few rows and as the UI of your application needs more, the data are retrieved automatically for you)
- conflict resolution (if someone else modified the same piece of information as you, you can choose what to do: overwrite their change or accept it)
- handle complex domain objects (hierarchies and different kind of associations).
And finally, if you want to create items using atomic operations, you can use: createItem(item), deleteItem(item).
My code doesn’t try to replicate all these features (that would be be a huge task). Instead, I’ve just tried to leverage the RemoteObject and AMFPHP, so as a Flex developer I have a smoother way to manage a collection of data. And because I used RemoteObject, I can use Value-Objects as a data model for an item and these objects are translated automatically for me by the framework (so, on the server I get arrays of VOs of PHP classes, and on the client I get VOs of ActionScript classes). Click here for a taste of what I hacked together (I made some restrictions so you cannot change the existent four records, but you can mess around with new ones). You can view the source code, just right click and choose view source.
For me, the beauty is that most of the time I can concentrate on my data model (add/delete/change items) and when I’m done, just call the commit() method on the PHPDataService, as opposed to using a HTTPService, which would require multiple calls on the service object, in addition to work on the data model.
Flex code
Below is a snippet of code that shows how to use my service (PHPDataService):
<mx:Script> <![CDATA[ [Bindable] private var authorsCollection:ArrayCollection = new ArrayCollection(); private function getData():void { myService.fill(authorsCollection); } private function save():void { myService.commit(); } private function addNewItem():void { authorsCollection.addItem(new YourValueObject()); myService.commit(); } private function deleteItem():void { authorsCollection.removeItemAt(1); myService.commit(); } private function faultListener(event:FaultEvent):void { Alert.show(event.fault.message, event.fault.name);} ]]> </mx:Script> <mx:Button label="Revert" click="myService.revertChanges()"/> <PHPDataService id="myService" destination="Author" source="Author" endpoint="http://localhost/amfphp/gateway.php" fault="faultListener(event)"/>
When you create the service you need to set the name of the remote class for destination and source properties, and the set the endpoint value as the URL to the amfphp/gateway.php file. Once you set these values, you are ready to call the methods on the object:
- fill(arrayCollectionToBeFilled) - retrieves all the records from the server
- commit() - send all the changes to the server (if there were deletes/updates/inserts all these are committed by this method)
- revertChanges() - all uncommitted changes are reverted to the original values
- getItem(filterCondition) - return one item
If you want to add a new item, you create a new item and you add it to the ArrayCollection that is managed by the service. The same goes when you want to delete something.
As I said before, by using AMFPHP I am able to pass around the data serialized using a VO. So this is the code in Flex for the VO:
package org.corlan {
[RemoteClass(alias="org.corlan.VOAuthor")]
[Bindable]
public class VOAuthor {
public var id_aut:int;
public var fname_aut:String;
public var lname_aut:String;
}
}
In case you’re wondering how i implemented the PHPDataService, here is the code:
package org.corlan {
import flash.events.EventDispatcher;
import mx.collections.ArrayCollection;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.PropertyChangeEvent;
import mx.rpc.AsyncToken;
import mx.rpc.Responder;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.remoting.mxml.RemoteObject;
import mx.utils.ObjectUtil;
[Event(name="result", type="mx.rpc.events.ResultEvent")]
[Event(name="getItem", type="org.corlan.ItemEvent")]
[Event(name="fault", type="mx.rpc.events.FaultEvent")]
/**
* This service wraps an RemoteObject to connect to the server.
* Although it is created and tested with AMFPHP, it should work with no or minimum
* changes with other server side technologies.
*
* Imlements simplistic behaviour of Flex Data Services, in that this service can manage an
* ArrayCollection items and save/read/delete items.
*
* If you configured the VO like you would do for any remote object, then you will get an
* array collection of VOs.
*
* You set the collection you want to be managed by calling the <code>fill()</code> method and
* you save the changes by calling the <code>commit</code> method. When the fill method returns the result,
* an ResultEvent is dispatched.
*
* You can retrieve one item by calling the <code>getItem</code> and sending the value you want
* to be used as a filter.
*
* No changes are committed until you call commit() method on the service.
* When you call commit method, all the deleted items are committed, then the updated items
* and finally the inserts (in that order). The calls are queued.
*
* You can call <code>revertChanges()</code> and all the uncommitted changes are reverted (delete/create/update).
* So this call don't change anything on the server.
*
* You need to register a listener for fault if you want to get error messages from
* the server.
*
* On the server side, this component expects to find this methods implemented:
* - fill() - returns all the records from the table
* - getItem(condition) - returns one item or none
* - saveItem(item) - saves one item (either insert or update)
* - saveCollection(collection) - saves an array of items (insert or update)
* - deleteItem(item) - delete an item
* - deleteCollection(collection) - deletes a collection of items
*/
public class PHPDataService extends EventDispatcher {
private var _collection:ArrayCollection;
private var _revert:ArrayCollection = new ArrayCollection();
private var _isChanged:Boolean = false;
private var _isWriting:Boolean = false;
private var _endpoint:String;
private var _source:String;
private var _destination:String;
private var _remote:RemoteObject;
private var _insert:ArrayCollection = new ArrayCollection();
private var _update:ArrayCollection = new ArrayCollection();
private var _delete:ArrayCollection = new ArrayCollection();
public function fill(collection:ArrayCollection = null):void {
if (collection != null) {
_collection = collection;
_collection.addEventListener(CollectionEvent.COLLECTION_CHANGE, onChangeCollection);
}
initializeRemote();
var token:AsyncToken = _remote.fill();
var responder:Responder = new Responder(onFill, onFault);
token.addResponder(responder);
}
public function getItem(id:Object):void {
initializeRemote();
var token:AsyncToken = _remote.getItem(id);
var responder:Responder = new Responder(onGetItemResult, onFault);
token.addResponder(responder);
}
public function commit():void {
if (!_isChanged)
return;
initializeRemote();
var responder:Responder;
var token:AsyncToken;
if (_delete.length > 0) {
token = _remote.deleteCollection(_delete);
responder = new Responder(onDelete, onFault);
token.addResponder(responder);
} else if (_update.length > 0) {
token = _remote.saveCollection(_update);
responder = new Responder(onUpdate, onFault);
token.addResponder(responder);
} else if (_insert.length > 0) {
token = _remote.saveCollection(_insert);
responder = new Responder(onInsert, onFault);
token.addResponder(responder);
}
}
public function revertChanges():void {
if (!_isChanged)
return;
if (_collection.length > 0)
_collection.removeAll();
for (var i:int = 0; i <_revert.length; i++) {
_collection.addItem(_revert.getItemAt(i));
}
}
private function initializeRemote():void {
if (_remote != null)
return;
_remote = new RemoteObject(destination);
_remote.source = source;
_remote.endpoint = endpoint;
_remote.showBusyCursor = true;
}
private function onDelete(event:ResultEvent):void {
var responder:Responder;
var token:AsyncToken;
_delete.removeAll();
if (_update.length > 0) {
token = _remote.saveCollection(_update);
responder = new Responder(onUpdate, onFault);
token.addResponder(responder);
} else if (_insert.length > 0) {
token = _remote.saveCollection(_insert);
responder = new Responder(onInsert, onFault);
token.addResponder(responder);
} else {
_isChanged = false;
fill();
}
}
private function onUpdate(event:ResultEvent):void {
_update.removeAll();
if (_insert.length > 0) {
var responder:Responder;
var token:AsyncToken = _remote.saveCollection(_insert);
responder = new Responder(onInsert, onFault);
token.addResponder(responder);
} else {
_isChanged = false;
fill();
}
}
private function onInsert(event:ResultEvent):void {
_insert.removeAll();
_isChanged = false;
fill();
}
private function onFill(event:ResultEvent):void {
var arr:Array = event.result as Array;
_isWriting = true;
if (_collection.length > 0)
_collection.removeAll();
if (_revert.length > 0)
_revert.removeAll();
for (var i:int; i<arr.length; i++) {
_collection.addItem(arr[i]);
_revert.addItem(ObjectUtil.copy(arr[i]));
}
_isWriting = false;
dispatchEvent(event);
}
private function onGetItemResult(event:ResultEvent):void {
var ev:ItemEvent = new ItemEvent(ItemEvent.GET_ITEM, event.bubbles, event.cancelable, event.result, event.token, event.message);
dispatchEvent(ev);
}
private function onFault(event:FaultEvent):void {
trace(event.fault.faultString);
dispatchEvent(event);
}
private function onChangeCollection(event:CollectionEvent):void {
if (_isWriting)
return;
trace(event.kind);
_isChanged = true;
switch (event.kind) {
case CollectionEventKind.ADD:
addFor(_insert, event.items, CollectionEventKind.ADD);
break;
case CollectionEventKind.UPDATE:
addFor(_update, event.items, CollectionEventKind.UPDATE);
break;
case CollectionEventKind.REMOVE:
addFor(_delete, event.items, CollectionEventKind.REMOVE);
break;
}
}
private function addFor(collection:ArrayCollection, items:Array, eventType:String):void {
for (var i:int; i<items.length; i++) {
var obj:Object;
if (items[i] is PropertyChangeEvent)
obj = (items[i] as PropertyChangeEvent).source;
else
obj = items[i];
if (collection.contains(obj))
return;
switch (eventType) {
case CollectionEventKind.REMOVE:
//remove the item from update or insert
if (_insert.contains(obj)) {
_insert.removeItemAt(
_insert.getItemIndex(obj));
} else if (_update.contains(obj)) {
_update.removeItemAt(
_update.getItemIndex(obj));
}
collection.addItem(obj);
break;
case CollectionEventKind.UPDATE:
//if the item is already on insert, don' add
if (!_insert.contains(obj))
collection.addItem(obj);
break;
case CollectionEventKind.ADD:
collection.addItem(obj);
break;
}
}
}
public function set endpoint(value:String):void {
_endpoint = value;
}
public function get endpoint():String {
return _endpoint;
}
public function set source(value:String):void {
_source = value;
}
public function get source():String {
return _source;
}
public function set destination(value:String):void {
_destination = value;
}
public function get destination():String {
return _destination;
}
}
}
PHP code
The Flex code uses a RemoteObject that expects to find these methods defined on the server object:
- fill() - should return an array of VOs
- getItem($condition) - returns one item
- saveItem($item) - saves (update/insert) one item
- saveCollection($array) - saves (update/insert) all the items from the given array
- deleteItem($item) - deletes the given item
- deleteCollection($array) - deletes all the items from the given array
To formalize that contract between the client and the server, I’ve created an abstract PHP class. And all the PHP classes I want to use with the Flex PHPDataService need to extend this class. The code for the abstract class is:
//connection info define( "DATABASE_SERVER", "localhost"); define( "DATABASE_USERNAME", "mihai"); define( "DATABASE_PASSWORD", "mihai"); define( "DATABASE_NAME", "flex360"); abstract class BasePHPService { protected $_connection; private $_errorMessage; public function BasePHPService() { $this->_connection = mysql_connect(DATABASE_SERVER, DATABASE_USERNAME, DATABASE_PASSWORD); mysql_select_db(DATABASE_NAME); } protected function isError() { $message = mysql_error($this->_connection); if ($message != '') { $this->_errorMessage = mysql_errno($this->_connection) . ': ' . $message; return true; } else { $this->_errorMessage = ''; return false; } } protected function getError() { return $this->_errorMessage; } /** * Escape the given value for SQL * * @param string $value * @return escaped value */ protected function escapeForSql($value) { if (is_null($value) || $value === '') { $tmValue = '\'' . $value . '\''; } else { $tmValue = '\'' . str_replace(array("'", "\\"), array("''", "\\\\"), $value) . '\''; } return $tmValue; } abstract function fill(); abstract function getItem($id); abstract function saveItem($item); abstract function saveCollection($collection); abstract function deleteItem($item); abstract function deleteCollection($collection); }
On the PHP side I have this VO defined in amfphp/services/vo/org/corlan/VOAuthor.php. As long as you put the VO under the amfphp/vo/ and then add the rest of the folders from the package name of the ActionScript VO class (thus the org/corlan/ folders), the AMFPHP and Flex RemoteObject can automatically de-serialize the message to the right VO type.
<?php class VOAuthor { public $id_aut; public $fname_aut; public $lname_aut; // explicit actionscript class var $_explicitType = "org.corlan.VOAuthor"; } ?>
If you want to throw an error from the server side that will be caught by the fault listener of the PHPDataService, you just use: throw new Exception(”error message you want to be consumed by the Flex client”);
For example the table might be setup not to accept empty values for a field. If the client sends such values, you can use this feature to send an error back to the client.
Here is the implementation for the Author PHP class that manages a simple table:
require_once ('../BasePHPService.php'); require_once ('./vo/org/corlan/VOAuthor.php'); /** * This class manages the table author_aut. * It is intended to be used as RemoteObject with AMFPHP. * To model the one row from the table, uses VOAuthor class. * * When you want to throw an error up to the Flex calling code * (where you can catch with the listener registered for fault) * you need to use: * <code>throw new Error("error message to be displayed in Flex application");</code> */ class Author extends BasePHPService { public function Author() { parent::BasePHPService(); } public function fill() { //retrieve all rows $query = 'SELECT id_aut, fname_aut, lname_aut FROM authors_aut ORDER BY fname_aut'; $result = mysql_query($query, $this->_connection); if ($this->isError()) throw new Exception($this->getError()); $ret = array(); while ($row = mysql_fetch_object($result)) { $tmp = new VOAuthor(); $tmp->id_aut = $row->id_aut; $tmp->fname_aut = $row->fname_aut; $tmp->lname_aut = $row->lname_aut; $ret[] = $tmp; } mysql_free_result($result); return $ret; } public function getItem($id) { $query = 'SELECT id_aut, fname_aut, lname_aut FROM authors_aut WHERE id_aut = '. $this->escapeForSql($id); $result = mysql_query($query, $this->_connection); if ($this->isError()) throw new Exception($this->getError()); $ret = array(); $row = mysql_fetch_object($result); $item = new VOAuthor(); $item->id_aut = $row->id_aut; $item->fname_aut = $row->fname_aut; $item->lname_aut = $row->lname_aut; mysql_free_result($result); return $item; } public function saveItem($author) { if ($author == NULL) return NULL; if ($author->id_aut > 0) { $query = 'UPDATE authors_aut SET fname_aut='.$this->escapeForSql($author->fname_aut).', lname_aut='.$this->escapeForSql($author->lname_aut).' WHERE id_aut='. $this->escapeForSql($author->id_aut); } else { $query = 'INSERT INTO authors_aut (fname_aut, lname_aut) VALUES ('.$this->escapeForSql($author->fname_aut).', '.$this->escapeForSql($author->lname_aut).')'; } $result = mysql_query($query, $this->_connection); if ($this->isError()) throw new Exception($this->getError()); return NULL; } public function saveCollection($collection) { for ($i=0; $i<count($collection); $i++) { $this->saveItem($collection[$i]); } } public function deleteItem($author) { if ($author == NULL || !($author->id_aut > 0)) return NULL; $query = 'DELETE FROM authors_aut WHERE id_aut = '. $this->escapeForSql($author->id_aut); mysql_query($query, $this->_connection); if ($this->isError()) throw new Exception($this->getError()); } public function deleteCollection($collection) { for ($i=0; $i<count($collection); $i++) { $this->deleteItem($collection[$i]); } } }
Final words
I wrote the class PHPDataServices with PHP and AMFPHP in mind, and that’s how I’ve tested the code. But with minimal changes it should work with ColdFusion, Java or other server side technologies that offer RemoteObject (Python, Ruby). You can download the project from here - you will find a readme.txt file inside of the archive explaining how to set up the project.
Paging a Data Grid on the client
My wife says that lately I have become obsessed, with data grids. Well, who am I kidding? While my wife is a psychologist and she does care about obsessions, she doesn’t care about Flex components. Anyway, the first time I saw a Flex data grid filled with items I asked myself: “What if I need to display more than 20 rows of data at the same time?” What visual hints, what UI should I use, to give the viewer a sense of what items he is looking at, how many there are left to see, and so on. Let’s take a look at it; here’s a data grid filled with a lot of data:
When I was a web developer with InterAKT Online, I worked on a product called NeXTensio list. This product was an extension for Dreamweaver, and it was used for generating lists from a table or from a query, and it provided navigation buttons and other visual items to help you view and edit the list items.
So, it is no wonder that, as soon I had some time to play, I started to build a Flex component that adds some interesting behavior to a data grid. You can see here the whole thing put it together, and you can download the project from here (in the root of the ZIP file there is a readme.txt file that explains how to setup the project). About my application:
- using a HTTPService, it reads all the data from the database; my example uses a MySQL table with 5.000 rows
- it displays the data using a data grid
- users navigate through the data grid using my custom component; the component adds buttons for navigating from page to page (where a page is the number of items you can view at the same time in the data grid), or you can directly navigate to the last/first page
- my custom component display some statistics on top of the navigation buttons (so you know what dataset are you looking at, and how many items there are)
- you can use the data grid toolbar, and in this case as you move around, the buttons and statistics are updated (just use the mouse or the keyboard to scroll the data grid, and you will see the buttons changing)
Here is a screen shot of the application:
How did I do it? Well, it wasn’t hard, but neither was it easy. The whole paging component is a custom Flex component (see the Paging.mxml file). All it needs is a reference to the data grid to which you want to add paging. You can configure how many buttons to render at the same time for moving through pages (I used 5 in my example). The trickiest part was to find how many items are displayed at one time in the data grid. When a data grid is initialized, the property rowCount from the data grid is 7, regardless of how many items are in the data grid. So, I need to have a listener registered on the data grid that it is triggered when rowCount changes. Because I couldn’t find an event for this, I ended up extending the data grid (see the MyDataGrid.mxml file) and I over-wrote the method setRowCount, to dispatch an event when rowCount changes.
For the rest of the story, have a look at the code and play with it. But please, don’t forget it isn’t production quality code, it’s just a proof of concept. Have fun!
Flex / AIR, PHP and user authentication
So you’ve built a Flex or AIR application that talks to a PHP server and you’re wondering how to authorize users? Actually, it is very easy to do this. You can do the same things you would do with a PHP web application: authenticate the user credentials, start a session and write some session values. Then, for any subsequent calls, first you check if the session values are set to determine if the user is logged in. This approach works in the same way for a Flex application deployed on the browser or deployed on the desktop (an AIR application), because once the server starts a session and sends the response back to the client, Flash player adds the session ID to any other call made to the same server (of course the calls must use HTTP or HTTPS).
So let’s recap what happens:
- The Flex client (could be a web app or an AIR app) makes a request to a PHP file to authenticate the user: http://localhost/login.php
- The “login.php” validates the user and if successful it starts a session and saves to the session some variables regarding the authenticated user
- The server sends a response back to the Flex client
- The Flex client makes new HTTP/HTTPS requests to the same server. The session id is appended to the request
- The PHP server scripts called from the client use the same session and they can retrieve from the session any info that was saved by “login.php“
Another way to handle the authentication is by using a token. The workflow is:
- The Flex client sends user credentials to the server
- If the user credentials are correct, the server sends back a token that is unique for the given user
- From that point on, whenever the Flex client makes a request to the server, it sends the token too.
Why should you use a token instead of relying on the session mechanism? I don’t think one method is always better than the other, it really depends on your architecture and your specific needs. For example if you use the token approach, you have two advantages: you don’t care about session expiration time and you don’t care about the server domain (for example you could have more domains sharing the same user database and using the token approach you don’t need to authenticate on each domain in order to start a session). On the other hand, using the session approach could be better if you can leverage existenting applications on the server side, or you don’t want the token appended to each request you make from Flex application.
I put together a simple Flex application to illustrate how you can authenticate from Flex on a PHP server. You can download the app from here and you can import the archive in Flex Builder using Import > Flex Builder > Flex project. Inside of the project is a folder “user_auth“. You should move this folder to your PHP web server and then you need to change, in the MXML file, the value of SERVER_URL constant. It should be the correct URL for your setup (it is defined right at top of the MXML file). On my computer, the constant has this value http://localhost/user_auth/. As a side note, this code works as well as an AIR application, just create an AIR project and copy the script code and the services/UI components into the MXML file and you are done.
The calls to the PHP server are made using either HTTPService or RemoteObject (I am using AMFPHP on the server side). If you run the application, you will see three areas with buttons: one for authentication using session and HTTPService, one for token authentication using HTTPService, and the last one for using RemoteObject and session. One way to test it is: open the the application in Firefox and click on the first button, a session will be started on the server and some values will be set in the session. If you open the same application in Safari (or any browser other than the one you just used) and click on the second button (Make Request to another page which…) you will get an error as the PHP script checks that some specific values are set in the session.
If you want to try the “Authenticate against LDAP” workflow, first you need to be able to access a LDAP server from the PHP server and you need to open the “ldap_auth.php” PHP file and set the correct values for RDN, DN, and hostname.
Flex / AIR projects and source control
Sooner or later every Flex developer will have to either work on the same project with others or send their project to someone (for example as an email attachment or via an FTP server). This task can look deceptively simple, but in fact there are some challenges. For example, let’s say we have a Flex PHP project and when we created the project we entered this information:
Holland Flex User Groups meeting
This Friday (June 13th) I will do a presentation about Flex Builder tips and tricks and some Flex - PHP examples. I can’t wait for it. I have only one problem: the meeting place is about 100 km from Amsterdam in a small town. I hope I will manage to get there in time. So if you don’t see my blog post about it this weekend then I got lost over there ![]()