Intégrer facilement Facebook Connect et Twitter Connect avec FosUserBundle pour Symfony 2.1

2013, Feb 19    

Avertissement : Cet article estime que vous savez travailler avec Symfony2, que vous savez récupérer vos clés d’API pour Twitter et Facebook qui seront nécessaires lors de la configuration.

De plus, j’utiliserai uniquement Doctrine lors de ce tutoriel. La version de Symfony est la 2.1.

Configurer FosUserBundle

Comme à chaque fois que l’on veut installer un nouveau Bundle, il vous faut l’ajouter dans votre composer.json.

{
    "require": {
        "friendsofsymfony/user-bundle": "*"
    }
}

Une fois cette ligne ajoutée, vous pouvez faire tourner votre composer.phar pour télécharger le bundle. Dans votre terminal :

$ php composer.phar update friendsofsymfony/user-bundle

Ca y est, vous voila avec le bundle « FosUserBundle » dans votre dossier vendor/.

Une fois ceci effectué, il vous faut l’enregistrer dasn votre AppKernel.php. Pour cela :

<?php
// app/AppKernel.php
 
public function registerBundles()
{
    $bundles = array(
        // ...
        new FOS\UserBundle\FOSUserBundle(),
    );
}

Vous suivez toujours ? Jusqu’ici rien de bien compliqué normalement. C’est la façon habituelle de commencer à installer les bundles que vous pouvez utiliser avec Symfony2. Maintenant passons à la configuration du-dit bundle.

Je redis que cet article vous guide pour installer les 3 bundles en utilisant l’ORM Doctrine. Ainsi, il vous suffit d’ajouter ces quelques lignes dans votre config.yml

# app/config/config.yml
fos_user:
    db_driver: orm
    firewall_name: main
    user_class: Your\UserBundle\Entity\User

Si vous suivez, vous vous rendez compte que l’on définie ici une nouvelle entité User. En effet, il va vous falloir la créer. Et cela est à nouveau très simple, suivez le guide !

<?php
// src/Your/UserBundle/Entity/User.php
 
namespace Your\UserBundle\Entity;
 
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
 
/**
 * @ORM\Entity
 * @ORM\Table(name="user")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;
 
    public function __construct()
    {
        parent::__construct();
    }
}

Attention : vous devez préciser un @ORM\Table(name= »user ») car le nom « User » est un mot-clé reservé par SQL et ne peut dont être utilisé comme nom de table.

C’est dans cet entité que vous pourrez rajouter de nouveau champ à votre entité User.

Allez l’installation de ce premier bundle est bientôt terminé. Encore un peu de configuration et de ligne de commandes et nous y sommes.

Il vous faut encore importer les routes par défaut utilisées par FosUserBundle.

# app/config/routing.yml
fos_user_security:
    resource: "@FOSUserBundle/Resources/config/routing/security.xml"
 
fos_user_profile:
    resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
    prefix: /profile
 
fos_user_register:
    resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
    prefix: /register
 
fos_user_resetting:
    resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
    prefix: /resetting
 
fos_user_change_password:
    resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
    prefix: /profile

Ceci fait, configurons le security.yml de notre application.

# app/config/security.yml
security:
    encoders:
        FOS\UserBundle\Model\UserInterface: sha512
 
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN
 
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username
 
    firewalls:
        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_provider: form.csrf_provider
            logout:       true
            anonymous:    true
 
    access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/, role: ROLE_ADMIN }

La partie acces_control est largement modulable par vous. Cela vous évitant de vérifier si votre utilisateur est bien identifié dans vos controlers. Le contrôle se fait directement par le path, un gain de temps et de lisibilité assez appréciable, n’est ce pas ?

La partie role_hierarchy vous permet d’établir l’ordre d’importances des différents rôles pour les utilisateurs. Ici les droits de ROLE_ADMIN contiennent les droits de ROLE_USER en plus des siens, et ROLE_SUPER_ADMIN contient les droit détenus par ROLE_ADMIN et donc ROLE_USER.

Nous retoucherons pas mal ces fichiers pour intégrer les connexion Facebook et Twitter.

Pour finir l’installation et la configuration de ce premier bundle, mettons à jour la base de donnée.

$ php app/console doctrine:schema:update --force

Voilà, tout est ok ! Pour en savoir plus sur la configuration vous pouvez lire le readme [en], pour la partie template [en] [billet en cours de rédaction sur ce blog aussi]

 

ATTENTION : Cet article commence à être assez vieux. Un article mis à jour et utilisant un autre bundle est en cours de rédaction. Vous pouvez suivre celui-ci, mais certaines informations peuvent être erronées.

 

Configurer FosFacebookBundle

Attaquons nous désormais à l’intégration du bundle permettant le Facebook Connect. Comme je le disais plus haut, je ne décrirai pas ici la configuration du côté de Facebook. Vous avez juste besoin de l’id de votre application, et de votre clé secrète pour configurer le bundle.

On ajoute donc le bundle au composer.json puis on le rapatrie dans notre dossier /vendor comme pour le précédent bundle

{
    "require": {
         "friendsofsymfony/facebook-bundle": "1.1.*"
    }
}
$ php composer.phar update friendsofsymfony/facebook-bundle

On l’ajoute à notre AppKernel. Oui comme tout à l’heure si vous suivez bien.

public function registerBundles()
{
    return array(
        // ...
        new FOS\FacebookBundle\FOSFacebookBundle(),
        // ...
    );
}

Une fois ceci fait, on ajoute les routes spécifiques à notre Facebook Connect.

_security_check:
    pattern:  /fb/login_check
_security_logout:
    pattern:  /fb/logout
_facebook_secured:
    pattern: /secured/

Une chose de moins à faire. Ici, la documentation officielle propose de mettre votre appId et votre secret key Facebook directement dans votre fichier de config. Pour ma part, je mets celles-ci ainsi que les permissions et la langue dans le fichier parameters.ini. Vous pouvez faire comme vous voulez bien entendu. Ce qui donne :

Fichier config.yml

fos_facebook:
   file:   %kernel.root_dir%/../vendor/facebook/src/base_facebook.php
   alias:  facebook
   app_id: %app_id%
   secret: %secret%
   cookie: true
   permissions: [%permissions%]
   culture: %facebook_culture%

Fichier parameters.ini

   ; Facebook
   app_id                 = id_de_l_application_facebook
   secret                  = clé_secrète_de_l_application_facebook
   permissions         = email
   facebook_culture = fr_FR

Une fois ces fichiers configurés, passons au fichier security.yml. Il y a pas mal de choses à rajouter je dois dire. Voici à quoi il devrait correspondre désormais

security:
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username
        my_fos_facebook_provider:
            id: my.facebook.user
 
    encoders:
        FOS\UserBundle\Model\UserInterface: sha512
 
    firewalls:
        main:
            pattern:      ^/
            fos_facebook:
                app_url: ""
                server_url: "http://your_url.dev"
                login_path: /fb/login
                check_path: /fb/login_check
                default_target_path: /
                provider: my_fos_facebook_provider
            form_login:
                login_path: /login
                check_path: /login_check
                provider: fos_userbundle
            logout:
                path: /logout
                anonymous: true
 
    access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
 
        # FOS facebook
        - { path: ^/secured/.*, role: [IS_AUTHENTICATED_FULLY] }
        - { path: ^/login_check, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/.*, role: [IS_AUTHENTICATED_ANONYMOUSLY] }
 
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
       ROLE_SUPER_ADMIN: ROLE_ADMIN
 
    services:
        my.facebook.user:
             class: Your\UserBundle\Security\User\Provider\FacebookProvider
             arguments:
                 facebook: "@fos_facebook.api"
                 userManager: "@fos_user.user_manager"
                 validator: "@validator"
                 container: "@service_container"

Cela fait beaucoup d’infos d’un coup mais on ne va s’attarder que sur le dernier point qui nous concerne directement pour la suite de la configuration pour la bonne marche du bundle. Vous voyez que l’on renseigne un chemin vers un Provider qui … n’existe pas encore :) Remédions à cela immédiatement !

Ajouter donc un fichier au chemin suivant : Your\UserBundle\Security\User\Provider\FacebookProvider

<?php
 
namespace Your\MyBundle\Security\User\Provider;
 
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use \BaseFacebook;
use \FacebookApiException;
 
class FacebookProvider implements UserProviderInterface
{
    /**
     * @var \Facebook
     */
    protected $facebook;
    protected $userManager;
    protected $validator;
 
    public function __construct(BaseFacebook $facebook, $userManager, $validator)
    {
        $this->facebook = $facebook;
        $this->userManager = $userManager;
        $this->validator = $validator;
    }
 
    public function supportsClass($class)
    {
        return $this->userManager->supportsClass($class);
    }
 
    public function findUserByFbId($fbId)
    {
        return $this->userManager->findUserBy(array('facebookId' => $fbId));
    }
 
    public function loadUserByUsername($username)
    {
        $user = $this->findUserByFbId($username);
 
        try {
            $fbdata = $this->facebook->api('/me');
        } catch (FacebookApiException $e) {
            $fbdata = null;
        }
 
        if (!empty($fbdata)) {
            if (empty($user)) {
                $user = $this->userManager->createUser();
                $user->setEnabled(true);
                $user->setPassword('');
            }
 
            $user->setFBData($fbdata);
 
            if (count($this->validator->;validate($user, 'Facebook'))) {
                throw new UsernameNotFoundException('The facebook user could not be stored');
            }
            $this->userManager->updateUser($user);
        }
 
        if (empty($user)) {
            throw new UsernameNotFoundException('The user is not authenticated on facebook');
        }
 
        return $user;
    }
 
    public function refreshUser(UserInterface $user)
    {
        if (!$this->supportsClass(get_class($user)) || !$user->getFacebookId()) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }
 
        return $this->loadUserByUsername($user->getFacebookId());
    }
}

Si vous avez un peu analysé ce fichier, vous vous rendrez compte qu’il va vous falloir ajouter des propriétés à votre entité User. Elle devrait ressembler à quelque chose dans le genre désormais :

<code>&lt;?php
 
namespace Acme\MyBundle\Entity;
 
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
 
class User extends BaseUser
{
    /**
     * @var string
     *
     * @ORM\Column(name="firstname", type="string", length=255)
     */
    protected $firstname;
 
    /**
     * @var string
     *
     * @ORM\Column(name="lastname", type="string", length=255)
     */
    protected $lastname;
 
    /**
     * @var string
     *
     * @ORM\Column(name="facebookId", type="string", length=255)
     */
    protected $facebookId;
 
    public function serialize()
    {
        return serialize(array($this-&gt;facebookId, parent::serialize()));
    }
 
    public function unserialize($data)
    {
        list($this-&gt;facebookId, $parentData) = unserialize($data);
        parent::unserialize($parentData);
    }
 
    /**
     * @return string
     */
    public function getFirstname()
    {
        return $this-&gt;firstname;
    }
 
    /**
     * @param string $firstname
     */
    public function setFirstname($firstname)
    {
        $this-&gt;firstname = $firstname;
    }
 
    /**
     * @return string
     */
    public function getLastname()
    {
        return $this-&gt;lastname;
    }
 
    /**
     * @param string $lastname
     */
    public function setLastname($lastname)
    {
        $this-&gt;lastname = $lastname;
    }
 
    /**
     * Get the full name of the user (first + last name)
     * @return string
     */
    public function getFullName()
    {
        return $this-&gt;getFirstname() . ' ' . $this-&gt;getLastname();
    }
 
    /**
     * @param string $facebookId
     * @return void
     */
    public function setFacebookId($facebookId)
    {
        $this-&gt;facebookId = $facebookId;
        $this-&gt;setUsername($facebookId);
        $this-&gt;salt = '';
    }
 
    /**
     * @return string
     */
    public function getFacebookId()
    {
        return $this-&gt;facebookId;
    }
 
    /**
     * @param Array
     */
    public function setFBData($fbdata)
    {
        if (isset($fbdata['id'])) {
            $this-&gt;;setFacebookId($fbdata['id']);
            $this-&gt;addRole('ROLE_FACEBOOK');
        }
        if (isset($fbdata['first_name'])) {
            $this-&gt;setFirstname($fbdata['first_name']);
        }
        if (isset($fbdata['last_name'])) {
            $this-&gt;setLastname($fbdata['last_name']);
        }
        if (isset($fbdata['email'])) {
            $this-&gt;setEmail($fbdata['email']);
        }
    }
}</code>

Vous pouvez modifier la méthode ‘setFBData’ selon comment vous voulez que cela fonctionne. Par exemple, à ce moment là même si l’utilisateur change son email ou son pseudo, à chaque connexion Facebook il reviendra à celui de Facebook. A vous de voir si c’est le fonctionnement que vous désirez … ou pas.

En ce qui concerne l’intégration dans vos templates, c’est une nouvelle fois assez simple

\{\{ facebook_initialize({'xfbml': true, 'fbAsyncInit': 'onFbInit();'}) }}<script type="text/javascript">// <![CDATA[
function goLogIn(){
      window.location.href="\{\{ path('_security_check') \}\}";
   }
   function onFbInit(){
      if(typeof(FB)!='undefined' && FB!=null ) {
         FB.Event.subscribe('auth.statusChange', function(response){
            if(response.session||response.authResponse){
               goLogIn();
            }else{
               window.location.href="\{\{ path('_security_logout') \}\}";
            }
         });
      }
   }
   function fblogin(){
      FB.login(function(response){},{scope:'email'});
   }
// ]]></script>

Il vous suffira d’appeler la fonction fblogin() pour déclencher le tout ;) Vous voilà désormais avec une connexion via Facebook opérationnel.

N’hésitez pas à explorer les fichiers, à les modifier pour bien comprendre leur fonctionnement si vous avez des doutes sur certains passages. Certes, vous déléguez votre développement à des bundles tierces mais il vous faut aussi quelque peu les maitriser pour bien les intégrer et en tirer tout la puissance. Ceci est valable pour les bundles Symfony2, mais aussi pour les librairies ou les scripts que vous utilisez. N’oubliez pas que le copier-coller est quand même l’ennemi du développeur !

Configurer FosTwitterBundle

Attaquons nous désormais à installation du bundle pour permettre la connexion à votre application avec Twitter. Avouez que se serait bête de s’arrêter là non ?

Si vous lisez l’article depuis le début vous pourriez même l’écrire vous même cette première partie non ? On ajoute au composer.json, on le rapatrie et on l’ajoute au Kernel.

{
    "require": {
        "friendsofsymfony/twitter-bundle": "*"
    }
}
$ php composer.phar update friendsofsymfony/twitter-bundle
public function registerBundles()
  {
      return array(
          // ...
          new FOS\TwitterBundle\FOSTwitterBundle(),
          // ...
      );
  }

Une fois ceci fait, passons au fichier config.yml. Comme je l’ai fait pour le bundle Facebook la consumer key, et la consumer secret sont à mettre dans le parameters.ini

fos_twitter:
        file: %kernel.root_dir%/../vendor/twitteroauth/twitteroauth/twitteroauth.php
        consumer_key: %twitter_key%
        consumer_secret: %twitter_secret%
        callback_url: http://votre_site.dev/twitter/login_check # à renseigner dans votre appli twitter aussi

Dans votre fichier security.yml, il vous faut ajouter les lignes pour twitter. Vous verrez, cela est très semblable au bundle pour Facebook

Dans la partie providers :

my_fos_twitter_provider:
    id: my.twitter.user

Dans la partie firewalls – main :

fos_twitter:
    login_path: /twitter/login
    check_path: /twitter/login_check
    default_target_path: /
    provider: my_fos_twitter_provider

Dans la partie services :

my.twitter.user:
        class: Your\MainBundle\Security\User\Provider\TwitterProvider
        arguments:
            twitter_oauth: "@fos_twitter.api"
            userManager: "@fos_user.user_manager"
            validator: "@validator"
            session: "@session"
    ajax_success_handler:
        class: FOS\TwitterBundle\Security\AjaxSuccessHandler
    ajax_failure_handler:
        class: FOS\TwitterBundle\Security\AjaxFailureHandler

Une nouvelle fois, vous aurez noté l’apparition d’un nouveau provider. Le voici :

&lt;?php
 
namespace Your\YourBundle\Security\User\Provider;
 
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\HttpFoundation\Session;
use \TwitterOAuth;
use FOS\UserBundle\Entity\UserManager;
use Symfony\Component\Validator\Validator;
 
class TwitterUserProvider implements UserProviderInterface
{
    /** 
     * @var \Twitter
     */
    protected $twitter_oauth;
    protected $userManager;
    protected $validator;
    protected $session;
 
    public function __construct(TwitterOAuth $twitter_oauth, UserManager $userManager,Validator $validator, Session $session)
    {   
        $this-&gt;twitter_oauth = $twitter_oauth;
        $this-&gt;userManager = $userManager;
        $this-&gt;validator = $validator;
        $this-&gt;session = $session;
    }   
 
    public function supportsClass($class)
    {   
        return $this-&gt;userManager-&gt;supportsClass($class);
    }   
 
    public function findUserByTwitterId($twitterID)
    {   
        return $this-&gt;userManager-&gt;findUserBy(array('twitterID' =&gt; $twitterID));
    }   
 
    public function loadUserByUsername($username)
    {
        $user = $this-&gt;findUserByTwitterId($username);
 
        $this-&gt;twitter_oauth-&gt;setOAuthToken($this-&gt;session-&gt;get('access_token'), $this-&gt;session-&gt;get('access_token_secret'));
 
        try {
            $info = $this-&gt;twitter_oauth-&gt;get('account/verify_credentials');
        } catch (Exception $e) {
            $info = null;
        }
 
        if (!empty($info)) {
            if (empty($user)) {
                $user = $this-&gt;userManager-&gt;createUser();
                $user-&gt;setEnabled(true);
                $user-&gt;setPassword('');
                $user-&gt;setAlgorithm('');
            }
 
            $username = $info-&gt;screen_name;
 
            $user-&gt;setTwitterID($info-&gt;id);
            $user-&gt;setTwitterUsername($username);
            $user-&gt;setEmail('');
            $user-&gt;setFirstname($info-&gt;name);
 
            $this-&gt;userManager-&gt;updateUser($user);
        }
 
        if (empty($user)) {
            throw new UsernameNotFoundException('The user is not authenticated on twitter');
        }
 
        return $user;
    }
 
    public function refreshUser(UserInterface $user)
    {
        if (!$this-&gt;supportsClass(get_class($user)) || !$user-&gt;getTwitterID()) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }
 
        return $this-&gt;loadUserByUsername($user-&gt;getTwitterID());
    }
}

Il vous faudra aussi ajouter certaines chose à votre entité User :

&lt;?php
 
        /** 
         * @var string
         */
        protected $twitterID;
 
        /** 
         * @var string
         */
        protected $twitter_username;
 
        /**
         * Set twitterID
         *
         * @param string $twitterID
         */
        public function setTwitterID($twitterID)
        {
            $this-&gt;twitterID = $twitterID;
            $this-&gt;setUsername($twitterID);
            $this-&gt;salt = '';
        }
 
        /**
         * Get twitterID
         *
         * @return string 
         */
        public function getTwitterID()
        {
            return $this-&gt;twitterID;
        }
 
        /**
         * Set twitter_username
         *
         * @param string $twitterUsername
         */
        public function setTwitterUsername($twitterUsername)
        {
            $this-&gt;twitter_username = $twitterUsername;
        }
 
        /**
         * Get twitter_username
         *
         * @return string 
         */
        public function getTwitterUsername()
        {
            return $this-&gt;twitter_username;
        }

Enfin dans le controller de votre choix :

&lt;?php
 
        /** 
        * @Route("/connectTwitter", name="connect_twitter")
        *
        */
        public function connectTwitterAction()
        {   
 
          $request = $this-&gt;get('request');
          $twitter = $this-&gt;get('fos_twitter.service');
 
          $authURL = $twitter-&gt;getLoginUrl($request);
 
          $response = new RedirectResponse($authURL);
 
          return $response;
 
        }

Cela vous permettant de faire facilement dans vos templates :

<a href="\{\{ path ('connect_twitter')\}\}">Twitter Connect</a>

 

Conclusion

Nous venons ainsi de voir comment intégrer ces 3 bundles. J’espère que cet article vous a bien aidé. Si vous rencontrez des soucis, n’hésitez pas à laisser un commentaire. De l’aide vous sera apportée. Je me répète, mais cet article est une base. Pour approfondir, consultez la doc de ces 3 bundles (dispo sur Github), faites des essais, cassez tout et vous comprendrez bien mieux comment cela marche !