Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Class mapping difficile en AS3.

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Compatible PHP. Cliquer pour en savoir plus sur les compatibilités.Par ekameleon (Marc Alcaraz), le 28 août 2006

Généralité

Pour ceux qui utilisent depuis un moment le protocol AMF(Action Message Format) et tout particulièrement Flash Remoting, il vous arrive surement d'utiliser dans vos services vos propres classes personnalisées.

Je vais baser mon article sur AMFPHP donc je ne sais pas si ce dont je vais vous parler et valable également en JAVA, .NET, etc. Il faudra si vous êtes dans une autre contexte tester par vous même la problématique dont je vais vous parler maintenant :)

Nous pouvons donc faire une différence entre le Class mapping en sortie (Flash vers Serveur) et en entrée (Serveur vers Flash). Dans AMFPHP il existe plusieurs solutions pour utiliser cette fonctionnalité, pour ma part dans AMFPHP 1.2 j'édite le fichier advancedsettings.php qui se trouve au même niveau que le fichier gateway.php et j'ajoute dans les listes incoming et outgoing mes correspondances. Vous pouvez retrouver sur le site de AMFPHP toutes les explications à ce sujet dans la documenation officielle : Class Mapping

Exemple AS2 de Class Mapping.

Pour illustrer un peu ma méthode d'utilisation, je vais créer un petit exemple avec une classe User qui me permettra de créer des instances ayant 3 propriétés “name”, “age” et “url”. Je vais pouvoir ainsi essayer de créer dans mon code PHP une instance de cette classe et je vais ensuite essayer de l'envoyer vers mon animation directement via un service AMFPHP.

1 - Nous allons commencer par créer un répertoire services/test/ de mon répertoire d'installation de AMFPHP.

2 - Je crée ensuite la classe User dans un fichier User.php dans le répertoire services/test .

<?php
 
	class User
	{
 
		// ----o Constructor
 
		function User ( $name, $age , $url )
		{
			$this->name = $name ;
			$this->age  = $age ;
			$this->url = $url ;
		}
 
		// ----o Public Properties
 
		var $age ;
		var $name ;
		var $url ;
 
	}
 
 
?>

3 - Je modifie le fichier advancedsettings.php situé à la racine de mon répertoire contenant AMFPHP :

<?php
/**
 * This file defines settings regarding custom class mappings
 * Custom class mapping is an advanced feature that enables mapping instances of 
 * custom Flash objects to PHP objects and vice-versa. If you want to implement
 * VOs (value objects) this is the perfect feature for this
 *
 * If you have no idea what a VO is chances are you don't really care about these
 * settings
 * 
 * Most of the time you will probably want to change the outgoing settings only
 * since incoming mappings are done for you automatically
 */
 
//One may choose to put mapped classes (incoming) outside of the services folder also
$gateway->setBaseCustomMappingsPath('services/vo/'); 
 
//Set incoming mappings
$incoming = array(
 
);
 
$gateway->setCustomIncomingClassMappings($incoming);
 
//Set outgoing mappings
$outgoing = array(
 
	'user' => 'test.User'	
 
);
 
$gateway->setCustomOutgoingClassMappings($outgoing);
 
?>

Je n'utilise pas le répertoire vo/ car je trouve sympatique de créer mes classes dans des répertoires contenus directement dans le répertoire services. Je vous avoue qu'ici vous pouvez faire un peu comme vous le sentez.

4 - Je crée ensuite dans mon répertoire services/ la classe Test pour récupérer une instance de ma classe User dans Flash :

<?php
 
require("test/User.php") ;
 
class Test {
 
	// -----o Constructor
 
	function Test () {
 
		$this->methodTable = array (
 
			"getUser" => array(
				"description" => "Returns a user.",
				"access" => "remote",
				"returns" => "test.User",
			)
 
                ) ;
 
	}
 
	// ----- Methods
 
	/**
	 * @desc Returns User instance.
	 * @access remote
	 */
	function getUser( $name, $age, $url ) 
	{
 
		return new User( $name, $age, $url ) ;
 
	}
 
}
 
?>

5 - Je vais maintenant créer un petit exemple super rapide côté Flash AS2 pour récupérer une instance de la classe User directement via mon service AMFPHP.

Il faut tout d'abord créer la classe AS2 test.User :

/**
 * @author ekameleon
 */
 
class test.User
{
 
	// ----o Constructor
 
	public function User(name:String, age:Number, url:String)
	{
 
		if ( name != null ) this.name = name ;
		if ( age > 0) this.age = age ;
		if ( url != null) this.url = url ;
 
		trace( "> User constructor : " + this.name ) ;
 
	}	
 
	// -----o Public Properties
 
	public var age:Number ;
	public var name:String ;
	public var url:String ;
 
	// ----o Public Methods
 
	static public function register():Void
	{
 
		Object.registerClass("test.User", test.User) ;	
 
	}	
 
	public function toString():String
	{
 
		return "[User:" + name + ", age:" + this.age + ", url:" + this.url + "]"  ;	
 
	}
 
}

Ensuite dans Flash, j'ouvre un nouveau document et je tape sur la timeline le code suivant :

import mx.remoting.*;
import mx.rpc.*;
import mx.utils.Delegate;
import mx.remoting.debug.NetDebug;
 
import test.User ;
 
//User.register() ;
 
var result:Function = function ( result:ResultEvent )
{
	trace("> result : " + result.result) ;
}
 
var error:Function = function( fault:FaultEvent ):Void 
{
	trace( "> Error : " + fault.fault.faultstring );
}
 
var gatewayUrl:String = "http://localhost/work/vegas/php/gateway.php";
var service:Service;
 
var responder:RelayResponder = new RelayResponder(this, "result", "error") ;
 
service = new Service(gatewayUrl, null, "Test", null, responder);
 
service.getUser("eka", 29, "http://www.ekameleon.net/blog/" );

J'utilise pour le moment le framework mx AS2 de Macromedia/Adobe pour gérer le service remoting.

Je lance l'animation en n'oubliant pas de lancer mon serveur local avant ;) (j'utilise WAMP pour mes tests).

J'obtiens dans mon panneau de sortie le message suivant :

> result : [object Object]

L'objet que je reçois via ma méthode getUser() n'est pas une instance de la classe User. En effet, il ne faut pas oublier qu'en AS1 ou AS2 il faut utiliser la méthode Object.registerClass pour enregistrer côté client vos classes pour des transferts de données via un service Remoting, une LocalConnection ou un SharedObject.

Si vous regardez de plus prêt la classe test.User AS2 ci-dessus, j'ai inséré à l'intérieur une méthode static User.register() qui permet quand vous le désirez d'initialiser cet enregistrement. La condition obligatoire pour récupérer dans votre animation une instance de type test.User est donc de lancer cette méthode statique à un moment donné avant de lancer la méthode du service remoting.

Je modifie donc le code principal de mon animation en enlevant le commentaire devant la ligne contenant l'instruction User.register().

import mx.remoting.*;
import mx.rpc.*;
import mx.utils.Delegate;
import mx.remoting.debug.NetDebug;
 
import test.User ;
 
User.register() ; // ici enregistrement de la classe User
 
var result:Function = function ( result:ResultEvent )
{
	trace("> result : " + result.result) ;
}
 
// etc....

Si vous relancez votre exemple vous obtenez maintenant dans votre panneau de sortie :

> User constructor : eka
> result : [User:eka, age:29, url:http://www.ekameleon.net/blog/]

A la réception de l'objet provenant du service PHP, une nouvelle instance de type User est créé (voir première ligne dans le panneau de sortie) et ensuite je peux vérifier que les propriétés name, age et url sont définies comme il faut.

C'est avec cette technique par exemple qu'il est possible lors d'une requête MYSQL de récupérer directement dans flash un objet de type RecordSet. On remarque en regardant de plus prêt cette classe dans le package mx.remoting que l'objet RecordSet généré via un service AMF utilise en interne une propriété serverInfo qui lui permet de s'initialiser au lancement du constructeur une fois l'objet désérialisé.

Problème en AS3.

Il faut maintenant que je vous parle du problème qui m'amène à écrire cet article.

Que se passe t'il en AS3 si l'on veut récupérer comme en AS2 une instance de type test.User via un service Remoting ?

Je vais donc rapidement reproduire l'exercice du dessus en AS3. Il faut juste se souvenir avant de commencer ce test qu'en AS3 la méthode Object.registerClass n'existe plus et qu'il faut maintenant utiliser la méthode flash.net.registeClassAlias à la place :)

1 - je crée un nouveau projet ActionScript dans Flex que je nomme TestClassMapping (je base le source path du projet dans un répertoire src/ ).

2 - je crée dans une classe test.User dans le répertoire src/test/

package test
{
 
	import flash.net.registerClassAlias ;
 
	public class User
	{
 
		// ----o Constructor
 
		public function User(name:String="", age:uint=0, url:String="")
		{
 
			trace( "> User constructor : " + this ) ;
 
			this.name = name ;
			this.age = age ;
			this.url = url ;
 
		}	
 
		// -----o Public Properties
 
		public var age:uint ;
		public var name:String ;
		public var url:String ;
 
		// ----o Public Methods
 
		static public function register():void
		{
 
			registerClassAlias("test.User", User) ;	
 
		}	
 
		public function toString():String
		{
 
			return "[User name:" + name + ", age:" + this.age + ", url:" + this.url + "]"  ;	
 
		}
 
	}
 
}

Ensuite je mets en place le contenu de ma classe TestClassMapping (default Application class) :

package
{
	import flash.display.Sprite;
 
	import flash.net.NetConnection;
	import flash.net.ObjectEncoding;
	import flash.net.Responder;
 
	import test.User ;
 
	public class TestClassMapping extends Sprite
	{
 
		// ----o Constructor
 
		public function TestClassMapping()
		{
 
			User.register() ; // enregister la classe.
 
			var url:String = "http://localhost/work/vegas/php/gateway.php" ;
 
			nc = new NetConnection() ;
			nc.objectEncoding = ObjectEncoding.AMF0 ;
 
			nc.connect( url ) ;
 
			var method:String = "Test.getUser" ;
			var responder:Responder = new Responder(_onResult, _onFault) ;
 
			nc.call(method, responder , "eka" , 29, "http://www.ekameleon.net/blog/") ;
 
		}
 
		// ----o Public Properties
 
		public var nc:NetConnection ;
 
		// ----o Private Properties
 
		private function _onResult ( result : Object ) : void {
			trace( "> result : " + result ) ;
		}
 
		private function _onFault ( fault : Object ) : void {
			var s : String = "";
			for( var i : String in fault ) {
				s+= i + " " + fault[i]  + "\n";
			}
			trace("> fault : " + s) ;
		}	
 
	}
}

J'utilise donc comme dans mon exemple AS2 la méthode static User.register() pour que la désérialisation se fasse correctement.

3 - Je lance en mode debug mon application et j'obtiens dans la console de sortie de Flex :

> User constructor : [User name:null, age:0, url:null]
> result : [User name:eka, age:29, url:http://www.ekameleon.net/blog/]

Tout semble bien se passer, l'objet généré est bien de type test.User mais si vous comparez la sortie de mon exemple AS2 et celle ci vous devez vous rendre compte qu'il y a une petite différence !

En effet, dans le constructeur de la classe mes propriétés name, age et url ne sont pas remplies au moment du lancement de la fonction constructeur comme en AS2 ! Il est donc impossible dans le constructeur de la classe d'utiliser ces propriétés… Il semblerait que la désérialization AMF crée d'abord l'instance et ensuite remplie les propriétés de l'objet. Il est donc impossible d'utiliser les valeurs des propriétés de l'objet désérialisé dans le constructeur de l'instance. A noter que je n'ai pas testé si c'est la même chose en AMF3 ? J'espère que ce n'est pas le cas !

Remoting AS3 avec ASGard.

J'ai découvert ce problème en reprenant la classe RecordSet que j'ai développé en AS2 dans mon package asgard.net.remoting pour cabler une nouvelle implémentation AS3.

En AS2 j'utilise dans le constructeur de ma classe la même technique que Adobe/Macromedia avec la propriété serverInfo et en fait il est impossible comme je vous l'ai démontré de récupérer la valeur de cette propriété directement dans le constructeur de la classe pour initialiser l'instance !

J'ai tout de même trouvé une solution pour remédier à ce problème en transformant cette propriété en une propriété virtuelle (get/set) et donc même si la propriété s'initialise aprés le lancement du constructeur la fonction setter me permet de lancer tout de même l'initialisation de l'instance au bon moment.

A noter que j'ai mi à jour ma version AS3 d'ASGard avec son package asgard.net.remoting. Vous pouvez retrouver les souces de ASGard dans le src de VEGAS.

Voilà rapidement un exemple d'utilisation de ASGard qui reprend l'exemple de l'article :

package
{
 
	import asgard.events.ActionEvent ;	
	import asgard.events.RemotingEvent ;
 
	import asgard.net.remoting.RemotingService;
	import asgard.net.remoting.RemotingAuthentification;
 
	import flash.display.Sprite ;
 
	import test.User ;
 
	public class TestAsgardRemoting extends Sprite
	{
 
		// ----o Constructor
 
		public function TestAsgardRemoting()
		{
 
			// ----o Register your shared Class.
 
			User.register() ;
 
			// ----o Create Service
 
			var service:RemotingService = new RemotingService() ;
 
			service.addEventListener(RemotingEvent.ERROR, onError) ;
			service.addEventListener(RemotingEvent.FAULT, onFault) ;
			service.addEventListener(ActionEvent.FINISH, onFinish) ;
			service.addEventListener(RemotingEvent.RESULT, onResult) ;
			service.addEventListener(ActionEvent.START, onStart) ;
			service.addEventListener(RemotingEvent.TIMEOUT, onTimeOut) ;
 
			service.gatewayUrl = "http://localhost/work/vegas/php/gateway.php" ;
			service.serviceName = "Test" ;
			service.methodName = "getUser" ;		
			service.params = ["eka", 29, "http://www.ekameleon.net"] ;
 
			// ----o Launch Service
 
			service.trigger() ;
 
		}
 
		// ----o Public Methods
 
		public function onError(e:RemotingEvent):void
		{
			trace("> " + e.type + " : " + e.code) ;
		}
 
		public function onFinish(e:ActionEvent):void
		{
			trace("> " + e.type) ;
		}
 
		public function onFault(e:RemotingEvent):void
		{
			trace("> " + e.type + " : " + e.getCode() + " :: " + e.getDescription()) ;
		}
 
		public function onProgress(e:ActionEvent):void
		{
			trace("> " + e.type ) ;
		}
 
		public function onResult( e:RemotingEvent ):void
		{
			trace("-----------") ;
			trace("> result : " + e.result) ;
			trace("-----------") ;
		}
 
		public function onStart(e:ActionEvent):void
		{
			trace("> " + e.type ) ;
		}
 
		public function onTimeOut(e:RemotingEvent):void
		{
			trace("> " + e.type ) ;
		}
 
	}
}

Si vous le désirez, je reviendrai vers vous pour vous montrez plus en détail l'utilisation de mon package asgard.net.remoting. Il me reste à cabler l'accés “proxy” des services comme je le fais en AS2. Je pense que cette solution est déjà stable pour vos utilisations quotidiennes en AS3.

A noter que j'ai pu recoder complètement une classe RecordSet dans le package asgard.data.remoting même sil me reste à cabler certaines fonctionnalités pas vraiment urgentes comme la gestion des pages etc. Vous pouvez donc facilement récupérer via ma classe RemotingService une instance de type RecordSet avec AMFPHP lors du renvoi vers vote animation d'une requête MYSQL comme en AS2.

Je trouve étrange que Adobe n'est pas rapidement créé des outils pour Remoting en AS3 ? Peut être qu'il se concentrent sur la version 3 du protocol AMF ?

Dans tous les cas, j'avais besoin rapidement de mes outils AS2 en AS3 et je suis contant d'avoir pu rapidement les mettre en place comme je le voulais (malgré quelques réactions étranges ;))

Si vous n'avez pas encore essayer mon framework OpenSource, vous pouvez récupérer les sources de VEGAS et de ASGard sur le SVN du projet. Plus d'info à ce sujet en lisant les articles suivant :

Par ALCARAZ Marc aka EKAMELEON (2006). Vous pouvez consulter ce tutorial et des commentaires sur mon blog.