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.
"Live Help / Call Center" application packaged using Flex Java WTP project and Export Flex Project Wizard
I am a little bit obsessed lately with the need to share a Flex project as easy as possible and to import a project with minimum work on my part. For example, I grabbed the LiveHelp / Call Center application made by Christophe Coenraets and I made it a single Flex with Java combined project with support for BlazeDS. What does this mean? It means if you have installed Flex Builder 3, WTP, and Apache Tomcat 5.5 you should be able to download this zip file, and then import the project using the Import > Flex > Import Flex project Wizard. You will have the BlazeDS server, Java code and Flex code in this project. After this, you’ll need to add this application to the Tomcat server. You can start the server from within Eclipse and run the mortgageapp.xml and callcenter.mxml and it should work like a charm.
However, because WTP links the name of the local JRE used in its configuration files, it is possible that the project will not work on your machine. The solution is very simple: just open the properties for the project and go to “Java Build Path”, then select the “Libraries” tab. Here you need to delete the “JRE System Library” entry and then click “Add Library” button. In the new window select the “JRE System Library” list item and click “Next”. On the next page select JRE 1.5 or newer and you are done.
LiveCycle Data Services ES Express and LiveCycle Data Services ES Single-CPU License
This spring at Flex360 in Milan, I presented a session about working with data in Flex Builder 3, and I was asked a lot about LCDS versus BlazeDS. When I asked questions about the use case, I found that most of the time the only reason to use BlazeDS instead of LCDS, was that BlazeDS is Open Source and thus free. Months later, I had a deja vu experience in Holland. It seems that very few people know that there used to be LiveCycle Data Services ES Express (for LCDS 2.5) and there is a LiveCycle Data Services ES Single-CPU License (for LCDS 2.51). This license, basically lets you use the full version of LCDS in production with this limitation: you have to deploy the LCDS on a machine with a single CPU (a multi-core CPU is counted as a single CPU) and a single application.
So, if you need the LCDS features and you can work within the license terms, then go and enjoy LCDS for free. I can think of a lot of applications built for small and medium intranets (CMS, mini ERPs and so on) that probably can be run successfully on a machine with a single CPU. And if the application grows over time, probably your client grew as well, and now they can afford to buy a license for multiple CPUs (they had the chance to test the technology and see its value gives and what problems it solves).
You can find more about the license here (click on “What happened to LiveCycle Data Services ES Express” for example) and this is a great post that explains the LCDS and BlazeDS products and licenses.
SQLAssembler: create a Flex and Java application with LCDS (LiveCycle Data Services) without server side code
LCDS 2.5 brought a new assembler, SQLAssembler. SQLAssembler lets you connect your Flex client to your database. Usually, when you read/write data with LCDS, you create your own Java adapter to handle these operations. With SQLAssembler, instead of writing the Java code for accessing the database, you configure access to the database and write the SQL for read/write/delete directly into the data-management-config.xml file (this is a configuration file used by Flex data services). Basically, you write some simple XML with some SQL for each operation, and you don’t need to write a single line of Java. But, you get a full CRUD application, with all the benefits of Flex Data Services: collaboration, conflict resolution, paging. If you need for some operations to use stored procedure instead of SQL statements, there is no problem as SQLAssemberl has support for them.
Why should you use SQLAssembler?
I will not try to fool you. Probably it isn’t good for complex applications, where you have a lot of business logic on your server. But, it could come in very handy when you need to create a simple application, maybe a quick prototype for example. And the best thing is that if, for some reason, you decide that you need a custom assembler or HibernateAssembler instead of SQLAssembler, there should be little to change in the Flex client code, if anything at all. And, as you will see in this article, I can use this assembler to create a full CRUD application for one-to-many relationship database setup.
Prerequisites
In order to be able to follow this article and to try the attached project, you need to have: Flex Builder 3, WTP (Web Tool Project) installed in Flex Builder, and LCDS 2.5 or newer. If you don’t have Flex Builder, you can download a trial from here; if you don’t have WTP installed, then check my article here; finally if you don’t have LCDS, you can download it from here. I use a combined Flex and Java project with LCDS. I have explained this kind of setup here. Finally you need to have Tomcat 5.5 installed (I created my project using Tomcat as a server for WTP deployment).
You can download the project from here (it is a pretty big download, so be patient) and you can import it in Flex Builder, using the Import Flex Builder wizard.
Finally, in the root of the project, there is a database.sql file you can use it to create the database. In my project, I used MySQL as a database server.
Database structure
The database structure is very simple: two tables, one for storing the authors and one for storing the books. One author can have as many books as needed:
Application description
I want to create a Flex application that lets me edit these two tables (full CRUD). I want to be able to choose an author, and see what books he wrote. This is the layout of the application (I know it isn’t a beautiful one, but that’s not the point of this article
).
Adding a Flex Nature to an existent Java (WTP) project
Sometimes you already have a J2EE project (made with WTP) and you want to be able to make it also a Flex project. Probably you don’t like the idea of creating a new Java/Flex project and copy the files. Neither do I
(If you want to create a new Java/Flex project then check my previous posts about creating a Java/Flex project and debugging this project).
Fortunately the solution is there in Flex Builder. You need to select the project in project explorer and right click to bring up the contextual menu for the project. Then you should select Flex Project Nature > Add Flex Project Nature:
Selecting this command will open a wizard that looks like the one for creating a new Flex project (the differences are that some options are disabled as the project already exists and you are just adding new things to it). So here is the first page and you can see you have the option to choose if you want to use LiveCycle Data Services / BlazeDS or not. On the second page you can enter the path for Flex WAR file (if you choose remote object access on first page). Click “Finish” and you got yourself a Java/Flex combined project.
![]() |
![]() |
Final words
I will not lie to you, these two features (combined Java/Flex project and Add Flex Nature) are very dear to me as I was the engineer who worked on them for Flex Builder 3
But besides the sweet memories and countless meetings to shape up these wizards and endless fights with my quality engineer on what should happen, I had another reason to write my first technical post on this subject: I talked in the last couple of months with Java developers who work with Flex and they didn’t know about these features yet
So, I am really curious if you guys find this helpful or not and if there are other things you still need (I have friends inside Flex Builder team so we can push our wishes
).
Debugging a combined Java/Flex project
In my previous post I described how you can install WTP in Flex Builder and how you can create a combined Java/Flex project and run it.
But what if you want to debug both the Flex and Java code from the project at the same time?
Actually, it is pretty simple. The steps are very similar with the ones described in the previous post for running the application.
Read more
Creating a combined Flex/Java project in Flex Builder w/o LCDS/BlazeDS
Whenever I work on a Flex project with Java backend I create a project in Flex Builder that combines Java and Flex. I find that this is the easiest way to work even if I don’t touch at all Java code and I just mess around with Flex/AS files. This is one piece of news brought by Flex Builder 3.
What advantages do you have using this setup? You can edit/run/debug both Java and Flex files in the same editor and same project; you can see the changes as soon as you save the modified files.
Let’s see what you need and how you can create a combined Flex/Java project with or without LiveCycle Data Services or BlazeDS.


