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.

5 comments

  1. Yeah, we all encounter this problem.
    What is really strange is that the get functions aren’t recognized… I don’t understand why that’s ignored…

    But anyway, I’m very surprised that AMF and PHP 5 won’t support sending private attributes since private is supported in PHP 5… I could understand if it was an earlier version.

  2. I guess the logic is that the default AMF class serialization is assuming that the classes are value objects, which traditionally have a bunch of public attributes and not much else. As for AMF/PHP5 issues – I suspect the problem is to do with the AMF deserialization rather than anything to do with PHP itself. If you implement IExternalizable I’d think that from the perspective of the server its getting a bunch of ‘public’ attributes anyway. Hopefully this is something that the AMFPHP developers will fix soon. I’m sure I read somewhere that Adobe were getting involved in AMFPHP (although I can’t find any articles on it right now), so perhaps that will give it a boost.

    Dave

  3. Although it’s painfully lame, here’s what I came up with:


    class MyDataModel {

    private var myInt:uint;
    private var myString:String;

    public function get dbMyInt():uint {
    return myInt;
    }
    public function set dbMyInt(val:uint):void {
    throw new Error("dbMyInt can't be set");
    }

    public function get dbMyString():String {
    return myString;
    }
    public function set dbMyString(val:String):void {
    throw new Error("dbMyString can't be set");
    }
    }

    I’m willing to live with runtime errors in order to have typed objects over associative arrays.

Leave a Reply

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