Let's Play with Symfony 1.2 and Doctrine
Par NiKo le vendredi 31 octobre 2008, 11:00 - Dev
- Lien permanent -
29 commentaires -
Tags :
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:

And if you click a post title:

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:

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.
29 commentaires (Ajouter un commentaire)
@RICCCO : il y'a un batch qui a été développé par la communauté pour transformer un schema Propel en schema Doctrine. Donc tu pourras continuer d'utiliser la syntaxe Propel bien que celle de Doctrine soit plus riche car Doctrine propose plus de fonctionnalités nativement.
http://trac.symfony-project.org/wik...
@NiKo : j'avais un peu de temps à perdre
Hé, les copaings, depuis quand on répond à des trolls ?
@Riccco : je trouve ton commentaire plutôt déplacé vis-à-vis de symfony et pas complètement juste. Tout d'abord, je te rappelle que symfony est un projet entièrement open-source sur lequel Fabien et le reste de la Core Team travaillent en plus de leurs heures de boulot normal. Estimons nous déjà heureux que Fabien ait produit un tel outil et qu'il l'entretien quotidiennement sous licence MIT. Rien que pour ça, j'estime que l'on ait pas à lui reprocher quoique ce soit. En effet, Fabien aurait très bien pu la jouer perso depuis le début en gardant symfony fermé et destiné uniquement aux projets de sa société Sensio.
Concernant les évolutions du framework, tu sembles mal renseigné. Voici un léger rappel historique :
- octobre 2005 : lancement du framework sous licence MIT
- août 2006 : lancement de symfony 1.0
- juin 2008 : lancement de symfony 1.1
- octobre / novembre 2008 : première bêta de symfony 1.2
symfony 1.0 a bien vécu pendant deux ans et a montré quelques lacunes (couplage fort, système de formulaire non orienté objet, validation avec YAML...) auxquelles les branches 1.1 et 1.2 viennent répondre. Le passage entre les versions 1.0 et 1.1 pose des problèmes d'incompatibilité puisque l'objectif de la 1.1 a été de refondre entièrement le coeur de symfony pour assurer un découplage plus important entre les objets et d'implémenter le nouveau framework de formulaires. Néanmoins, la Core Team a fourni une documentation pour la migration ainsi qu'un plugin (sfCompat10Plugin) qui se charge de faciliter la transition. La version 1.2 qui approche en état stable n'apporte pas de réels changements par rapport à la 1.1 si ce n'est l'implémentation du framework de formulaires dans l'admin generator et la mise en place d'une architecture REST. La migration de la 1.1 vers la 1.2 se fera bien plus en douceur que de la 1.0 à la 1.1.
Quant à la documentation, elle arrive également. Je ne vois pas pourquoi tu râles après le manque de documentation sachant que Fabien, Jonathan Wage et les autres de la Core Team s'efforcent d'écrire des billets clairs et intéressants sur le blog officiel et de mettre à jour les chapitres du symfony book. Tu parles également du YAML de Doctrine qui n'est pas le même que celui de Propel. C'est normal, ce sont deux APIs différentes et indépendantes de symfony, qui ont été pensées différemment et qui ont été intégrées au framework. La doc de Doctrine (comme celle de Propel) existe bel et bien. Il suffit de se rendre sur le site officiel du projet Doctrine pour consulter une documentation très garnie.
Quant aux exemples du blog, ils sont suffisamment simples et explicites pour te donner les bases nécessaires et suffisantes pour prendre en main les composants du framework sans trop de difficulté. Libre à toi de les tester, de lire la doc, de consulter des blogs symfony pour parfaire tes compétences.
Dans symfony 1.2, Propel et Doctrine sont les deux ORMs intégrés par défaut au framework sous forme de plug-ins. Néanmoins, Propel restera l'ORM utilisé par défaut dans toutes les prochaines versions de la branche 1.x de symfony. Donc tu pourras toujours utiliser Propel avec symfony 1.2, 1.3... En revanche, lorsque symfony 2.0 sortira en 2009, Doctrine passera en tant qu'ORM par défaut et Propel restera lui aussi livré comme second ORM. Mais symfony 2.0 est encore bien loin d'être prêt d'après Fabien.
@SHAYATIN : PHP est déjà un langage très verbeux à la base mais c'est aussi ce qui fait sa force. Personnellement, je trouve que la verbosité de symfony permet au développeur de relire plus facilement son code et de le comprendre davantage.
Quant à doctrine tu n'écris pas les requêtes. Toutes les commandes de base type LEFT JOIN, RIGHT JOIN, WHERE... passent par des méthodes de l'objet Doctrine_Query. Au final ça ressemble beaucoup à du SQL mais en gardant l'abstraction de la base de données et la création d'objets.
Si toutefois tu veux abstraire complètement la syntaxe de Doctrine, tu peux utiliser le plug-in DbFinder de François. Mais au final tu te fais chier avec une couche supplémentaire et donc des performances en moins aussi.
++
"le but de symfony a la base etait de rendre le code lisible et comprehensible par n'importe qui ( de la secretaire au geek confirmé )"
Haha, elle est bien bonne celle-là X-)
@NiKo, excellent article, merci. Rien à voir, mais le nom de domaine aerial-type.com (qui renvoie vers la page de l'auteur du nouveau thème du blog) est parqué
Je pensais pas que l'hiver était la saison des trolls. Peut-être leurs origines nordiques ?
franchement, en comparant avec RoR, django ou TG je suis de plus en plus horifié par le php, c'est moche, c'est lourd c'est verbeux, étant un grand fan de Sf jusqu'a présent je ne peut m'empecher de regretter la direction qu'a pris le php et symfony pour le futur.
le but de symfony a la base etait de rendre le code lisible et comprehensible par n'importe qui ( de la secretaire au geek confirmé ) hors aujourd'hui c'est moche et c'est verbeux.
quant à l'orm, l'un des but principal n'etait-il pas de ne plus faire de requêtes SQL ?
quand je vois ça :
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();
}
j'ai du mal a discerner la difference au niveau de la syntaxe...
Bref symfony est et restera le meilleur framework PHP pour le moment, mais si on compare à ce qui se fait hors PHP, vraiment Sf est loin derriere.....
@Niko> bien joué, je fais ça aussi pour le faire redescendre
j'ai juste à tourner ma chaise.
Super blog au demeurant
@NiKo: c'est gentil. Merci à toi
@riccco> Je te fais un gros poutou
Salut,
Désolé, j'ai assez fait de pub pour Symfony pour ne pas pousser un coup de gueule. Je ne vais pas être agréable, je m'en excuse platement par avance.
J'en ai vraiment marre que tout change tout le temps avec symfony.
Je passe de la gestion des formulaires 1.0 à la 1.1 : tout change, pas de docs, rien que des exemples les plus simples du monde, rien sur le fait d'ajouter des elements dynamiques aux formulaires, pas vraiment de tutos assez poussés sur le fait de nester des formulaires et de plus, l'intégration du nouveau systeme de gestion de formulaire n'est pas au point : "One of the main limitation of Propel forms in symfony 1.1 was the inability to auto-save objects from embedded forms" : oui on s'en est rendu compte par nous même effectivement, mais ça aurait été pas mal de le préciser avant c pendant !
Quand je me rends compte que les widgets dates sont vraiment horribles, qu'il faut se faire ses widgets en intégrant des helpers 1.0 pour avoir un champ date qui ressemble à quelque chose.
Enfin maintenant on passe à la 1.2 , cool, et qu'est ce que je vois, Doctrine est intégré par défaut, ce qui implique à terme, de travailler plutôt avec cet ORM puisque apparamment, le developeur de Doctrine fait partie de l'équipe symfony.
Donc super, je matte le fichier YAML de cet article et je constate que la syntaxe change, c pas un problème, on regarde dans la doc..., ah, mais au fait, elle est ou la doc ?
En plus on se rends compte que l'admin generator via doctrine est buggé! Déjà que l'admin generator 1.1 etait basé sur la 1.0....
Comme le dit la chanson :" Misère, Misère..."
Arretez de nous balancer des exemples aussi bidons dont on se rends bien compte très rapidemment qu'il ne serve pas à grand chose pour un vrai projet.
Symfony c qd même plus que très bien pour moi, ce site est également très bien , mais bordel, j'en ai marre de tout le temps à avoir à m'adapter ( je fais pas mal de géolocalisation avec diverses API en ce moment ce qui fait que j'en ai un peu ma claque )
Excusez moi, ça va mieux , bonne journée.
riccco
@NiKo merci pour l'article,
Pour activer Doctrine et désactiver Propel je prefère utiliser :
$this->enableAllPluginsExcept('sfPropelPlugin');
Yes your example works ! I forgot to copy this 2 lines in my code :
$comment = new BlogComment();
$comment->setBlogPost($this->post);
That's why my copy didn't work... Shame on me ^^
I will ask Jon for accessors.
Thanks
sikkle> What's the bug, where's the ticket?
Hugo> My example works. I've just uploaded an archive containing the whole code of this tutorial, you can test it by yourself.
For the accessor generator, I don't know, look at the documentation or ask Jon, it's a coworker you know
NiKo, your tutorial is a bit buggy at the comment form part. When we submit the form, the post foreign key in the comment table is not set automatically. So, this is my version of your executeShow() action and BlogCommentForm class, which works well :
<?php // executeShow() method public function executeShow(sfWebRequest $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(); // Pass a new CommentBlog() object and the related post object in an associative array of options $this->form = new BlogCommentForm(new BlogComment(), array('post' => $this->post)); if ($request->isMethod('post') && $this->form->bindAndSave($request->getParameter('blog_comment'))) { $this->redirect('post/show?slug='.$this->post->getSlug()); } } // BlocCommentForm class BlogCommentForm extends BaseBlogCommentForm { // Declare the related post object protected $post = null; // Overload the form constructor public function __construct($object = null, $options = array(), $CSRFSecret = null) { // Check and set the related post object, which is located in the options associative array if (isset($options['post']) && $options['post'] instanceOf BlogPost) { $this->post = $options['post']; } parent::__construct($object, $options, $CSRFSecret); } 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)); // Set the default value of the hidden tag $this->setDefault('blog_post_id', $this->post->getId()); } }I think we can set directly the post object as the same way it's possible with Propel to avoid to pass the primary key in an hidden input tag, but I didn't find how to do it yet.
A simple question for you about Doctrine. Do you know if it's possible to ask Doctrine to generate the accessors as it was possible in previous versions of Doctrine with the generate_accessors option for schema.yml ? It's not very "developper friendly" with Eclipse or other IDEs not to have an auto-completion because getters and setters are not explicitly generated...
Hugo.
Il semble que doctrine ne comprenne pas bien les requêtes de ce tutoriel pour le moment avec postgresql.
Un ticket a été soumis, quelqu'un réussi a faire fonctionne ce tuto avec postgresql ?
COil> http://prendreuncafe.com/blog/post/... (la fin)
Et bien, un article en anglais ça fait bizarre.
Symfony n'aura bientôt plus de défaut !! 
NiCoS> You know, I think coding something like Dotclear requires a lot of high level skills that I don't have (and don't actually plan to acquire). But you're true, I'm really lazy
"I'm too lazy and not skilled enough."
I think you're _far_ more lazy than not skilled enough
MrTuTu> Fixed.
stef> Oh okay, I don't use the doctrine admin gen, that's probably why I missed this one
Niko> Bon en plus clair, pour l'instant, il y a un beans...
En désactivant propel :
$this->disablePlugins('sfPropelPlugin');
Tu désactives (très indirectement) l'admin-generator :
#.- symfony doctrine:init-admin backend post Post
No pb...
Par contre :
http://yourhost/backend_dev.php/pos...
Fatal error: Class 'PropelColumnTypes' not found in
lib/vendor/symfony/lib/generator/sfAdminGenerator.class.php on line 287
[sic] ...
Du coup en attendant la résolution du pépin par la team core :
// $this->disablePlugins('sfPropelPlugin');
A++
euh,... je crois que je lien vers PHPDoctrine est "moisi". C'est pas plutôt http://www.doctrine-project.org ?
Nice tutorial NiKo, many thanks. I will try it this weekend and start to perform the redesign of my website with symfony 1.2 and Doctrine
> not skilled enough
ahah, fortune.
NiCoS> Yes, but I decided to let a good tool (Doctlear) doing the job. I don't want to code another blogms, I'm too lazy and not skilled enough.
Does it mean that PUC will be {soon|at last
} generated by SF 1.2 & Doctrine
If I remember well some months (nearly years) ago, it was nearly done :-P
stef> Faut bien que j'm'occupe. Pour le disablePlugins() sur Propel, c'est explicité dans les premières étapes du tuto
Apparemment : de la crève naît le billet...
Cool ! I adopt 1.2 and his mate doctrine...
1 thing...
If you disable symfony doctrine: sfPropelPlugin you have an uggly error with admin generator :
Fatal error</b>: Class 'PropelColumnTypes'
So...
# $this->disablePlugins('sfPropelPlugin'); // vince trick
@pluch