Monthly Archives: juin 2010

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 remote 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.