Issue Details (XML | Word | Printable)

Key: ZF-6843
Type: Bug Bug
Status: Open Open
Priority: Major Major
Assignee: Wade Arnold
Reporter: Matthias Steinböck
Votes: 0
Watchers: 2
Operations

If you were logged in you would be able to see more operations.
Google issue summary
Zend Framework

(De)Serialization of objects implementing ArrayAccess or extending ArrayObject and using magic functions __get and __set

Created: 28/May/09 07:10 AM   Updated: 15/Aug/09 07:01 PM
Component/s: Zend_Amf
Affects Version/s: 1.8.1
Fix Version/s: None

Time Tracking:
Not Specified


 Description  « Hide

Defining the environment

I recognised those two problems trying to send Doctrine-models via AMF.

On PHP-side i have typical doctrine-classes in a specified relation:

abstract class BaseArea extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->setTableName('area');
        $this->hasColumn('id', 'integer', 4, array('type' => 'integer', 'primary' => true, 'autoincrement' => true, 'length' => '4'));
        $this->hasColumn('content_id', 'integer', 4, array('type' => 'integer', 'length' => '4'));
        $this->hasColumn('followed_by', 'integer', 4, array('type' => 'integer', 'length' => '4'));
    }

    public function setUp()
    {
       $this->hasMany('Point as Points', array('local' => 'id',
                                                'foreign' => 'area_id'));
    }
}

abstract class BasePoint extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->setTableName('point');
        $this->hasColumn('id', 'integer', 4, array('type' => 'integer', 'primary' => true, 'autoincrement' => true, 'length' => '4'));
        $this->hasColumn('top', 'integer', 4, array('type' => 'integer', 'length' => '4'));
        $this->hasColumn('left', 'integer', 4, array('type' => 'integer', 'length' => '4'));
        $this->hasColumn('area_id', 'integer', 4, array('type' => 'integer', 'length' => '4'));
    }

    public function setUp()
    {
        $this->hasOne('Area', array('local' => 'area_id',
                                    'foreign' => 'id'));
    }
}

Normal access to depending objects (in this case Points) works like this in PHP:

$result = Doctrine::getTable('Area')->findAll();

foreach($result[0]->Points as $point) {
   // do some freaky stuff with this point
}

Points is a Doctrine_Collection implementing ArrayAccess.

First problem

Fetching an $area-instance from the db and sending it via AMF does not serialize the Points.
This happens because Zend_AMF does not know that there is a "hidden" property called "Points" as it is only handled via the magic functions __get and __set (see doctrine source code for this). Now thinking about how to solve this i think i know, that AMF cannot be changed to use a configuration parameter like "concernMagicProperties":

$server = new Zend_AMF_Server();
$server->concernMagicProperties = true;

Well. I think this is not possible (and maybe also not practical for all occasions). So i made the following change. Doctrine supports adding kind of "unused" fields like this:

$area->mapValue('_hiddenFields', array('Points'));

This makes sure Doctrine continues to work (and does not complain about integrity with the database) and Zend_AMF can access this property to find out which hidden fields should be serialized. In a normal environment with the same problem (not using Doctrine) this could be reached by an public property _hiddenFields which will not be serialized or, maybe more beautiful (this idea just comes to my mind), implementing a Zend_AMF_Value_MagicBehaviour:

interface Zend_AMF_Value_MagicBehaviour {
    /** @return array ***/
    public function getHiddenFields();
}

Second Problem

After changing Zend_AMF to implement the first behaviour (using the public property _hiddenFields) i recognised the second problem:
AMF serializes the Doctrine_Collection as a named object - which is entirely correct, but the problem is, that there must be a matching AS-class which would have required me to implement sort of my own AS-Doctrine-Collection with the big problem, that the property names are unknown at the time of compilation (the properties are called 1 2 3 4 5...) and as AS has no possibility to handle this - or i don't know it (no magic __set or so) - the only possibility was to change Zend_AMF so that this class gets serialized as an Array. AS has enough ways to change this array and send the list back.

To solve this i made a dirty hack in Zend_AMF to check whether the given object is an instance of Doctrine_Collection and handle it as array. More beautiful would be a similar behaviour as mentioned above:

interface Zend_AMF_Value_HandleAsArray {}

If a class implements this Interface, Zend_AMF should handle the object as an array (not only if it IS an array).

Summarizing there are two problems to solve:

  • How can magic "hidden" properties be serialized
  • How can classes that implement ArrrayAccess or extend ArrayObject be serialized as an Array

I documented my specific changes in a patch which might not be the best, but it was a fix for me to use Doctrine.
I would like to hear your opinion on this topic, also if there is another way of design.

(offtopic: btw: why doesn't zend framework trac has codeformatting for php? )



Robert Cesaric added a comment - 15/Aug/09 07:00 PM

Here's a hack I wrote to send Doctrine models via AMF: http://cesaric.com/?p=303. It'll get all the nested collections of a data graph and convert them to standard objects and ActionScript ArrayCollections.

On a similar note, any luck going the other way around in trying to send ActionScript objects over AMF to the corresponding Doctrine model object?

I can't seem to get ActionScript classes to map to the respective Doctrine model classes. I'm using the $server->setClassMap(); method and it works fine as long as I'm not mapping to a php class that extends "Doctrine_Record" (my BaseContact class below does extend Doctrine_Record). If I comment out "extends BaseContact" below, Zend_Amf type casts it just fine.

class Contact extends BaseContact
{

public function construct(){ $this->mapValue("_explicitType","com.example.vo.ContactVO"); }

}

Any suggestions? Btw, great work on Zend_Amf. It's coming along nicely!