Skip navigation

Category Archives: Observer

Observer ce qui est.
Les gens « métier » ne font pas abstraction des choses. Ils voient leur domaine pleinement et « de plein droit ».
C’est au développeur d’observer le domaine d’application, pour mieux l’analyser.

Salut à toutes et à tous,

Ce petit billet pour vous présenter la transformation d’un modèle en code, ou plus simplement, la génération de code au travers d’un modèle.

Dans l’état de l’art du développement logiciel, on fait abstraction du code en créant des « templates », ou en bon vieux français, des « patrons » ou encore des « modèles » 🙂
Cela nécessite donc qu’un template se réfère, dans sa terminologie, à un modèle pour injecter les données.

Je vais ici utiliser le méta-modèle défini dans mon précédent billet ainsi que le modèle et l’outillage réalisé à titre d’exemple.

L’objectif est d’utiliser ce modèle

Simple modèle d'exemple

Pour générer un fichier YAML correspondant à ceci (dans sa forme et non ses données):

Produit:
..columns:
….name: { type: string(255), notnull: true, unique: true }
..relations:
….Categories: { class: Categorie, foreignAlias: Produits, refClass: ProduitCategorie, local: produit_id, foreign: categorie_id }

Categorie:
..columns:
….name: { type: string(255), notnull: true, unique: true }
..relations:
…. Produit: { class:Produit, foreignAlias: Categories, refClass: ProduitCategorie, local: categorie_id, foreign: produit_id }

ProduitCategorie:
..columns:
….produit_id: { type: integer(20), primary: true }
….categorie_id: { type: integer(20), primary: true }
..relations:
….Produit: { local: produit_id, foreign: id, foreignAlias: Produits }
….Categorie: { local: categorie_id, foreign: id, foreignAlias: Categories }

. représente ici un espace, tant les espaces sont importants dans les fichiers yaml.

Voici le code du programme de génération :

[%
var m : Model := Model.allInstances().at(0);

for( class in m.defines )
{
%]
[%=class.name%]:
[%
if( class.column.size() > 0 ) { %]
..columns:
[% for( column in class.column ) { %]
….[%=column.name%]: { [%=column.generateProperties()%] }
[% }
}
if( class.relation.size() > 0 ) { %]
..relations:
[% for( relation in class.relation ) { %]
….[%=relation.name%]: { [%=relation.generateProperties()%] }
[% }
}
%]

[%
}

operation OneToOne generateProperties() : String {
return ‘local: ‘ + self.relates.name.toLowerCase()+’_id’ + ‘, foreign: id, foreignAlias: ‘ + self.relates.generateSingularName();
}

operation OneToMany generateProperties() : String {
return ‘otm’; –je vous laisse vous amuser
}

operation ManyToMany generateProperties() : String {
return ‘class : ‘ + self.relates.name + ‘, foreignAlias: ‘ + self.owner.generateSingularName() + ‘, refClass: ‘ + self.refClass.name + ‘, local: ‘ + self.owner.name.toLowerCase+’_id’ + ‘, foreign: ‘ + self.relates.name.toLowerCase+’_id’;
}

operation Column generateProperties() : String {
return ‘type: ‘+ self.type.generateTypeName()+'(‘+self.type.max+’)’ + ‘, primary: ‘ + self.primary + ‘, notnull: ‘ + self.generateIsNull() + ‘, unique: ‘ + self.generateIsUnique();
}

operation Column generateIsUnique() : String {
return self.unique;
}

operation Column generateIsNull() : String {
return self.notnull;
}

operation Type generateTypeName() : String {
if(self.isTypeOf(sfDoctrine!Integer)){ return ‘integer’; }
}

operation Class generateSingularName () : String {
return self.name;
}
%]

Et voici le résultat généré dans un fichier gen_schema.yml (il faut configurer un lanceur pour exécuter la génération, le site d’Epsilon explique ça très bien, voir les newsgroup aussi) :

Produit:
..columns:
….id: { type: integer(20), primary: true, notnull: true, unique: true }
..relations:
….Categories: { class : Categorie, foreignAlias: Produit, refClass: ProduitCategorie, local: produit_id, foreign: categorie_id }

Categorie:
..columns:
….id: { type: integer(20), primary: true, notnull: true, unique: true }
..relations:
….Produits: { class : Produit, foreignAlias: Categorie, refClass: ProduitCategorie, local: categorie_id, foreign: produit_id }

ProduitCategorie:
..columns:
..categorie_id: { type: integer(20), primary: true, notnull: true, unique: false }
….produit_id: { type: integer(20), primary: true, notnull: true, unique: false }
..relations:
….Categorie: { local: categorie_id, foreign: id, foreignAlias: Categorie }
….Produit: { local: produit_id, foreign: id, foreignAlias: Produit }

N.B.: il y a quelques différences du fait de modifications entre temps (pluriel/singulier, etc), mais on fait ce qu’on veut en tout point, ne vous concentrez donc pas sur les incohérences entre mes deux billets, ce n’est qu’un exemple réalisé en deux temps 😉

Salut à toutes et à tous,

Epsilon est une lettre grec qui signifie en mathématique un élément très petit. Dans 1984, il signifie la plus basse caste de la hiérarchie des Hommes. Entre autres significations pleines de sens

Ce petit billet donc pour vous présenter l’avancée actuelle du projet d’aide à la modélisation de schémas pour sfDoctrine, plug-in de Symfony (cadre applicatif Php5) qui le ponte à Doctrine (Objet-Relational Mapper en Php5).

Cet outil repose, vous l’aurez deviné, fervants lecteurs de mon blog, exclusivement sur des outils open-source que sont Eclipse et ses plug-ins EMF et Epsilon.

Petites explications avant tout : Doctrine est un ORM qui permet de relater un objet en une table relationnel, et vice-versa. Ceci permet d’écrire des logiciels à l’aide du paradigme « objet » tout en utilisant les meilleurs techniques de persistances que sont les Relational DataBase Management Systems (RDBMS ou SGDBR, type MySQL ou PostgreSQL dans l’univers open-source).

Il y a donc des classes, comme on parle de type de choses, puis ces choses à proprement parlé.

Dans le monde de sfDoctrine, on parle donc de Classes et d’Objets, qui sont les instances de classes, de la même manière qu’on parle de « voitures » et de « ma voiture », ou de types de choses et des choses en rapport à ces types. « Typiquement », une « voiture » (la mienne par exemple) est une instance de la classe « Voiture ».
Dans les modèles suivants, les boites jaunes représentes donc des types (ou des classes, techniquement) et les liens représentes ces « relations ».
On pourra par la suite « instancier » ces types de choses (comme on le verra avec la classe Class qui permettra de définir, en l’instanciant par deux fois, les « Classes » Produit et Categorie-). Il est hyper important de comprendre qu’on parle à différents moments dans différents niveaux d’abstraction. Voyez le contexte du vocable pour pouvoir vous repérer. Si ambiguïté, dites le moi en commentaire, j’expliciterai ou corrigerai au besoin.

sfDoctrine relate ces deux univers (objet et relationnel) en les mixant au sein d’un même type d’objet, appelé Class (qui est différent de celui du paradigme objet, par « extension » abstraite : on saute de niveau d’abstraction, l’un permettant à l’autre d' »exister » -ou plutôt d’être défini comme on l’entend-).

Trêve de bla-bla, voici une capture d’écran d’un simple méta-modèle de sfDoctrine réalisé à titre d’exemple :

Méta-modèle de sfDoctrine, diagrame d'entré

On voit ici qu’un modèle permet de définir des classes, et que les deux sont des choses nommés, elles ont obligatoirement un nom.

Voici maintenant le diagramme du méta-modèle des « columns » qui est partie intégrante du méta-modèle ci-dessus (qu’on peut voir en tant que « package » ou « dossier » nommé « Columns ») :

Description de la EClass Column d'une EClass Class

Description de la EClass Column d'une EClass Class

On voit ici qu’une classe possèdes 0 à plusieurs colonnes, et qu’une colonne est elle aussi une chose obligatoirement nommée, en plus de posséder obligatoirement un type qui doit avoir un max de défini.

Et enfin le diagramme des « Relations » :

Méta-modèle des Relations d'une Class

On voit ici qu’une classe peut posséder 0 ou plusieurs relations, relation (en italique, soulignant que c’est « abstrait » -mais à ce niveau-ci d’abstraction-) qui peut être de trois sortes:
une relation de type mono-valuée OneToOne ou un à un, qui permet de dire, par exemple, qu’une voiture n’a qu’un moteur,
une relation de type multi-valué ou un à plusieurs, qui permet de dire, par exemple, qu’une voiture peut avoir plusieurs roues,
une relation de type multi-valué bi-partie ou plusieurs à plusieurs, qui permet de dire, par exemple, qu’un produit peut être lié à plusieurs catégories et qu’une catégorie peut être liée à plusieurs produits.

La big-picture du point de vue du phénomène « Class » :

Big-picture d'un point de vue de Class

Pour finir, et faire comprendre au lecteur ce que permet l’excellentissime plug-in Epsilon, voici une capture « finale » d’un modèle que l’on va créer ensemble ici, pas-à-pas, en usant d’assistance pour créer les colonnes ids, la classe ProduitCategorie ainsi qu’une relation m:m sur deux. Ce sera plus clair par la suite, vous verrez.

Simple modèle d'exemple

Ici, nous avons définis deux types d’objets que sont « Produit » et « Catégorie », nous avons dit que les deux possèdent une colonne nommée « id » (qui s’inscrit dans une perspective SGBDR donc) et qu’ils sont tous deux liés par une relation « ManyToMany », ou « plusieurs-à-plusieurs » comme dit plus haut (perspective « objet »).

Maintenant, pour aider dans la conception de (méta-)modèle, Epsilon fournit tout un ensemble de langages permettant d’écrire des outils de transformation, validation, d’assistance, de mélange, entre autres, qui sont utilisables ensuite par la personne créant un modèle.

Ici, et pour l’instant, j’ai créé trois outils d’assistance :

addIdentifierAsPrimaryKeyToClass pour les EClass Class, qui permet d’ajouter une colonne « id » à une classe en particulier ou à toutes les classes du modèle, si ces colonnes n’existent pas bien sûr (pas de doublon, vérification d’existence par condition),

createManyToManyReferenceClass pour les EClass ManyToMany (spécialisation de la EClass abstraite Relation), qui permet d’aider à créer une classe de cross-référence entre deux classes (sans doublon aussi, évidement) en ayant seulement précisé l’élément relaté par la relation m:m,

setClassPluralAndSingularProperties pour les EClass Class, qui permet de spécifier automatiquement les propriétés « plural » et « singular » qui sont de type EString et qui permettent de préciser les formes plurielle et singulier d’une EClass Class.

Allons dans le détail, et commençons simplement par le dernier qui est le plus simple :

Je créé un fichier modèle de mon méta-modèle (j’ouvre mon méta-modèle puis clique-droit sur l’EClass « Model » et « Create Dynamic Instance », je séléctionne le fichier qui supportera l’instance du modèle)

Première étape : créer une instance du modèle

puis je l’enregistre (n’importe où, peu importe).

Seconde étape : créer une classe d'objet

Vous remarquez que j’ai nommé mon « Model » avec la chaîne de caractère « EspaceDuSommeil ».

Une fois que j’ai cliqué, je me retrouve avec un « Model » ayant un « enfant » (child) de type Class qui ne porte pas de nom.
En cliquant dessus la fenêtre du bas nommée « Properties » (que l’on voit dans l’image ci-dessus en bas qui permet de voir « EspaceDuSommeil ») affiche les propriétés relatives à cet élément, ainsi il me permet de cliquer sur la propriété « Name » et d’y définir le « nom » de mon objet « Class » (si vous remontez jusqu’à la première image, vous remarquerez la relation d’héritage entre Named et Class, qui fait que Class « is_a » (est un) Named, Named voulant dire qu’il est nommé -j’aurai pu mettre Nameable pour dire qu’il peut être nommé, mais cela dépend des besoins, ici il me faut absolument un nom-). J’y saisi donc le mot « Produit », et voici :

Troisième étape : saisir le nom du type d'objet

On fait de même pour créer un type d’objet nommé « Catégorie », puis on va s’occuper de spécifier les propriétés « plural » et « singular » en cliquant seulement sur un petit bouton, grâce à mon script écrit en EWL, langage définit par le plug-in Epsilon :

Quatrième étape : cliquer sur l'assistant pour automatiquement définir les propriétés "plural" et "singular"

Remarquez que les champs « Plural » et « Singular » de la fenêtre de propriété sont vides. Pour l’instant.

Cinquième étape : se fouler le doigt en cliquant

Et tadam ! Voilà le résultat ! Notez qu’il n’y a qu’ajout d’un « s » final pour créer la forme plurielle. La forme singulière est une copie du nom de la classe, simplement. Après, c’est open-source, donc qui veut modifier le comportement n’a qu’à apprendre et modifier selon ses nécessitées.

Pour comprendre, pour qui voudra, voici le script qui permet de réaliser cela :

wizard setClassPluralAndSingularProperties {

guard : self.isTypeOf(Class) and self.name.size() > 0

title : ‘Set plural and singular properties of ‘ + self.name

do {
self.plural := self.generatePluralProperty();
self.singular := self.generateSingularProperty();
}
}

wizard setModelClassesPluralAndSingularProperties {

guard : self.isTypeOf(Class)

title : ‘Set plural and singular properties of classes (Model-wide)’

do {
for( class in Class.allInstances() )
{
class.plural := class.generatePluralProperty();
class.singular := class.generateSingularProperty();
}
}
}

operation Class generatePluralProperty () : String {
return self.name + ‘s’;
}

operation Class generateSingularProperty () : String {
return self.name;
}

Comme vous l’aurez sûrement remarqué, j’ai codé deux « wizards » (assistants) différents, l’un ne se rapproche qu’à la classe séléctionnée lors du clique droit, l’autre s’occupe de définir ce qu’il y a à définir poru toutes les classes (à l’aide de la boucle for(… in …){}).

Simple, non -je parle aux développeurs ici ;-D- ?

Voyons un peu plus complexe comme script, celui permettant d’ajouter une colonne « id » aux classes et qui s’utilise exactement de la même manière -séléction de la classe en question puis clique-droit->wizards-> »add Id… for XXX » ou [..]model-wide »:

wizard addIdentifierAsPrimaryKeyToClass {

guard : self.isTypeOf(Class) and not self.column.exists(c|c.name.matches(‘id’))

title : ‘Add an Id column as primary key to ‘ + self.name

do {

self.column.add( self.generateIdPKUnikColumn() );
}
}

wizard addIdentifiersAsPrimaryKeyToClasses {

guard : self.isTypeOf(Class)

title : ‘Add an Id column as primary key to classes (Model-wide)’

do {
for( class in Class.allInstances() )
{
if( not class.column.exists(c|c.name.matches(‘id’) ) )
{
class.column.add( class.generateIdPKUnikColumn() );
}
}
}
}

operation Class generateIdPKUnikColumn () : Column {
var idcolumn : Column;
idcolumn := Column.createInstance();
idcolumn.name := ‘id’;
idcolumn.primary := true;
idcolumn.unique := true;
var idtype : Model!Integer;
idtype := Model!Integer.createInstance();
idtype.max = 20;
idcolumn.type = idtype;
return idcolumn;
}

Et voilà !

Sixième étape : lâcher de clique pour ajout de la colonne 'id' à toutes les classes

Tadam :

Septième étape : apprécier le résultat :-D

Maintenant, pensez au nombre de manipulations gagnés lorsqu’il y a 10,50,100 classes ? 🙂
Les affictionados de Doctrine me diront : imbécile, Doctrine n’a pas besoin qu’on spécifie la colonne id ! Oui, certes, mais ce n’est qu’un exemple d’utilisation ;-P

Voyons autrement plus complexe, la création en un clique d’une classe de référence pour une association m:m :

Code:

wizard createManyToManyReferenceClass
{
guard : self.isTypeOf(ManyToMany) and self.relates.isDefined()

title: ‘Create ManyToMany\’s Reference Class between ‘ + self.owner.name + ‘ and ‘ + self.relates.name

do
{
self.owner.definedIn.defines.add( self.createReferenceClass() );
}
}

operation ManyToMany createReferenceClass() : Class {
self.name := self.relates.name;

–define variable container for reference Class of ManyToMany.refClass assigned to both ManyToMany sides
var refClass : Class;
refClass := self.owner.definedIn.defines.selectOne(c|c.name.matches(
self.owner.name.firstToUpperCase() + self.relates.name.firstToUpperCase()
)
);
if( not refClass.isDefined() )
{
refClass := Class.createInstance();
refClass.name := self.owner.name.firstToUpperCase() + self.relates.name.firstToUpperCase();
}

–defines self.owner refClass reference
self.refClass := refClass;

–define variable containers for columns pks
var a : Column;
var b : Column;
a := Column.createInstance();
b := Column.createInstance();
a.name := self.relates.getIdColumnName();
b.name := self.owner.getIdColumnName();
a.primary := true;
b.primary := true;

–define variable containers for OneToMany holders in refClass
var aR : OneToMany;
var bR : OneToMany;
aR := OneToMany.createInstance();
bR := OneToMany.createInstance();
aR.relates := self.relates;
bR.relates := self.owner;
aR.name := self.relates.name;
bR.name := self.owner.name;

–add columns and relations to refClass
refClass.column.add(a); refClass.column.add(b); refClass.relation.add(aR); refClass.relation.add(bR);

–add ManyToMany relation to related class
var rC : ManyToMany;
rC := ManyToMany.createInstance();
rC.owner := self.relates;
rC.name := self.owner.name;
rC.refClass := refClass;
rC.relates := self.owner;
self.relates.relation.add(rC);

return refClass;
}

operation Class getIdColumnName() : String {
return self.name.toLowerCase() + ‘_id’;
}

operation Class getPluralName() : String {
return self.name + ‘s’;
}

Création d'une relation m:m entre Produit et Catégorie

Je définie comme nécessaire la propriété « relates » qui me permet d’appeler par la suite le script d’automation :

Définition de la classe relaté par la propriété m:m de la classe Produit comme étant Catégorie

Puis j’appel le script qui s’occupe de tout définir pour moi, pour autant que cela me convienne :

Dernière étape : cliqueeerrrrr

Appréciez VOTRE travail :

La classe de cross-référence est créée automatiquement, la classe cross-référencée Categorie est automatiquement modifiée en conséquence ainsi que la classe Produit et leurs relations m:m en regard

La m:m de Categorie/Produit automatiquement créée

La classe de cross-référence entre Produit et Categorie automatiquement créée et ses propriétés

Tiens, j’ai oublié de coder l’ajout des formes plurielles et singulières, qu’à cela ne tienne, on modifie le code et c’est repartie !
En modifiant le script, je me rend compte que j’ai oublié de faire pas mal de checks sur l’existence d’élements, ce qui implique que si on exécute deux fois cet assistant, il crééra d’autant de classes de référence, c’est un comportement non désiré : hop on modifie en conséquence :

wizard createManyToManyReferenceClass
{
guard : self.isTypeOf(ManyToMany) and self.relates.isDefined()

title: ‘Create ManyToMany\’s Reference Class between ‘ + self.owner.name + ‘ and ‘ + self.relates.name

do
{
self.owner.definedIn.defines.add( self.createReferenceClass() );
}
}

operation ManyToMany createReferenceClass() : Class {
self.name := self.relates.name;

–define variable container for reference Class of ManyToMany.refClass assigned to both ManyToMany sides
var refClass : Class;
refClass := self.owner.definedIn.defines.selectOne(c|c.name.matches(
self.owner.name.firstToUpperCase() + self.relates.name.firstToUpperCase()
)
);
if( not refClass.isDefined() )
{
refClass := Class.createInstance();
refClass.name := self.owner.name.firstToUpperCase() + self.relates.name.firstToUpperCase();
}

–set plural and singular forms for refClass
refClass.plural := self.owner.getPluralName() + self.relates.getPluralName();
refClass.singular := self.owner.name + self.relates.name();

–defines self.owner refClass reference
self.refClass := refClass;

–define variable containers for columns pks
var a : Column;
var b : Column;
var aToAdd: Boolean;
var bToAdd: Boolean;

aToAdd = false; bToAdd = false;
–try to get existing columns
a := refClass.column.selectOne(c|c.name.matches(self.relates.getIdColumnName()));
b := refClass.column.selectOne(c|c.name.matches(self.owner.getIdColumnName()));

–if none exist, create them and mark watchdogs for further add
if( not a.isDefined())
{
a := Column.createInstance();
a.name := self.relates.getIdColumnName();
aToAdd := true;
}
if( not b.isDefined())
{
b := Column.createInstance();
b.name := self.owner.getIdColumnName();
bToAdd  := true;
}
a.primary := true;
b.primary := true;

–define variable containers for OneToMany holders in refClass
var aR : OneToOne;
var bR : OneToOne;
var aRtoAdd: Boolean;
var bRtoAdd: Boolean;
aRtoAdd := false; aRtoAdd := false;
–try to get existing relations
aR := refClass.relation.selectOne(r|r.name.matches(self.relates.name));
bR := refClass.relation.selectOne(r|r.name.matches(self.owner.name));

–if none exists, create them and mark watchdogs for further add
if( not aR.isDefined()){
aR := OneToOne.createInstance();
aR.name := self.relates.name;
aRtoAdd := true;
}
if( not bR.isDefined()){
bR := OneToOne.createInstance();
bR.name := self.owner.name;
bRtoAdd := true;
}
aR.relates := self.relates;
bR.relates := self.owner;

–add columns and relations to refClass only if watchdog says to do so
if(aToAdd){ refClass.column.add(a); }
if(bToAdd){ refClass.column.add(b); }
–relations
if(aRtoAdd){ refClass.relation.add(aR); }
if(bRtoAdd){ refClass.relation.add(bR); }

–add ManyToMany relation to related class
var rC : ManyToMany;
var rCtoAdd: Boolean;
rCtoAdd := false;
rC := self.relates.relation.selectOne(r|r.name.matches(self.owner.name));
if(not rC.isDefined())
{
rC := ManyToMany.createInstance();
rC.name := self.owner.name;
rCtoAdd := true;
}
rC.owner := self.relates;
rC.refClass := refClass;
rC.relates := self.owner;

–add relation to refClass only if needed
if(rCtoAdd){ self.relates.relation.add(rC); }

return refClass;
}

operation Class getIdColumnName() : String {
return self.name.toLowerCase() + ‘_id’;
}

operation Class getPluralName() : String {
return self.name + ‘s’;
}

j’ai mis en gras autant que j’ai pu les différences.

Et voilà, on aura beau lancer plusieurs fois de suite l’assistant, il ne crééra pas plusieurs classes de références poluantes 😉

On finira avec une capture d’écran de la classe de cross-référence :

OneToOne relation de la classe de référence entre celle-ci et Produit

« Et voilà ! », il ne reste plus qu’à écrire un transformateur de modèle à modèle (EGL, Epsilon Generative Language)!

Mais avant ça, il sera bien utile d’écrire un validateur de modèle (EVL, Epsilon Validation Language) !

Comme vous l’aurez compris, le tout pourra être écrit à l’aide du plug-in Epsilon !

Alors, le développement dirigé par les modèles, C’EST PAS DE LA BALLE ? 😀

Le prochain billet (« Les Epsilons endoctrinés génèrent pour se libérer ») portera sur la génération de code : transformer le modèle dans sa syntaxe abstraite en un fichier concret (« ./config/doctrine/schema.yml »), qui, on le verra, sera utilisé par sfDoctrine/Symfony pour générer des classes Php5 de liaison Objet-Table Relationnel, mais aussi, dans la même perspective, qui permettra de générer des outils de gestion type CRUD en quelques lignes de commandes (voir symfony-project.org et son tutoriel Jobeet pour Doctrine).

Enfin je tiens à remercier particulièrement d’abord les « teams » Eclipse, EMF et GMF, entre autres et surtout Dimitris Kolovos qui est l’initiateur de l’outil Epsilon. Sans quoi tout ça ne serai possible !

N.B.: la réalisation du méta-modèle, du modèle d’exemple, des scripts et de ce billet ont nécessités un peu plus de 4h.
Pensez à relativiser le temps pris lorsqu’on connait l’outil, que le transformateur modèle->code (schema.yml), le validateur de modèle et autres assistants seront réalisés quant à la réalisation d’un schéma complexe pour sfDoctrine…
Relativiser tout ça quant à l’écriture d’applications web via l’ingénierie par les modèles (perte de temps dans la résolution de bugs, d’écriture du code en suivant les règles de coding, etc). C’est tout simplement monstrueux comme capitalisation (je parle bien d’un capital HUMAIN, pas spéculatif).

A bon entendeur 😉

(copyleft until credits) Texte et images Stéphane Erard.