Monthly Archives: juillet 2010

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. ;)