Tag Archives: symfony

Intégrer le single sign-on Twitter avec Yapl4twitter dans symfony

Le but de ce mini tutorial est d’implémenter la fonctionnalité single sign-on de Twitter sur un site construit autour de Symfony. Pour ce faire, j’ai utilisé la bibliothèque Yapl4twitter (que j’ai développé).

On n’oubliera pas de consulter Sign in with twitter afin de mieux comprendre comment fonctionne cette fonctionnalité.

Un article où il y a plus de code que de texte, mais je pense que c’est assez clair. ;-)

Installer Yapl4twitter et le configurer

$ cd lib/vendor
$ git clone git://github.com/mycroft/yapl4twitter.git
[...]

On le rajoute dans config/autoload.yml pour que les classes soient préloadées:

autoload:
    twitter
:
        name
: twitter
        path
: %SF_LIB_DIR%/vendor/yapl4twitter
        recursive
: off

On va ensuite avoir besoin de clefs valide d’une application. On prendra donc soin d’en créer une sur https://dev.twitter.com/apps et de créer le config/app.yml adéquat:

all:
    base_url
:              http://mysite.com

    twitter
:
        consumer_key
:      <Consumer key>
        consumer_secret
:   <Consumer secret>

        request_token_url
: https://api.twitter.com/oauth/request_token
        access_token_url
:  https://api.twitter.com/oauth/access_token
        authorize_url
:     https://api.twitter.com/oauth/authorize
        authenticate_url
:  https://api.twitter.com/oauth/authenticate

Le module et ses actions dans symfony

On crée un module spécifique au login/logout. Mon application s’appelle « frontend », tout simplement:

$ php symfony generate:module frontend twitter

On aura dans ce module deux actions: login et logout. On n’oubliera pas d’en créer les routes dans apps/frontend/config/routing.yml:

login:
  url
: /login
  param
: { module: twitter, action: login }

logout
:
  url
: /logout
  param
: { module: twitter, action: logout }

Il ne reste plus qu’à implémenter les deux actions:

    public function executeLogin(sfWebRequest $request)
    {
        $oa = new OAuth(
                sfConfig::get('app_twitter_consumer_key'),
                sfConfig::get('app_twitter_consumer_secret')
        );

        // Premiere etape: Pas de parametre oauth_token: On ne vient pas de twitter.com
        // On fait donc la demande d'un request token et on redirige l'utilisateur vers
        // twitter.com.
        if(FALSE == $request->hasParameter('oauth_token'))
        {
            $request_token_url = sfConfig::get('app_twitter_request_token_url');

            $callback_url = sfConfig::get('app_base_url');
            $callback_url.= $this->getController()->genUrl('@login');

            $request_token = $oa->getRequestToken($request_token_url,
                                                  $callback_url);

            // On redirige vers l'authenticate_url qui permet de ne pas avoir
            // a accepter l'application une nouvelle fois.
            $next_url = sfConfig::get('app_twitter_authenticate_url');
            $next_url.= '?oauth_token=' . $request_token['oauth_token'];

            $this->redirect( $next_url );
        }
        // Seconde etape: On a un parametre oauth_token. On vient donc de twitter.com
        // On va recuperer a partir du oauth_token et oauth_verifier l'oauth_token_secret.
        else
        {
            $oauth_token = $request->getParameter('oauth_token');
            $oauth_verifier = $request->getParameter('oauth_verifier');

            $access_token = $oa->getAccessToken(sfConfig::get('app_twitter_access_token_url'),
                                                $oauth_token,
                                                $oauth_verifier);

            // On a le token, on enregistre dans la session le screen_name et
            $this->getUser()->setAttribute('screen_name', $access_token['screen_name']);
            $this->getUser()->setAttribute('oauth_token', $access_token['oauth_token']);
            $this->getUser()->setAttribute('oauth_token_secret', $access_token['oauth_token_secret']);
            $this->getUser()->setAuthenticated(TRUE);

            // On pourrait avoir une table ou l'on stocke ces informations pour plus tard:
            // $account = AccountTable::getInstance()->findOneByScreenName($access_token['screen_name']);
            // if( ! $account )
            // {
            //     $account = new Account();
            //     $account->screen_name = $access_token['screen_name'];
            //     $account->user_id = $access_token['user_id'];
            //     $account->oauth_token = $access_token['oauth_token'];
            //     $account->oauth_token_secret = $access_token['oauth_token_secret'];
            //     $account->save();
            // }

            // $this->getUser()->setAttribute('account_id', $account->id);

        // On est loggue, on retourne sur la page principale:
            $this->redirect('@homepage');
        }

        return sfView::NONE;
    }

    // logout: On invalide tout.
    public function executeLogout(sfWebRequest $request)
    {
        $this->getUser()->setAttribute('screen_name', NULL);
        $this->getUser()->setAttribute('oauth_token', NULL);
        $this->getUser()->setAttribute('oauth_token_secret', NULL);
        $this->getUser()->setAuthenticated(FALSE);

        return $this->redirect('@homepage');
    }

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 ; ?>

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 …

Faire cohabiter sur son système plusieurs versions de symfony

A cause de mon travail, je dois pouvoir travailler sur plusieurs versions de symfony (ayant des projets en 1.0+propel ou 1.4+propel/doctrine). J’ai donc arreté d’utiliser pear pour mettre à jour symfony et ait décidé d’utiliser un script perso pour mettre à jour

Création de l’emplacement des trees symfony:

$ cd /usr/share/pear
$ mkdir symfony ; cd symfony

Et on les initialise:

$ svn co http://svn.symfony-project.com/branches/1.0 symfony10
$ svn co http://svn.symfony-project.com/branches/1.3 symfony13
$ svn co http://svn.symfony-project.com/branches/1.4 symfony14

Ensuite, un petit script pour mettre à jour tout ce monde de façon quotidienne:

$ cat > /root/bin/updateSymfony.sh << EOF
#!/bin/sh

MY_SF_DIR=/usr/share/pear/symfony

for i in $MY_SF_DIR/*
do
  cd $i
  svn up
done

echo 'All done !'
EOF

$ chmod u+x /root/bin/updateSymfony.sh

Et voilà. Plus qu’à mettre dans le cron ou de le lancer manuellement.

On pourra lancer le vice à créer des alias pour pouvoir utiliser les différentes versions:

$ grep symfony ~/.bashrc
alias sf10=/usr/share/pear/symfony/symfony10/data/bin/symfony
alias sf13=/usr/share/pear/symfony/symfony13/data/bin/symfony
alias sf14=/usr/share/pear/symfony/symfony14/data/bin/symfony

Collaborer sur votre projet Symfony 1.4 avec git

J’expérimente actuellement de nouveaux outils de travail, et git est l’un d’entre eux. Le but à terme serait d’évaluer la solution et de la comparer à subversion.

Voici alors un petit tutoriel pour débuter un projet symfony (1.4) avec git:

Tout d’abord, je garde les sources de symfony hors du tree de mon projet, pour la bonne raison que j’utilise plusieurs versions de symfony à la fois (sur différents projets), et que j’ai préfèré les centraliser dans /usr/share/php au lieu de les dispatcher un à un et devoir les maintenir dans mon tree. De plus, symfony 1.4 n’est pas maintenu sous git par les développeurs officiels (Il existe bien un symfony-git.sf.net non officiel, mais il ne semble pas être mis à jour régulièrement). Finalement, la taille du projet est moindre car ne contient que nos fichiers.

$ alias symfony=/usr/share/pear/symfony/symfony14/data/bin/symfony
$ symfony -V
symfony version 1.4.6-DEV (/usr/share/pear/symfony/symfony14/lib)

Création du répertoire du projet

$ mkdir myproject
$ cd myproject

Initialisation du dépot:

$ git init

Création de notre projet symfony:

$ symfony generate:project azure
[...]
$ ls
apps  cache  config  data  lib  log  plugins  symfony  test  web

Premier commit:

$ git commit -m "Initial commit"
[master (root-commit) 20c9217] Initial commit
 11 files changed, 161 insertions(+), 0 deletions(-)
 create mode 100644 config/ProjectConfiguration.class.php
[...]

Dans les actions à mener également, on peut faire ignorer les modifications dans cache et dans log:

$ echo 'cache/*' >> .gitignore
$ echo 'log/*' >> .gitignore
$ cat .gitignore
log/*
cache/*

$ git add .gitignore && git commit -m 'Add an .gitignore with cache/* and log/*'
[master 5a9e245] Add an .gitignore with cache/* and log/*
[...]

Et voilà. A partir de ce moment on peut commencer sérieusement à travailler sur notre projet ! :)