Prendre un Café

L'espace d'expression de Nicolas Perriault

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

mardi 20 avril 2010

Faire part de naissance

Ceux qui me suivent sur twitter en ont eu la primeur, mais j’officialise sur ce blog : Akei, ma société, est née.

Akei

Akei met en avant une offre de services axée sur le conseil, la formation et le développement Web, autour de technologies variées comme PHP, Symfony, Python, Django ; mais plus que tout sur la qualité.

Je ne m’étalerai pas sur les longueurs et turpitudes administratives proprement kafkaïennes qu’il m’a fallu affronter pour accoucher du bébé (et qui durent encore, soit dit en passant), mais je veux juste profiter de l’instant pour vous inciter à regarder ce site, à en lire les contenus, et à prendre contact si vous vous reconnaissez dans la vision présentée, pour éventuellement - pourquoi pas ? - travailler ensemble sur vos projets Web.

mercredi 17 février 2010

User Dependent Forms with Symfony

While sadly not being at Symfony Live 2010 with pals, I’m drowning my sorrow into tech tutorials writing.

Sometimes you want to design forms and contextualize them regarding the current user session. For example:

  • Manage the display of some fields regarding the user authentication status and credentials;
  • Filter some queries used to get the values available in a <select/> tag;
  • Allow multiple steps form validation with step state persistence (wizzard);
  • etc.

So how can we achieve this without using an evilish sfContext::getInstance()->getUser() call in the configure() method of the form?

Symfony 1.2 1.3 and 1.4 introduced the generation of a project-wide BaseForm class we can use here to setup some convenient methods to allow user setters and getters:

<?php
class BaseForm extends sfFormSymfony
{
  static protected $user = null;
 
  static public function getUser()
  { 
    return self::$user;
  }
 
  static public function getValidUser()
  {
    if (!self::$user instanceof sfBasicSecurityUser)
    {
      throw new RuntimeException('No valid user instance available');
    }
    
    return self::$user;
  }
 
  static public function setUser(sfBasicSecurityUser $user)
  {
    self::$user = $user;
  }
}

Okay, we now can set a user instance as a static property of all available forms within the project, nice. Wait, how the hell will we set the instance, and when?

Symfony dispatch a very useful context.load_factories event when all the factories - including the user one - are instanciated and available in the context. So we can listen to this event and set the user instance to the forms when it’s ready. We’ll do this in the ProjectConfiguration.class.php:

<?php
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    // ...
    
    $this->dispatcher->connect('context.load_factories', array($this, 'listenToLoadFactoriesEvent'));
  }
 
  public function listenToLoadFactoriesEvent(sfEvent $event)
  {
    BaseForm::setUser($event->getSubject()->getUser());
  }
}

So now we’ve set a static user instance in every form at project configuration time, we can use it in any form.

For example, imagine a form where only authenticated users having an admin credential can see, fill and submit an hypothetic is_published field:

class myForm extends BaseForm
{
  public function configure()
  {
    // will throw an exception if no user is available
    $user = self::getValidUser();
    
    $this->setWidgets(array(
      'title' => new sfWidgetFormInputText(),
      'body' => new sfWidgetFormTextarea(),
    ));
    
    $this->setValidators(array(
      'title' => new sfValidatorString(array('min_length' => 5)),
      'body' => new sfValidatorString(array('min_length' => 20)),
    ));
    
    if ($user->isAuthenticated() && $user->hasCredential('admin'))
    {
      $this->widgetSchema['is_published'] = new sfWidgetFormInputCheckbox();
      $this->validatorSchema['is_published'] = new sfValidatorBoolean(array(
        'required' => false,
      ));
    }
    
    // ...
  }
}

Here a basic user won’t see and won’t be able to submit any value for the is_published field. Note that all those changes won’t ever affect the controllers or the model, and the forms will still be easily testable because the forms and the user instance are not tightly coupled.

As a side note, you can also use the form options to eventually pass a user instance and fallback on the static instance in case it’s not available:

class myForm extends BaseForm
{
  public function configure()
  {
    // will throw an exception if no user is available
    $user = $this->getOption('user', self::getValidUser());
    
    // ...
  }
}

In a controller, you would use it this way:

<?php 
class fooActions extends sfActions
{
  public function executeBar(sfWebRequest $request)
  {
    $this->form = new myForm(array('user' => $this->getUser()));
    
    // ...
  }
}

A typical unit test suite of the form would be:

$t = new lime_test(2, new lime_output_color());
 
$user = new sfBasicSecurityUser(new sfEventDispatcher(), new sfSessionTestStorage(array(
  'session_path' => sys_get_temp_dir(),
)));
 
$form = new myForm(array('user' => $user));
$t->is(count($form->getWidgetSchema(), 2, '->configure() displays 2 fields when user is not authenticated'));
$user->setAuthenticated(true);
$user->addCredential('admin');
$t->is(count($form->getWidgetSchema(), 3, '->configure() displays 3 fields when user is authenticated and is an admin'));
// ... I'll let you complete the suite by yourself 

Now imagine we want to filter the choices offered by a one to many relationship field of a Doctrine form regarding user credentials:

class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    // will throw an exception if no user is available
    $user = $this->getOption('user', self::getValidUser());
    
    // ...
    
    // Category choices
    $categoryQuery = $this->getUserCategoryQuery($user);
    $this->widgetSchema['category_id'] = new sfWidgetFormDoctrineChoice(array(
      'model' => 'Category',
      'query' => $categoryQuery,
    ));
    $this->validatorSchema['community_id'] = new sfValidatorDoctrineChoice(array(
      'model' => 'Category',
      'query' => $categoryQuery,
    ));
  }
  
  /**
   * Please note that this method would naturally better fit in the model, in the 
   * ArticleTable class; We put this here for conciseness
   */
  protected function getUserCategoryQuery(sfBasicSecurityUser $user)
  {
    $query = Doctrine:getTable('Category')->createQuery('c');
    
    // If user is not an admin, only allow choice of published categories
    if (!$user->hasCredential('admin'))
    {
      $query->where('c.is_published = 1');
    }
    
    return $query;
  }
}

There are many more cool things you can achieve by offering user session access to a form, while keeping all the stuff easy to test.

As usual, if you have a better way, feel free to tell us about it in the comments.

dimanche 29 novembre 2009

Embedding Relations in Forms with Symfony 1.3 and Doctrine

Symfony 1.3 and 1.4 have been released some days ago in RC2 and you should really take a look at it, because they improve a lot the way you work with the framework, especially the forms one. It’s been a couple of days I started to implement a new bookmarks management feature on a project I’m working on currently, and the new model relationship embedding feature of 1.3’s forms framework just saved me a lot of days.

Here’s a quick and dirty example on how to setup a willingly simplistic bookmarks management system using it

The schema

Here’s the naive Doctrine schema I’ll use for this example:

# in config/doctrine/schema.yml
User:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
      notnull: true
  relations:
    Bookmarks:
      type: many
      class: Bookmark
      local: id
      foreign: user_id
      onDelete: CASCADE
 
Bookmark:
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name:
      type: string(255)
      notnull: true
    url:
      type: string(255)
      notnull: true
    user_id:
      type: integer(4)
      notnull: true
  relations:
    User:
      type: one
      local: user_id
      foreign: id

As you can see, it’s really naive. Anyway.

Some fixtures

What would be a Doctrine schema without fixtures? Quite nothing I guess, so here we go:

# in data/fixtures/fixtures.yml
User:
  niko:
    name: niko
 
Bookmark:
  niko_bookmark1:
    User: niko
    name: Slashdot
    url: http://slashdot.org/
  niko_bookmark2:
    User: niko
    name: Delicious
    url: http://delicious.com/
  niko_bookmark3:
    User: niko
    name: Digg
    url: http://digg.com/

Building stuff

You guessed it, after having suited the config/databases.yml file to set up a database of our choice, we can now build the database, all the classes we need and load the previously defined fixtures:

$ ./symfony doctrine:build --all --and-load

A basic user’s bookmarks management form

What we want do do now is to manage one User’s bookmarks in a single form. Let’s do it now in a new UserBookmarksForm.class.php file where we’ll build a form extending the autogenerated UserForm one:

<?php
// lib/form/doctrine/UserBookmarksForm.class.php
class UserBookmarksForm extends UserForm
{
  public function configure()
  {
    // We don't want to edit the User object
    unset($this['name']);
 
    // Existing bookmark forms
    $this->embedRelation('Bookmarks');
    
    $this->widgetSchema->setNameFormat('user_bookmarks[%s]');
  }
}

All the magic is done by calling the embedRelation() method here: all BookmarkForm instances will be embedded and managed automatically in and by our form.

Let’s use a simple controller to manage the form[1]:

<?php
class testActions extends sfActions
{
  public function executeBookmarks(sfWebRequest $request)
  {
    $this->form = new UserBookmarksForm($user = Doctrine::getTable('User')->findOneByName('niko'));
    if ($request->isMethod('post') && $this->form->bindAndSave($request->getParameter('user_bookmarks')))
    {
      $this->getUser()->setFlash('notice', 'Bookmarks list updated');
      $this->redirect('test/index');
    }
  }
}

And a basic template:

<form action="." method="post">
  <?php echo $form->renderHiddenFields() ?>
  <?php echo $form->renderGlobalErrors() ?>
  <table>
    <?php echo $form ?>
    <tr>
      <td></td>
      <td><input type="submit"/></td>
    </tr>
  </table>
</form>

You should see something like this:

simple.png

The form is functional, you can edit several bookmarks at one. Neat? But wait, we can’t add a new one!

Embedding a bookmark creation form

Let’s enhance our form by embedding an empty BookmarkForm form instance in order to add a new bookmark:

<?php
// lib/form/doctrine/UserBookmarksForm.class.php
class UserBookmarksForm extends UserForm
{
  public function configure()
  {
    unset($this['name']);
    
    // Bookmark creation form
    $newBookmarkForm = new BookmarkForm();
    $newBookmarkForm->setDefault('user_id', $this->object->id);
    $this->embedForm('new', $newBookmarkForm);
    
    // Existing bookmark forms
    $this->embedRelation('Bookmarks');
    
    $this->widgetSchema->setNameFormat('user_bookmarks[%s]');
  }
  
  protected function doBind(array $values)
  {
    if ('' === trim($values['new']['name']) && '' === trim($values['new']['url']))
    {
      unset($values['new'], $this['new']);
    }
    
    parent::doBind($values);
  }
}

Here I just override the doBind() method to remove the bookmark creation embedded form if nothing has been submitted by the user in its fields; that’s probably because he just wants to edit existing bookmarks[2].

By refreshing the page you should now be able to see the new form:

withAdd.png

So now you’re able to add a new Bookmark and/or edit existing instances within the same simple form. Neat? Wait, we can’t delete existing bookmarks!

Deleting existing bookmarks

So you would like to have a checkbox next to every existing bookmark embedded form to schedule its deletion on saving the form? Wow, that’s getting tricky, I like that.

The Symfony forms framework has evolved a lot, but what we’re trying to achieve here will involve some fine-tunning. Don’t be too much afraid seeing the code ;)

First, we’ll have to add the checkbox widget to the BookmarkForm form, but only if an existing Bookmark object has been bound to it:

<?php
// lib/form/doctrine/BookmarkForm.class.php
class BookmarkForm extends BaseBookmarkForm
{
  public function configure()
  {
    $this->widgetSchema['user_id'] = new sfWidgetFormInputHidden();
    $this->validatorSchema['url'] = new sfValidatorAnd(array(
      new sfValidatorString(array('max_length' => 255)),
      new sfValidatorUrl(),
    ));
    
    if ($this->object->exists())
    {
      $this->widgetSchema['delete'] = new sfWidgetFormInputCheckbox();
      $this->validatorSchema['delete'] = new sfValidatorPass();
    }
  }
}

Refreshing the form page should display something like that:

withAddAndDelete.png

The UserBookmarksForm now holds this code:

<?php
// lib/form/doctrine/UserBookmarksForm.class.php
class UserBookmarksForm extends UserForm
{
  /**
   * Bookmarks scheduled for deletion
   * @var array
   */
  protected $scheduledForDeletion = array();  
  
  /**
   * Configures the form
   *
   */
  public function configure()
  {
    unset($this['name']);
    
    // Bookmark creation form
    $newBookmarkForm = new BookmarkForm();
    $newBookmarkForm->setDefault('user_id', $this->object->id);
    $this->embedForm('new', $newBookmarkForm);
    
    // Existing bookmark forms
    $this->embedRelation('Bookmarks');
    
    $this->widgetSchema->setNameFormat('user_bookmarks[%s]');
  }
  
  /**
   * Here we just drop the bookmark embedded creation form if no value has been 
   * provided for it (it somewhat simulates a non-required embedded form)
   *
   * @see sfForm::doBind()
   */
  protected function doBind(array $values)
  {
    if ('' === trim($values['new']['name']) && '' === trim($values['new']['url']))
    {
      unset($values['new'], $this['new']);
    }
    
    if (isset($values['Bookmarks']))
    {
      foreach ($values['Bookmarks'] as $i => $bookmarkValues)
      {
        if (isset($bookmarkValues['delete']) && $bookmarkValues['id'])
        {
          $this->scheduledForDeletion[$i] = $bookmarkValues['id'];
        }
      }
    }
    
    parent::doBind($values);
  }
  
  /**
   * Updates object with provided values, dealing with evantual relation deletion
   *
   * @see sfFormDoctrine::doUpdateObject()
   */
  protected function doUpdateObject($values)
  {
    if (count($this->scheduledForDeletion))
    {
      foreach ($this->scheduledForDeletion as $index => $id)
      {
        unset($values['Bookmarks'][$index]);
        unset($this->object['Bookmarks'][$index]);
        Doctrine::getTable('Bookmark')->findOneById($id)->delete();
      }
    }
 
    $this->getObject()->fromArray($values);
  }
  
  /**
   * Saves embedded form objects.
   *
   * @param mixed $con   An optional connection object
   * @param array $forms An array of forms
   */
  public function saveEmbeddedForms($con = null, $forms = null)
  {
    if (null === $con)
    {
      $con = $this->getConnection();
    }
 
    if (null === $forms)
    {
      $forms = $this->embeddedForms;
    }
    
    foreach ($forms as $form)
    {
      if ($form instanceof sfFormObject)
      {
        if (!in_array($form->getObject()->getId(), $this->scheduledForDeletion))
        {
          $form->saveEmbeddedForms($con);
          $form->getObject()->save($con);
        }
      }
      else
      {
        $this->saveEmbeddedForms($con, $form->getEmbeddedForms());
      }
    }
  }
}

The code should be self-explanatory… or not. But anyway, you should now be able to add a new bookmark and delete existing ones as well, in a single form and process, and we’re still using the very same controller from the beginning!

Okay, let’s admit it, we’re reaching the limits of Symfony and probably those of my knowledge of it as well, but… IT WORKS.

But, what about the templating?

As of now we’ve used the convenient <?php echo $form ?> trick, but we now want to have full fine-grained control over the way our embedded form fields will look like.

Let’s update the form template accordingly:

<form action="." method="post">
  <?php echo $form->renderHiddenFields() ?>
  <?php echo $form->renderGlobalErrors() ?>
  <!-- Embedded new bookmark form -->
  <fieldset>
    <legend><?php echo __('Create a new bookmark') ?></legend>
    <?php echo $form['new']->renderError() ?>
    <div class="form-row">
      <?php echo $form['new']['name']->renderLabel() ?>
      <div class="form-field">
        <?php echo $form['new']['name']->render() ?>
        <?php echo $form['new']['name']->renderError() ?>
      </div>
    </div>
    <div class="form-row">
      <?php echo $form['new']['url']->renderLabel() ?>
      <div class="form-field">
        <?php echo $form['new']['url']->render() ?>
        <?php echo $form['new']['url']->renderError() ?>
      </div>
    </div>
  </fieldset>
  <!-- /Embedded new bookmark form -->
  <!-- Embedded existing bookmark forms -->
  <?php foreach ($form['Bookmarks'] as $i => $eForm): ?>
    <fieldset>
      <legend><?php echo sprintf(__('Edit bookmark #%d'), $i+1) ?></legend>
      <?php echo $eForm->renderError() ?>
      <div class="form-row">
        <?php echo $eForm['name']->renderLabel() ?>
        <div class="form-field">
          <?php echo $eForm['name']->render() ?>
          <?php echo $eForm['name']->renderError() ?>
        </div>
      </div>
      <div class="form-row">
        <?php echo $eForm['url']->renderLabel() ?>
        <div class="form-field">
          <?php echo $eForm['url']->render() ?>
          <?php echo $eForm['url']->renderError() ?>
        </div>
      </div>
      <div class="form-row">
        <div class="form-field">
          <?php echo $eForm['delete']->render() ?>
          <?php echo $eForm['delete']->renderError() ?>
          <?php echo $eForm['delete']->renderLabel(__('Delete this bookmark')) ?>
        </div>
      </div>
    </fieldset>
  <?php endforeach; ?>
  <!-- /Embedded existing bookmark forms -->
  <input type="submit" value="<?php echo __('Update your bookmarks') ?>"/>
</form>

Now you can really control the semantics of the HTML code of the form, field by field.

As always, if you have better solutions in mind, feel free to post them in the comments.

Notes

[1] I’ll leave you to create an app, module and route, okay?

[2] Of course this can be a lot improved, but the example is kept as simple as possible to ease understanding.

mercredi 4 novembre 2009

An Interactive PHP Shell: phpsh

Just found this nice project contributed by the Facebook team, phpsh. Basically, it’s an interactive and advanced command line interface to php, a kind of super-php -r similar to what you can find in Python or Ruby. Ironically enough, phpsh is mostly written in Python, by the way.

To install phpsh, just get it from github[1]:

$ git clone git://github.com/facebook/phpsh.git
$ cd phpsh
$ python setup.py build
$ sudo python setup.py install

To run phpsh:

$ phpsh
Starting php
type 'h' or 'help' to see instructions & features
php> ="hello world"
hello world
php> =2 + 2
4
php> $a = 8

php> =$a
8

Note that you don’t have to open or close <?php tags, you can print something just by prepending an = sign to the command, and no need to type semicolum at the end of a call. Easy[2].

You can access a function documentation by just prepending the d keyword to its name:

php> d strlen
 
# strlen
 
(PHP 4, PHP 5)
 
strlen -- Get string length
 
### Description
 
int strlen ( string $string )
 
Returns the length of the given string . 
 
### Parameters
 
string     
 
The [string][1] being measured for length. 
 
### Return Values
 
The length of the string on success, and 0 if the string is empty. 
 
### 
 
   [1]: #language.types.string

You can define your own classes and functions, and run them:

php> function foo($a){echo $a.'!';}
php> foo('bar')
bar!

Same goes for classes:

php> class Foo {public function bar($a){echo $a.'!';}}
php> $f = new Foo()
php> $f->bar('baz')
baz!

You can even execute shell command from there:

php> ! ls -la

Of course, you can import include and require files. As a concrete example, let’s roughly play with the symfony API:

php> ! symfony -V
symfony version 1.2.2-DEV (/Users/niko/Sites/vendor/symfony12/lib)
php> c /Users/niko/Sites/vendor/symfony12/lib/autoload/sfCoreAutoload.class.php
Extra includes are: ['/Users/niko/Sites/vendor/symfony12/lib/autoload/sfCoreAutoload.class.php']
php> sfCoreAutoload::register()
php> =sfYaml::load('foo: bar')
array(
  "foo" => "bar",
)
php> =sfYaml::dump(array('foo' => array('bar' => 'baz')))
"foo:\n  bar: baz\n"

Last but not least, when enough playing, type q or press ctrl + d to quit phpsh.

Notes

[1] So yeah, so you need git. And Python, of course.

[2] Anybody putting "rather use python then" will have serious troubles, even if I strongly agree.

lundi 12 octobre 2009

A Symfony 1.3 Gem: Open Files Listed in Exceptions with the Editor of your Choice

I’ve just stumbled upon a nice new feature of Symfony 1.3, the ability to open any linked file within standard exception page with the text editor of your choice, at least if it provides an url scheme/protocol to open them. Texmate provides natively this feature, so let’s see how to configure a symfony project to allow its files to be opened directly in textmate when encountering a stack trace.

In the config/ folder of the project, create a settings.yml file with this content:

dev: #yeah, we never know
  .settings:
    file_link_format: "txmt://open?url=file://%f&line=%l"

That’s it. Now any standard exception page (in dev environment only, right?) will provide a txmt:// link to every PHP file listed in the stack trace. And at the correct line number. Neat.

mardi 6 octobre 2009

Simple Continuous Integration of a Symfony Project using Hudson

I love tests, I just cannot program without them anymore, especially with Symfony. And as I write a lot of them on a daily basis, the full test suite can take a very long time to be executed ; running a symfony test:all command before every granular commit suppose you have to wait sometimes more than 5, 10 minutes or more for the whole tests to be ran: hardly acceptable. Post commit hooks can be a solution, but what if a lot of devs are working simultaneously on the same project? Added to the previously seen test execution duration, it can become a problem difficult to deal with.

Here comes continuous integration : regularly, eg. every hour, the symfony project will be built up and configured from scratch, then the full test suite will be executed and the resulting log will be aggregated. It’s a very convenient way to keep an eye on the overall code quality and integrity of the project.

There are several open source continuous integration software available on the market: Cruise Control (and phpUnderControl), Continuum, Integrity, the uncertain but awesomely looking Sismo which powers the Symfony continous integration server… But I recently had the opportunity to test Hudson, a java based one. It’s very simple to install, setup and configure, and has tons of plugins. So let’s see how it can be used to test a Symfony project.

img hudson_tests_weather

Hudson Installation

That’s really not the hardest part, because you just need a working JRE 1.5 installation on your machine, and to retrieve the latest version of the program.

Then, you can launch it directly from a shell prompt:

$  java -jar /path/to/hudson.war

If you fear to break something or just want to give it a go without the hassle of firing a term, just launch the available Java Web Start version.

On debian/ubuntu, it’s just as easy as:

$ sudo echo deb http://hudson-ci.org/debian binary/ >> /etc/apt/sources.list
$ sudo apt-get update
$ sudo apt-get install hudson
$ sudo /etc/init.d/hudson start

Configuration and Symfony Project Integration

Once Hudson is installed an running, head up to http://localhost:8080/ and start playing with the web based user interface of Hudson. The navigation and configuration forms are quite obvious, and therefore efficient. Creating a project following the provided guidelines is easy and can be achieved within minutes:

First, create a new Job, choosing the Build a free-style software project option.

Then, configure the subversion repository[1] used by your project by entering its base URL in the Source Code Management field You can schedule builds, watch for SCM activity and even trigger builds from remote script using a neat XMLRPC interface[2].

img hudson_svn

img hudson_hudson_scheduling

Configuring the building process is as easy as writing down some commands, like the one you’d execute to setup your project on a new box:

img hudson_build_commands

As of Symfony 1.3, test result logs can be exported in a jUnit XML file, so Hudson will be able to parse them and provide some useful reports, metrics and charts:

huson_test_results.png

Of course, you can configure failure notifications: email, atom feed, there’s even a twitter plugin!

As a conclusion, Hudson is a very simple but efficient continous integration tool which can easily be configured to monitor the quality of your Symfony projects… at least if you write tests ;)

Notes

[1] If you’re not using Subversion, lot of plugins are available for other SCM in the huge Hudson plugins repository

[2] Useful if you wish to use post-commit build hook

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

- page 1 de 10