PHP: Autres fonctions méconnues de la programmation objet

Ceci est la seconde partie de PHP: Quelques fonctions méconnues de la programmation objet.

__get, __set, __isset, __unset

PHP permet d’affecter à une instance d’une classe n’importe quelle valeur (presque comme à un tableau de hashage) pour autant que ça ne soit pas une variable privée:

<?php

class Objet {};
$obj = new Objet;
$obj->exemple = 1;
echo $obj->exemple . "\n"; // affiche '1'

Lors de l’affectation d’une variable de telle façon, PHP appelle en fait les fonctions __set et __get pour l’affectation et la lecture de la valeur. Il est évidement bien entendu possible de les réécrire afin de définir un nouveau comportement:

<?php // Objet.class.php

class Objet
{
    private $data;
    public function __construct()
    {
        $data = array();
    }

    public function __set($name, $value)
    {
        echo "Setting $name to $value\n";
        $this->data[ $name ] = $value;
    }

    public function __get($name)
    {
        echo "returns $name\n";
        return $this->data[ $name ];
    }
};
<?php // fichier test.php

require_once("Objet.class.php");
$obj = new Objet;
$obj->priv = 1;
echo $obj->priv;

Cet exemple retournera:

$ php test.php
Setting priv to 1
return priv
1

Cela permet de dévier les affectations des variables dans les instances de classe, mais aussi de “protéger” le contenu des classes. Il faut également noter que si l’appel s’effectue pour une variable déclarée privée dans la classe, on sera également redirigé par __get et __set et l’on n’aura plus l’erreur comme quoi on fait appel à une variable privée.

De plus, de la même façon que pour __get et __set, on aura également __isset et __unset qui serviront à redéfinir le comportement lors d’un appel à isset ou à unset.

__call et __callStatic

Comme pour les variables, il est possible d’attraper les appels des fonctions non définies et privées dans les classes, statiques ou non en utilisant __call et __callStatic.

<?php // Objet.class.php
class Objet
{
    public function __call($func, $args)
    {
        echo "Calling $func\n";
    }

    public static function __callStatic($func, $args)
    {
        echo "Calling static $func\n";
    }
};
<?php // test.php

class Objet {};
$obj = new Objet;
$obj->exemple = 1; echo $obj->exemple . "\n";

Et à l’exécution:

$ php test.php
Calling hello
Calling static hello

Ces deux fonctions, ajoutées aux setters/getters du dessus, permettent de “wrapper” entièrement des instances de classes permettant des utilisations bien plus intéressantes qu’un simple héritage.

__clone

__clone est appelé lorsque l’on crée une copie d’une instance d’objet en appelant clone. Cela résulte à deux instances de la classe totalement indépendante avec le même contenu. Bien que je ne sois pas sûr que clone soit utilisé tous les jours, __clone peut éventuellement être utilisé afin de modifier l’un des deux objets avant ou après le clonage, ou encore de ne pas effectuer ce clonnage afin d’assurer d’avoir un singleton.

__sleep, __wakeup

Finalement, __sleep et __wakeup sont utilisées lors de la sérialisation/désérialisation d’un objet. Elles seront appelées respectivement à l’utilisation de serialize et unserialize. A noter que __sleep doit retourner un array avec la liste des champs à sérialiser.

Comme les courts exemples sont mieux que les longs discours, voici une classe ouvrant un fichier à sa création, le refermant à sa sérialisation, pour le réouvrir à la désérialisation:

<?php // Objet.class.php

class Objet
{
    private $file_name = NULL;
    private $file_hd = NULL;

    public function __construct($file_name)
    {
        $this->file_name = $file_name;
        $this->file_open();
    }

    public function __destruct()
    {
        if($this->file_hd !== NULL)
            $this->file_close();
    }

    private function file_open()
    {
        echo "Opening file\n";
        $this->file_hd = fopen($this->file_name, "a+");
    }

    private function file_close()
    {
        if(NULL === $this->file_hd)
            return false;

        echo "Closing file\n";
        fclose($this->file_hd);
        $this->file_hd = NULL;
    }

    public function __sleep()
    {
        echo "* Serialization.\n";
        $this->file_close();

        return(array('file_name'));
    }

    public function __wakeup()
    {
        echo "* Unserialization.\n";
        $this->file_open();
    }
};

Et testons tout ça:

<?php // fichier test.php

require_once("Objet.class.php");

$obj = new Objet('helloworld.txt');

$serializedObj = serialize($obj);
unset($obj);
var_dump($serializedObj);

$newObj = unserialize($serializedObj);

echo "Fin du script\n";

On peut vérifier le comportement:

$ php test.php
Opening file
* Serialization.
Closing file
string(62) "O:5:"Objet":1:{s:16:"Objetfile_name";s:14:"helloworld.txt";}"
* Unserialization.
Opening file
Fin du script
Closing file

J’espère que quelqu’un aura découvert quelque chose ! :)

Pour ces deux articles, je me suis un peu inspiré d’un article analogue m’ayant poussé à lire un peu plus la doc de PHP: 9 magic methods for php (thinkvitamin.com).

PHP: Quelques fonctions méconnues de la programmation objet

En parcourant la doc de PHP, et spécialement le chapitre réservé aux objets (http://www.php.net/manual/en/language.oop5.php), j’ai découvert pas mal de fonctions que je ne connaissais pas malgré que je repratique de façon courante depuis les deux dernières années.

Même si PHP5 a été introduit en 2004, ces méthodes relatives aux objets sont toujours en constantes évolutions.

__autoload

Si l’on fait appel à une classe (ou une interface) encore non déclarée, PHP effectuera un appel à la fonction __autoload si celle ci a été déclarée. Cela a pour but de décharger les longues listes d’appels à require_once/include visant à inclure les différents fichiers sources définissant ces classes.

Exemple simple:

<?php // fichier Objet.class.php
class Objet
{
};
<?php // fichier test.php
function __autoload($class_name)
{
    echo "Autoloading class $class_name\n";
    $class_file = $class_name . '.class.php';
    if(FALSE === file_exists($class_file))
    {
        throw new Exception('Class "' . $class_name . '" could not be autoloaded (file missing)');
    }
    require_once $class_name . '.class.php';
    if(FALSE === class_exists($class_name))
    {
        throw new Exception('Class "' . $class_name . '" could not be autoloaded (not in file)');
    }
    echo "Autoloading done!";
    return true;
}

// Utilisation de la classe "Objet" définie dans Objet.class.php
$obj = new Objet;

// Tentative d'utilisation de la classe "ObjetNonDefini" qui provoquera une erreur:
$obj2 = new ObjetNonDefini;
$ php test.php
Autoloading class Objet
Autoloading done!
Autoloading class ObjetNonDefini
PHP Fatal error:  Uncaught exception 'Exception' with message 'Class "ObjetNonDefini" could not be autoloaded (file missing)' in /home/mycroft/tmp/php/test.php:9
Stack trace:
#0 /home/mycroft/tmp/php/test.php(26): __autoload('ObjetNonDefini')
#1 {main}
  thrown in /home/mycroft/tmp/php/test.php on line 9

__construct et __destruct

Comme les autres langages programmation objet, PHP propose de définir des constructeurs/destructeurs génériques qui seront appelés lors de la construction (et de la destruction) des dits objets.

A noter que le destructeur sera appelé même si l’objet n’est pas explicitement détruit (à la fin d’un script par exemple).

<?php // Objet.class.php
class Objet
{
    public function __construct()
    {
        echo "Constructor\n";
    }

    public function __destruct()
    {
        echo "Destructor\n";
    }
};
<?php // test.php
require_once("Objet.class.php");
$obj = new Objet;
echo "Fin du script.\n";
$ php test.php
Constructor
Fin du script.
Destructor

Les objets et la visibilité des méthodes privées

PHP gère aussi les variables et méthodes privées, accessible que dans la classe où celles ci existent. Cependant, surement du fait à comment sont gérés les objets dans PHP, il est possible d’appeler une méthode privée d’une instance d’un objet hors de cette instance, à la seule condition que l’on appelle dans un autre objet du même type. Ceci est également expliqué dans le chapitre “Visibility from other objects” de la documentation.

Exemple:

<?php // Objet.class.php
class Objet
{
    private $obj_name = NULL;

    public function __construct($name = NULL)
    {
        $this->obj_name = $name;
    }

    // La méthode privée en question
    private function privateMethod()
    {
        echo "Methode privée dans " . $this->obj_name . "\n";
    }

    // Une méthode publique qui va servir de "tremplin" vers la fonction privée d'une instance tierce
    public function test(Objet $t)
    {
        $t->privateMethod();
    }
};
<?php // fichier test.php
require_once("Objet.class.php");
$obj = new Objet('objet n°1');
$obj2 = new Objet('objet n°2');
// C'est l'objet "obj" qui va appeler une méthode privée appartenant à obj2:
$obj->test($obj2);

Et cela marche:

$ php test.php
Methode privée dans objet n°2

Cependant, cela est à mon avis tout simplement une abération du langage qui ne devrait être utilisée.

__toString

La fonction __toString est une fonction toute simple permettant de définir la façon dont la classe va réagir quand on demandera à la convertir en String (en faisant un appel à “echo”, par exemple).

// En reprenant l'Objet ci dessus en rajoutant la méthode:
    public function __toString()
    {
        return "class " . $this->obj_name;
    }
// Et dans test.php
echo $obj;
// Retournera 'class objet n°1'

__invoke

En restant sur la même page de documentation, une autre fonction existe pour définir comment agira la classe si on l’appelle comme étant une fonction. Il s’agit de la fonction __invoke.

// En reprenant Objet.class.php, en y ajoutant:
    public function __invoke()
    {
        echo "Appel de echo sur " . $this . "\n";
    }
<?php // fichier test.php
require_once("Objet.class.php");
$obj = new Objet('objet n°1');
$obj();

Et cela donnera:

$ php test.php
Appel de echo sur class objet n°1

Il reste encore beaucoup de fonctions à découvrir (setters et getters/callers génériques…), et celles ci feront l’objet d’une suite à cet article.

Réflexions sur les URL shortener

Soyons clairs:

Bien que les URL shortener peuvent être utiles dans certains rares cas (comme utilisation de services de partage de contenus très fortement restreints par leur taille comme Twitter), je ne leur trouve que des défauts.

Beaucoup d’explications sont déjà données par Joshua Schachter sur son weblog. Voici une synthèse de ce qu’il y dit:

⇀ Ils ajoutent une surcouche (appel dns, connexion et requète http surement superflue);
⇀ Il s’agit d’un service centralisé. Et comme tous les services centralisés, une fois qu’ils disparaissent, il n’y a plus aucun moyen de résoudre les liens créés. Des dizaines de liens sont donc inutilisables même si le contenu est encore présent sur le web. Et bien que la plupart d’entre eux proposent de choisir son url “courte”, celles ci se retrouvent rapidement utilisées par donc et deviennent donc innaccessible.

Je rajouterai également:

⇀ Les URL shorteners détruisent la sémantique des liens. On passe d’un lien de type http://en.wikipedia.org/wiki/URL_shortening#Use_the_smallest_space_possible devient http://bit.ly/9as7Ex. Et de suite, on ne comprend plus trop le contenu.

C’est pour ça que j’ai développé une ébauche du mien (qui se trouve à l’adresse http://mkz.me/, en référence à mon propre domaine). Ne cherchez pas, jusqu’à nouvel ordre, il est et restera closed source (tellement sa trivialité est desespérante) et pire: son utilisation n’est qu’à mon seul usage (et celui de certains de mes proches). Seul moi et une petite minorité d’élu y avons accès. Pourquoi ?

Parce que quitte à en utiliser un (ce qui est quasi obligatoire sur Twitter, je le répète), autant en utiliser le mien (et être responsable de la qualité des liens créés et de leur résolution). D’autant plus que de cette façon, je suis plus à même de contrôler son utilisation et d’effectuer un suivi.

De plus, cela règle le problème du contrôle des urls courtes customisables. Je suis le seul à pouvoir choisir et contrôler “l’espace” des urls utilisables.

En utilisant un service tiers, on ne contrôle pas non plus certains paramêtres qui comme le type de redirection utilisé (Je pense aux 301 Moved permanently, 302 Found ou 307 Temporary Redirect). Ces types définissent le caractère temporaire ou permanent de la redirection. Cela influe sur le client/navigateur mais également sur les moteurs de recherches. Et personnellement, je veux pouvoir décider de si une adresse sera permanente ou non. Quand on utilise des services tiers, bien que la plupart font du 301, on n’en a pas la certitude.

A lire également sur le sujet:

URL Shorteners: Which Shortening Service Should You Use?;
Une longue liste des shorteners existants;
Shorter is Sweeter: A look at url at URL Shorteners.

Synchronisez vos évènements avec node.js

J’ai passé mon après midi à tester node.js (framework basé sur V8 pour développer des serveurs d’I/O de façon évènementielle), node-websocket-server (composant websocket pour node.js) et l’élément <video> html5. Au final, ça m’a donné de synchroniser le lancement de plusieurs vidéos lancées dans plusieurs navigateurs à partir d’un point unique (j’ai comme idée de créer un cinéma virtuel sur le net où tout le monde pourra voir la même vidéo en même temps ;) ).

Au final, même si j’ai pris un peu de temps, c’est assez simple et il commence à y avoir pas mal d’exemples pour faire tout ça sur le net.

On commence par installer et compiler Node.js:

$ git clone http://github.com/ry/node.git
[...]
$ cd node
$ ./configure
[...]
$ make
[...]
$ file build/default/node
ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, not stripped

Une fois compilé, on pourra valider avec le petit test “hello world” présent sur la home page du site.

Ensuite, pour communiquer entre mon navigateur et le serveur, j’ai décidé d’utiliser les websockets. Pour cela, il faut avoir la lib spéciale pour node.js, et il s’agit de node-websocket-server. Il ne faut pas oublier de setter la variable NODE_PATH pour donner l’emplacement de la bibliothèque, car cela est indispensable au lancement de node.js

$ git clone http://github.com/miksago/node-websocket-server.git
[...]
$ export NODE_PATH=$(pwd)/node-websocket-server/lib

Il faut maintenant se fabriquer un tout petit serveur simple qui relaie juste les évènements.

C’est parti pour serveur.js:

// Modules nécessaires
var http = require("http");
var ws = require("ws");
var sys = require("sys");

function dummy(req, res){
};

var httpServer = http.createServer(dummy);

var server = ws.createServer({
  debug: true
}, httpServer);

server.addListener("listening", function(){
    sys.puts("Listening...");
});

server.addListener("connection", function(conn){
    server.send(conn.id, "Connected as: "+conn.id);
    conn.addListener("message", function(message){
        sys.puts("Message: " + message);
        conn.broadcast("<"+conn.id+"> "+message);
    });
});

server.addListener("close", function(conn){
    sys.puts("Closing...");
    conn.broadcast("<"+conn.id+"> disconnected");
});

// lancement du serveur sur le port 8000
server.listen(8000);

Il faut lancer le serveur. Rien de plus facile:

$ ./node/node serveur.js
Listening…

Pour la partie cliente, pour gagner du temps, on va utiliser jquery. Ca donnera ça pour le html:

<html>
<head>
<script src="jquery-1.4.2.min.js" type="text/javascript"></script>
<script src="client.js" type="text/javascript"></script>
</head>
<body>

<input id="start" type="button" value="start video" />
<input id="stop" type="button" value="stop video" />
<input id="reset" type="button" value="reset video" />

<video id="video" src="lavide.ogv" style="width: 100%; height: 100%;" tabindex="0"></video>

</body>
</html>

Et pour le javascript (client.js):

$(document).ready(function(){
    // La websocket que l'on stocke "globalement"
    var ws;

    if ("WebSocket" in window) {
    // Ou connecter la socket:
        ws = new WebSocket("ws://localhost:8000/service");
        ws.onopen = function() {};
        ws.onmessage = function (evt) {
            // Quand on reçoit un message du serveur...:
            var received_msg = evt.data;

            if(received_msg.match(/start/gi)) { $('#video').get(0).play(); };
            if(received_msg.match(/stop/gi)) { $('#video').get(0).pause(); };
            if(received_msg.match(/reset/gi)) { $('#video').get(0).currentTime = 0; };
        };
        ws.onclose = function() {};
    } else {
        // Pas de support WebSocket.
    }

    // Et les évènements locaux (les boutons)...:
    $('#start').click(function() {
        ws.send("start");
        $('#video').get(0).play();
    });

    $('#stop').click(function() {
        ws.send("stop");
        $('#video').get(0).pause();
    });

    $('#reset').click(function() {
        ws.send("reset");
        $('#video').get(0).currentTime = 0;
    });
});

Et voilà. C’est tout. Chargez la page dans plusieurs sessions du navigateur, et tout devrait rouler. ;)

Uploader et partager son projet git sur un serveur

J’ai créé mon projet versionné sous git, et maintenant, je veux le partager aux autres. Voici les étapes à suivre pour faire un clone du projet, l’uploader et le mettre à disposition.

On clone avec l’option ‘–bare’ le projet. Cela permet de créer un dépot composé uniquement de la base de données git. Ce dépot pourra ensuite être cloné par d’autres:

$ git clone --bare test-sf-git test-sf.git
Initialized empty Git repository in /home/mycroft/tmp/test-sf.git/
$

On upload, ici sur mon serveur ‘remote-server’ en ssh dans mon public_html. Il sera du coup disponible:

$ scp -r test-sf.git login@remote-server:www/
9de29bb2d1d6434b8b29ae775ad8c2e48c5391                            100%   15     0.0KB/s   00:00    
ade6c53443196f3146de569b58b36bfcb5a98f                            100%  196     0.2KB/s   00:00    
cb726ceadb9b58db9877d442399cc84de9a10b                            100%   51     0.1KB/s   00:00

Sur le serveur, il faudra mettre à jour les infos de ce dernier afin que le dépôt soit utilisable:
$ ssh login@remote-server
$ cd www/test-sf.git
$ git-update-server-info
$

De retour en local dans test-sf-git, on défini ce dépot cloné comme origin, cela nous servira pour pusher nos futurs modifs:

$ cd test-sf-git
$ git remote add origin login@remote-server:test-sf.git
$

On vérifie la manip’:

$ git show origin
* remote origin
  Fetch URL: login@remote-server:test-sf.git
[...]

Pour mettre à jour ce dépôt, après de futurs modifications, on pourra push:

$ git push origin
Everything up-to-date

(Là évidement j’avais pas de modification …)

De même, dans l’autre sens, on pull-era le dépôt:

$ git pull origin master
From remote-server:test-sf
 * branch            master     -> FETCH_HEAD
Already up-to-date.

Une personne tierce pourra à partir de ce moment clone notre dépôt:

$ cd ~/tmp
$ git clone http://remote-server/~login/test-sf.git
Initialized empty Git repository in /home/mycroft/dev/test-sf/.git/

$

Utiliser Mongodb pour collecter des tweets avec le sample du firehose

Dans le cadre de tests à mon travail, j’ai eu l’occasion de gouter à l’API de streaming twitter (enfin qu’à sa version de démonstration, samplée à uniquement 5% du traffic total). Le but de la maneouvre ici est de récupérer et de stocker le flux et d’opérer des requêtes ponctuelles sur la base (récupération de tweets d’un user donné, requêtes par localités, etc.).

Je vous livre ici les scripts

<?php

$sampleUrl = 'http://stream.twitter.com/1/statuses/sample.json';

$user = '** votre login twitter **';
$pass = '** votre password twitter **';

// Connexion sur la base Mongo
$mongo_cnx = new Mongo();
$db = $mongo_cnx->twitter;

$fp = fopen("http://" . $user . ":" . $pass . "@stream.twitter.com/1/statuses/sample.json", "r");

while($data = fgets($fp))
{
    // Récupération du tweet complet fourni par Twitter
    $tweet = json_decode(trim($data));
   
    // Si le tweet n'a pas d'auteur, on le drop. C'est le cas sur les effacements de tweets.
    if( ! isset($tweet->user) )
        continue;
   
    // On découpe le tweet et son user (qui seront tous deux stockés dans les deux tables suivantes):
    $tweet_user = get_object_vars($tweet->user);
    $tweet_mesg = get_object_vars($tweet);
    unset($tweet_mesg['user']);
   
    $tweet_mesg['user_id'] = $tweet_user['id'];
   
    // Stockage du tweet et de son auteur:
    $db->user->insert($tweet_user);
    $db->tweet->insert($tweet_mesg);
   
    echo "Tweet " . $tweet_mesg['id'] . " done.\n";
}

fclose($fp);

Et on le lance:

$ php fetch_twitter.php
...
Tweet 16774425803 done.
Tweet 16774426103 done.
Tweet 16774426201 done.
Tweet 16774426202 done.
Tweet 16774426200 done.
...

Pendant le runtime, on peut regarder évoluer la base:

$ ./bin/mongo
MongoDB shell version: 1.4.3
url: test
connecting to: test
type "help" for help
> use twitter
switched to db twitter
> db.tweet.find().count()
457811
> db.user.find().count()
457811
...

Les applications sont multiples, en voici une simple qui prend les dix derniers tweets, et remonte leurs auteurs:

<?php

$mongo_cnx = new Mongo();
$db = $mongo_cnx->twitter;

$count = $db->tweet->count();

echo "There is $count tweets.\n";

$cur = $db->tweet->find()->skip( $count - 10 );

function getUser($db, $user_id)
{
    $user = $db->user->findOne( array( 'id' => $user_id ) );
    return $user;
}

foreach($cur as $tweet)
{
    $user = getUser($db, $tweet['user_id']);
    echo "From " . $user['name'] . ": " . $tweet['text'] . "\n";
}

En voici une seconde rapide qui va remonter les tweets des utilisateurs ayant définie une langue “fr”:

<?php

$mongo_cnx = new Mongo();
$db = $mongo_cnx->twitter;

$cur = $db->user->find(array("lang" => "fr"));

foreach($cur as $user)
{
    $tweet_cur = $db->tweet->find(array("user_id" => $user['id']));
    foreach($tweet_cur as $tweet)
    {
        echo $tweet['text'] . "\n";
    }  
}

Développeurs: Vous n’êtes pas seuls !

Je suis en pleine réflexion ces temps-ci quant au travail fourni par deux apprentis que j’encadre. Ils sont de niveau bac+2, et j’ai fais pour ma part un stage à ce niveau, donc je peux faire un parallèle entre eux et moi. Et quelque chose me frappe: Ils sont seuls au monde. Quand ils développaient, ils ont oublié qu’il existait des gens autour d’eux, des gens avec qui ils doivent travailler, des gens à qui il faut vendre leur produit et des gens qui doivent utiliser leur produit et à qui il faut assurer un support.

De plus, dans une très petite société (moins de 10), un développeur doit dépasser son contexte. Il doit pouvoir s’adapter à chaque situation, et celle ci peut passer du tout au tout à chaque moment de la journée de travail. Les développeurs l’oublient. Comme ils restent dans leur bulle, ils oublient que d’autres gens vont utiliser ce qu’ils produient. Ces développeurs produisent leur code à leur image, lisent une spec, la code, au mieux la teste, mais oublient tout simplement tout le reste.

⇀ Ils oublient leurs collègues développeurs, en décidant de ne pas respecter les conventions de codage, les règles de nommage, les principes de base, de documenter leur code, de le commenter;

⇀ Ils abandonnent l’ingénieur système, qui met en production leur application, en ne discutant pas avec eux aux moyens de remonter les problèmes (logs), en ne testant pas préalablement les package générés, en développant sur leur système en oubliant la configuration des systèmes en prod, en ne documentant pas les fichiers de configuration, les variables, les comportements attendus en cas de problème;

⇀ Ils ne facilitent pas la vie de la QA, en ne leur fournissant pas les moyens d’exploiter avec satisfaction l’application produite à des fins de tests: En faisant une application inutilisable, la QA prendra le moins de temps possible à la valider, et au final le produit ne sera pas testé. C’est au développeur de définir avec lui les moyens de tests, outils, jeux de données prèts à être utilisés;

⇀ Ils oublient de vendre leur application. Qui n’a jamais été la “victime” de l’effet démo ? Personne. Et la faute à qui ? Une coupure réseau pendant une démo d’une appli sur le net, certes, ça arrive. Mais la plupart du temps, c’est juste la faute du développeur, qui est resté dans sa propre idée de l’utilisation du soft. Il a juste oublié d’imaginer donc de jouer un simple test fonctionnel;

⇀ Et au final, et pour moi le pire, ils ignorent totalement le client final. Et là je me demande à quoi ils servent si ils ne réfléchissent pas un seul instant à cet utilisateur qui va devoir utiliser l’outil qu’ils produisent pour eux au quotidien. Je ne parle pas juste que de l’ergonomie générale de l’application qu’ils auront oublié, du manuel d’utilisation qu’ils n’auront pas rédigé, mais des tests de leur application qu’ils ne prennent même pas la peine de faire (je ne parle pas des tests unitaires, fonctionnels qu’ils sont censés faire !), du bâclage de la correction des bugs, des petits détails qui rendent utilisables le logiciel en question… toutes ces choses qui en font un outil formidable.

Alors, je n’ai qu’un conseil: Regardez autour de vous. Vous n’êtes pas seuls. Vous êtes entourés de gens surement compétants, surement formidables, de clients pressés d’utiliser vos produits (et au final de vous nourrir, pensez-y), alors ne les oubliez pas. Ils sont là, allez leur parler, communiquez, échangez. Pour moi, dans monde idéal, avant de la vendre, le développeur devrait faire tester à plusieurs personnes de sa société plus longtemps qu’un simple test leur application. Ca peut suffire de faire d’un tool qui passerait durement pour un prototype à un outil prêt à mettre en production.

Symfony: forward global à toute une application en utilisant les Filters

Récemment, mon apprenti a commencé à me rajouter dans chaque action de chaque contrôleur un même morceau de code qui régule un peu le flow d’execution:

// extrait de apps/myapp/modules/mymodule/actions/actions.class.php
public function executeMyAction(sfWebRequest $request)
{
    $state = Account::getState();
    if( $state == Account::ACCOUNT_NOT_READY )
         $this->forward('mymodule', 'getready');

    [...]
}

public function executeMyOtherAction(sfWebRequest $request)
{
    $state = Account::getState();
    if( $state == Account::ACCOUNT_NOT_READY )
         $this->forward('mymodule', 'getready');

    [...]

}

[...]

J’étais à la recherche d’une solution un peu plus ’sexy’ et ne pas à modifier 30 contrôleurs si un jour j’avais besoin de modifier ce code. J’ai pensé à la fonction ‘preExecute‘, qui permet de lancer du code avant de rentrer dans une action, mais la portée ne se limite qu’au module. Je voulais quelque chose d’un peu plus global.

J’ai trouvé la solution en browsant le code de sfGuard: Il me suffit de créer un Filter utilisant cet extrait de code suivant:

public function execute($filterChain)
{
    [...]
   
    if($this->isFirstCall())
    {
        // Je sais, ça a l'air d'être une fonction statique, n'en prenez pas compte, c'est moche (c).
        if( Account::getState() == Account::ACCOUNT_NOT_READY )
        {
            $this->getContext()->getController()->forward('mymodule', 'getready')
            throw new sfStopException();
        }
    }

    $filterChain->execute();
}

Note: Initialement, j’avais mis des “throw new sfStopException();”. Cela permet d’interrompre l’exécution des Filters, et donc de ne pas lancer d’autres possibles redirections et obtenir en final un résultat correct. Cependant, après discussion avec mon pote mirmo qui m’a dit que c’était pas top, je me suis rendu compte que forward faisait déjà le throw de l’exception:

// extrait de lib/vendor/symfony/lib/action/sfAction.class.php
public function forward($module, $action)
{
    if (sfConfig::get('sf_logging_enabled'))
    {
        $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Forward to action "%s/%s"', $module, $action))));
    }

    $this->getController()->forward($module, $action);

    throw new sfStopException();
}

Alors au final, il suffit de retirer les exceptions, de faire un simple return des forward et de compléter notre sfFilter, ce qui donnera:

<?php

// extrait de apps/myapp/lib/activeUserFilter.class.php
class activeUserFilter extends sfFilter
{
    public function execute($filterChain)
    {
        $moduleName = $this->getContext()->getModuleName();
        $actionName = $this->getContext()->getActionName();

        // We don't redirect if we are trying to call site/success.
        if($this->isFirstCall() && ($moduleName != 'mymodule' && $actionName != 'success'))
        {
            $state = Account::getState();
            if( $state == Account::ACCOUNT_NOT_READY )
            {
                return $this->getContext()->getController()->forward('mymodule', 'getready')
            }
        }

        $filterChain->execute();
    }
}

Enfin, on l’active juste en rajoutant ce sfFilter dans apps/myapp/config/filters.yml:

# insert your own filters here
active:
  class: activeUserFilter

Update:

Ayant eu à nouveaux des problèmes, j’ai trouvé une autre solution peut être un peu plus simple:

$lastActionEntry = $this->getContext()->getActionStack()->getLastEntry();
return $lastActionEntry->getActionInstance()->forward('...', '...');

A voir donc …

Installer l’extension PHP de Mongodb et son utilisation

Pour utiliser une base MongoDB avec PHP, il faut installer l’extension Mongo (http://www.php.net/manual/en/mongo.installation.php). Comme c’est assez simple, ayant déjà tous les outils pour compiler une extension PHP, j’ai du coup préféré la voie de la compilation:

On commence par se créer un répertoire, et on choppe les sources sur le dépôt officiel git:

$ cd /tmp/mongodb/php
$ git clone http://github.com/mongodb/mongo-php-driver.git
Initialized empty Git repository in /tmp/mongodb/php/mongo-php-driver/.git/
remote: Counting objects: 4106, done.
remote: Compressing objects: 100% (1338/1338), done.
remote: Total 4106 (delta 3100), reused 3670 (delta 2742)
Receiving objects: 100% (4106/4106), 1.02 MiB | 269 KiB/s, done.
Resolving deltas: 100% (3100/3100), done.

La compilation et l’installation du module ne diffère pas par rapport à un autre module PHP:

$ cd mongo-php-driver
$ phpize
Configuring for:
PHP Api Version:         20041225
Zend Module Api No:      20060613
Zend Extension Api No:   220060519

$ ./configure --enable-mongo
[...]
configure: creating ./config.status
config.status: creating config.h
config.status: executing libtool commands

$ make
[...]

Build complete.
Don't forget to run 'make test'.

Et finalement, en tant que root:

$ make install
Installing shared extensions:     /usr/lib/php/modules/

$ cat > /etc/php.d/mongo.ini << EOF
> extension=mongo.so
> EOF

On pourra vérifier la présence de l’extension:

$ php -i |grep -i mongo
/etc/php.d/mongo.ini,
MongoDB Support => enabled
[...]

Il ne reste plus qu’à tester du code php. La documentation est ici http://www.php.net/manual/en/mongo.manual.php.
Dans le script suivant, je prends pour exemple la base que j’ai créée dans un précédent article, et j’en adapte mes actions à un petit script PHP qui s’explique – presque – tout seul:

<?php

// Connection sur le serveur
$mongo_cnx = new Mongo();

// On utilise la base de données 'testdb' avec laquelle
// on a fait des tests précédemment:
$db = $mongo_cnx->testdb;

// On 'scanne' la collection 'people' dans laquelle
// on va retrouver nos enregistrements:
foreach($db->people->find() as $person)
{
    echo $person['name'] . ' - ' . $person['age'] . "\n";
}
// xin - 24
// patrick - 28
// patrick - 27

// On peut rajouter de nouvelles données:
$db->people->insert( array('name' => 'red', 'age' => 12) );

// On peut également faire des requetes plus précises comme dans
// le 'pseudo shell' mongo:
$red = $db->people->findOne( array('age' => 12) );
// $red sera:
// array(3) {
//   ["_id"]=>
//   object(MongoId)#7 (0) {
//   }
//   ["name"]=>
//   string(3) "red"
//   ["age"]=>
//   int(12)
// }

// Mise à jour de notre objet, en spécifiant un nouveau champs:
$red['city'] = 'Paris';

// Une mise à jour prend en premier argument le critère descriptif
// des objets à updater et l'objet à utiliser comme modification.
$db->people->update(array('name' => 'red', 'age' => 12), $red);
echo "There is now " . $db->people->count() . " person(s) in database.\n";

// On vérifie que la modification a été effectuée:
var_dump( $db->people->findOne( array('name' => 'red') ) );
// array(4) {
// ...
//   ["city"]=>
//   string(5) "Paris"

// Et les supprimer:
$r = $db->people->remove($red);
if( $r ) echo "Record deleted !\n";

$red = $db->people->findOne( array('name' => 'red') );
var_dump($red);
// Renvoie NULL.

Mongodb: Installation et premiers pas.

Mongodb est un soft opensource de DB non relationel (Nosql), orienté document sans schéma écrit en C++. L’avantage des systèmes Nosql sont les hautes performances et les contraintes moindre grâce à la non présence de schéma de données.

J’ai testé il y a quelques mois HBase, mais même si les performances étaient relativement bonnes pour ce que je voulais faire (je le décrirai ici un jour), je voulais me faire une idée sur les autres solutions, et plus particulièrement Mongodb puis peut être plus tard Tokyo Cabinet (ou son successeur Kyoto Cabinet).

Je décris dans cet article comment installer et tester rapidement un serveur mongodb. Dans des articles suivants, je testerai notamment le drivers PHP et comment faire des requêtes de lecture et écriture en base.

Télécharger mongdb:

$ mkdir /tmp/mongodb ; cd !-1:1
$ wget http://downloads.mongodb.org/linux/mongodb-linux-i686-1.4.3.tgz
$ tar xvfz mongodb-linux-i686-1.4.3.tgz

Lancer le serveur:

$ mkdir /tmp/mongodb/db/
$ cd mongodb-linux-i686-1.4.3
$ ./bin/mongod --dbpath /tmp/mongodb/db/
Fri Jun  4 11:07:26 Mongo DB : starting : pid = 28548 port = 27017 dbpath = /tmp/mongodb/db/ master = 0 slave = 0  32-bit
Fri Jun  4 11:07:26 db version v1.4.3, pdfile version 4.5
Fri Jun  4 11:07:26 git version: 47ffbdfd53f46edeb6ff54bbb734783db7abc8ca
[...]

Dans un autre terminal, lancement d’un client et test de quelques requêtes:

On utilise la db “testdb”:

> use testdb
switched to db testdb

Dans laquelle on enregistre quelques valeurs:

> person = { 'name': 'xin', 'age': 24 }
{ "name" : "xin", "age" : 24 }
> db.people.save( person )
> person = { 'name': 'patrick', 'age': 28 }
{ "name" : "patrick", "age" : 28 }
> db.people.save( person )

On vérifie le contenu de la base:

> db.people.find()
{ "_id" : ObjectId("4c08c38bb91ea16336bdddf2"), "name" : "xin", "age" : 24 }
{ "_id" : ObjectId("4c08c394b91ea16336bdddf3"), "name" : "patrick", "age" : 28 }

On voit que mongodb rajoute pour chaque enregistrement un id interne.

On peut limiter le nombre de résultat par requête:

> db.people.find().limit(1)
{ "_id" : ObjectId("4c08c38bb91ea16336bdddf2"), "name" : "xin", "age" : 24 }

Ou encore affiner le résultat en cherchant des enregistrements par valeurs spécifiques:

> db.people.find({ 'age' : 24 })
{ "_id" : ObjectId("4c08c38bb91ea16336bdddf2"), "name" : "xin", "age" : 24 }

On pourra enregistrer le résultat des requêtes dans des variables:

> var my_result = db.people.findOne( { 'age': 28 } )      
> print(tojson(my_result))
{
        "_id" : ObjectId("4c08c394b91ea16336bdddf3"),
        "name" : "patrick",
        "age" : 27
}

Plus d’information sur la page de tutoriel sur mongodb.org.