Current state

The current state of class mapping with AMFPHP lib is using an _explicitType property on any PHP Object.

If we look inside the PHP code responsible for serialization/deserialization, we will see that this value will be encoded to specify the type to flash. If nothing is specified, Object will be used (in case of a php stdclass), or Array in case of arrays. This function is function getClassName(&$d) l. 280 of core/amf/io/AMFBaseSerializer.php.

In Drupal, there is this property called type which is for node entity the content type. For fields there is a function to get the description of thoses.

A concrete example of class mapping: PHP Part

You remember the website I showed to demonstrate a drupal 7 driven flash website (*see at bottom for link) ? Let me give you a concrete example:

Defining a Menu content type

When you go to the menus pages, you'll see menus which are a content type of type menu. Here is the code of this content type:

  $types = array(
    'menu' => array(
      'type' => 'menu',
      'name' => st('Menu'),
      'base' => 'node_content',
      // Use <em>Menus</em> to create restaurant menus.
      'description' => st("Utilisez les <em>Menus</em> pour créer des cartes du restaurant."),
      'locked' => 1,
    ),
  );

Defining a generic Image Field

Pretty straighforward isn't it ? Now I want to add a simple Image Field to thoses menus:

  $fields = array(
    'image' => array(
      'field_name' => 'image',
      'type' => 'image',
      'cardinality' => 1,
      'translatable' => TRUE,
      'locked' => FALSE,
      'indexes' => array('fid' => array('fid')),
      'settings' => array(
        'uri_scheme' => 'public',
        'default_image' => FALSE,
      ),
      'storage' => array(
        'type' => 'field_sql_storage',
        'settings' => array(),
      ),
    ),
  );

Attaching the field to the menu

And I attach as a bundle to the menu content type. It's called an instance, e.g. an instance of the generic field definition which applies for the menu type. The whole package is called a bundle.

  $instances = array(
    'menu_image' => array(
      'field_name' => 'image',
      'label' => 'Image du menu',
      'bundle' => 'menu',
      'description' => st("Charger une image descriptive du menu."),
      'required' => TRUE,
 
      'settings' => array(
        'file_directory' => 'menu',
        'alt_field' => 1,
      ),

      'widget' => array(
        'type' => 'image_image',
        'settings' => array(
          'progress_indicator' => 'throbber',
          'preview_image_style' => 'thumbnail',
        ),
        'weight' => -1,
      ),

      'display' => array(
        'default' => array(
          'label' => 'hidden',
          'type' => 'image',
          'settings' => array('image_style' => 'menu', 'image_link' => ''),
          'weight' => -1,
        ),
        'teaser' => array(
          'label' => 'hidden',
          'type' => 'image',
          'settings' => array('image_style' => 'medium', 'image_link' => 'content'),
          'weight' => -1,
        ),
      ),
    ),
  );

Puting the pieces together

Here is the boilerplate code to make it working (I put it in the .install file).

  // Declare the content type to the field API.
  foreach ($types as $type) {
    $type = node_type_set_defaults($type);
    node_type_save($type);
    node_add_body_field($type);
  }

  // Create all the fields we are adding to our content type.
  foreach ($fields as $field) {
    field_create_field($field);
  }

  // Create all the instances for our fields.
  foreach ($instances as $instance) {
    $instance['entity_type'] = 'node';
    field_create_instance($instance);
  }

Here we are, let's recapitulate before going to the flash part. We created a Bundle called Menu, with a generic field ImageField attached to this bundle and a LongTextWithSummaryField (the Body field).

A concrete example of class mapping: AS3 Part

In the flash side, I like working with strong types instead of generic object, this is less error prone and generally I can delegate the work to my designer if I done my part of class mapping correctly. The thing that I love is that the deserialization is done completely natively by the flash player but you'll have to use a trick that I will introduce in this article.

MenuVO: Value Object of my menu objet

In any serious service driven flash application, you will use what we call a Value Object, which is a concrete mapping of the php class you want to use in flash, as bundles are not really a concrete class, we will see how to handle that. Here is my VO (Value Object) for the menu content type:

package fr.teo.site.services.vo
{
  import org.drupal7.fields.ImageField;
  import org.drupal7.fields.LongTextWithSummaryField;
  import org.drupal7.services.vo.NodeVO;
  
  public dynamic final class MenuVO extends NodeVO
  {
    private var _image:ImageField;
    private var _body:LongTextWithSummaryField;
    
    public function get image():ImageField
    {
      return _image;
    }
    public function set image(value:*):void
    {
      _image = value[language][0];
    }
    public function get body():LongTextWithSummaryField
    {
      return _body;
    }
    public function set body(value:*):void
    {
      _body = value[language][0];
    }
  }
}

You remember my ImageField and my LongTextWithSummaryField ? They are private member only, I'll have put them public if I had a real php class to map, but remember drupal entities are not php classes. So this is basically my two fields attached to the content type called MenuVO.

The trick is in the accessors, remember that fields comes to you as a localized object, you'll have your language put in the definition of the field, and the cardinality:

  menu:{
    body:{
      en:["Blah blah blah", "Another field", "Again another field"] // in case of you allowed three body.
      fr:["Blah blah blah", "Un autre champ", "Encore un autre champ"] 
    }
  }

The second trick is to put getter upper than setters, your IDE will then think that it's a LongTextWithSummaryField versus a * type. Very useful for auto-completion.

You noticed that my MenuVO extends a class which is called a NodeVO ? Smart developper :) It is actually a simple class which provides all basic fields for a node such as created, updated, nid, language, ... Wait, language ? That's usefull one.

The * placeholder and how to set natively a type

Look at the setter for image, why did I put a * placeholder for the type of my object ? Remember the * mean "Any Type" in flash, that's exactly what we want. The flash player doesn't know yet what is the type of your object, and actually it's just an Array, and he is right, remember the structure of a field ? It comes as an array with all your languages specified, even if you have only one language, it will come as an array.

I'm taking the current value, which is keyed by my language (in this case "und" because my website handle only one language but it can be "en", "de", etc.). I then take the first value, in this case I had only one image so I'm taking the one. For the getter, I return a strong type, and my designer is happy, he got what he wants without worrying about service communication and finding the right field in this obscure generic Object he was used to get.

The code for NodeVO, ImageField and LongTextWithSummaryField can be found at my github here: https://github.com/sylvainlecoy/Drupal-AS3-Remoting-Framework. For instance here is the body field:

package org.drupal7.fields
{
  public class LongTextWithSummaryField
  {
    public var format:String;
    public var safe_summary:String;
    public var safe_value:String;
    public var summary:String;
    public var value:String;
  }
}

The AS 3 Service Class: pretty simple as well

First I design the interface, I have only two methods, getActualite (for home page) and getMenus (for menus):

package fr.teo.site.services
{
  public interface ITeoService
  {
    function getActualites():void;
    function getMenus():void;
  }
}

I implemented a XMLRPCGateway class when the AMFPHP module wasn't out before deciding to port it myself to drupal7, so here it is the AMFPHPGateway:

public final class AMFPHPGateway extends Actor implements ITeoService
  {
    private var connection:NetConnection = new NetConnection();

    public function AMFPHPGateway()
    {
      connection.connect('/amfphp');
      registerClassAlias('org.drupal.article', ArticleVO);
      registerClassAlias('org.drupal.menu', MenuVO);
      registerClassAlias('org.drupal.fields.image', ImageField);
      registerClassAlias('org.drupal.fields.text_default', LongTextWithSummaryField);
    }
    
    public function getActualites():void
    {
      var responder:Responder = new Responder(getActualitesResult, getActualitesStatus);
      connection.call('teo.actualites', responder);
    }
    
    public function getActualitesResult(articles:Array):void
    {
      dispatch(new ActualiteEvent(ActualiteEvent.RETRIEVE_ACTUALITE, Vector.<ArticleVO>(articles)));
    }
    public function getActualitesStatus(obj:*):void
    {
      var debug:Boolean = true;
    }
    
    public function getMenus():void
    {
      var responder:Responder = new Responder(getMenusResult, getMenusStatus);
      connection.call('teo.menus', responder);
    }
    
    public function getMenusResult(menus:Array):void
    {
      dispatch(new MenuEvent(MenuEvent.RETRIEVE_MENU, Vector.<MenuVO>(menus)));
    }
    public function getMenusStatus(obj:*):void
    {
      var debug:Boolean = true;
    }
  }

I am using the robotlegs framework (that's why this class extends Actor, implementing an interface was a good way for me to switch between the XMLRPCGateway and AMFPHPGateway through dependency injection).

Look deep at the method getMenuResult, first it's taking an array as a parameter, meaning that flash already knows that there will be array, and that's true because drupal send an array of menus. I am casting to a Vector of menus, which is actually a simple array but strongly typed. You don't need to do it it's a personnal choice because your array will be already typed of MenuVO. I prefere Vector over Arrays because in the IDE you can have auto completion on types and it doesn't occures with generic arrays. In my mediator class I then can use menus[0].body.safe_value or menu[1].image.uri and it made my life really easier.

Look at the simplicity of my mediator class:

public final class LaCarteMediator extends Mediator
  {
    [Inject]
    public var service:ITeoService;
    
    override public function onRegister():void
    {
      service.getMenus();
      addContextListener(MenuEvent.RETRIEVE_MENU, retrieveMenus);
    }
    
    private function retrieveMenus(event:MenuEvent):void
    {
      for each (var menu:MenuVO in event.menus) {
        laCarte.menus.addMenu(menu.title, menu.body.safe_value, menu.image.uri);
      }
    }
    
    public function get laCarte():LaCarte
    {
      return viewComponent as LaCarte;
    }
  }

Now you wonder how the hell it can be so simple ? I will explain you my idea of implementing the php part to specify a type to entities and fields.

The PHP Part, and what this feature request is about

Remember my service class ? See above, there was 4 stranges lines of code:

      registerClassAlias('org.drupal.article', ArticleVO);
      registerClassAlias('org.drupal.menu', MenuVO);
      registerClassAlias('org.drupal.fields.image', ImageField);
      registerClassAlias('org.drupal.fields.text_default', LongTextWithSummaryField);

This is basically because I'm using pure AS3, I guess with flex it will be the [RemoteClass(name="org.drupal.article")] tag. No need for registering manually. How those Fully Qualified Class Name (FQCN) are generated ? Well, this is very simple as well.

Here is the code to load up menus and send them ready to be typed:

    $result->_explicitType = 'org.drupal.' . $result->type;
    $field_info_instances = field_info_instances('node', $result->type);
    foreach ($field_info_instances as $field_name => $field_info) {
      $fields = $result->{$field_name}[$result->language];
      $result->{$field_name}[$result->language] = array();
      foreach ($fields as $field) {
        $field = (object) $field;
        $field->_explicitType = 'org.drupal.fields.' . $field_info['display']['flash']['type'];
        $result->{$field_name}[$result->language][] = $field;
      }
    }

You noticed that the code have two nested loops and that can be bad for performances, well, it's not worst than implementing it in a hook (because the hook mecanism is costly as well, and I think more costly than loops), but... Good news for all, we can do a lot better.

My contribution ends here, with this snippet, but we can provide better performances by changing the AMFPHP project (I already rewritten some part to handle only php5). This project became very stable, less than one release per year, can be easily tweaked, and we can rewrite the Serializer classes to handle $node->type instead of setting an _explicitType value. If we don't want to hack, we can use the snippet above (working well) and I have to provides some tests.

What do you guys think ? I am ready to provide full functionalities, unit tests and tutorials for this.

*Link to the website: http://www.teorestaurant.com/
**By the way I'm french, forgive me if there is any mistakes, and don't hesitate to correct me ! :-)

Comments

sylvain lecoy’s picture

Or even better, we can ask for a feature request in drupal core to put a "type" property on each fields, users, nodes, and change the AMFPHPSerializer to check the type instead of _explicitType.

That's why I written a so long article to prepare arguments for it ;)