AMFPHP, PHP 5.3 and namespaces

I spent a very frustrating day discovering that AMFPHP (trunk) doesn’t support class mapping to PHP classes that exist in the brand new PHP 5.3 namespaces.  So, if you happen to have the following class mapping:

   1: namespace vo;

   2:  

   3: class User {

   4:     $_explicitType = "vo.User";

   5: }

   1: package vo;

   2:  

   3: [RemoteClass(alias="vo.User")]

   4: class User {

   5:  

   6: }

… AMFPHP won’t pick up the fact that these are supposed to be the same class and will create an associative array when you send User as a parameter to a service.  It works fine in the opposite direction (i.e. returning User back to AS3 is ok).

I tried out ZendAMF and WebORB for PHP, but eventually decided that whilst they definitely have their merits, these frameworks are way too heavyweight for my application; ZendAMF requires the whole Zend Framework stack in order to work and WebORB is 22MB!  Instead I set about patching AMFPHP to support namespaces.  And here it is!

In amfphp/core/amf/io/AMFBaseSerializer.php change line 392 to:

   1: $classname = (strstr($typeIdentifier, "/")) ? str_replace("/", "", $typeIdentifier) : substr($mappedClass, $lastPlace);

Then if you want to class map to a PHP class in a namespace instead of using . notation, put forward slashes (/).  In fact PHP in its infinite wisdom uses backslashes () to delimit namespaces but as these denote escape characters terrible things happen when we start serializing strings and rather than have to escape the backslash all the time I just plumped for the forward slash.  Therefore our example, which in PHP namespace notation refers to the class voUser would change to:

   1: namespace vo;

   2:  

   3: class User {

   4:     $_explicitType = "/vo/User";

   5: }

… and our AS3 class would become:

   1: package vo;

   2:  

   3: [RemoteClass(alias="/vo/User")]

   4: class User {

   5:  

   6: }

Tada!  AS3 to PHP class mapping working as expected again!

Writing private attributes to a database using AMF in AS3 (IExternalizable)

Yesterday may have been one of the most frustrating days of my entire career. The brief – simple; take a bunch of classes acting as models (with a single top-level model containing all others in a tree hierarchy) and write their contents to a database. Client-side technology is Flash CS3 with Actionscript 3, server-side technologies are PHP 5 and MySQL. After having a look at the problem I decided that the quickest and most elegant way of achieving this would be to use Flash Remoting with AMFPHP on the server and to use custom class mapping to effectively pass the top level model and all its children to the server so they arrive with the same class structure as they left.

The code to make a Flash Remoting call is pretty standard stuff (note that this assumes the existence of onResult and onFault handlers):

   1: var myService:NetConnection = new NetConnection();
   2: myService.objectEncoding = ObjectEncoding.AMF0;
   3: myService.connect("http://localhost/amfphp/gateway.php");
   4:
   5: var responder = new Responder(onResult, onFault);
   6: myService.call("MyService.write_data", responder, myDataModel);

Once I’d implemented this along with a simple AMFPHP service containing the write_data function I was surprised to find that no data was getting passed to the server. After a lot of fiddling around I discovered the reason why:

By default AMF only encodes public attributes.

This is pretty annoying since no programmer worth their salt is going to be making all the attributes of their model public. That’s fine, thought I, I’ll implement some public getters:

   1: class MyDataModel {
   2:     private var myInt:uint;
   3:     private var myString:String;
   4:
   5:     public function get dbMyInt():uint {
   6:         return myInt;
   7:     }
   8:
   9:     public function get dbMyString():String {
  10:         return myString;
  11:     }
  12: }

Beautiful. Except for one thing.

By default AMF only encodes real attributes.

So this doesn’t work at all – the getters are completely ignored and still no data is passed to the server.

The ‘official’ solution

Finally I hit upon the real solution – use the IExternalizable interface to override the default behaviour of the AMF encoder and select what you want to encode/decode. Note that in order to use this you need to set the object encoding to AMF3 with myService.objectEncoding = ObjectEncoding.AMF3 (and you need to use AMFPHP 1.9+ as earlier versions don’t support AMF3).

   1: class MyDataModel implements IExternalizable {
   2:     private var myInt:uint;
   3:     private var myString:String;
   4:
   5:     public function writeExternal(output:IDataOutput) {
   6:         output.writeInt(myInt);
   7:         output.writeUTF(myString);
   8:     }
   9:
  10:     public function readExternal(input:IDataInput) {
  11:         myInt = input.readInt();
  12:         myString = input.readUTF();
  13:     }
  14: }

Perfect, except for one thing… as soon as you implement IExternalizable AMFPHP no longer works, giving the classic NetConnection.Call.BadVersion – and no amount of fiddling seems to fix it. In desperation I turned to WebORB PHP, but it has the same problem (see this forum post) and I didn’t have time to properly work out SabreAMF. From reading around various forums and blogs it appears that IExternalizable works fine with BlazeDS, but I have seen no reports of it working with anything else.

A working solution

By this point I’d pretty much given up doing it ‘properly’; the job needed to get done and this had taken far too long already. I created a class that acts as a superclass for all models:

   1: class SerializableModel {
   2:     public function toObject():Object {
   3:         return new Object();
   4:     }
   5: }

By making all classes override this function explicitly adding their private attributes we circumvent the problems with IExternalizable and end up with an associative array on the PHP side which we can iterate through. Unfortunately this doesn’t allow us to map the classes to equivalent PHP classes on the server, but this does at least get the job done.

   1: class MyDataModel extends SerializableModel {
   2:     private var myInt:uint;
   3:     private var myString:String;
   4:
   5:     public override function toObject():Object {
   6:         var object:Object = super.toObject();
   7:
   8:         object.myInt = myInt;
   9:         object.myString = myString;
  10:
  11:         return object;
  12:     }
  13:

  15: }

Finally in the remote method call we pass the object constructed by this function using:

   1: : myService.call("MyService.write_data", responder, myDataModel.toObject());

A better solution?

I’d love to hear one! Please post comments with any suggestions or ideas on how to do this better. Even better, if anyone knows how to patch AMFPHP 1.9 to accept AMF messages constructed with the IExternalizable interface I would love to see it.