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:

  1. Create an instance of Data Service, and set the destination for i.
  2. Call the method fill() on this instance, and provide an ArrayCollection to be filled with what the server sends back to the client.
  3. 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.
  4. 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.

5 thoughts on “Kind of a Data Service implementation for PHP using AMFPHP and RemoteObject

  1. Pingback: Flex, PHP and Dataservices | The Flashchemist

  2. Pingback: Munich Flex and PHP user group meeting : Mihai CORLAN

  3. Hi Corlan,i want to ask about returning php array to arraycollection..I’m using flex 3 with wamp..What should i do and where should i start from?Thanks…

  4. @handoyo

    I have some tutorials regarding remoting for PHP, you can read them…

    you can transform an array into an arraycollection very easily:
    var arrCollection:ArrayCollection = new ArrayCollection(arrayReturnedFromPHP);

  5. Great tutorial,

    im developing a project where i need to connect my interface to multiple sources, to avoid login on all of the this tutorial will allow me to use the same connection, and trigger the right results.

    Keep on this grear Tutorial Mihai!

    Best Regards,
    João Cunha

Leave a Reply

Your email address will not be published. Required fields are marked *