Tuesday, September 20, 2011

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 theconfig/ProjectConfiguration.class.php file:

  php   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:

  yaml 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:

  yaml 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:

  yaml 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 thelib/model/doctrine/BlogPostTable.class.php file, which represents our blog_post table and available operations on it:

  php <?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 <?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, inapps/main/modules/post/templates/indexSuccess.php:

  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:

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

  php <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.

No comments: