Tag Archives: php

PHP, Reflection et Annotations

J’ai récemment eu à coder à mon taf des jeux de tests pour valider le comportement de mon serveur de pub, et pour ça, j’ai codé des petits scripts tout bête pour créer/modifier les données dans une base postgresql. J’aurai pu prendre un ORM tout fait (propel, doctrine…), mais j’ai juste codé mes classes tout bêtement et le plus simplement du monde. Un exemple:

class Voiture
{
    public $id;
    public $marque;
    public $couleur;
};

Pour les requêtes d’insert, au lieu d’avoir des méthodes spécialisées dans chaque classe, j’ai codé juste une fonction save() qui prend l’objet à sauver, et qui parse grâce aux fonctions get_class_* ses propriétés pour générer les requêtes d’INSERT ou d’UPDATE. Cette fonction ressembait dans un premier temps à:

function save($obj)
{
    $class_name = get_class($obj);
    $class_vars = get_class_vars($class_name);

    $fields_names = '';
    $fields_values = '';

    $fields_count = 0;

    foreach($class_vars as $var_name => $var_value)
    {
        if(isset($obj->$var_name))
        {
            $fields_names .= $var_name . ', ';
            if(is_string($obj->$var_name))
                $fields_values .= '\'' . $obj->$var_name . '\', ';
            else
                $fields_values .= $obj->$var_name . ', ';

            $fields_count ++;
        }
    }

    if(0 == $fields_count)
    {
        return -1;
    }

    // Remove trailing ', ';
    $fields_names = substr($fields_names, 0, strlen($fields_names) - 2);
    $fields_values = substr($fields_values, 0, strlen($fields_values) - 2);

    return "INSERT INTO " . $class_name . "(" . $fields_names . ") VALUES (" . $fields_values . ");";
}

Ca me contentait dans pas mal de cas simples, mais très rapidement, cela à montré ses limites. J’avais besoin de connaître plus précisement les types des champs dans la base. Et la solution a été d’utiliser les annotations, pour ne pas avoir de longues et lourdes modifications à faire dans toutes mes classes d’objet. PHP ne propose pas out of the box un système complet d’annotation, mais incorpore les outils pour développer un comportement analogue: les classes Reflection.

Ces classes permettent de faire du « reverse engineering » sur les classes, les interfaces etc pour retrouver les différentes composantes de ces objets, qu’ils soient instanciés ou non. Et de plus, elles permettent de récupérer les commentaires des classes, des méthodes et des propriétés s’ils existent. Il n’y a qu’à les mettre en application:

<?php

/**
 * Commentaire classe voiture
 */

class Voiture
{
    /**
     * Commentaire propriete id
     */

    public $id;
}

$voiture = new Voiture;

$reflection = new ReflectionClass($voiture);
echo "Classe:\n";
echo $reflection->getDocComment() . "\n";

foreach($reflection->getProperties() as $property)
{
    echo $property->name . ":\n";
    echo $property->getDocComment() . "\n";
}

Ce qui donne:

$ php test_2.php
/**
 * Commentaire classe voiture
 */
id:
/**
     * Commentaire propriete id
     */
$

Il ne resterait plus maintenant qu’à définir ses keywords, parser les commentaires propement pis à coder son propre mini-orm sans modifier les objets finaux.

Et voilà un début:

<?php

class DbObject
{
    public function save()
    {
        $schema = $this->buildSchema($this);
        /* Ici y a du code */

        return TRUE;
    }

    private function buildSchema()
    {
        $class_name = get_class($this);
        $class_vars = get_class_vars($class_name);

        $class_reflection = new ReflectionClass($this);
        echo $class_reflection->getDocComment();

        $schema = array();

        foreach($class_vars as $var_name => $var_value)
        {
            $prop_reflection = $class_reflection->getProperty($var_name);
            $comment = $prop_reflection->getDocComment();

            $comment = preg_replace(',\/\*\*(.*)\*\/,', '$1', $comment);
            $comments = preg_split(',\n,', $comment);

            $key = $val = NULL;
            $schema[$var_name] = array();

            foreach($comments as $comment_line)
            {
                if(preg_match(',@(.*?): (.*),i', $comment_line, $matches))
                {
                    $key = $matches[1];
                    $val = $matches[2];

                    $schema[$var_name][trim($key)] = trim($val);
                }
            }
        }
        var_dump($schema);

        return $schema;
    }
};

class Voiture extends DbObject
{
    /**-
     * @DbType: integer
     * @DbValue: 0
     */

    public $id;

    /** @DbType: string
     *
     * Blabla.
     *
     */

    public $marque;

    /** @DbType: string */
    public $couleur;
};

$voiture = new Voiture;
$voiture->id = 1;
$voiture->marque = 'renault';
$voiture->couleur = 'bleu';
$voiture->save();

buildSchema retournera dans cet exemple:

$ php test_annotations.php
array(3) {
  ["id"]=>
  array(2) {
    ["DbType"]=>
    string(7) "integer"
    ["DbValue"]=>
    string(1) "0"
  }
  ["marque"]=>
  array(1) {
    ["DbType"]=>
    string(6) "string"
  }
  ["couleur"]=>
  array(1) {
    ["DbType"]=>
    string(6) "string"
  }
}

Pour finir, quelques petites références sur le sujet:

Et au final, au lieu de réinventer une roue bien vieille, je suis parti utiliser Addendum:

PHP: Créer un cache avec ob_start/ob_get_contents

Dans PHP, il y a des tonnes de fonctions qu’on n’utilise pas très souvent.

Ce soir, je vais vous montrer qu’on peu avec les fonctions de contrôle de sortie créer facilement un système de cache très simpliste.

Attention: Ce système de cache est loin d’être parfait, en fait, c’est surement le pire qu’on puisse coder: il est peut performant (car en PHP), non partagé, et devrait comporter un système de locking.

Voilà le code:

<?php

class SimpleCache
{
    var $is_started = FALSE;
    var $dir = '/tmp/cache';

    public function __construct($auto_start = FALSE)
    {
        if(TRUE == $auto_start)
        {
            $this->startCache();
        }
    }

    public function __destruct()
    {
        $this->endCache();
    }

    public function getCacheFileName()
    {
        $request_data = sha1(json_encode($_REQUEST));
        $file_name = $this->dir . '/' . $request_data . '.cache';

        return($file_name);
    }

    public function startCache()
    {
        $file_name = $this->getCacheFileName();

        if(file_exists($file_name))
        {
            echo file_get_contents($file_name);
            die();
        }

        ob_start();
        $this->is_started = TRUE;
    }

    public function endCache()
    {
        $file_name = $this->getCacheFileName();

        if(FALSE == $this->is_started)
            return;

        $contents = ob_get_contents();

        $f = fopen($file_name, 'w+');
        if($f)
        {
            fwrite($f, $contents);
            fclose($f);
        }

        ob_end_flush();
    }
}

Comment ça marche: A n’importe quel moment, on démarre le mécanisme du cache en appelant la fonction startCache. Celle ci va soit stocker en mémoire ce qu’il va afficher sur stdout avec ob_start, soit si le fichier de cache existe, ressortir le contenu. Ce fichier de cache sera différent à chaque type de requête car il doit son nom à la fonction getCacheFileName, qui calculera un nom unique à pour chaque requète différente. Il est évident que cela casse toute sorte de « dynanisme ».

A la fin du script, lors du garbage collector, ou alors dès que le développeur le veut, la fonction endCache est appelée. Celle ci va stocker dans un fichier la sortie du script, récupéré grâce à ob_get_contents depuis l’appel à startCache.

On pourra utiliser cette classe de cette façon:

<?php

require_once 'SimpleCache.class.php';

$cache = new SimpleCache( TRUE );

/* Code à cacher */
echo time();

Et voilà le résultat:

$ lynx -dump http://localhost/cache/index.php
   1290807633

$ sleep 5 ; lynx -dump http://localhost/cache/index.php
   1290807633

$ ls /tmp/cache/
97d170e1550eee4afc0af065b78cda302a97674c.cache

$ cat /tmp/cache/97d170e1550eee4afc0af065b78cda302a97674c.cache
1290807633

Le résultat est bien caché dans /tmp/cache et récupéré à l’appel suivant comme désiré.

Premiers pas pour créer une application Facebook Connect avec Symfony

Ce tutoriel va vous expliquer comment créer une application très simple Facebook Connect, en gérant le fait d’être loggué sur Facebook ou pas, qui va nous permettre de faire des appels sur l’API Graph et utilisant le Javascript SDK.

Je passe très rapidement sur la création de l’application symfony. Il y a déjà des tas de tutoriaux pour cela, je vous conseille de reprendre les tutoriaux, en particulier celui de Jobeet.

On crée donc l’application (sans ORM dans mon cas):

cd devel/
mkdir -p sfbook/lib/vendor
curl http://www.symfony-project.org/get/symfony-1.4.8.tgz | tar -xzv -C !-1:2 -f -
SFBOOK=$(pwd)/sfbook; cd $SFBOOK; ./lib/vendor/symfony-1.4.8/data/bin/symfony generate:project sfbook --orm=none

Puis on récupère le php-sdk Facebook et on l’on active facebook dans l’autoloader:

cd lib/vendor ; git clone https://github.com/facebook/php-sdk.git
cat > $SFBOOK/config/autoload.yml << EOF
autoload:
    facebook:
        name: facebook
        path: %SF_LIB_DIR%/vendor/php-sdk/src
        recursive: off
EOF

On crée une application, ‘frontend’ et deux modules dans celle ci ‘default’ et ‘home’. Le premier module servira pour se logger, le 2nd pour l’application en elle même et dans notre exemple, on récupèrera juste la liste de ses amis.

php symfony generate:app frontend
php symfony generate:module frontend home
php symfony generate:module frontend default

Le module « home » ne sera accessible qu’aux utilisateurs authentifiés sur facebook. On le protège donc:

mkdir apps/frontend/modules/home/config
cat >> apps/frontend/modules/home/config/security.yml << EOF
all:
    is_secure: true
EOF

Et on oublie pas de modifier apps/frontend/config/settings.yml en rajoutant dans la section all: l’action suivantes:

 .actions:
    login_module
:          default
    login_action
:          login

A ce moment, on crée l’application sur facebook. On définit uniquement le Site URL (Onglet Website) pour accéder à notre application symfony et on note nos application Id, api key et application secret.

Comment ça va marcher ? On va mettre dans le layout général le code « connect »: Un script javascript facebook va être chargé et nous redirigera directement sur la page de login si jamais la session facebook venait à être fermée par l’utilisateur. Le template default/login contiendra le bouton facebook connect, et le template home/index qui ne s’affichera que si la session

On ajoute dans apps/frontend/template/layout.php le code suivant dans le :

<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>
<script>
  FB.init({appId: '[APP_ID]', status: true, cookie: true, xfbml: true});
  FB.Event.subscribe('auth.sessionChange', function(response) {
    if (response.session) {
        window.location = '<?php echo url_for('home/index'); ?>';
    } else {
        window.location = '<?php echo url_for('default/login'); ?>';
    }
  });
</script>

Le module « default » n’aura que l’action « login ». Cette action s’occupe d’authentifier l’utilisateur. Si l’utilisateur est loggué sur facebook, alors on le redirige vers home/index. Voilà le code de apps/frontend/default/actions/actions.class.php:

<?php
class defaultActions extends sfActions
{
    public function executeLogin(sfWebRequest $request)
    {
        $facebook = new Facebook(array('appId' => '[APP_ID]',
                                       'secret' => '[APP_SECRET]',
                                       'cookie' => TRUE ));
        $session = $facebook->getSession();

        if(NULL !== $session)
        {
            $me = $facebook->api('/me', 'GET', array('access_token' => $session['access_token']));

            // On verifie qu'on existe.
            if(NULL !== $me)
            {
                $this->getContext()->getUser()->setAuthenticated(true);
                $this->redirect('home/index');
            }
        }

        $this->getContext()->getUser()->setAuthenticated(false);

        return sfView::SUCCESS;
    }
}

Le template et default/login contiendra uniquement le bouton facebook connect:

<div id='content' align='center'>
<h1>Welcome !</h1>
<fb:login-button></fb:login-button>
</div>

Dans home, on va mettre un peu ce que l’on veut. Pour le coup, on va par exemple récupérer et afficher ses amis facebook:

<?php
class homeActions extends sfActions
{
    public function preExecute()
    {
        $this->facebook = new Facebook(array('appId' => '[APP_ID]',
                                             'secret' => '[APP_SECRET]',
                                             'cookie' => TRUE ));
        $this->session = $this->facebook->getSession();
    }

    public function executeIndex(sfWebRequest $request)
    {
        $this->friends = $this->facebook->api('/me/friends',
                                              'GET',
                                              array('access_token' => $this->session['access_token']));

        return sfView::SUCCESS;
    }
}

Le template associé est très simple:

My friends:<br />
<br />
<?php foreach($friends['data'] as $friend) : ?>
<?php echo $friend['name'] . "<br />"; ?>
<?php endforeach; ?>

Et voilà. Si en visitant le site on est déjà loggué facebook, mais que l’utilisateur n’a jamais utilisé l’application, on aura le droit à l’écran de login pour installer l’application. Ensuite, l’application affichera directement home/index.

On pourra même rajouter un bouton « logout » dans le layout global:

<?php if($sf_user->isAuthenticated()) : ?>
<input type='button' onclick='FB.logout(function(response) {});' value='logout' />
<?php endif ; ?>

Indexation et recherche dans vos applications PHP grace à Elasticsearch

Récemment, je cherchais comment faire de l’indexation de flux RSS pour pouvoir par la suite monter un véritable et pertinent moteur de recherche. Alors que certains préconiseront l’implémentation Zend de Lucene, je voulais tester quelque chose d’autre que je pourrai réutiliser sans trop de mal avec d’autres applications développées dans d’autre langages. C’est là qu’on a porté à mon attention ElasticSearch. ElasticSearch est une solution opensource, basée sur Lucene proposant un moteur d’indexation et de recherche distribué RESTful.

Il est temps lire rapidement la doc et de tester son efficacité. Je suis pressé mais je reste néanmoins un peu sérieux: Je ne vais pas utiliser la bibliothèque PHP, je vais faire la mienne. Elle sera bien plus simple mais permettra de bien voir ce que l’on fait.

Télécharger et lancer ElasticSearch: C’est tout simple:

$ wget http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.11.0.zip
$ unzip elasticsearch-0.11.0.zip
$ cd elasticsearch-0.11.0
elasticsearch-0.11.0$ bin/elasticsearch -f
[...]

On a donc besoin d’une interface permettant de créer l’index, de rajouter des documents dans cet index et de faire des recherches dedans. C’est ce que je fais dans la classe suivante:

<?php

class Elastic
{
    // On est en REST. Donc appels HTTP avec Curl. Rien de spécial, on agit
    // directement en local sur le port 9200 (par défaut).
    public function request($method = 'GET', $where, $params = NULL)
    {
        $ch = curl_init();

        $url = 'http://localhost:9200/' . $where;

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_HEADER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURL_HTTP_VERSION_1_1, TRUE);

        if($method == 'POST')
        {
            curl_setopt($ch, CURLOPT_POST, TRUE);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
        }
        else if($method != 'GET')
        {
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
            if(NULL !== $params)
            {
                curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
            }
        }

        $response = curl_exec($ch);

        curl_close($ch);

        return $response;
    }

    // Creation d'un index
    // cf: http://www.elasticsearch.com/docs/elasticsearch/rest_api/admin/indices/create_index/
    public function createIndex($name)
    {
        $response = $this->request('PUT', $name . '/');
        $code = json_decode( $response , TRUE );

        if(isset($code['ok']) && $code['ok'] == TRUE)
            return TRUE;
        else
            return $code['error'];

        return FALSE;
    }

    // Ajout d'un document d'un type donné dans un index
    // cf: http://www.elasticsearch.com/docs/elasticsearch/rest_api/index/
    public function index($indexName, $type, $id, $document)
    {
        if(is_array($document) || is_object($document))
        {
            $doc = json_encode($document);
        }
        else
        {
            $doc = $document;
        }
        $response = $this->request('PUT', $indexName . '/' . $type . '/' . $id, $doc);
        return $response;
    }

    // Recherche toute simple
    // Il y a 15000 moyens de faire des recherches avec ElasticSearch, moi je ne fais
    // que l'une des plus simples.
    // cf: http://www.elasticsearch.com/docs/elasticsearch/rest_api/search/
    public function search($indexName, $type, $query)
    {
        $q = json_encode( array('query' => array('query_string' => array('query' => $query))) );
        $response = $this->request('POST', $indexName . '/' . $type . '/_search' , $q);
        return $response;
    }

};

Il n’y a plus qu’à tester ça sur le sample du firehose Twitter. J’ai mis à jour ma lib twitter pour l’occasion. On va récupérer chaque tweet et l’indexer dans l’ElasticSearch.

<?php

require 'Twitter.class.php';
require 'Elastic.class.php';

$cs_key = '...';
$cs_secret = '...';

$oauth_token = '...';
$oauth_token_secret = '...';

$e = new Elastic();

// Creation d'un index. Si il existe deja, cela sera ignore
$e->createIndex('twitter');

$t = new Twitter($cs_key, $cs_secret, $oauth_token, $oauth_token_secret);

$fd = $t->getSampleFirehose();

while($data = fgets($fd))
{
    $data = trim($data);
    $datas = json_decode($data, TRUE);
    if(FALSE == is_array($datas))
        break;

    if( FALSE == array_key_exists('id', $datas) )
    {
        continue;
    }
    $id = $datas['id'];
    $user_name = $datas['user']['screen_name'];
    $text = $datas['text'];

    $e->index('twitter', 'tweet', $id, array('user' => $user_name, 'text' => $text));
}

$t->closeSampleFirehose($fd);

Voilà, on indexe. Maintenant, on recherche. Un petit script d’une demi douzaine de ligne suffit:

<?php

require 'Elastic.class.php';

if($argc != 2)
{
    echo "Missing arg.";
    break;
}

$e = new Elastic();

$r = $e->search('twitter', 'tweet', $argv[1]);

$results = json_decode($r, TRUE);

echo "Results: " . $results['hits']['total'] . " hit(s)\n";

foreach($results['hits']['hits'] as $hit)
{
    echo "Tweet " . $hit['_id'] . "; " . $hit['_source']['user'] . " said: " . $hit['_source']['text'] . "\n";
}

On teste les résultats:

$ php search.php usa
Results: 0 hit(s)

$ php search.php wow
Results: 3 hit(s)
Tweet 27119101300; KStewyLove said: Wow, my ftfth chapter is so... terrible. :/
Tweet 27119097100; crowchild1997 said: wow rob is slaying in this song! via @youtube
Tweet 27120707400; KrockGFW said: Good Tuesday Morn! Welcome back to reality folks!! Hope your turkey was as moist as mine...wait wow thats a gross word "moist"...ewwww

$ php search.php 'fin*'
Results: 2 hit(s)
Tweet 27119099500; MissIcele said: @justinbieber RT if your excited to finally come to Vancouver Canada! i'm praying that you won't cancel again!follow me please! [:
Tweet 27119181800; WillieJoe said: RT @GAA_BEO: Coldrick backs umpire moves: All-Ireland final referee David Coldrick last night gave a guarde... Via  ...

$ php search.php 'rt AND retweet'
Results: 1 hit(s)
Tweet 27119183300; TayKiddCuhD said: RT @K1SS3S4LIFE: RETWEET IF YOUR #TEAMILOVE

Lire des flux rss en PHP avec curl et SimpleXmlElement

Juste une quick hack rapide vieux de 15000 ans mais qui peut toujours être utile, il s’agit de la récupération via curl de flux RSS et de leur lecture avec SimpleXml.

Et comme pour moi du code bien commenté peut se passer de long discours, c’est parti:

⇀ Téléchargement du flux avec curl

<?php
// Download du flux avec curl:
$curl_hd = curl_init('http://rss.slashdot.org/Slashdot/slashdot');
// On garde une version Atom dans le coin pour la demo:
//$curl_hd = curl_init('http://rss.slashdot.org/Slashdot/slashdotatom');

// On a besoin de la reponse ...
curl_setopt($curl_hd, CURLOPT_RETURNTRANSFER, true);
// ... mais pas des headers:
curl_setopt($curl_hd, CURLOPT_HEADER, 0);

// Recuperation dans $rss du flux, et fermeture de curl.
$rss = curl_exec($curl_hd);
curl_close($curl_hd);

⇀ Deux fonctions bien utiles pour parser les flux RSS et Atom

function parse_rss($doc)
{
    // Pour chaque element...
    foreach($doc->channel->item as $item)
    {
        echo $item->title . "\n";
        echo $item->link . "\n";
        echo $item->description . "\n\n";
    }  
}  

function parse_atom($doc)
{
    // Pour chaque element...
    foreach($doc->entry as $item)
    {
        echo $item->title . "\n";
        echo $item->link->attributes() . "\n";
        echo $item->content . "\n\n";
    }  
}

⇀ Et pour finir, le parsing du flux téléchargé ($rss) avec SimpleXml:

// Creation et parsing de l'element telecharge avec SimpleXml:
$rss_doc = new SimpleXmlElement($rss, LIBXML_NOCDATA);

// Pour faire la distinction entre un fichier atom et un rss,
// on regarde juste quels tags sont presents. Dans le cas
// d'un "channel", ca sera du rss, dans le cas de l'atom,
// ce sera un "entry".
if(isset($rss_doc->channel))
{
    parse_rss($rss_doc);
}  
elseif(isset($rss_doc->entry))
{
    parse_atom($rss_doc);
}

That’s all.