13/04/2012

Flux personnalisés et filtres en PHP (Streams)

Lorsque vous faites un fopen(), ou toute autre fonction équivalente, PHP vous retourne une ressource, sous forme d'un flux. Il existe différent types de flux en PHP :

<p>fopen('file://...') // fichier</p>
<p>fopen('php://temp') // fichier temporaire</p>
<p>fopen('php://memory') // en mémoire</p>
<p>fopen('php://stdout') // sortie de la console</p>
<p>etc.</p>

Bref, il y en a pas mal...

Mais on oublie souvent qu'on peut aussi ajouter ses propres types de flux. Par exemple je vais créer un type de flux "twitter" pour lire mes tweets :

$fp = fopen("twitter://@halleck45", "r");
if ($fp) {
    while (!feof($fp)) {
        var_dump(fgets($fp, 140));
    }
}

Création d'un nouveau type de flux

C'est relativement facile : il suffit d'ajouter un nouveau gestionnaire de flux, c'est à dire une classe qui respecte le prototype StreamWrapper.

stream_wrapper_register("twitter", "TwitterStream");

Plutôt que d'implémenter toutes les méthodes de ce prototype, concentrons nous sur le principal, et créons 4 méthodes :

  • stream_open(), qui va ouvrir notre flux
  • stream_close(), pour le fermer
  • stream_read($size) qui va être appelée à chaque lecture dans le flux
  • stream_eof(), pour indiquer qu'on arrive à la fin

Pour simplifier l'exemple, le flux Twitter est en lecture seule. Pour la même raison, on va rapatrier tous les tweets d'un coup et les stocker dans notre objet dans un tableau.

Pour démarrer, créons une petite fonction qui va aller chercher les tweets d'un utilisateur donné :

ini_set('allow_url_fopen', 1);
define('TWITTER_PWD', 'votre-mot-de-passe');
define('TWITTER_LOGIN', 'votre-login');

function example_stream_twitter_fetch($username) {
    $ch = curl_init();
    $url = sprintf('http://api.twitter.com/1/statuses/user_timeline.json?screen_name=%s&include_entities=true&include_rts=true&count=20', $username);
    curl_setopt($ch, CURLOPT_URL,$url);
    curl_setopt($ch, CURLOPT_USERPWD, TWITTER_LOGIN . ':' . TWITTER_PWD);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $result = curl_exec($ch);
    curl_close($ch);
    return $result ? json_decode($result) : null;
}

Maintenant le gestionnaire de flux :

class TwitterStream {

    protected $_json;
    protected $_offset = 0;

    public function stream_open($path, $mode, $options, &$opened_path) {

        //
        // Read only !
        if ($mode != 'r') {
            trigger_error("Unsupported mode", E_USER_WARNING);
            return false;
        }

        //
        // Calling example_stream_twitter_fetch() in order to fetch data from twitter
        $result = false;
        if (preg_match('!@(\w*)!', $path, $match)) {
            $result = example_stream_twitter_fetch($match[1]);
        }

        if (!$result) {
            if (($options & STREAM_REPORT_ERRORS)) {
                trigger_error("Username not found", E_USER_WARNING);
            }
            return false;
        }

        $this->_json = $result;
        return (bool) $result;
    }

    public function stream_close() {
        $this->_json = null;
        return true;
    }

    public function stream_read($count) {
        return $this->_json[$this->_offset++]->text;
    }

    public function stream_eof() {
        return $this->_offset >= sizeof($this->_json);
    }

}

Rien de bien compliqué. Il faut juste bien penser à lancer un warning en cas de problème...

Et ça suffit :

stream_wrapper_register("twitter", "TwitterStream");

$fp = fopen("twitter://@halleck45", "r", STREAM_REPORT_ERRORS);
if ($fp) {
    while (!feof($fp)) {
        var_dump(fgets($fp, 140));
    }
}

Pratique non ?

Bon, bien sûr l'exemple est trivial, ne serait-ce parce que les tweets peuvent dépasser les 140 caractères à cause des liens ; mais je pense que vous aurez compris l'intérêt de la chose :-)

Appliquer des fitres sur des flux

Autre "truc pratique" assez peu utilisé mais vraiment utile : on peut appliquer des filtre sur des flux, même sur les flux "natifs".

Par exemple, je veux convertir le texte que j'écris dans un fichier en l33t, en utilisant cette fonction :

function l33t($string) {
    return str_replace(array('l', 'e', 't'), array('1', '3', '7'), $string);
}

Nous allons enregistrer le filtre "l33t" et l'associer à la classe "l33t_filter' :

stream_filter_register("l33t", "l33t_filter");

Cette classe l33t_filter doit hériter de la classe native php_user_filter :

class l33t_filter extends php_user_filter {

    function filter($in, $out, &$consumed, $closing) {
        
        while ($bucket = stream_bucket_make_writeable($in)) {
            //
            // leet -> l33t
            $bucket->data = l33t($bucket->data);
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        // on retournerait PSFS_ERR_FATAL en cas d'erreur bloquante
        return PSFS_PASS_ON;
    }
}

On peut par exemple imaginer appliquer des filtres de cryptage, de contrôle...

Alors, convaincu ? :-) Avez-vous déjà utilisé votre propres flux ou filtre de flux en PHP ?

blog comments powered by Disqus