Prendre un Café

L'espace d'expression de Nicolas Perriault

Aller au contenu | Aller au menu | Aller à la recherche

Recherche - symfony

mardi 15 septembre 2009

Optimize your Doctrine Workflow with Specialized Queries

I’m currently working on a big Symfony project, with a lot of Doctrine models and complex queries to write. I found a way to organize all of them in an object-oriented and cleaner way than using the traditionnal addNamedQuery() and createNamedQuery() methods workflow[1].

The idea is to create dedicated query classes for a given model ; this way, you can provide useful methods to build the business-related parts of your query.

As usual, the theory is more understandable with a concrete example. Let’s consider this simple Doctrine model[2] :

Disclaimer: The provided examples have been written in a hurry, so mistakes might have been not detected by my attentive proof-reading ;)

BlogAuthor:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true  
    name:
      type: string(255)
  relations:
    Post:
      type: one
      class: BlogPost
      local: id
      foreign: author_id
 
BlogPost:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    author_id:
      type: integer(4)
      notnull: true
    title:
      type: string(255)
    content:
      type: string(65535)
  relations:
    Author:
      type: one
      class: BlogAuthor
      local: author_id
      foreign: id
    Comments:
      type: many
      class: BlogComment
      local: id
      foreign: post_id
 
BlogComment:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    post_id:
      type: integer(4)
      notnull: true
    author:
      type: string(255)
    content:
      type: string(5000)
  relations:
    Post:
      type: one
      class: BlogPost
      local: post_id
      foreign: id

Now let’s imagine a Query class dedicated to query the BlogPost table:

<?php 
class BlogPostQuery extends Doctrine_Query
{
  static public function create($conn = null, $class = null)
  {
    return parent::create($conn, 'BlogPostQuery')
      ->from('BlogPost p');
  }
  
  public function addPosts($fields = 'p.*')
  {
    return $this->addSelect('p.*');
  }
  
  public function addComments($fields = 'c.*')
  {
    return $this
      ->addSelect($fields)
      ->leftJoin('p.Comments c')
      ->addGroupBy('c.id');
  }
  
  public function addAuthors($fields = 'a.*')
  {
    return $this
      ->addSelect($fields)
      ->leftJoin('p.Author a')
      ->addGroupBy('a.id');
  }
  
  public function addCommentsCount($alias = 'nb_comments')
  {
    return $this
      ->addSelect(sprintf('COUNT(c.id) as %s', $alias))
      ->addGroupBy('c.id');
  }
  
  public function filterByAuthorName($authorName)
  {
    return $this
      ->andWhere('a.name = ?', $authorName);
  }
}

So how can we use this query object? Here are some sample uses:

// Retrieve all posts
$posts = BlogPostQuery::create()
  ->addPosts()
  ->fetchArray();
 
// Retrieve all posts with comments
$posts = BlogPostQuery::create()
  ->addPosts()
  ->addComments()
  ->fetchArray();
 
// Retrieve all posts with comments and their count per post
$posts = BlogPostQuery::create()
  ->addPosts()
  ->addComments()
  ->addCommentsCount('yataa')
  ->fetchArray();
 
// Retrieve all post with chuck as its author and related comments
$posts = BlogPostQuery::create()
  ->addAuthors()
  ->addPosts()
  ->addComments()
  ->filterByAuthorName('chuck')
  ->fetchArray();
 
// and so on... 

Of course, this example of use is not really relevant as our model is really simple, but when you’re dealing with dozens of internationalized objects, it can help cleaning your model classes, controllers and improving the organization of your work.

Update and important precisions

Some people are having negative feedback regarding this technique, claiming it will encourage people using the custom query object directly in the controllers; that’s absolutely not the case as the queries are to be used only within the model layer, for example in the BlogPostTable class:

<?php
class BlogPostTable extends Doctrine_Table
{
  static public function getPostsWithCommentsByAuthor($authorName)
  {
    return BlogPostQuery::create()
      ->addPosts()
      ->addComments()
      ->filterByAuthorName($authorName)
      ->fetchArray()
    ;
  }
}

And in a controller:

class blogActions extends sfActions
{
  public function executeListByAuthor(sfWebRequest $request)
  {
    $this->posts = BlogPostTable::getPostsWithCommentsByAuthor($request->getParameter('author'));
  }
}

Notes

[1] … or raw queries written directly within controllers, but you may know that this is really bad ;)

[2] I’m using Doctrine 1.2 beta (bundled with upcoming symfony 1.3) in the provided example.

vendredi 4 septembre 2009

30 Symfony Best Practices, the slides from my talk at SymfonyDay Cologne '09

Hey there, it’s been a while, huh?

Today I gave a talk about Symfony best practices at the Symfony Day event in Cologne, Germany; you can get the slides on slideshare or directly browse them below:

The Symfony Day event has been purely awesomely incredibly well organized, many kudos and thanks to Interlutions and to all the attendees. You’re all great people. Thanks.

Symfony Day '09 Cologne

jeudi 16 juillet 2009

Little Symfony Forms Tricks

Hey, it’s been a long time I didn’t blog something clever here on Symfony, let me try to remedy this.

I’ve just stumbled upon this blog post about the use of sfValidatorCallback, which is quite cool because by using this particular validator you can virtually employ any kind of php callable to validate something. But as the author warned, it can be problematic to tie the symfony form validation framework to your model classes.

Personnaly, I rather prefer to declare a method in the form class itself to validate some value without boring myself writing each time a new sfValidatorBase derived class[1] :

<?php
class myForm extends sfForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'name' => new sfWidgetFormInput(),
    ));
    
    $this->setValidators(array(
      'name' => new sfValidatorCallback(array('callback' => array($this, 'validateChuckNorris'))),
    ));
  }
  
  public function validateChuckNorris(sfValidatorBase $validator, $value)
  {
    // you can't validate chuck norris, but chuck norris can invalidate you
    if ('Chuck Norris' === $value)
    {
      throw new sfValidatorError($validator, 'invalid');
    }
  }
}

Of course, if you got the exact same need in another form, you should create a dedicated validator class. If you want custom error messages and options, you’ll have to create a dedicated class as well. But for simple and casual needs, this is just enough.

The neat thing with the sfValidatorCallback validator is you can even validate form schema values (say, an array containing all the values bound to the form) the same way, eg. using a post validator. Let’s see an example reusing the form shown previously:

<?php
class myForm extends sfForm
{
  static protected $choices = array(
    'none' => 'No emails', 
    'commercials' => 'Commercial emails', 
    'news' => 'Annoucement emails', 
    'alerts' => 'Alert emails',
  );
 
  public function configure()
  {
    $this->setWidgets(array(
      //...
      'email' => new sfWidgetFormInput(),
      'opt_in' => new sfWidgetFormSelectRadio(array('choices' => self::$choices)),
    ));
    
    $this->setValidators(array(
      //...
      'email' => new sfValidatorEmail(array('required' => false)),
      'opt_in' => new sfValidatorChoice(array('choices' => array_keys(self::$choices))),
    ));
    
    $this->validatorSchema->setPostValidator(new sfValidatorCallback(array(
      'callback' => array($this, 'validateSchema'),
    )));
  }
  
  // We want the user to provide his email if he choosed to receive stuff by email
  public function validateSchema(sfValidatorBase $validator, array $values)
  {
    if ($values['opt_in'] !== 'none' && !$values['email'])
    {
      throw new sfValidatorErrorSchema($validator, array(
        'email' => new sfValidatorError($validator, 'required'),
      ));
    }
  }
}

As a cool side effect, it will also make testing your form validation very easy, because you just have to test the callable method of your form.

Here’s another trick I stole to Kris on a project we worked together on lately, using a formatter callback to alter the presentation of a widget. Here’s an example showing how to get rid of the unordered list displaying a collection of checkboxes by default, by inlining them instead:

<?php
class myForm extends sfForm
{
  static protected $choices = array(
    'none' => 'No emails', 
    'commercials' => 'Commercial emails', 
    'news' => 'Annoucement emails', 
    'alerts' => 'Alert emails',
  );
 
  public function configure()
  {
    $this->setWidgets(array(
      //...
      'opt_in' => new sfWidgetFormSelectRadio(array(
        'choices' => self::$choices,
        'formatter' => array($this, 'formatInline'),
      )),
    ));
    
    $this->setValidators(array(
      //...
      'opt_in' => new sfValidatorChoice(array('choices' => array_keys(self::$choices))),
    ));
  }
  
  public function formatInline($widget, $inputs)
  {
    $formatted = array();
    
    foreach ($inputs as $input)
    {
      $formatted[] = $input['input'].' '.$input['label'];
    }
 
    return join(' ', $formatted);
  }
}

There are tons of little tricks like these which make the life of a developer using the forms framework easier, I’ll try to share them with you progressively.

Notes

[1] All my examples are using latest Symfony 1.2.

lundi 1 décembre 2008

Meet Joe Beet

Sorry for the lame title of this blog post, but I’m happy enough to announce the immediate availability of both symfony 1.2 and Jobeet, the new symfony advent calendar!

Jobeet It is a set of 24 tutorials, published day-by-day between December 1st and Christmas. Each tutorial is meant to last one hour, and will be the occasion to see the ongoing development of a web application with symfony, from A to Z.

So don’t hesitate to start reading day one of this great tutorial :-)

Edit: I forgot to mention the simultaneous release of the symfony and Doctrine book :)

vendredi 31 octobre 2008

Let's Play with Symfony 1.2 and Doctrine

It’s been quite a long time I didn’t give a go to Doctrine, so as it’s gonna be bundled by default in with the upcoming 1.2 release of symfony, I thought it was a good occasion to play with it.

So let’s checkout the 1.2 SVN branch of symfony and create a test project with a main application[1]:

$ mkdir sf12test && cd sf12test
$ mkdir -p lib/vendor
$ svn co http://svn.symfony-project.com/branches/1.2 lib/vendor/symfony
$ php lib/vendor/symfony/data/bin/symfony generate:project sf12test
$ ln -s ../lib/vendor/symfony/data/web/sf web/sf
$ ./symfony generate:app main

Create a webserver vhost pointing to the web folder of the project directory. I’ve already explained plenty of times how to achieve this step.

Now, let’s enable the sfDoctrinePlugin and disable the Propel one by editing the setup() method of the config/ProjectConfiguration.class.php file:

public function setup()
  {
    $this->disablePlugins('sfPropelPlugin');
    $this->enablePlugins('sfDoctrinePlugin');
  }

You can list the available tasks running this simple command:

$ ./symfony list doctrine

Managing the Database Schema

First, configure your config/databases.yml file to set the database connection parameters. If you want to quick test Doctrine, use a local SQLite db, like this:

all:
  doctrine:
    class:    sfDoctrineDatabase
    param:
      dsn:    sqlite://<?php echo dirname(__FILE__).'/../data/data.db' ?>

We’re going to make a very simple weblog application, so let’s configure our database schema. We can do it in YAML[2], so fire up your favorite editor/IDE and edit a brand new config/doctrine/schema.yml:

BlogPost:
  actAs:
    Sluggable:
      fields:       [title]
    Timestampable:
  columns:
    title:          string(255)
    body:           clob
    author:         string(255)
 
BlogComment:
  actAs:            [Timestampable]
  columns:
    blog_post_id:   integer
    author:         string(255)
    email:          string(255)
    content:        clob
  relations:
    BlogPost:
      class:        BlogPost
      local:        blog_post_id
      foreign:      id
      foreignType:  many
      type:         one

Note that Doctrine offers several pretty cool features including native behaviors (timestampable and slugable are used here).

Now, create a data/fixtures folder and put a data.yml file in, containing some test data in YAML format:

BlogPost:
  p1:
    title: My first post
    body: |
      This is cool.
    author: NiKo
    created_at: "<?php echo date('Y-m-d H:i:s', time() - 86400) ?>"
  p2:
    title: My second post
    body: |
      This is still cool.
    author: NiKo
    created_at: "<?php echo date('Y-m-d H:i:s', time() - 7200) ?>"
  p3:
    title: Third post
    body: |
      Is this one cool?
    author: Roger Hanin
    created_at: "<?php echo date('Y-m-d H:i:s') ?>"
 
BlogComment:
  c1:
    BlogPost: p3
    author: John
    email: john@doe.com
    content: Hey, you're right there.
    created_at: "<?php echo date('Y-m-d H:i:s', time() - 86400) ?>"
  c2:
    BlogPost: p3
    author: Paul
    email: paul@doe.com
    content: Nope, he's not.
    created_at: "<?php echo date('Y-m-d H:i:s') ?>"

Okay, now run the command below to generate the needed files, create the database and fill it with the data fixtures:

$ ./symfony doctrine:build-all-load

We can run several DQL queries in command line to check if everything is fine. DQL is very powerful, and compatible with a lot of RDBMS. You’ll find more information on DQL on the doctrine website.

For example, to find all blog posts:

$ ./symfony doctrine:dql "From BlogPost p"
found 3 results
-
  id: '21'
  title: 'My first post'
  body: "This is cool.\n"
  author: NiKo
  slug: my-first-post
  created_at: '2008-10-29 15:14:25'
  updated_at: '2008-10-30 15:14:25'
-
  id: '22'
  title: 'My second post'
  body: "This is still cool.\n"
  author: NiKo
  slug: my-second-post
  created_at: '2008-10-30 13:14:25'
  updated_at: '2008-10-30 15:14:25'
-
  id: '23'
  title: 'Third post'
  body: "Is this one cool?\n"
  author: 'Roger Hanin'
  slug: third-post
  created_at: '2008-10-30 15:14:25'
  updated_at: '2008-10-30 15:14:25'

Another example, to find informations about the blog post with slug third-post and its associated comments:

$ ./symfony doctrine:dql "Select p.title, p.author, c.author, c.content From BlogPost p, p.BlogComment c Where p.slug = 'third-post' Group by c.id"
found 3 results
-
  id: '23'
  title: 'Third post'
  author: 'Roger Hanin'
  BlogComment: [{ id: '15', author: John, content: 'Hey, you''re right there.' }, { id: '16', author: Paul, content: 'Nope, he''s not.' }]

Put the Query Logic in the Model

The Model part of any MVC architecture must contains the business data and associated logic. In other words, these data and logic should never be handled anywhere else, to decouple your components at max. So we’ll add some query methods in the lib/model/doctrine/BlogPostTable.class.php file, which represents our blog_post table and available operations on it:

<?php
class BlogPostTable extends Doctrine_Table
{
  public function getAll()
  {
    return Doctrine_Query::create()->
      select('p.title, p.slug, p.body, p.author, p.created_at, count(c.id) numcomments')->
      from('BlogPost p, p.BlogComment c')->
      orderBy('p.created_at DESC')->
      groupBy('p.id')->
      execute();
  }
 
  public function getOneBySlug($slug)
  {
    $posts = Doctrine_Query::create()->
      from('BlogPost p')->
      leftJoin('p.BlogComment c')->
      where('p.slug = ?')->
      orderBy('c.created_at ASC')->
      limit(1)->
      execute(array($slug));
 
    return isset($posts[0]) ? $posts[0] : null;
  }
}

A Weblog is About Web Interface, uh?

Okay, let’s add pretty controllers and templates to give some life to our blog. First, generate a post module in the main app:

$ ./symfony generate:module main post

Then, edit the apps/main/modules/post/actions/actions.class.php file:

<?php
class postActions extends sfActions
{
  public function executeIndex($request)
  {
    $this->posts = Doctrine::getTable('BlogPost')->getAll();
  }
  
  public function executeShow($request)
  {
    $this->post = Doctrine::getTable('BlogPost')->getOneBySlug($slug = $request->getParameter('slug'));
    $this->forward404Unless($this->post, 'No post with slug=' . $slug);
    $this->comments = $this->post->getBlogComment();
  }
}

We should have display templates too. The first one will show the posts list, in apps/main/modules/post/templates/indexSuccess.php:

<?php foreach ($posts as $post): ?>
  <?php include_partial('post/post', array('post' => $post, 'numComments' => $post->getNumcomments())) ?>
  <hr/>
<?php endforeach; ?>

Note that we must create the _post partial template, in apps/main/modules/post/templates/_post.php:

<h2><?php echo link_to($post->getTitle(), 'post/show?slug='.$post->getSlug()) ?></h2>
<p>
  <small>Posted by <?php echo $post->getAuthor() ?> on <?php echo $post->getCreatedAt() ?>
  <?php if (isset($numComments)): ?>
    - <?php echo $numComments ?> comments
  <?php endif; ?>
  </small>
</p>
<?php echo $post->getBody(ESC_RAW) ?>

The other main template will display one post and its comments, in apps/main/modules/post/templates/showSuccess.php:

<?php include_partial('post/post', array('post' => $post)) ?>
 
<h2>Comments</h2>
<?php if (!count($comments)): ?>
  <p>No comment yet.</p>
<?php else: ?>
<?php foreach ($comments as $comment): ?>
  <p><small>By <?php echo $comment->getAuthor() ?> on <?php echo $comment->getCreatedAt() ?></small></p>
  <blockquote><?php echo $comment->getContent() ?></blockquote>
<?php endforeach; ?>
<?php endif; ?>

That’s it. A rough but functional weblog if you lauch your browser to yourhost/main_dev.php/post/index:

step2.png

And if you click a post title:

step1.png

Good News, the Forms Framework Works with Doctrine Too

Symfony 1.1 introduced the new forms framework, and good news, Doctrine can take part of it. So maybe you’ve already noticed it, we have form classes generated already, in the lib/form/doctrine folder of the project.

So let’s add a neat commenting system to our blog, by first editing the lib/form/doctrine/BlogCommentForm.class.php file:

<?php
class BlogCommentForm extends BaseBlogCommentForm
{
  public function configure()
  {
    unset($this['id'], $this['created_at'], $this['updated_at']);
    
    $this->widgetSchema['blog_post_id'] = new sfWidgetFormInputHidden();
    
    $this->validatorSchema['author']  = new sfValidatorString(array('min_length' => 3));
    $this->validatorSchema['email']   = new sfValidatorEmail();
    $this->validatorSchema['content'] = new sfValidatorString(array('min_length' => 5));
  }
}

Now, use the form in the executeShow() method of our controller:

<?php
// ...
  public function executeShow($request)
  {
    $this->post = Doctrine::getTable('BlogPost')->getOneBySlug($slug = $request->getParameter('slug'));
    $this->forward404Unless($this->post, 'No post with slug=' . $slug);
    $this->comments = $this->post->getBlogComment();
    
    $comment = new BlogComment();
    $comment->setBlogPost($this->post);
    $this->form = new BlogCommentForm($comment);
    
    if ($request->isMethod('post') && $this->form->bindAndSave($request->getParameter('blog_comment')))
    {
      $this->redirect('post/show?slug='.$this->post->getSlug());
    }
  }

And in the showSuccess.php template, we’ll append the form display:

<h3>Add a comment</h3>
 
<?php echo $form->renderFormTag(url_for('post/show?slug='.$post->getSlug())) ?>
  <table>
    <?php echo $form ?>
    <tr>
      <td></td><td><input type="submit"/></td>
    </tr>
  </table>
</form>

We’ve now a pretty commeting system added to our blog, thanks to all the goodness provided by symfony and Doctrine:

step3.png

Conclusion

The time when everyone choosed Propel because it was more stable than Doctrine seems to be over. Doctrine is robust, and performs quite well on my box. Furthermore, it handles complex relationships and dynamic object hydratation natively and better than Propel. Doctrine is also very well integrated into symfony, certainly because Jonathan Wage - the Doctrine lead developer - now works for Sensio, creator and main sponsor of symfony.

Notes

[1] Note that Windows users should replace calls to ./symfony by php symfony.

[2] If you hate YAML, you can still write Doctrine table definition classes in raw PHP by hand

mercredi 17 septembre 2008

Request For Comments: sending emails in symfony 1.2

There should be a standard way for sending emails in upcoming symfony 1.2, and I’m in charge of implementing it, using the excellent SwiftMailer library. So I’ve created a dedicated RFC page on the symfony wiki just to get your opinions on the Right Way It Should Be Done®.

If you’re interested, there’s also a dedicated thread on the devs mailing list.

Edit: Swift integration didn’t make it for symfony 1.2, and here are my main reasons:

  • I didn’t get enough time
  • Swift is strangely coded from an architecture point of view: you must override the SMTP connection class to be able to send a mail using PHP mail() native function ; so it’s hard to extend or tweak it easily
  • I didn’t get enough time
  • Swift looked like a dead project at the time symfony 1.2 was released
  • I didn’t get enough time

To be honnest I’d like to see it integrated for symfony 1.3, but maybe with another lib… Stay tuned.

dimanche 14 septembre 2008

Back from the camp!

I’m back from the symfony camp 2008. It’s been a really great pleasure to meet the community[1], there are so many smart people using symfony all around the world, it’s just amazing.

I gave a one day training session on symfony 1.1 to eleven attendees the day before, I think it was somewhat interesting even if it may have been too short to see every important aspects of the framework…

The conferences were also great, and some slides are already available:

Stefan has also taken some shots of the event.

Last and as a side note, as of now all my symfony related posts will be written in English, as it’s the main language used by the community.

Notes

[1] even if the weather was… well, dutch wet

vendredi 25 juillet 2008

Partager la session utilisateur entre Flash/Flex et symfony avec AmfPHP

Pour les besoins d'un projet récent, j'ai eu besoin de valider la possibilité de gérer l'authentification et l'accès à la session symfony (côté serveur) depuis une interface générée par Adobe Flex (en Flash, donc côté client).

Pour cela, j'ai utilisé la librairie AmfPHP en version 1.9beta2, certes pas très récente mais suffisament fonctionnelle pour satisfaire à ce besoin précis. Voyons comment ça se passe concrètement. L'avantage de la démonstration ci-dessous est qu'elle ne nécessite pas d'installer Flex puisque AmfPHP fournit un navigateur de services (browser) qui nous suffira pour valider notre concept.

Installation du plugin sfGuard

Je pars du principe que tout le monde a une installation de symfony 1.1 nanti d'une application main, ainsi qu'un projet et un virtual host apache fonctionnels pointant sur local.mademo.org. Si ce n'est pas le cas, voila de quoi vous mettre à jour.

On commence par installer le plugin sfGuard, qui se chargera de la persistance des droits et permissions utilisateurs en base de données, et fournira les utilitaires d'authentification et de manipulation de la session côté serveur :

$ ./symfony plugin:install sfGuardPlugin
$ ./symfony propel-build-all
$ ./symfony cc

On charge quelques données de test dans notre base de données nouvellement mise à jour :

$ mkdir data/fixtures 
$ cp plugins/sfGuardPlugin/data/fixtures.yml.sample data/fixtures/fixtures.yml
$ ./symfony propel:data-load main

Ce jeu de données de test nous fournit par défaut un compte admin (mot de passe admin) qui nous servira à tester notre service d'authentification.

Ensuite, il nous faut modifier notre classe apps/main/lib/myUser.php gérant la session utilisateur afin qu'elle étende désormais la classe sfGuardSecurityUser, fournie par le plugin sfGuard :

<?php
// Fichier apps/main/lib/myUser.php
class myUser extends sfGuardSecurityUser
{
}

Installation et configuration d'AmfPHP

Nous allons installer la librairie AmfPHP dans le sous-répertoire web/ de notre projet[1], et aménager quelque peu notre arborescence pour accueillir les services AmfPHP :

$ cd /path/to/project
$ svn export https://amfphp.svn.sourceforge.net/svnroot/amfphp/tags/1.9beta2 web/amfphp
$ mkdir lib/amfphp-services
$ mv web/amfphp/services/amfphp lib/amfphp-services/

Ceci fait, nous allons éditer plusieurs fichiers d'amfphp afin de l'adapter à notre environnement symfony. Tout d'abord, commençons par éditer la valeur de la variable $servicesPath dans le fichier web/amfphp/globals.php :

<?php
// ...
$servicesPath = dirname(__FILE__).'/../../lib/amfphp-services/';

Enfin, il nous faut "patcher"[2] le fichier web/amfphp/core/amf/app/Filters.php, qui initialise la session PHP sans définir le nom de la session. Ici, nous utiliserons le nom de la session symfony par défaut, "symfony" (ligne 105 du fichier) :

102     //Fix for godaddy not allowing ini_get
103     $sessionName = "PHPSESSID";
104   }
105   session_name('symfony');
106   session_start();
107   $session_id = session_id();

Création d'un service permettant le partage de la session utilisateur

Voila, nous pouvons maintenant créer un service de gestion de l'authentification, que nous nommerons pompeusement UserSessionService et que nous enregistrerons dans le fichier lib/amfphp-services/UserSessionService.php :

<?php
require_once dirname(__FILE__).'/../../config/ProjectConfiguration.class.php';
 
/**
 * This class tests the symfony session within an AmfPHP context
 *
 */
class UserSessionService
{
  /**
   * Symfony context
   * @var sfContext
   */
  protected $context = null;
  
  /**
   * Symfony session
   * @var sfGuardSecurityUser
   */
  protected $user = null;
  
  /**
   * Public constructor
   *
   */
  public function __construct()
  {
    $configuration = ProjectConfiguration::getApplicationConfiguration('main', 'dev', true);
    $this->context = sfContext::createInstance($configuration);
    $this->user = $this->context->getUser();
  }
  
  /**
   * Checks wheter user is authenticated or not
   *
   * @return boolean
   */
  public function isAuthenticated()
  {
    return $this->getUser()->isAuthenticated();
  }
  
  /**
   * Authenticates user
   *
   * @param  string  $username
   * @param  string  $password
   * @return boolean True if user has been successfully authenticated
   */
  public function login($username, $password)
  {
    if ($this->isAuthenticated())
    {
      return true;
    }
    
    $user = sfGuardUserPeer::retrieveByUsername($username);
    
    if (!is_null($user) && $user->checkPassword($password))
    {
      $this->getUser()->signIn($user);
      return true;
    }
    
    return false;
  }
  
  /**
   * Signs out a user 
   *
   */
  public function logout()
  {
    return $this->getUser()->signOut();
  }
  
  /**
   * Retrieves the current symfony context
   *
   * @return sfContext
   */
  protected function getContext()
  {
    return $this->context;
  }
  
  /**
   * Retrieves the current symfony user session
   *
   * @return sfGuardSecurityUser
   */
  protected function getUser()
  {
    return $this->user;
  }
}

Pour tester notre service, utilisons le navigateur de service proposé par AmfPHP. Pour cela, il faut lancer un navigateur sur http://local.mademo.org/amfphp/browser/index.html :

Naviagateur de services AmfPHP

Via cette interface, elle même réalisée en Flex, on peut tester les méthodes publiques définies dans notre service, manipuler les arguments, et constater que nous arrivons à nous authentifier et que nous accédons bien à la même session utilisateur que dans symfony : login, logout et test du statut d'authentification.

En conclusion

On pourrait aller beaucoup plus loin dans cet exemple, en proposant par exemple une classe proxy en ActionScript 3 représentant un utilisateur du système (dans notre cas, une instance de la classe sfGuardUser), cette dernière reproduisant tout ou partie de ses méthodes et propriétés, et donc d'utiliser l'ORM Propel directement depuis Flash... Je vous laisse faire vos tests si le coeur vous en dit.

D'autre part, même si la librairie AmfPHP semble un peu passée au niveau architecture, elle reste néanmoins très efficace pour publier des services PHP dans Flash au travers du protocole AMF. J'ai eu vent d'autres librairies comme WebORB ou SabreAMF, mais je ne sais pas vraiment ce qu'elles valent... Des avis dans l'assistance ?

Notes

[1] Du coup, on expose certains scripts AmfPHP, mais la librairie n'est malheureusement que prévue pour fonctionner en ce sens...

[2] Oui, c'est terriblement crade, je ne comprend d'ailleurs pas qu'AmfPHP n'aie pas prévu ce cas de figure...

- page 2 de 7 -