Archives mensuelles : avril 2013

MongoDB : les bases pour bien débuter (3/3)

Revenons sur la collection MongoDB qui nous sert d’exemple depuis le début et effectuons quelques opérations de mise à jour élémentaires. Voici donc pour rappel à quoi ressemble notre collection :

sebastien.ferrandez@sebastien:~$ mongo
MongoDB shell version: 2.0.6
connecting to: test
> show dbs;
admin   0.203125GB
gens    0.203125GB
local   (empty)
> use gens;
switched to db gens
> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

mongo-db-logo

Opérations de mise à jour

Avec inc

Supposons qu’il nous faille mettre à jour l’âge d’une personne : aujourd’hui c’est l’anniversaire de Maïa, elle a 7 ans ! Voici plusieurs façons de faire : tout d’abord nous incrémentons la clé age de nos documents pour lesquels age est supérieur à 5.

db.gens.update({ age:{$gt: 5}}, {$inc: {age: 1}});

Avec set

Nous ciblons uniquement le prénom

db.gens.update({ prenom: "maïa"}, {$set: {age: 7}});

Nous faisons un mélange des deux précédentes requêtes :

db.gens.update({ age:{$gt: 5}}, {$set: {age: 7}});

Opérations de suppression

Suppression par ObjectId

> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517e33cf4937f8d068f9e9aa"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 7 }
> db.gens.remove( {"_id": ObjectId("517e33cf4937f8d068f9e9aa")});
> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }

Suppression par champ quelconque

Supprimons tous les gens qui s’appellent « ferrandez » :

> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
> db.gens.remove({nom: 'ferrandez'});
> db.gens.find();

Notez que cette fois-ci je n’ai pas mis de guillemets autour de la clé (nom) et j’ai mis des apostrophes autour du nom pour que vous voyez bien qu’il n’est pas obligatoire de mettre tout ça entre guillemets systématiquement !

Suppression de la première occurrence seulement

En mettant justOne à 1 (le premier paramètre), seul le premier document satisfaisant aux critères sera supprimé :

> db.gens.find();
{ "_id" : ObjectId("517e36c14937f8d068f9e9ab"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 7 }
{ "_id" : ObjectId("517e36e54937f8d068f9e9ac"), "nom" : "ferrandez", "prenom" : "sébastien" }
> db.gens.remove({nom: 'ferrandez'}, 1);
> db.gens.find();
{ "_id" : ObjectId("517e36e54937f8d068f9e9ac"), "nom" : "ferrandez", "prenom" : "sébastien" }

Suppression de l’intégralité des documents d’une collection

> db.gens.remove();

Voilà ! Pour ceux d’entre vous qui ont déjà des connaissances en langage SQL, vous avez les bases pour débuter avec MongoDB. Nous allons bientôt rentrer en détail dans le fonctionnement de MongoDB et en particulier nous attarder sur l’aspect dénormalisation !

MongoDB : les bases pour bien débuter (2/3)

Dans le billet précédent, nous avons vu quelques unes des requêtes DDL nous permettant de manipuler les structures élémentaires de MongoDB, comme les bases de données, les collections et les documents. L’heure est maintenant venue de requêter sur nos données !

Le kit MongoDB, qu'on reçoit à toutes les confs (source http://xenodesystems.blogspot.mx/)

Le kit MongoDB, qu’on reçoit à toutes les confs (source http://xenodesystems.blogspot.mx/)

Notre jeu de données de démarrage

Il est très simple :

> db.gens.insert({"nom":"ferrandez"});
> db.gens.insert({"nom":"ferrandez", prenom: "léo"});
> db.gens.insert({"nom":"ferrandez", prenom: "maïa", age:6});
> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

MongoDB est schema-less (prononcez « skimaless »), nous pouvons donc très bien insérer des documents qui n’ont pas la même structure, regardez bien nos trois documents, ils ne comportent pas le même nombre de champs !

Poser des index

Vous le savez, pour que vos requêtes aient un temps d’exécution satisfaisant (surtout si elle sont appelées fréquemment), il est absolument nécessaire qu’elles s’appuient sur des index. MongoDB dispose d’index au niveau des collections (l’équivalent des tables en SQL). Le champ _id est indexé par défaut et il est impossible de le supprimer.

Sur un seul champ

Pour poser un index sur le champ nom dans l’ordre ascendant (1, ce sera -1 pour DESC) :

db.gens.ensureIndex({nom:1});

Les index composés

Pour poser un index sur les champs nom ET prenom (ASC tous les deux) :

db.gens.ensureIndex({nom:1, prenom:1});

Comme en SQL, si vos requêtes se basent sur prenom, l’index ne sera pas utilisé !
Si vous requêtez sur le nom ou sur le nom ET le prénom, celui-ci sera sollicité.

Les index uniques

Rien de plus simple :

db.gens.ensureIndex({nom:1}, {unique: true});

Ici en réalité la pose de cet index va échouer car notre champ nom contient les mêmes valeurs :

> db.gens.ensureIndex({nom:1}, {unique: true});
E11000 duplicate key error index: gens.gens.$nom_1  dup key: { : "ferrandez" }

Voir les index d’une collection

db.gens.getIndexes();
[
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "ns" : "gens.gens",
                "name" : "_id_"
        },
        {
                "v" : 1,
                "key" : {
                        "nom" : 1
                },
                "ns" : "gens.gens",
                "name" : "nom_1"
        },
        {
                "v" : 1,
                "key" : {
                        "nom" : 1,
                        "prenom" : 1
                },
                "ns" : "gens.gens",
                "name" : "nom_1_prenom_1"
        }
]

Supprimer un index

On va supprimer l’index nommé ‘nom_1_prenom_1’ :

> db.gens.dropIndex('nom_1_prenom_1')
{ "nIndexesWas" : 3, "ok" : 1 }
> db.gens.getIndexes();
[
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "ns" : "gens.gens",
                "name" : "_id_"
        },
        {
                "v" : 1,
                "key" : {
                        "nom" : 1
                },
                "ns" : "gens.gens",
                "name" : "nom_1"
        }
]

Nous voyons qu’il a été effectivement supprimé (et nous voyons aussi au passage qu’un index est posé par défaut par MongoDB sur _id).

Les requêtes

Sur une seule valeur

Cherchons toutes les personnes qui ont 6 ans :

> db.gens.find({age:6});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Cherchons toutes les personnes qui s’appellent « ferrandez » :

> db.gens.find({nom:"ferrandez"});
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Sur deux valeurs

Cherchons toutes les personnes qui ont 6 ans et qui s’appellent « ferrandez » :

> db.gens.find({nom:"ferrandez", age:6});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Avec des opérateurs de comparaison

Cherchons tous les « ferrandez » qui ont plus de 6 ans (gt = greater than)

> db.gens.find({nom:"ferrandez", age: {$gt: 6}});

Cherchons tous les « ferrandez » qui ont moins de 6 ans (lt = less than)

> db.gens.find({nom:"ferrandez", age: {$lt: 6}});

Ces deux requêtes ne ramènent aucun résultat, ce qui n’est pas le cas des suivantes avec l’égalité…

> db.gens.find({nom:"ferrandez", age: {$lte: 6}});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }
> db.gens.find({nom:"ferrandez", age: {$gte: 6}});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

L’équivalent du IN SQL

Les gens dont l’âge est 6, 7 ou 8 :

> db.gens.find({age: {$in: [6, 7, 8]}});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Les expressions régulières (regexp)

Les gens dont le nom commence par la lettre f :

> db.gens.find({nom:/^f/});
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

MongoDB : les bases pour bien débuter (1/3)

Le but de ce billet est d’effectuer une première incursion dans l’univers MongoDB en voyant les commandes de bases dans le shell mongo et en faisant un parallèle avec ce qui existe dans le monde du SQL. Nous allons commencer en manipulant les bases : bases de données, collections et documents. Le but n’est pas de se noyer d’emblée dans l’architecture interne de Mongo (le sharding, les replica sets, etc.) mais d’attaquer d’emblée des choses concrètes !

mongo_logo

L’emblème de MongoDB

Installation de MongoDB sur GNU Linux Debian
Nous allons faire les choses proprement au lieu d’insérer à la sauvage dans le fichier sources.list. Vous devez évidemment être sudoer pour effectuer ceci :

echo 'deb http://downloads-distro.mongodb.org/repo.fr.ian-sysvinit dist 10gen' > /tmp/mongodb.list
sudo cp /tmp/mongodb.list /etc/apt/sources.list.d/
rm /tmp/mongodb.list

sudo apt-get update
sudo apt-get install mongodb-10gen

A l’issue de l’installation, le serveur MongoDB doit être démarré automatiquement (le daemon mongod pour être plus précis).

Les principales commandes

Lancer le shell

Tout part de là ! En tapant « mongo » vous devriez y accéder car /bin doit être dans votre PATH. Si vous rencontrez des problèmes, lancez

/usr/bin/mongo

Quitter le shell

Rien de plus simple: faites donc Ctrl+C (ce bon vieux SIGINT !) ou tapez exit à l’invite, vous sortirez du shell.

Obtenir le numéro de version

Les données concernant votre installation apparaissent sous la forme d’un objet JSON, la commande a exécuter est en gras :

> db.runCommand({buildinfo: 1});
{
        "version" : "2.0.6",
        "gitVersion" : "nogitversion",
        "sysInfo" : "Linux z6 3.8-trunk-amd64 #1 SMP Debian 3.8.3-1~experimental.1 x86_64 BOOST_LIB_VERSION=1_49",
        "versionArray" : [
                2,
                0,
                6,
                0
        ],
        "bits" : 64,
        "debug" : false,
        "maxBsonObjectSize" : 16777216,
        "ok" : 1
}

Lister les bases de données

La commande permettant de faire ça est show dbs.

sebastien.ferrandez@sebastien:~$ mongo
MongoDB shell version: 2.0.6
connecting to: test
> show dbs;
local   (empty)

Comme je viens d’installer MongoDB, rien d’étonnant à ce qu’aucune base de données n’apparaisse ! Il est possible d’obtenir davantage d’informations en utilisant la base de données admin dont l’usage est réservé comme son nom l’indique à l’admin.

> use admin
switched to db admin
> db.runCommand({listDatabases: 1});
{
        "databases" : [
                {
                        "name" : "local",
                        "sizeOnDisk" : 1,
                        "empty" : true
                }
        ],
        "totalSize" : 0,
        "ok" : 1
}

Créer une collection

La collection en langage MongoDB est l’équivalent de la table en relationnel, on les crée soit en y insérant le tout premier document (l’équivalent du tuple en relationnel) soit en exécutant la commande suivante :

> db.createCollection('gens');
{ "ok" : 1 }

Insérer un document dans une collection

Notre collection s’appelle gens, insérons-y un premier document :

db.gens.insert( { nom: "ferrandez", prenom:"sebastien", age: 35 } );

Lister les documents contenus dans une collection

Pour faire l’équivalent d’un SELECT * FROM gens, j’utilise la commande find sans paramètres :

> db.gens.find();
{ "_id" : ObjectId("517941f3b12e1948c04f6d5e"), "nom" : "ferrandez", "prenom" : "sebastien", "age" : 35 }

Insérons un nouveau document et observons le changement dans le find :

> db.gens.insert( { nom: "ferrandez", prenom:"sandrine", age: 34, sexe:"F" } );
> db.gens.find();
{ "_id" : ObjectId("517941f3b12e1948c04f6d5e"), "nom" : "ferrandez", "prenom" : "sebastien", "age" : 35 }
{ "_id" : ObjectId("51794334b12e1948c04f6d5f"), "nom" : "ferrandez", "prenom" : "sandrine", "age" : 34, "sexe" : "F" }

Notez que, contrairement à une table, le nombre de champs n’est pas contraint par l’intention du schéma, en effet mon deuxième document a un champ de plus que le premier : sexe.
Dans les objets JSON que j’enregistre, un champ _id est déterminé par MongoDB. Il est évidemment tout à fait possible de forcer le sien :

> db.gens.insert( { _id: 10, nom: "ferrandez", prenom:"christophe", age: 40 } );
> db.gens.find();
{ "_id" : ObjectId("517941f3b12e1948c04f6d5e"), "nom" : "ferrandez", "prenom" : "sebastien", "age" : 35 }
{ "_id" : ObjectId("51794334b12e1948c04f6d5f"), "nom" : "ferrandez", "prenom" : "sandrine", "age" : 34, "sexe" : "F" }
{ "_id" : 10, "nom" : "ferrandez", "prenom" : "christophe", "age" : 40 }

Il est possible de manipuler des identifiants auto-incrémentés mais si vous insistez pour utiliser ce mécanisme offert par certains SGBDR comme MySQL c’est peut-être que vous avez une vision trop « relationnelle » de vos données ! Si toutefois vous persistiez à vouloir bénéficier de ce mécanisme, vous trouverez le lien à la fin de ce billet.

Supprimer une collection

Il vous faut utiliser drop, comme en SQL !

> db.gens.drop();
true

Supprimer une base de données

Deux manières de procéder, en faisant un use pour se connecter à la base de données à effacer :

> show dbs;
admin   0.203125GB
gens    0.203125GB
local   (empty)
> use gens;
switched to db gens
> db.runCommand({dropDatabase: 1});
{ "dropped" : "gens", "ok" : 1 }
> show dbs;
admin   0.203125GB
local   (empty)

ou bien en exécutant directement :

> db.gens.runCommand({dropDatabase: 1});
{ "dropped" : "gens", "ok" : 1 }

Pour aller plus loin…

Au sujet des identifiants auto-incrémentés : La documentation MongoDB (anglais)

Doctrine : les migrations avec le bundle DoctrineMigrationsBundle (3/3)

Voici la dernière partie de mon billet concernant les migrations Doctrine et le bundle associé.

Les relations des entités Doctrine

Nos élèves ont une relation avec leur filière. Nous en avons parlé au tout début lorsque nous définissions les cardinalités :

  • un élève a une seule filière
  • une filière a 0 ou plusieurs élèves

doctrine
Il va nous falloir définir ceci au niveau de nos entités Doctrine. Nous allons donc « tirer un fil » entre nos entités Eleve et Filiere. Doctrine a des moyens de mettre ça en place et nous allons voir comment ceci se matérialise avec les annotations, que nous avons choisi de privilégier lorsque nous avons généré nos entités.

Entre Eleve et Filiere, il y a une relation de un à plusieurs (one to many en anglais). Nous la voulons bi-directionnelle dans le sens où nous souhaitons pouvoir à partir d’un élève récupérer sa filière et, à partir d’une filière, récupérer ses élèves. Dans l’entité Eleve, nous sommes du côté plusieurs de la relation (car plusieurs élèves correspondent à une filière). Voici comment nous allons nous servir des annotations Doctrine :

   /**
     * @ORM\ManyToOne(targetEntity="Filiere", inversedBy="eleves")
     * @ORM\JoinColumn(name="filiere_id", referencedColumnName="id", nullable=false)
     **/
    private $filiere;

Nous définissons une variable d’instance privée filiere qui va aider Doctrine à stocker la filière d’un élève (il n’y en a qu’une possible, souvenez-vous). La cible de la relation que nous sommes en train de décrire dans l’entité Eleve est l’entité Filiere, c’est ce que nous dit targetEntity. Le inversedBy qui suit signifie que de l’autre côté de la relation (dans l’entité Filiere) nous trouverons l’attribut chargé de gérer cette relation sous le nom d’eleves (notez bien le pluriel car de l’autre côté de la relation nous aurons plusieurs élèves pour notre filière). JoinColumn décrit la manière physique dont cette relation va exister. En lisant, vous devinez que dans notre table nous allons avoir un champ rajouté suite à l’écriture de cette relation qui va identifier une filière pour un élève, que ce champ s’appellera filiere_id et qu’il référencera une colonne appelée id…Vous voyez déjà venir gros comme une maison la contrainte d’intégrité référentielle. Le nullable=false signifie qu’il faut obligatoirement une filière à un élève ; là aussi vous devez imaginer que cette clause entraînera la présence d’un NOT NULL dans le schéma relationnel.

Du côté de l’entité Filiere, nous avons l’annotation Doctrine suivante pour la variable d’instance eleves dont nous avons déjà parlé :

     /**
     * @ORM\OneToMany(targetEntity="Eleve", mappedBy="filiere")
     **/
    private $eleves;

    public function __construct()
    {
        $this->eleves = new ArrayCollection;
    }

Nous prenons la relation dans l’autre sens – « un vers plusieurs » – parce qu’une filière a plusieurs élèves. Voilà pourquoi cette fois c’est one to many en lieu et place du many to one qu’on trouvait dans Eleve. La cible, c’est bien Eleve et l’attribut qui va servir c’est bien filiere (private $filiere). Dans Eleve nous avions inversedBy, dans Filiere nous avons mappedBy. Doctrine détermine le côté « possesseur » de la relation par inversedBy et l’autre côté (« inverseur ») par mappedBy.

Comme nous avons potentiellement plusieurs élèves pour une filière, nous allons avoir à gérer plusieurs objets. Pour ce faire, Doctrine nous propose son type ArrayCollection, que vous allez pouvoir utiliser en faisant :

use Doctrine\Common\Collections\ArrayCollection;

Notre variable d’instance privée eleves sera de ce type et nous l’initialiserons dans le constructeur de notre classe.

Nous avons construit notre première relation au niveau objet ! L’heure est venue de mettre notre schéma de bases de données à jour avec tout cela ! Allons-y !

app/console doctrine:migrations:diff

Ouvrons le fichier ainsi généré pour vérifier que notre relation est bien en place :

$this->addSql("ALTER TABLE eleve ADD filiere_id INT NOT NULL");
$this->addSql("ALTER TABLE eleve ADD CONSTRAINT FK_ECA105F7180AA129 FOREIGN KEY (filiere_id) REFERENCES filiere (id)");
$this->addSql("CREATE INDEX IDX_ECA105F7180AA129 ON eleve (filiere_id)");

Ce que nous envisagions est effectivement arrivé, nous avons bel et bien une contrainte d’intégrité référentielle posée entre la table eleve et la table filiere !

Lançons maintenant la migration :

app/console doctrine:migrations:migrate

Notre table eleve a bien un champ en plus : filiere_id !

sebastien.ferrandez@sebastien:~/migrations$ mysql -u root -p -e 'use exercice; desc eleve'
Enter password: 
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | int(11)     | NO   | PRI | NULL    | auto_increment |
| nom        | varchar(40) | NO   |     | NULL    |                |
| prenom     | varchar(40) | NO   |     | NULL    |                |
| age        | int(11)     | NO   |     | NULL    |                |
| filiere_id | int(11)     | NO   | MUL | NULL    |                |
+------------+-------------+------+-----+---------+----------------+

Occupons-nous ensuite des cours: un cours a des relations avec salle, enseignant et filière, en fait cours est au centre de tout…Quelles sont-elles exactement ? Reprenons ce que nous avions établi :

  • une filière est composée d’un ou plusieurs cours
  • un cours n’a qu’un seul et unique enseignant mais un enseignant a un ou plusieurs cours
  • un cours se tient dans une seule salle à un moment donné et une salle peut abriter plusieurs cours. On peut très bien créer un cours sans salle car il arrive qu’on ne puisse décider de suite dans quelle salle il se tiendra. Elle sera rajoutée plus tard.

Les relations nous apparaissent évidentes. Un lien de type « un à plusieurs » existe entre cours et enseignant (« un à plusieurs » dans le sens enseignant → cours et « plusieurs à un » dans le sens inverse). Il en va de même pour les salles ou les filières. La seule différence réside en le fait qu’un cours a forcément un enseignant ou une filière alors qu’il n’a pas forcément de salle (on peut le créer sans salle pour lui en affecter une quand on a consulté le planning des salles et qu’on en a trouvé une de disponible). Nous allons donc nous retrouver à ajouter des annotations de type ManyToOne dans notre entité Cours.

    /**
     * @ORM\ManyToOne(targetEntity="Filiere", inversedBy="cours")
     * @ORM\JoinColumn(name="filiere_id", referencedColumnName="id", nullable=false)
     **/
    private $filiere;

    /**
     * @ORM\ManyToOne(targetEntity="Salle", inversedBy="cours")
     * @ORM\JoinColumn(name="salle_id", referencedColumnName="id")
     **/
    private $salle;

    /**
     * @ORM\ManyToOne(targetEntity="Enseignant", inversedBy="cours")
     * @ORM\JoinColumn(name="enseignant_id", referencedColumnName="id", nullable=false)
     **/
    private $enseignant;

Vous noterez que salle est le seul endroit où nullable est à sa valeur par défaut, c’est à dire true et qu’il sera donc possible d’enregistrer un cours sans salle. Dans la mesure où nous avons pour un cours soit 0 (pour une salle) soit un (enseignant, filière) inutile ici d’utiliser les ArrayCollection comme c’était le cas pour matérialiser la relation filière/élève.
Dans chacune de ces entités cibles nous auront une variable d’instance nommée cours, c’est aussi ce que nous disent ces annotations.

Dans l’entité Salle :

    /**
     * @ORM\OneToMany(targetEntity="Cours", mappedBy="salle")
     **/
    private $cours;

    public function __construct()
    {
        $this->cours = new ArrayCollection;
    }

Ici par contre une salle est susceptible d’accueillir plusieurs cours (NFA057 le jeudi soir, NFA053 le mardi), la notion de collection a ici du sens. Idem pour les enseignants :

    /**
     * @ORM\OneToMany(targetEntity="Cours", mappedBy="enseignant")
     **/
    private $cours;

    public function __construct()
    {
        $this->cours = new ArrayCollection;
    }

Ou encore Filiere :

    /**
     * @ORM\OneToMany(targetEntity="Cours", mappedBy="filiere")
     **/
    private $cours;

Vous avez remarqué que le champ mappedBy de l’annotation pointe vers la variable d’instance correspondante dans l’entité Cours. Dans le cas de Filiere, l’entité avait déjà une instanciation de la classe ArrayCollection dans son constructeur, nous devons en rajouter une autre pour les cours, comme fait dans les autres entités, plus haut, ce qui donnera :

public function __construct()
    {
        $this->cours = new ArrayCollection;
        $this->eleves = new ArrayCollection;
    }

Une fois que nous avons mis nos relations dans nos 4 entités, il nous reste à exécuter une migration !

app/console doctrine:migrations:diff
app/console doctrine:migrations:migrate

feront apparaître les contraintes d’intégrité référentielle entre la table cours et enseignant, salle et filiere.

Maintenant que nous avons établi notre architecture objet avec nos entités, regardons avec MySQL Workbench à quoi ressemble notre schéma et surtout s’il satisfait à toutes les contraintes que nous avons énoncées au début du présent document :

snapshot2

En naviguant à travers les relations, nous sommes sûrs que nous avons effectué du bon travail car elles correspondent en tous points à ce que nous souhaitions.

Au sujet des types de données utilisés par Doctrine, nous voyons qu’ils ne correspondent pas forcément à ce que nous recherchons en terme de plage de valeurs (des INTEGER pour gérer des âges – pas signés qui plus est -, des DATETIME alors que des TIMESTAMP, deux fois moins gourmands en espace disque, suffiraient). Il y a beaucoup à redire de ce côté là sur ce schéma mais sachez qu’il est toujours possible de forcer Doctrine à utiliser des types choisis par nos soins avant de passer une migration ! Il suffit d’en faire état dans les annotations de nos entités. Attention cependant, changer le type des données choisies par Doctrine peut faire que les changements d’état de votre schéma (de la colonne modifiée, pour être plus exact) ne sont pas pris en compte comme ils devraient l’être par Doctrine, qui ne retrouve plus ses types « natifs », puisque vous les avez modifiés.

Les 10 (mauvaises) habitudes du développeur

Récemment il m’est venu à l’idée, dans un but ludique évidemment, de compiler quelques-uns des défauts les plus couramment rencontrés chez les développeurs. Ce n’est pas une liste exhaustive ni même une charge en règle contre les miens, mais plutôt une espèce d’introspection, d’auto-critique. En effet, quel développeur aurait la prétention de dire qu’il n’a jamais été la victime, consentante ou pas, d’ au moins un des points listés ci-dessous ?

dirty-computer-keyboard-used-with-gloves

1 – Le test…en prod !

Les fonctionnalités sont testées à la va-vite dans un coin par le développeur avant d’être parachutées sans pitié en production. Après tout, on ne teste jamais mieux qu’en condition réelle, n’est-ce pas ? Le (mauvais) développeur n’a jamais entendu parler des tests unitaires, fonctionnels ou d’intégration. Pour lui, le test est un processus visant à démontrer que tout marche à merveille, qu’il a fait du bon travail ! Non, c’est justement tout le contraire : le test est un processus AGRESSIF ! Alors évidemment, des bugs vont survenir en production et le développeur ira les corriger directement sur le serveur, à grands coups de Ctrl+S ou bien, à peine moins sale, en passant autant de fois que nécessaire des branches de hotfixes !

2 – L’appétit vorace pour la salade de technologies

Très fréquente chez les jeunes développeurs et principalement dans le milieu du développement web. On utilise des outils à la mode, sans trop chercher à connaître leur niveau de maturité, leur fréquence de mise à jour ou leur résistance à de fortes charges, une fois déployés. Tout le monde en parle et ces technos font le buzz sur des forums très fréquentés, c’est forcément qu’il y a une raison : il nous FAUT les avoir !!! Le code finit par ressembler à une grande marmite dans laquelle on aurait jeté tous les ingrédients trouvé sur les étagères de la cuisine et à terme le code va se retrouver emprisonné dans un écosystème de développement trop hétérogène et qui n’est pas sans rappeler la jungle…C’est une dérive du principe DRY (Don’t Repeat Yourself); comme tout a (presque) déjà été fait, on se contente d’agréger des outils tierce-partie sans forcément en mesurer toutes les conséquences. Ne vous rendez pas inutilement dépendants des autres : si ça existe et que c’est éprouvé, prenez ! Sinon, prenez…le temps de le faire !

3 – Le code zombie (qui ne meurt jamais vraiment)

Même lorsque certains développeurs travaillent avec des outils de gestion des versions (SVN, Git), ils conservent la mauvaise habitude de commenter des pavés de code entiers, comme s’ils voulaient garder tout ça « au cas où »…Le vieux code n’est d’aucune utilité, et encore moins si vous utilisez du version control ! Vivez et laissez mourir !

4 – Le freinage technique

Le développeur qui est réfractaire au changement constitue un frein technique. Cette fossilisation des pratiques dans des temps reculés (le cambrien ?) est pénalisante pour le reste de l’équipe. Il le sait, mais l’évolution n’est pas la première de ses préoccupations : il a souvent connu l’époque des langages procéduraux et ne voit pas bien l’intérêt de toutes ces choses « nouvelles » qui sont selon lui des lubies de programmeurs inexpérimentés (« Les objets ne sont que des containers passifs de données », « Les classes ne servent à rien », « Les contraintes d’intégrité référentielles contraignent trop le schéma des données »…). Son adage favori ?

Ça marche, alors on ne touche pas !

5 – Le nommage inconsistant des variables

Le (mauvais) développeur ne voit pas trop ce qu’on gagne à donner des identifiants « parlants » à des variables. S’il pouvait toutes les appeler « variable », il le ferait volontiers ! S’il a besoin de matérialiser une assurance, il appellera sa variable « a » : a comme assurance, enfin ! Et puis si demain les cas d’utilisation mettent en évidence la nécessité d’avoir 3 assurances et bien qu’à cela ne tienne, il créera a2 et a3 ! J’ai vu de tout personnellement; des objets nommés o, des booléens nommés b et même des variables portant le nom des personnes qui les avaient créées…les identifiants de variable doivent être courts et évocateurs !

6 – L’absence d’optimisation

Quand on écrit une requête SQL, pourquoi se préoccuper du temps qu’elle prend à s’exécuter ? Elle est syntaxiquement correcte et fait (à peu près) le job et puis ça sert à quoi les ORM ? On leur fait aveuglément confiance et tout se passera bien, ils sont faits pour ça ! De même pour le cache web, c’est géré par une couche dédiée à ça dans mon framework favori alors pourquoi devrai-je en plus m’intéresser à tout cela, j’ai déjà bien assez de travail comme ça ! Attention à ce genre de négligences quand on travaille sur des environnements à fort taux de charge ! Ne vous contentez pas de tourner la clé du véhicule et de l’utilisez, ouvrez le capot pour tirer le maximum de bénéfices de vos outils et des bonnes pratiques de développement !

7 – Le refus catégorique de la documentation

Le (mauvais) développeur pense qu’il est bon et c’est bien là le drame ! Pour lui, il suffit de lire son (excellent) code pour que tout apparaisse soudainement évident aux yeux de ses camarades codeurs qui vont avoir à reprendre son travail. La documentation c’est compliqué, il faut mettre des mots sur ce que l’on a fait, détailler, éventuellement justifier des choix…Ne négligez pas cette étape, un développeur qui ne documente pas ce qu’il fait, c’est du savoir qui va potentiellement s’évaporer !

8 – Le copier/coller comme méthode de développement

La programmation par Ctrl+C/Ctrl+V est un mal qui sévit depuis toujours…A quoi bon réfléchir, il suffit de dupliquer les choses autant de fois qu’il sera nécessaire. Et la maintenance ? Et les 10 classes à passer en revue à la moindre modification ? L’abus de Ctrl+C/Ctrl+V fait perdre 10 fois plus de temps que ce qu’il en fait gagner, mais on ne s’en rend compte souvent que des mois ou des années après; cette façon de faire est une véritable bombe à retardement, génératrice de dette technique !

9 – L’obsession de la centralisation

Le (mauvais) développeur croit comprendre que, pour limiter les modifications à tout va quand le temps du changement sera venu, il suffit simplement de tout mettre au même endroit ! Ses classes sont des monolithes de 3000 lignes qui font tout à la fois.

10 – L’euphorie de la découverte

Elle se produit lorsque le développeur trouve de nouveaux outils; quand il découvre les classes abstraites, il veut en mettre de partout, idem quand il découvre l’héritage, les fonctions statiques ou les designs patterns. Lorsque la solution est trouvée avant le problème, rien de bon ne peut en résulter. Souvenez-vous de cet adage populaire :

Quand tout ce qu’on a comme outil c’est un marteau, on a tendance à voir tous les problèmes comme des clous

photo de clavier sale

On a les développements qu’on mérite !

Doctrine : les migrations avec le bundle DoctrineMigrationsBundle (2/3)

Maintenant que nous avons installé notre bundle dédié aux migrations des données via Doctrine, nous pouvons créer le bundle qui va servir de support à notre exercice et que nous allons appeler Scolarite car il est censé matérialiser le fonctionnement de notre université, la fabrique de développeurs ! Voici le résumé de la création de notre bundle en mode interactif :

Symfony/app/console generate:bundle
Bundle namespace: FabriqueDeDevs/ScolariteBundle
Bundle name [FabriqueDeDevsScolariteBundle]: 
Target directory [/home/sebastien.ferrandez/migrations/Symfony/src]: 
Configuration format (yml, xml, php, or annotation): yml
Do you want to generate the whole directory structure [no]? yes
You are going to generate a "FabriqueDeDevs\ScolariteBundle\FabriqueDeDevsScolariteBundle" bundle
in "/home/sebastien.ferrandez/migrations/Symfony/src/" using the "yml" format.

Generating the bundle code: OK
Checking that the bundle is autoloaded: OK
Confirm automatic update of your Kernel [yes]? 
Enabling the bundle inside the Kernel: OK
Confirm automatic update of the Routing [yes]? 
Importing the bundle routing resource: OK

doctrine-logo

Les cours à la fabrique de développeurs

Nous souhaitons modéliser de manière très simpliste la gestion des cours; des élèves s’inscrivent chez nous qui choisissent des filières composées d’un certain nombre de cours. Ces cours sont assurés par des enseignants dans des salles. Nous établissons les règles suivantes, de manière tout à fait arbitraire :

  • un élève appartient à une seule filière
  • une filière est composée d’un ou plusieurs cours
  • un cours n’a qu’une seule filière
  • un enseignant a un ou plusieurs cours
  • un cours n’a qu’un seul et unique enseignant mais un enseignant a un ou plusieurs cours
  • un cours se tient dans une seule salle à un moment donné et une salle peut abriter plusieurs cours. On peut très bien créer un cours sans salle car il arrive qu’on ne puisse décider de suite dans quelle salle il se tiendra, celle-ci sera rajoutée plus tard.

J’ai mis en gras les objets du domaine d’application qui vont donc être éligibles au titre d’entité. Nous allons commencer par les plus simples et nous allons tout générer en ligne de commande sans passer par le mode interactif :

Symfony/app/console generate:doctrine:entity --no-interaction \

--entity=FabriqueDeDevsScolariteBundle:Eleve \

--fields="nom:string(40) prenom:string(40) age:integer"

Ce que nous allons faire systématiquement c’est changer l’annotation Doctrine laissée vierge et qui contient le nom que la table cible va avoir.
Rendons nous dans le fichier nouvellement généré :

vi Symfony/src/FabriqueDeDevs/ScolariteBundle/Entity/Eleve.php

Et rajoutons la ligne en gras dans son entête:

/**
 * Eleve
 *
 * @ORM\Table(name="eleve")
 * @ORM\Entity
 */

Maintenant que nous avons généré une première entité, notre schéma de bases de données va devoir refléter ce changement, c’est à dire passer de l’état initial 0 à l’état 1 « une table élève est à créer ».
Nous faisons usage de l’argument diff :

Symfony/app/console doctrine:migrations:diff

Un fichier est généré :

Generated new migration class to "/home/sebastien.ferrandez/migrations/Symfony/app/DoctrineMigrations/Version20130420192803.php" from schema differences.

Vous devinez aisément le format de ces fichiers : VersionYYYYMMDDHHMMSS. Voyons ce que celui-ci contient :

class Version20130420192803 extends AbstractMigration
{
    public function up(Schema $schema)
    {
        // this up() migration is autogenerated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql");

        $this->addSql("CREATE TABLE eleve (id INT AUTO_INCREMENT NOT NULL, nom VARCHAR(40) NOT NULL, prenom VARCHAR(40) NOT NULL, age INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB");
    }

    public function down(Schema $schema)
    {
        // this down() migration is autogenerated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql");

        $this->addSql("DROP TABLE eleve");
    }
}

Vous voyez d’emblée deux méthodes, up et down; la première est exécutée dans le sens ascendant, c’est à dire en cas de migration et l’autre dans le sens descendant, dans le cas d’une rétro-migration (d’un rollback). On retrouve dans l’un les actions antagonistes de l’autre, si dans up vous avez un create table, vous aurez un drop table dans le down.
C’est dans ce fichier que vous pouvez changer les types de données dans vos requêtes SQL avant génération des éléments de la base de données ! Quoiqu’il en soit, les changements du diff ne sont pas appliqués à la base de données tant que vous n’avez pas utilisé la commande migrate :

Symfony/app/console doctrine:migrations:migrate

Voici ce que me donne à l’écran l’exécution de cette commande :

Migrating up to 20130420192803 from 0

  ++ migrating 20130420192803

     -> CREATE TABLE eleve (id INT AUTO_INCREMENT NOT NULL, nom VARCHAR(40) NOT NULL, prenom VARCHAR(40) NOT NULL, age INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB

  ++ migrated (0.08s)

  ------------------------

  ++ finished in 0.08
  ++ 1 migrations executed
  ++ 1 sql queries

Je migre bien de l’état 0 vers l’état 20130420192803 en faisant un CREATE TABLE eleve. Tout se passe conformément à ce que nous imaginions…Continuons avec notre deuxième entité, celle qui concerne nos enseignants :

Symfony/app/console generate:doctrine:entity --no-interaction \

--entity=FabriqueDeDevsScolariteBundle:Enseignant \

--fields="nom:string(40) prenom:string(40) age:integer"

Nous allons faire la même chose que précédemment :

  • donner, dans notre entité, un nom à la table qui va être créée
  • faire un diff
  • faire un migrate

Le diff produit un fichier de migration qui va cette fois concerner la table enseignant, dont nous venons de mettre le nom dans l’entité qui va la gérer. Le migrate, lui, va donner :

WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)y
Migrating up to 20130420214445 from 20130420192803

  ++ migrating 20130420214445

     -> CREATE TABLE enseignant (id INT AUTO_INCREMENT NOT NULL, nom VARCHAR(40) NOT NULL, prenom VARCHAR(40) NOT NULL, age INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB

  ++ migrated (0.05s)

  ------------------------

  ++ finished in 0.05
  ++ 1 migrations executed
  ++ 1 sql queries

Nous en profitons pour voir comment évolue les tuples de table migration_versions, gérée par Doctrine :

sebastien.ferrandez@sebastien:~/migrations$ mysql -u root -p -e 'use exercice; select * from migration_versions'
Enter password: 
+----------------+
| version        |
+----------------+
| 20130420192803 |
| 20130420214445 |
+----------------+

Vous voyez que toute commande migrate entraîne la création d’une ligne dans cette table ! Nous continuons avec la création des autres entités :

Symfony/app/console generate:doctrine:entity --no-interaction \
--entity=FabriqueDeDevsScolariteBundle:Salle \
--fields="nom:string(40)"

Symfony/app/console generate:doctrine:entity --no-interaction \
--entity=FabriqueDeDevsScolariteBundle:Cours \
--fields="nom:string(40) date:datetime duree:integer"

Symfony/app/console generate:doctrine:entity --no-interaction \
--entity=FabriqueDeDevsScolariteBundle:Filiere\
--fields="nom:string(40)"

L’idée est d’isoler chaque changement du schéma de base de données (pour faire plus simple, chaque création de table) dans une migration. Si nous passons une gigantesque migration qui contient TOUT, en cas de nécessité de rétro-migrer, nous nous retrouverions à faire un grand pas en arrière au lieu de reculer à petits pas.
Nous avons fini avec les entités, nous allons à présent dans la dernière partie, « raccorder » celles qui doivent l’être ! A suivre…

symfony_black_03

Doctrine : les migrations avec le bundle DoctrineMigrationsBundle (1/3)

Nous allons voir comment se servir du bundle de migrations fourni par Doctrine avec Symfony et Composer. Placez vous dans le répertoire qui servira de base à cet exercice. Pour ma part j’ai choisi la localisation suivante :

mkdir $HOME/migrations && cd $HOME/migrations

Nous allons tout d’abord installer Composer dans ce répertoire. Bien évidemment, il vous faudra disposer de curl. Si vous ne l’avez pas installé, faites-le de suite :

sudo apt-get install curl

Ensuite installez Composer :

curl -sS https://getcomposer.org/installer | php
#!/usr/bin/env php
All settings correct for using Composer
Downloading...

Composer successfully installed to: /home/sebastien.ferrandez/migrations/composer.phar
Use it: php composer.phar

Maintenant que nous avons Composer installé, nous allons faire une installation de Symfony via cet outil :

php composer.phar create-project symfony/framework-standard-edition Symfony/ 2.2.1

Des bundles sont déployés sur votre machine (citons Doctrine, Swiftmailer, Monolog, Assetic) et vous les trouverez en faisant :

ls -l Symfony/vendor/

Vous vous en doutez, tous ne nous seront pas utiles. Pour nettoyer votre install et garder ce que vous voulez, n’hésitez pas à mettre à jour votre fichier composer.json.
Prenons quelques secondes pour regarder ce composer.json généré par notre create-project :

cat Symfony/composer.json

Attardons nous sur la partie « require » de notre objet JSON :

    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.2.*",
        "doctrine/orm": "~2.2,>=2.2.3",
        "doctrine/doctrine-bundle": "1.2.*",
        "twig/extensions": "1.0.*",
        "symfony/assetic-bundle": "2.1.*",
        "symfony/swiftmailer-bundle": "2.2.*",
        "symfony/monolog-bundle": "2.2.*",
        "sensio/distribution-bundle": "2.2.*",
        "sensio/framework-extra-bundle": "2.2.*",
        "sensio/generator-bundle": "2.2.*",
        "jms/security-extra-bundle": "1.4.*",
        "jms/di-extra-bundle": "1.3.*"
    }

Vous y retrouvez tous les bundles dont vous avez vu défiler le nom à l’installation, c’est plutôt rassurant !

L’heure est maintenant venue d’installer le bundle de migrations Doctrine. Vous êtes toujours à la racine de votre projet et vous exécutez :

php composer.phar require doctrine/doctrine-migrations-bundle dev-master -d Symfony
composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing doctrine/migrations (v1.0-ALPHA1)
    Downloading: 100%         

  - Installing doctrine/doctrine-migrations-bundle (dev-master 99c0192)
    Cloning 99c0192804134a8c1d0588777bd98bdbc2dbb554

Vous refaites un :

cat Symfony/composer.json

Pour vérifier que votre fichier composer.json a été mis à jour. Normalement, la partie require devrait maintenant ressembler à ça :

    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.2.*",
        "doctrine/orm": "~2.2,>=2.2.3",
        "doctrine/doctrine-bundle": "1.2.*",
        "twig/extensions": "1.0.*",
        "symfony/assetic-bundle": "2.1.*",
        "symfony/swiftmailer-bundle": "2.2.*",
        "symfony/monolog-bundle": "2.2.*",
        "sensio/distribution-bundle": "2.2.*",
        "sensio/framework-extra-bundle": "2.2.*",
        "sensio/generator-bundle": "2.2.*",
        "jms/security-extra-bundle": "1.4.*",
        "jms/di-extra-bundle": "1.3.*",
        "doctrine/doctrine-migrations-bundle": "dev-master"
    },

Si c’est le cas, alors vous avez installé le bundle avec succès ! Il nous faut cependant faire quelques « réglages »; commençons par ajouter notre bundle fraîchement installé dans le fichier AppKernel.php

vi Symfony/app/AppKernel.php

Nous y rajoutons la ligne suivante (vous pouvez laisser la dernière virgule car il s’agit d’une dangling comma) :

new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),

Ensuite il nous faut évidemment renseigner nos paramètres d’accès à la base de données afin que Doctrine puisse s’y connecter et donc faire :

vi Symfony/app/config/parameters.yml

Vous renseignerez dans ce fichier les paramètres de votre base de données (le driver utilisé – dans le doute laissez PDO -, l’IP de la machine cible, votre nom d’utilisateur et enfin le mot de passe).Dans la partie database, mettez le nom de votre base de données: assurez-vous bien entendu qu’elle existe ! La mienne s’appelle « exercice », je la crée sans plus attendre en ligne de commande (mon serveur MySQL est sur ma machine locale, je ne précise donc pas -h localhost) :

mysql -u root -p -e 'create database exercice'

Pour m’assurer que tout est en place avant de m’amuser avec les migrations, je lance une dernière commande :

sebastien.ferrandez@sebastien:~/migrations$ Symfony/app/console doctrine:migrations:status

 == Configuration

    >> Name:                        Application Migrations
    >> Database Driver:             pdo_mysql
    >> Database Name:               exercice
    >> Configuration Source:        manually configured
    >> Version Table Name:          migration_versions
    >> Migrations Namespace:        Application\Migrations
    >> Migrations Directory:        /home/sebastien.ferrandez/migrations/Symfony/app/DoctrineMigrations
    >> Current Version:             0
    >> Latest Version:              0
    >> Executed Migrations:         0
    >> Executed Unavailable Migrations: 0
    >> Available Migrations:            0
    >> New Migrations:                  0

Tout semble fonctionner ! Comme je suis d’un naturel curieux et que je ne crois que ce que je vois, je regarde si des choses se sont passées dans ma base de données :

sebastien.ferrandez@sebastien:~/migrations$ mysql -u root -p -e 'use exercice; show tables'
Enter password: 
+--------------------+
| Tables_in_exercice |
+--------------------+
| migration_versions |
+--------------------+

Et la réponse est OUI ! Une nouvelle table a été créée pour gérer les versions de mes migrations ! Tout est maintenant réuni pour s’adonner sans retenue aux joies de la migration avec Doctrine !
Rendez-vous donc bientôt dans la partie 2 pour y voir du concret !!!

PHP : quelques code smells

Cette expression code smell a été inventée par Kent Beck et elle est apparue pour la première fois dans le livre « Refactoring: Improving the Design of Existing » de Martin Fowler; signifiant littéralement « odeurs de code », cette expression peut être vue comme matérialisant une « piste olfactive » qui vous indique – à vous, fins limiers du code – que quelque chose ne va pas dans des développements (les vôtres ou des développements hérités – legacy). Voici quelques unes de ces « odeurs » que l’on rencontre le plus fréquemment; j’ai parfois utilisé des noms « officiels » tout comme des noms que j’emploie personnellement pour les désigner au quotidien.

Les commentaires : les commentaires expliquent brièvement ce qui se passe dans le code. Pour reprendre l’adage populaire : « Trop de commentaire tue le commentaire », soyez concis, ne détaillez pas le code trop finement, sinon vous paraphrasez !

Le code dupliqué : il indique un mauvais design. C’est un signe qu’il est temps de refactorer.

La forêt de « if » : si vous noyez votre code dans une jungle de branchements conditionnels, c’est que vous faites fausse route. Refactorez là encore en utilisant des designs patterns tels que Strategy, par exemple.

La « monstro-classe » : typique du mauvais design, du non respect des principes SOLID et en particulier du premier, dit de la Single Responsibility. Ces classes font tout, le café, le service, le parking…Un jour ou l’autre vous pairez cher ces 3000 lignes d’inepties.

La syllogomanie : on conserve du code mort dans de gros pavés de commentaires…sans savoir pourquoi… »au cas où ». Et les systèmes de gestion de version, ça sert à quoi ?

La foire aux variables d’instances : on en crée plein, pour y stocker des valeurs qui ne servent que temporairement à l’objet, qui du coup enfle et enfle et enfle.

La classe fantôme : elle n’a pas vraiment d’utilité, de raison d’être, elle ne fait rien de significatif ou bien délègue l’intégralité de ses tâches. Elle doit être impitoyablement éliminée du design.

Le nommage inapproprié : s’applique aux variables ou aux méthodes; des exemples vu dans les entreprises pour lesquelles j’ai travaillé : des méthodes nommées « find2 », « find3 » et qui faisaient presque la même chose que la méthode originelle « find » (et les Design Patterns, ça sert à quoi ?), des variables nommées $jb, du nom de leur créateur, ou $a, $b, $o, des méthodes avec des noms horriblement longs et ridicules, comme « faitCeQuilFautPourLeTac() » (tout ça est vrai, je n’invente rien !).

L’absence de typage : PHP est faiblement typé comme vous le savez, mais vous pouvez au moins typer les paramètres d’appel des fonctions (objets/interfaces/tableaux). Usez en et abusez en pour verrouiller votre conception objet et ne pas permettre tout et n’importe quoi !

Pour aller plus loin :

Smells to refactoring (PHP, anglais)

Comment écrire du code inmaintenable (anglais)

Code smells (anglais)

smell

Image empruntée au site Rin’s treasure

PHP : La loi de Déméter en quelques exemples

La loi de Déméter (LoD en anglais, pour Law of Demeter) est un principe de design de systèmes dits « orientés objet ». Son principe élémentaire tient en une phrase :

« Ne parle qu’à tes amis »

Ce principe de conception orientée objet à été évoqué pour la première fois en 1987 à la Northeastern University de Boston (Massachussets) par Ian Holland qui travaillait alors sur un projet du nom de Demeter. Le but de cette « loi » est de maintenir un couplage laĉhe entre les classes.

Ce que permet LoD

LoD nous donne pour principe la chose suivante :

Une méthode dans un objet donné doit seulement invoquer les méthodes des types d’objets suivants : l’objet lui-même, ses objets paramètres, les objets qu’il crée, les objets qu’il a en composition

L’objet lui-même


class A {
    public function __construct() {
        $this->_faireUnTruc();
    }

    protected function _faireUnTruc() {}
}

L’objet instance de A sera autorisé à appeler ses propres fonctions (tout de même…)

Ses paramètres


class B {
    public function faireQuelqueChose() {}
}

class A {
    public function __construct(B $b) {
        $b->faireQuelqueChose();
    }

    protected function _faireUnTruc() {}
}

N’importe quel objet créé dans la classe

class C {
    public function faireDesMiracles() {}
}

class A {
    public function __construct() {
        		$c = new C;
        		$c->faireDesMiracles();
    }
}

Les objets en composition

class D {
    public function faireAutreChose() {}
}

class A {
    private $_d;

    public function __construct() {
        $c = new D;
    }

    public function faireDesChoses() {
        $this->_d->faireAutreChose();
    }
}

Ce qui remet en cause LoD

class C {
    public function faireBeaucoupDeChoses() {}
}

class B {
    private $_c;

    public function __construct(C $c) {
        $this->_c = $c;
    }

    public function getC() {
        return $this->_c;
    }
}

class A {
    private $_d;

    public function __construct() {
        $c = new C;
        $b = new B($c);
        $b->getC()->faireBeaucoupDeChoses();
    }
}

Dans cet exemple, ce n’est pas le chaînage des méthodes en lui-même qui pose un problème mais le fait que A fasse appel à C à travers B. Idéalement, A fait appel à B qui lui, délègue le service demandé à C de sorte que nous n’aurions plus que :

$b->faireBeaucoupDeChoses();

et


class B {
    private $_c;

    public function __construct(C $c) {
        $this->_c = $c;
    }

    public function faireBeaucoupDeChoses() {
        return $this->_c->faireBeaucoupDeChoses();
    }
}

Ici, chaque classe parle à ses amis les plus proches (A à B, B à C) et Déméter n’en sera pas offusquée !

Pour en savoir plus…

Un billet du blog d’Avdi Grimm (anglais)

La page du projet Demeter (anglais)

Pimcore : une étude d’opportunité

PIM, vous dîtes ?

Le PIM (Product Information Management, gestion de l’information produit en français) a pour objectif de fiabiliser, de détailler et de centraliser les informations sur les produits d’une entreprise. Dans l’environnement de commerce multi-canal actuel, il est nécessaire de contrôler la cohérence et la pertinence de ces informations avant d’alimenter les multiples canaux de diffusion de celles-ci (sur différents médias).

Le projet Pimcore

Pimcore se veut un moyen d’agréger, de consolider et d’enrichir ces informations en provenance de diverses sources de données (ERP, système legacy, SGBDR).

Pimcore est un projet développé par l’entreprise autrichienne Elements1, il est disponible et forkable sur la plate-forme Github :

https://github.com/pimcore/pimcore.git

Un dépôt contenant une démo prête à l’emploi est également accessible sur Github :

https://github.com/ElementsDev4/PimcoreDemo.git

Pré-requis techniques

Pimcore a pour pré-requis :

  • l’installation d’Apache (Nginx n’a pas été testé mais ceci ne devrait pas poser de problèmes, il s’agit à priori de traduire les règles de réécriture Apache en règles Nginx1)
  • La présence de MySQL comme SGBDR et la possibilité de garantir à Pimcore que les tables qu’il utilise sont bien en inter-classement UTF-8

La version qui a été utilisée lors de mes tests est 1.4.52 (Build 1953). Elle embarque une version 3 d’ExtJS et jQuery 1.7.1.ExtJS 4 à ce stade des développements n’est pas pris en charge par Pimcore.

PHP

Les paramètres de PHP requis sont les suivants :

  • version >= 5.3
  • memory_limit doit être au moins égale à 256M
  • magic_quotes_gpc doit être à off
  • safe_mode doit être à off (toutefois, cette fonctionnalité est devenue obsolète depuis PHP 5.3.0 et a été supprimée dans PHP 5.4.0)
  • mcrypt
  • pdo_mysql
  • iconv
  • dom
  • simplexml
  • gd
  • multibyte support (mbstring)
  • zlib / zip / bz2
  • openssl

Les paramètres optionnels sont :

  • l’extension PECL APC
  • l’extension PECL memcache
  • l’activation de pcntl
  • soap (pour utiliser l’API des webservices)
  • curl (pour utiliser l’API Google)

Il faut aussi penser à modifier les paramètres relatifs à la taille maximale des uploads.

« Fine-tuning » au niveau du serveur HTTP

La documentation officielle recommande d’augmenter le nombre maximum de fichiers ouvrables :

« Étant donné que Pimcore repose sur Zend Framework et d’autres bibliothèques assez lourdes, Apache doit charger un grand nombre de fichiers à chaque requête. Sur Debian, la limite est de 1024 fichiers ouverts simultanément, ce qui est suffisant dans la majeure partie des cas, mais pas pour les sites web à fort trafic »

Principe général de fonctionnement

schema_general

Le principe peut se résumer ainsi: « On extrait du contenu depuis une source de données, on l’agrège, on l’enrichit, on le diffuse ».

Présentation technique du produit

Pimcore fonctionne sur une base logicielle de Zend Framework, qu’il embarque dans l’archive téléchargeable depuis le site officiel1.Nul besoin donc d’installer Zend.

Compatibilité avec les navigateurs

Pimcore prend en charge un certain nombre de navigateurs de nouvelle génération :

  • Firefox >= 3
  • IE >= 8
  • Safari >= 5
  • Chrome >= 3
  • Plug-ins browser : Adobe Flash Player >= 10.1

Le cache

En raison de l’absence de prise en compte de la mise en cache par tags de Memcache, Pimcore a implémenté un cache « maison » basé sur Memcache: Pimcore_Cache_Backend_Memcached.

Cache backend

Sur le backend (interface d’administration), différents stratégies de caching sont disponibles:

  • Pimcore_Cache_Backend_Memcached
  • Cache MongoDB
  • Cache fichier (à éviter!)

Cache frontend

Côté front, seul Pimcore_Cache_Backend_Memcached est disponible.

Ouput-cache

Cible uniquement les requêtes HTTP de type GET. Pimcore rajoute ses entêtes de type X-Pimcore-Cache-*. Si une durée de vie a été spécifiée, les entêtes Cache-Control et Expires sont également présents (intéressant pour une exploitation avec un accélérateur comme Varnish…).

Pimcore déconseille vivement l’utilisation de la classe « Zend_Cache_Backend_Memcache » fournie par le framework Zend.

Memcache (ou plus exactement son implémentation « maison » par Pimcore) est indispensable au bon fonctionnement du produit sous peine de voir les temps de réponse augmenter de manière drastique !

Aspects techniques

Le coeur de Pimcore

Pimcore se décompose en trois modules fondamentaux:

  • Les documents : on créé des pages (ou des templates) qu’on remplit avec du contenu
  • Les « assets »: les ressources statiques qu’on insère dans les documents (images, vidéos, PDF…)
  • Les objets : utilisables en mode édition ou bien (et c’est ça qui nous intéresserait davantage) en mode batch avec un script CLI.

Le backend/frontend

Le backend est l’interface d’administration Pimcore : c’est là qu’on prépare les templates, qu’on agrège le contenu, qu’on gère les traductions…le frontend est la partie « visible » par l’utilisateur final sur le terminal de son choix (tablette, smartphone, web).

Développer avec Pimcore

Se servir de Pimcore suppose une bonne maîtrise de MVC, de PHP et de Zend Framework.

Développer dans le backend Pimcore c’est finalement savoir se servir du triplet MVC : savoir programmer une action au sein d’un contrôleur, savoir utiliser des vues si besoin, utiliser des patterns de routage, créer des objets « modèle ».

L’internationalisation (i18n)

Tous les documents Pimcore sont traduisibles. Évidemment, comme Pimcore embarque Zend, c’est Zend_Translate qui gère cet aspect en sous-main.
Le backend est traduit en français, le frontend a un module qui facilite la traduction sur les pages proposées à l’utilisateur final.

Les objets Pimcore

On crée des objets (Data Objects, selon la terminologie Pimcore) dont on fait correspondre les propriétés avec des éléments graphiques. Ces éléments graphiques ont des types prédéfinis par Pimcore.

Exemple : Un objet Pneu a comme propriétés « description », « image », « prix », « date_creation », on fera correspondre ces propriétés avec des éléments visuels Pimcore, respectivement « Object_Class_Data_Textarea », « Object_Class_Data_Image », « Object_Class_Data_Numeric » et « Object_Class_Data_Date ».

Cette étape de mapping est incontournable.
C’est également à ce niveau là que vont être détaillées les relations entre objets.
Les objets peuvent être importés comme exportés :

  • l’importation peut se faire sous la forme d’un batch écrit en PHP CLI
  • l’exportation se fait en CSV

Une attention particulière doit être portée sur la gestion de la capacité mémoire durant les imports, si ceux-ci portent sur un nombre conséquent d’objets. La gestion des versions, si elle n’est pas essentielle, doit être désactivée durant les importations (Version::disable()).

J’ai trouvé cette partie de la documentation assez sibylline. C’est un domaine à creuser, notamment en parcourant le forum pour y lire les retours d’expérience d’autres utilisateurs.

Les additifs (plug-ins)

Il est possible d’en programmer, tout comme il est possible d’en utiliser certains fournis par défaut :

PhpSearch est un plug-in qui sert de moteur de recherche full-text et qui fournit suggestion et auto-complétion. Il en existe quelques autres, hélas peu documentés :

http://www.pimcore.org/wiki/display/PLUGINS/Plugin+Documentations

Toutefois, le développement de plug-ins est plutôt bien détaillé sur ce lien :
http://www.pimcore.org/wiki/display/PIMCORE/Plugin+Anatomy+and+Design

Les test unitaires

Pimcore bootstrappe PHPUnit dans sa suite de test « maison » PimUnit. Un tutoriel vidéo donné en lien en fin de document explique comment installer cet outil dans l’AGL PHP Storm.

Cette fonctionnalité est quelque chose de nécessaire, dont la présence joue clairement en la faveur du choix final du produit.

Conclusion

Forces du produit

  • Pimcore a été primé en 2010 dans la catégorie produits open source innovants par l’éditeur Packt. Ces « Awards » ont récompensé de grands noms comme Joomla, Drupal, WordPress, Jquery, Sencha, Prestashop…
  • Pimcore a déjà des références solides avec des catalogues de grande dimension (plusieurs dizaines de milliers d’objets): Eurotours, Intersport, Expert…
  • L’interface est développée sur une base ExtJS, qui procure une user experience plutôt plaisante. L’utilisation du drag and drop rend le logiciel assez facile à utiliser et lui confère l’apparence d’un logiciel desktop.
  • Le système de plug-ins rend Pimcore extensible.
  • Un mécanisme de mise à jour du produit
  • Courbe d’apprentissage peu abrupte pour des développeurs déjà habitués au MVC
  • La présence de PHPUnit pour l’écriture de tests unitaires (qu’il faut s’astreindre à effectuer !)
  • Compatibilité revendiquée avec des solutions ETL open-source écrites en Java telles que Kettle ou Talend Studio2
  • Une API assez complète donne accès au cœur de Pimcore, sa documentation fait l’objet d’une mise à jour quotidienne (générée sur le dernier nightly build)

Inconvénients et limitations

  • Une communauté limitée pour l’instant (quelques centaines d’utilisateurs enregistrés sur le forum)
  • Une documentation peu exhaustive, certaines fois inexistante et truffée de fautes
  • Il faut être programmeur pour se servir de Pimcore; une personne sans connaissance basiques de PHP, de l’architecture MVC ne saura pas utiliser Pimcore
  • Interface agréable à l’utilisation certes mais pas forcément très intuitive (il faut parfois fouiller pour trouver)
  • Nécessite une configuration serveur assez particulière, notamment demandeuse en terme de capacité mémoire, les bibliothèques utilisées par Pimcore étant « lourdes »
  • Un import disponible seulement en CSV, il faut écrire un script d’importation à partir de nos sources de données
  • Pimcore a été validé sur des machines fonctionnant avec un serveur HTTP Apache, aucune documentation officielle n’existe sur l’intégration avec Nginx

Réflexions

Zend Framework va bientôt évoluer dans sa version 2 (actuellement en bêta1). Comment Pimcore va-t-il intégrer cette évolution ? Cette évolution elle-même sera-t-elle aussi importante que pour Symfony 1.4 → 2.0 ?

Il faut voir à moyen terme quelle va être la place accordée à MongoDB dans Pimcore. Pour l’heure, il existe juste une classe de gestion de cache basée sur MongoDB mais rien qui concerne le modèle de données à proprement parler.

Sébastien Ferrandez, Juillet 2012.

Liens :

Pimcore

Zend Framework