Doctrine 2 & i18n

Doctrine, Zend 3 Commentaires »

Doctrine 2 est dépourvu – en natif – de système de traduction.
Grâce au système d’extension, des contributeurs dont certains membres de l’équipe de développement Doctrine ont développés Gedmo, un pack d’extension pour Doctrine.

Cette extension inclue :

  • Tree
  • Translatable
  • Sluggable
  • Timestampable
  • Loggable
  • Sortable

Nous nous intéresserons à Translatable qui nous permettra de traduire nos données.

Travaillant avec le Framework Zend, les exemples seront orientés pour cette architecture, il est à noter que cela n’est pas un pré-requit et que cela fonctionnera avec ou sans Zend Framework.

Fonctionnement

Le module i18n de Gedmo fonctionne grâce à l’utilisation d’un table annexe contenant la traduction de certaines propriétés de vos Entités.
La table de traduction contient donc le nom du champs traduite, la langue, la valeur, le type d’objet ainsi qu’une reference vers cet objet.

Par défaut, l’extension aura besoin de la table : ext_translations
Les champs suivant seront ainsi necessaire :

  • id
  • content
  • field
  • locale
  • foreign_key
  • object_class

Voici un jeu de donnée qui vous permettra de mieux comprendre l’utilisation de ce type de structure :

  • content : Un día me voy a comprar el mundo
  • field : title
  • locale : es_es
  • foreign_key : 3
  • object_class : \Entity\Article
  • content : Un jour j’achèterai le monde
  • field : title
  • locale : fr_fr
  • foreign_key : 3
  • object_class : \Entity\Article
  • content : Le monde semble être à l’agonie, que pouvons nous faire pour le sauver.
  • field : description
  • locale : fr_fr
  • foreign_key : 3
  • object_class : \Entity\Article

A noter, que les données relatives à la langue par défaut de votre application sont stockés au sein de la table gerant directement votre entité.
Par défaut, nous utilisons la langue « en_US« , ce qui signifie que la table ext_translations ne contiendra pas de traduction pour « en_US ».
Dans mon exemple, ces données seront stockés dans la table « articles » de l’entité \Entity\Article

Ainsi, l’extension stockera l’ensemble des traductions de votre application au sein d’une même table.
Attention, le champs « forien_key » ne correspond pas à une clé étrangère pour votre base de donnée relationnelle.
Une des possibilités de l’extension consiste à pouvoir définir une table spécifique à la traduction d’une Entités, ainsi vous pourrez définir une vrai clé étrangère au sein de votre RDBMS.

Exploitation

Au niveau de l’exploitation des données,
Vous définirez la langue utilisée au sein de votre application lors de l’execution, cela correspondra normalement à la langue de l’utilisateur.
Il ne restera plus qu’a utiliser votre application comme vous en avez l’habitude.

Nous partons du principe que votre environnement Doctrine est correctement configuré et fonctionnel.

Voici la configuration minimum a définir :

$classLoader = new \Doctrine\Common\ClassLoader( 'Gedmo', APPLICATION_PATH . '/../library/');
$classLoader->register();
 
$config 	= new \Doctrine\ORM\Configuration();
 
$driverChain		= new \Doctrine\ORM\Mapping\Driver\DriverChain();
$entityDriver 		= new \Doctrine\ORM\Mapping\Driver\XmlDriver ();
 
$translatableDriver = $config->newDefaultAnnotationDriver( APPLICATION_PATH . '/../library/DoctrineExtensions/Gedmo/Translatable/Entity');
 
$driverChain->addDriver ($entityDriver,		'Entity\Article');
$driverChain->addDriver ($translatableDriver, 	'Gedmo\Translatable');
 
$config->setMetadataDriverImpl($driverChain);
 
$eventManager		= new \Doctrine\Common\EventManager();
$translatableListener 	= new \Gedmo\Translatable\TranslationListener();
 
$translatableListener->setTranslationFallback( true );
$eventManager->addEventSubscriber($translatableListener);
 
$entityManager = \Doctrine\ORM\EntityManager::create($conn, $config, $eventManager);

Ensuite, il faut configurer vos entités.
J’utilise le XML Mapping, vous trouverez facilement l’équivalence pour YAML / Annotation.

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
      xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
	<entity name="Entity\Article" table="articles">
		<id name="idArticle" type="integer" column="id_article">
			<generator strategy="AUTO" />
		</id>
		<field name="title" type="string">
			<gedmo:translatable/>
		</field>
		<field name="description" type="string">
			<gedmo:translatable/>
		</field>
	</entity>
</doctrine-mapping>

Nous avons simplement ajouté le noeud XML gedmo:translatable à notre champs afin de spécifier que celui-ci peut-être traduit.
A noter qu’il faut inclure l’espace de nom xmlns:gedmo.

Voici un exemple d’utilisation avec Doctrine

//En utilisant la langue par défaut qui est l'anglais,
$article 	= $entityManager->find('\Entity\Article', 3);
$article->getTitle (); 
//One day I'll buy the world
 
//En spécifiant la langue espagnole,
$translatableListener->setTranslatableLocale("es_es");
$article 	= $entityManager->find('\Entity\Article', 3);
$article->getTitle (); 
//Un día me voy a comprar el mundo

Entity de traduction

Nous avons vu précédemment que les traductions sont par défaut stockées dans la table ext_translations.
Voyons comment utiliser une table dédiée à la traduction d’une entité. Cela permettra en plus d’avoir une structure de base de donnée plus propre, et d’avoir des clés étrangère au sein de votre RDBMS.

<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
      xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
	<entity name="Entity\Article" table="articles">
		<gedmo:translation entity="Entity\ArticleTranslation" />
		..
	</entity>
</doctrine-mapping>

Créons la class ArticleTranslation qui n’a rien de bien extraordinaire, sauf qu’elle doit étendre AbstractTranslation qui va définir les differentes propriétés de votre table de traduction.

namespace Entity
 
use Gedmo\Translatable\Entity\AbstractTranslation;
use Doctrine\ORM\Mapping as ORM;
 
class ArticleTranslation extends AbstractTranslation
{
 
}

A cela, ajoutons le fichier de mapping.

<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
      xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
	<entity name="Entity\ArticleTranslation" table="articles_translation" />
</doctrine-mapping>

Il ne vous reste plus qu’a créer la table article_translation avec les champs énuméré en début d’article.

Conclusion

Comparer à Doctrine 1, la mise en place de l’internationalisation pour votre application peut sembler plus difficile. Par contre, l’exploitation de cette implementation est tout simplement user-friendly.

Mots-clefs :, , , , , , , , , ,
 

Ordonner les resultats Doctrine avec une table de composition

Doctrine 3 Commentaires »

Prenons le schéma ci-dessus.
Au niveau de la définition, nous avons deux entités : Bureau ( rooms ) et Utilisateur ( users ). Ces deux entités sont liées par une relation manyToMany car un utilisateur peut avoir plusieurs bureaux, et un bureau peut avoir plusieurs utilisateurs.
Pour pouvoir intégrer l’ordonnancement des utilisateurs au sein d’une pièce, nous avons ajouté le champs « index » sur la table de liaison.

Grâce à cette définition, nous pouvons avoir les cas suivants :
- Robert est à la place #2 dans le bureau d’accueil
- Robert est à la place #1 dans le bureau du président
- Annie est à la place #1 dans le bureau du président

Définition des Domain Model Object

doctrine
Nous allons maintenant définir les liaisons Doctrine entre ces objets.
Je n’utiliserai pas la définition YAML.

abstract class Model_Base_Room extends Model_Base
{
    public function setTableDefinition()
    {
        $this->setTableName('rooms');
        $this->hasColumn('id_room as idRoom', 'integer', 4, array(
             'type' => 'integer',
             'fixed' => 0,
             'unsigned' => false,
             'primary' => true,
             'autoincrement' => true,
             'length' => '4',
             ));
        $this->hasColumn('title', 'string', 80, array(
             'type' => 'string',
             'fixed' => 0,
             'unsigned' => false,
             'primary' => false,
             'notnull' => true,
             'autoincrement' => false,
             'length' => '80',
             ));
    }
 
    public function setUp()
    {
        parent::setUp();
        $this->hasMany('Model_User as users', array(
             'refClass' => 'Model_CompoRoomsUsers',
             'local' => 'id_room',
             'foreign' => 'id_user'));
    }
}
 
abstract class Model_Base_User extends Model_Base
{
    public function setTableDefinition()
    {
        $this->setTableName('users');
        $this->hasColumn('id_user as idUser', 'integer', 4, array(
             'type' => 'integer',
             'fixed' => 0,
             'unsigned' => false,
             'primary' => true,
             'autoincrement' => true,
             'length' => '4',
             ));
        $this->hasColumn('firstname', 'string', 80, array(
             'type' => 'string',
             'fixed' => 0,
             'unsigned' => false,
             'primary' => false,
             'notnull' => true,
             'autoincrement' => false,
             'length' => '80',
             ));
    }
 
    public function setUp()
    {
        parent::setUp();
        $this->hasMany('Model_Room as rooms', array(
             'refClass' => 'Model_CompoRoomsUsers',
             'local' => 'id_user',
             'foreign' => 'id_room'));
    }
}
 
abstract class Model_Base_CompoRoomsUsers extends Model_Base
{
    public function setTableDefinition()
    {
        $this->setTableName('compo_rooms_users');
        $this->hasColumn('id_room as idRoom', 'integer', 4, array(
             'type' => 'integer',
             'fixed' => 0,
             'unsigned' => false,
             'primary' => true,
             'autoincrement' => false,
             'length' => '4',
             ));
        $this->hasColumn('id_user as idUser', 'integer', 4, array(
             'type' => 'integer',
             'fixed' => 0,
             'unsigned' => false,
             'primary' => true,
             'autoincrement' => false,
             'length' => '4',
             ));
        $this->hasColumn('index', 'integer', 4, array(
             'type' => 'integer',
             'fixed' => 0,
             'unsigned' => false,
             'primary' => false,
             'notnull' => true,
             'autoincrement' => false,
             'length' => '4',
             ));
    }
 
    public function setUp()
    {
        $this->hasOne('Model_User as user', array(
             'local' => 'id_user',
             'foreign' => 'id_user'));
 
        $this->hasOne('Model_Room as room', array(
             'local' => 'id_room',
             'foreign' => 'id_room'));
    }
}

Ordonner avec DQL

Après cette longue définition, nous pouvons passer au code DQL qui permettra d’ordonner les résultats en fonction de l’index de notre table de liaison.

$query	= Doctrine_Query::create ()
	->select ("room.*, users.*")
	->from ("Model_Room room")
	->leftJoin ("room.users users")
	->orderBy ("room.id_room, users.Model_CompoRoomsUsers.index ASC");

Prenez note du fait que nous trions dans un premier temps par identifiant de bureau et ensuite par index. Si nous n’utilisions qu’un tri par index, l’ordre des bureaux serai incohérent.

Pour aller plus loin

Nous avons décrit le fonctionnement pour ordonner des résultats à partir d’une table de liaison.
Mais vous pouvez aussi utiliser cette méthode pour conditionner le résultat.

Mots-clefs :, , , ,
 

Transfer Object Assembler avec Doctrine 1

Doctrine, Zend 7 Commentaires »

doctrineLe pattern Transfert Object TO, Data Transfert Object DTO, ou encore Value Object VO permet d’échanger des données entre deux applications.

Wikipédia : Son but est de simplifier les transferts de données entre les sous-systèmes d’une application logiciel. Les objets de transfert de données sont souvent utilisés en conjonction des objets d’accès aux données.

Qui dit Transfert Object, veux aussi dire Transfert Object Assembler.

Conversion d’object

Le pattern Transfert Object Assembler permet de passer d’un DTO à un Objet d’accès au données, et inversement.
C’est le moyen le plus simple pour travailler avec le pattern Data Transert Object et un ORM.

Domain Object Model -> Data Transfert Object

Avec l’ORM Doctrine, le transfert d’un « Domain Object Model DOM » ou ici Doctrine_Record vers un DTO est très simple.
Il suffit de réaliser un parser qui analyse l’objet issu de Doctrine, et qui remplit les données du DTO.

Data Transfert Object -> Domain Object Model

Convertir un DTO vers un DOM peut s’avère plus complexe.
En effet, Doctrine utilise des collections d’objet qui lui sont propres, mais a aussi besoin de connaitre les clés primaires et leurs contenus pour pouvoir créer un objet. Car un objet issu de Doctrine_Record n’a pas les même propriétés quand il est existant en base de donnée, ou qu’il est en phase d’être sauvegardé.

Implémentation de Transfer Object Assembler

Une implémentation du pattern Transfert Object Assembler vient de voir le jour après moult et moult péripéties.

Cette implémentation permet donc le transférer de DTOs à partir de DOMs et inversement.
Associer au pattern Service Layer, cela peut s’avérer être très pratique.

Voici un exemple simple d’utilisation :

$oProgram = new ProgramDTO ();
$oProgram->title = "Dorothe";
 
$oVideoDTO = new VideoDTO ();
$oVideoDTO->idVideo = 30;
$oVideoDTO->program = $oProgram;
 
$oAssembler = $new Assembler ();
$oVideo = $oAssembler->createDomainObject ($oVideoDTO);
$oVideo->save ();

Nous avons ici créé une nouvelle entrée dans la table programs, et réalisé une liaison entre ce nouvel objet et la vidéo existante d’identifiant 30.
Le tout one-shot !

L’intérêt de cette implémentation, c’est d’être fonctionnel avec :

  • Les objets persistant
  • Les objets non-persistant
  • Les compositions d’objets sur N niveau

Utilisation avec le pattern Service Layer

Nous allons voir l’exemple le plus simple, soit l’implémentation des méthodes CRUD pour un objet générique.
L’utilisation du nommage des classes provient de l’utilisation de Zend Framework.

/**
 * @param Model_DTO_Video
 * @return Model_DTO_Video
 */
public function create (Model_DTO_Video $poVideoDTO)
{
	$oModelVideo		= null;
	$oAssembler		= new Model_Utils_Assembler ();
	$oModelVideo		= $oAssembler->createDomainObject($poVideoDTO, true);
	$oModelVideo->save ();
	$poVideoDTO->idShow	= $oModelVideo->idShow;
	return $poVideoDTO;
}
 
/**
 * @param Model_DTO_Video
 * @return Model_DTO_Video
 */
public function read (Model_DTO_Video $poVideoDTO)
{
	$query	= Doctrine_Query::create ()
		->select ("video.*")
		->from ("Model_Video video")
		->where ("video.id_show = ?", $piIdVideo);
	$oModelVideo	= $query->fetchOne ();
	$oAssembler	= new Model_Utils_Assembler ();
	return $oAssembler->createTransfertObject ($oModelVideo);
}
 
/**
 * @param Model_DTO_Video
 * @return Model_DTO_Video
 */
public function update (Model_DTO_Video $poVideoDTO)
{
	$oModelVideo	= null;
	$oAssembler	= new Model_Utils_Assembler ();
	$oModelVideo	= $oAssembler->createDomainObject($poVideoDTO, true);
	$oModelVideo->save ();
 
	return $poVideoDTO;
}
 
/**
 * @param Model_DTO_Video
 * @return Model_DTO_Video
 */
public function delete (Model_DTO_Video $poVideoDTO)
{
	$oModelVideo	= null;
	$oAssembler	= new Model_Utils_Assembler ();
	$oModelVideo	= $oAssembler->createDomainObject($poVideoDTO, false);
	$oModelVideo->delete ();
 
	return $poVideoDTO;
}

Téléchargement

Contenu de l’archive :

  • Implémentation du pattern Transfer Object Assembler
  • Interface du pattern Transfer Object Assembler
  • Classe de définition d’un DTO

Pour faire fonctionner l’assembler. Vous devez définir une classe qui hérite de Assembler, et ainsi surchargez la propriété $_relationships avec vos liaisons entre DTO et DOM.

Dernière mise à jour : 20 Septembre 2010
Transfert_Object_Assembler_Doctrine_1.2.zip – 6 Ko.

Mots-clefs :, , , , , , , ,
 
Designed by NattyWP Wordpress Themes.
Images by desEXign.