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

MongoDB : une présentation en 10 slides

J’ai récemment déterré un petit diaporama créé pour une présentation d’une demi-heure de MongoDB aux développeurs du site e-commerce Allopneus (avec l’aide de mon compère Lucas Filippi).

Cette présentation a été rédigée à la suite de notre venue à la conférence MongoDB en juin 2012 sur Paris.

Le document en PDFTECH-TALK-001

Le site de Lucas Filippi : LUFI

Installer phpUnit avec Composer

Téléchargez d’abord Composer. Placez vous dans le répertoire de votre choix (vérifiez naturellement que vous y avez les droits d’écriture) et tapez :

curl -sS https://getcomposer.org/installer | php

ensuite vous installez phpUnit à proprement parler :

php composer.phar require "phpunit/phpunit"
Please provide a version constraint for the phpunit/phpunit requirement: 3.7.*     
composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing symfony/yaml (v2.2.1)
    Downloading: 100%         

  - Installing phpunit/php-text-template (1.1.4)
    Downloading: 100%         

  - Installing phpunit/phpunit-mock-objects (1.2.3)
    Downloading: 100%         

  - Installing phpunit/php-timer (1.0.4)
    Downloading: 100%         

  - Installing phpunit/php-token-stream (1.1.5)
    Downloading: 100%         

  - Installing phpunit/php-file-iterator (1.3.3)
    Downloading: 100%         

  - Installing phpunit/php-code-coverage (1.2.9)
    Downloading: 100%         

  - Installing phpunit/phpunit (3.7.19)
    Downloading: 100%         

phpunit/php-code-coverage suggests installing ext-xdebug (>=2.0.5)
phpunit/phpunit suggests installing phpunit/php-invoker (>=1.1.0,<1.2.0)

Vérifiez la bonne tenue de votre fichier composer.json :

cat composer.json 
{
    "require": {
        "phpunit/phpunit": "3.7.*"
    }
}

Vous trouverez l’exécutable phpunit dans le sous-répertoire « vendor/bin ». Lancez le et vous verrez la traditionnelle liste des options s’afficher à l’écran !

Bons tests unitaires !

PHP objet : le late static binding en 5 minutes chrono

913_always_late_car_decal__04163

Top, c’est parti ! Tout d’abord, qu’est-ce que le late static binding (résolution statique à la volée), me direz-vous ? C’est un mécanisme qui, dans un contexte d’appel statique, permet de faire référence à la classe courante (la « vraie » classe). Voici comment nous faisions (avant PHP 5.3) dans un contexte d’héritage statique (A a une méthode statique dont B hérite tout en l’écrasant – même si elle est identique à l’originale) :


class A {
    public function pointDentree() {
        self::methode();
    }

    public static function methode() {
        echo __CLASS__, PHP_EOL;
    }
}

class B extends A {
    public static function methode() {
        echo __CLASS__, PHP_EOL;
    }
}

$b = new B;
$b->pointDentree();

Nous instancions un objet de la classe B, nous nous attendons donc à avoir « B » affiché à l’écran or c’est A qui s’affiche, signe que la résolution statique ne se fait pas au runtime (exécution) mais à la définition de la classe (compilation). Pour obtenir le comportement attendu, il suffit de faire usage du mot clé static :


class A {
    public function pointDentree() {
        static::methode();
    }

    public static function methode() {
        echo __CLASS__, PHP_EOL;
    }
}

class B extends A {
    public static function methode() {
        echo __CLASS__, PHP_EOL;
    }
}

$b = new B;
$b->pointDentree();

Ici j’ai bien B qui s’affiche ! PHP nous donne les raisons de la limitation du mot clé self :

Les références statiques à la classe courante, avec self:: ou __CLASS__, sont résolues en utilisant la classe à laquelle appartiennent les fonctions, celle où elles ont été définies (source : http://php.net/manual/fr/language.oop5.late-static-bindings.php)

Utilisez donc à bon escient ce mécanisme de late static binding dans vos développements, un effet de bord est si vite arrivé quand on fait de l’héritage statique !

MySQL : différence entre = et LIKE dans un SELECT

like a boss !

LIKE…a bo$$ !

On entend souvent dire qu’utiliser l’opérateur arithmétique = ou faire un LIKE sur une chaîne de caractères dans MySQL revient au même. Oui et non…Oui car au final les tuples qui « remontent » de nos tables sont les mêmes mais non car en coulisses l’impact sur le temps d’exécution n’est pas forcément le même.
Voyons sans plus tarder un exemple avec une table composée de 2 champs; un champ char(6) qui occupe donc 6 octets puisque les chaînes générées le sont avec des caractères ASCII et un champ varchar(6) qui va occuper 7 octets – un préfixe d’un octet + nos 6 octets de caractères – dans laquelle nous insérons des chaînes de caractères aléatoires et en nombre assez conséquent (100 000 entrées). Ne vous inquiétez pas si cela prend quelques secondes à s’exécuter…

CREATE TABLE test_like (
    cha char(6) NOT NULL,
    var varchar(6) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- le début de notre proc' stock'

DROP PROCEDURE IF EXISTS insertion;

DELIMITER //
CREATE PROCEDURE insertion()
BEGIN
    DECLARE i INT DEFAULT 1;

    WHILE (i<=100000) DO
        INSERT INTO test_like VALUES
        (
        SUBSTRING(MD5(RAND()) FROM 1 FOR 6),
        SUBSTRING(MD5(RAND()) FROM 1 FOR 6));
        SET i=i+1;
    END WHILE;
END
//
CALL insertion();

C’est vrai que de prime abord faire

 select * from test_like
where cha like 'ee43b9' 

ou

 select * from test_like
where cha = 'ee43b9' 

sur notre champ de type CHAR revient au même, tout comme :

select * from test_like
where var like 'fb7bb8'

produit le même résultat que

select * from test_like
where var = 'fb7bb8'

J’ai lu sur certains forums anglophones que le comportement de = ou like dépendait du type de données de la colonne (CHAR/VARCHAR), c’est peut-être vrai sur certains RDBMS, mais pas avec MySQL, vous en avez la preuve concrète. Le changement est surtout visible lorsque l’on pose un index sur la colonne concernée :

ALTER TABLE test_like ADD INDEX (cha)

Ainsi, voici ce que nous donne un EXPLAIN. Tout d’abord sur le LIKE :

explain select * from test_like where cha like 'ee43b9'

+—-+————-+———–+——-+—————+——+———+——+——+————-+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+—-+————-+———–+——-+—————+——+———+——+——+————-+
| 1 | SIMPLE | test_like | range | cha | cha | 18 | NULL | 1 | Using where |
+—-+————-+———–+——-+—————+——+———+——+——+————-+
1 row in set (0.01 sec)

Et ensuite sur le = :

explain select * from test_like where cha = 'ee43b9'

+—-+————-+———–+——+—————+——+———+——-+——+————-+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+—-+————-+———–+——+—————+——+———+——-+——+————-+
| 1 | SIMPLE | test_like | ref | cha | cha | 18 | const | 1 | Using where |
+—-+————-+———–+——+—————+——+———+——-+——+————-+
1 row in set (0.01 sec)

Nous obtenons la même chose sur le champ de type VARCHAR après avoir posé un index dessus. Une recherche avec un LIKE sur un champ indexé donne un type = range alors qu’une recherche sur un champ indexé donne avec une égalité stricte un type = ref. Voyons ce que dit la documentation officielle à ce sujet :

ref peut être utilisé pour les colonnes indexées, qui sont comparées avec l’opérateur =.

C’est bien le cas ! Pour le range :

range peut être utilisé lorsqu’une colonne indexée est comparée avec une constante comme =<>>>=<<=,IS NULL<=>BETWEEN ou IN.

Les exemples de la documentation officielle montrent bien que range peut-être AUSSI utilisé avec un = mais plutôt sur des types de données numériques…

Les tests réalisés sur un serveur MySQL situé sur le réseau local

Pour le champ de type CHAR

CHAR(6) indexé, LIKE sur une valeur existante, bypass du cache de requêtes MySQL :

SELECT SQL_NO_CACHE *
     FROM test_like
     WHERE cha LIKE 'edd34e'
1 total, Traitement en 0.0103 sec.

CHAR(6) indexé, LIKE sur une valeur inexistante, bypass du cache de requêtes MySQL :

SELECT SQL_NO_CACHE *
     FROM test_like
     WHERE cha LIKE 'fb7bb8'
MySQL n'a retourné aucune ligne. ( Traitement en 0.0007 sec. )

CHAR(6) indexé, = sur une valeur existante, bypass du cache de requêtes MySQL :

SELECT SQL_NO_CACHE *
     FROM test_like
     WHERE cha =  'edd34e'
Traitement en 0.0008 sec.

CHAR(6) indexé, = sur une valeur inexistante, bypass du cache de requêtes MySQL :

SELECT SQL_NO_CACHE *
     FROM test_like
     WHERE cha =  'udd34e'
MySQL n'a retourné aucune ligne. ( Traitement en 0.0006 sec. )

Pour le champ de type VARCHAR

VARCHAR(6) indexé, LIKE sur une valeur existante, bypass du cache de requêtes MySQL :

SELECT SQL_NO_CACHE *
     FROM test_like
     WHERE var LIKE  '64eaee'
1 total, Traitement en 0.0455 sec.

VARCHAR(6) indexé, LIKE sur une valeur inexistante, bypass du cache de requêtes MySQL :

SELECT SQL_NO_CACHE *
     FROM test_like
     WHERE var LIKE  '88eaee'
MySQL n'a retourné aucune ligne. ( Traitement en 0.0659 sec. )

VARCHAR(6) indexé, = sur une valeur existante, bypass du cache de requêtes MySQL :

SELECT SQL_NO_CACHE *
     FROM test_like
     WHERE var =  '6753ec'
1 total, Traitement en 0.0520 sec.

VARCHAR(6) indexé, = sur une valeur inexistante, bypass du cache de requêtes MySQL :

SELECT SQL_NO_CACHE *
     FROM test_like
     WHERE var =  '8888ec'
MySQL n'a retourné aucune ligne. ( Traitement en 0.0513 sec. )

D’après ce petit benchmark très rapide et sans grande prétention :

  • sur un VARCHAR, like est un peu plus rapide que =
  • sur un CHAR intégralement rempli, = est beaucoup plus rapide que like

PHP : le registre d’autoload de la SPL

Pour bénéficier de l’autoloading (ou chargement automatique), on peut utiliser la fonction built-in __autoload ou bien la pile (ou registre) d’autoload. Je privilégie davantage la seconde, parce qu’une classe est pour moi une unité de programme plus structurée (et plus structurante !) qu’une simple fonction jetée à la hâte dans le code. Voici un exemple très simple d’utilisation de la pile de chargement automatique des classes.

class Autoload
{
    private static $_classDir = './classes/';

    public static function classesAutoloader($class)
    {
        $path = static::$_classDir . "$class.php";

        if (file_exists($path) && is_readable($path)) {
            require $path;
        }
    }
}

spl_autoload_register('autoload::classesAutoloader');
// hop !, la méthode statique est placée dans la pile !

Ici nous définissons une classe avec une fonction statique chargée de donner à PHP les moyens d’aller trouver les fichiers contenant les définitions de classes. Le nom de la classe à trouver est injecté par le langage dans les fonctions de chargement automatique: c’est cette variable $class qui va nous servir à construire un chemin vers le fichier qui contient la définition de la classe.

Une fois notre méthode implémentée, nous la plaçons dans la pile d’autoloading grâce à un appel à spl_autoload_register (notez bien l’insensibilité à la casse) et le tour est joué. Evidemment, vous empilerez autant de méthodes que souhaité même si cet exemple, qui se veut simplissime, n’en contient qu’une (attention toutefois à la surenchère…).

L’auto-loading est devenue une pratique courante depuis la sortie de PHP 5 et plus encore depuis l’avènement des frameworks MVC en PHP, il est indispensable en 2013 de comprendre en quoi consiste ce mécanisme !

Pour plus d’info : la documentation PHP

PHP : un exemple simple du design pattern Template Method

Vous cherchiez un design pattern facile à aborder ? Le design pattern template method est celui qu’il vous faut ! Son principe est très simple : dans une classe, une méthode dite template est composée de sous-méthodes dont on sait que chaque sous-classe l’implémentera à sa manière. Ces sous-méthodes sont généralement en type d’accès protégé car invoquées uniquement par cette fameuse méthode template; l’extérieur n’a pas à connaître les mystères de votre implémentation (encapsulation, vous dîtes ?). Bien entendu, étant donné que chaque classe fille implémentera ces méthodes comme bon lui semble, il convient de les signifier comme abstraites dans la classe mère.

Imaginons une classe TunnelCommande qui expose une méthode template nommée finaliserCommande; cette méthode décrit un algorithme en spécifiant ce qui devra être fait par ses sous-classes et dans quel ordre. Cette classe comporte la méthode payePort dite « adaptateur » (hook) qui peut être réécrite dans les classes filles. Elle sert à conditionner une partie du flot d’exécution de l’algorithme de la méthode template. Son utilité dans notre cas est de permettre à un certain type d’utilisateur de s’affranchir du paiement des frais de port.

abstract class TunnelCommande
{
    public function finaliserCommande(): void
    {
        $this->faireTotal();
        
        if ($this->payePort()) {
            $this->ajouterFraisPort();
        }

        $this->rediriger('page_paiement');
    }

    public function payePort(): bool
    {
    	return true;
    }

    public function rediriger(string $template): void
    {
    	echo "Redirection vers ", $template, PHP_EOL;
    }

    abstract protected function faireTotal(): void;
    abstract protected function ajouterFraisPort(): void;
}

Nous avons deux classes concrètes qui dérivent TunnelCommande et implémentent les méthodes abstraites en leur donnant un comportement spécifique. Dans CommandePremium, la méthode ajouterFraisPort qui est imposée par la classe mère abstraite ne fait rien, payePort renvoyant false elle ne sera de toutes façons jamais invoquée dans ce scénario là.


class CommandeClient extends TunnelCommande
{
    protected function faireTotal(): void
    {
        echo "Je fais le total", PHP_EOL;
    }

    protected function ajouterFraisPort(): void 
    {
        echo "J'applique les frais de port du client normal", PHP_EOL;
    }
}

class CommandePremium extends TunnelCommande
{
    protected function faireTotal(): void
    {
        echo "Appliquer 5% de rabais pour les clients Premium", PHP_EOL;
    }

    protected function ajouterFraisPort(): void
    {
        return;
    }

    public function payePort(): bool
    {
    	return false;
    }
}

$premium = new CommandePremium;
$premium->finaliserCommande();

$standard = new CommandeClient;
$standard->finaliserCommande();

Dans ce design pattern, tout le travail est fait dans la classe mère, abstraite. Quand je dis « tout le travail », je parle de la structure générale de l’algorithme, de l’ordre des opérations. Evidemment, la responsabilité de l’implémentation des détails de cet algorithme « général » est déléguée aux classes dérivées, via le mécanisme d’abstraction.