Friday 12 November 2010

Facebook Integration on a symfony project - part I

Introduction
Facebook is the world's leading social network, with over 500 million users. As so, it is most useful to integrate your symfony project with it, bringing more users to your website, and providing them some commodities.
There is a symfony plugin, sfFacebookConnectPlugin, that does most of the things you would want. Unfortunately, and at the time I write this, it uses the old REST API, wich is no longer recommended, since facebook has adopted the Graph API. As so, this tutorial will cover how to make the integration using the official PHP SDK.

What Will I Cover?
- Facebook Connect
- Authentication
- Permissions
- Wall Publish
- App Tab (now on facebook pages only :/ )

Requirements
You will need:
- To be using sfGuardPlugin
- Propel (some simple queries, used old Criteria for compatibility)
- Have a field in your user profile table to store the facebook uid (optional, but recommended; preferably an index to speed up search)
- PHP SDK
- Javascript SDK
- PHP cUrl enabled
- A Facebook App: http://www.facebook.com/developers/

You can download the Javascript SDK from http://connect.facebook.net/en_US/all.js

Facebook Connect
You must have already seen the facebook popup in many sites, either asking for your login details (if not logged on facebook), or for permissions, or for sharing content. To get the same experience on your symfony project, you must use Javascript SDK.
After downloading the .js and placing it in your web/js directory (and perhaps renaming it facebook.js), you will need to include it in all the pages you want the user to be facebook connected. This is so because the user may logout of facebook at any time, so you need to keep checking the user login status everytime a page loads. So go ahead and add facebook.js to the application config/view.yml, on the javascripts section.
Now you need to call the initializer on your pages. You may either hardcode it on your layout file, create a partial for it in case you use many different layouts, or even create a slot, if you will only need it on certain pages. It's up to you and whatever your needs are.
So this is how you can call the initializer:

<div id="fb-root"></div>
<script>
  FB.init({
    appId  : 'YOUR APP ID',
    status : true, // check login status
    cookie : true, // enable cookies to allow the server to access the session
    xfbml  : true  // parse XFBML
  });
</script>

You will need to have the div "fb-root" somewhere on the page.
There is also an asynchronous way to do it, check this link

Authentication
The single click signin is, in my opinion, the best feature in facebook integration.

The Button:
The login button comes with the Javascript SDK, and is one of the facebook "social plugins".
You can put it anywhere by simply typing <fb:login-button></fb:login-button>. You can also configure it by passing arguments (check the social plugin page) and write any text inside the tag to personalize the button text.
Pressing the button will either popup a window asking for facebook user and password (if user is not logged on and has no cookie stored), or for permissions if it's the first visit to your site, or nothing at all for the rest. But before the popup windows can appear, it is needed to overcome javascript security concern limitation, cross-domain requests. For that, you will need the file xd_reciever.html inside you web/ directory. You can create it and paste this code inside it.

The login:
You will now want to login the user into your site. You will need to check the user status on the facebook platform and redirect him accordingly. You can do that with the following code, which has to be used after FB.init is executed (can be right after it, still inside the script tag):


    FB.Event.subscribe('auth.sessionChange', function(response) {
        if (response.session) {
          // A user has logged in, and a new cookie has been saved
            window.location="<?php echo url_for('@facebook_connect',true); ?>";
        } else {
          // The user has logged out, and the cookie has been cleared
                window.location="<?php echo url_for('@homepage',true); ?>";
        }
      });

This code will check any change in the user facebook session. It redirects the user to the route facebook_connect  (you can of course just link it to the module/action route) if he has/is logged in, and to the homepage if he has logged out. You now need to create the action to manage the user session and authenticate the user into sfGuard; I will use facebook/connect on this tutorial.
On the connect action you will switch to the PHP SDK. This sdk will allow you to connect to facebook and retrieve the user account details, allowing you to authenticate him on your application. You will only need to have the facebook.php file somewhere inside your lib folder, so that it is autoloaded.
I will cover three user cases:

  1. The user has logged on to your website before, using facebook connect.
  2. The user is registered to your website, but its the first time he logs using facebook connect.
  3. The user is new to your website, therefore, he is not in your database.

        $facebook = new Facebook(array(
                'appId'  => 'YOUR APP ID GOES HERE',
                'secret' => 'YOUR APP SECRET GOES HERE',
                'cookie' => true,
        ));

        $session = $facebook->getSession();
        $me = null;
        // Session based API call.
        if ($session) {
          try{
                $uid = $facebook->getUser();
                $me = $facebook->api('/me');
                $c=new Criteria();
                $c->add(YOUR_PROFILE_CLASSPeer::FACEBOOK_UID,$uid);
                $user=YOUR_PROFILE_CLASSPeer::doSelectOne($c);
                if($user) //User case 1 - you have the fb uid in your database
               {
                        $this->getUser()->signin($user->getsfGuardUser());
               }else
               {
                        $c=new Criteria();
                        $c->add(YOUR_PROFILE_CLASSPeer::EMAIL,$me['email']);
                        $user=YOUR_PROFILE_CLASSPeer::doSelectOne($c);
                        if($user) //User case 2 - you don't have fb uid, but you do have his e-mail
                        {
                                //Store his uid
                                $user->setFacebookUid($uid);
                                $user->save();
                                $this->getUser()->signin($user->getsfGuardUser());
                        }else
                        {
                          //User case 3 - you might register an account for him, or redirect to register form with some pre-filled fields.
                        }
               }

               if($referer=$request->getReferer())
               {
                       //Your user might be coming from an external link, so redirect to the intended link
                       $this->redirect($request->getReferer());
               }else
                       $this->redirect('@homepage');
          }
          catch (FacebookApiException $e) {
            error_log($e);
          }
      }

You may ask: why check for the user facebook uid, if all we need is the e-mail??? For starters, you can change the e-mail associated with your facebook account at any time. Secondly, it happens that a facebook user can have multiple e-mails associated with his account. Being so, you may prevent a facebook connected user from your database from having to register again on your website, just because his facebook e-mail has changed.

21 comments:

  1. This is great, you should do one for twitter as well!

    ReplyDelete
  2. $me

    Notice: Undefined index: email. There is no Email :(

    ReplyDelete
  3. oh about the e-mail... the basic permissions on facebook only show whatever the user has visible on his info. So if his e-mail is private you will need to ask for additional permissions. I'll cover this on part II, in a few days :)

    ReplyDelete
  4. Yes. The Email Permission. It's work. :)

    ReplyDelete
  5. Great tutorial, keep it coming, can not wait to read part II.

    ReplyDelete
  6. Thanks for your tutorial, but I'm not able to get everything up & running.

    I receive a javascript error on page load:

    "FB.Type.createClass2 is not a function"

    When I click on the FB login button I also get a javascript error:

    "e is undefined
    FB.provide('',{ui:function(e,b){if(!e....onent(FB.UIServer._resultToken));}});"

    Do you have any tips or ideas?

    ReplyDelete
  7. Junni, have you verified if the script tag that loads the js file is actually on the page? You may have forgotten to add the filename to the view.yml.

    ReplyDelete
  8. Hi Ricardo,

    Yes, it is in the header and accessible.

    ReplyDelete
  9. Just found it why the js error came up: I had already a Facebook widget which was loading the Featureloader.js. When I removed this one, the error was gone.

    ReplyDelete
  10. Hi , very nice tutorial! I have ne problem maybe you may help me . I have application on symfony 1.2 and from some reason fb:login tags doesn't render login botun , I try create one static page coping source . Then it work fine .

    ReplyDelete
  11. terminator, please check if the script tag loading the js is actually on the source code of the page. You may have forgotten to add the filename to the view.yml.

    ReplyDelete
  12. Many tnx for you example, I use Doctrine myself, so adjusted it for this ORM. Make sure you define the guard user profile in you schema.yml. A tuturial can be found here: http://www.symfony-project.org/blog/2008/11/12/call-the-expert-customizing-sfdoctrineguardplugin.

    Below is the Doctrine Query I use to retrieve the user profile:

    $profile = Doctrine_Query::create()
    ->from('Profile p')
    ->innerJoin('p.User u')
    ->where('p.facebook_uid = ?', $uid)
    ->fetchOne();

    ReplyDelete
  13. Thanks a ton for publishing this blog. It was extremely helpful. Ricardo Oliveira you are literally my idol. :)

    In my social symfony quest I found this twitter oauth link helpful too: http://geekexmachina.com/2010/01/authentication-via-oauth-only-in-symfony/#comment-29

    Also thanks Peter Paauwe, the doctrine information in your comment was essential.

    ReplyDelete
  14. Actually, the sfFacebookConnectPlugin supports the Graph API. It is included as a submodule of the plugin in the lib/vendor folder. We'll try to update the doc asap but time is missing ;)

    To use the graph you just need to call the sfFacebookClient method of the sfFacebook class and use the php-sdk method api. For instance, if you want to publish something on the feed of your user you can do it like this :
    sfFacebook::getFacebookClient()->api($user->getProfile()->getFacebookUid().'/feed', 'POST', array('message' => sprintf('Cool new post in the %s feed', $user->getUsername()));

    ReplyDelete
  15. those are great news Benjamin! Although its not hard to use FB API, using sfFacebookConnectPlugin would speed things up considerably! Please do update the docs asap so people know how to use it properly :)

    ReplyDelete
  16. Hy Ricardo and Benjamin, I'm using the sfFacebookConnectPlugin updated by the Benjamin to return the new php-sdk facebook class and it works well!
    It is good today to start a new project, not to update the old ones, because need some methods to be re-implemented using the new php-sdk and the Graph API, some of then on the oficial symfony's documentation like RequireLogin, InCanvas, etc.
    Yesterday I've done some modifications to work with the sfGuard on my old project but nothing special yet.
    I'll send it to Fabrice and ask what is planned to be implemented on the sfFacebookConnectPlugin.

    ReplyDelete
  17. An Note: The version of the facebook php sdk of the Fabrice's package ( where I downloaded the Benjamin changes ) is outdated ( v2.0.3 ). Is good to get the lastest version from the Facebook developer's website.
    rgds.

    ReplyDelete
  18. can I use the facebook connect plugin without the sfGuardPlugin?

    ReplyDelete
  19. I think the main idea of the plugin is to integrate sfGuard with Facebook, but you might still get some advantages out of it, even without sfGuard... So, it's a maybe :P

    ReplyDelete

Pages