Big Thanks
A lot of awesome stuff has been added recently to the next major symfony release, 1.2. Fabien has worked very hard to add without a doubt the most sophisticated features of any PHP framework that exists today. Not only are they nice features but he has implemented them in a OO way so that it is easy for me to implement the same features with another ORM, Doctrine. All this is done with very little work by me. So, give a big thanks to him if you enjoy this.
Real World Example
In this article I will start from the beginning with a brand new symfony 1.2 project so you can get going with Doctrine. We will use a schema for your typical, run-of-the-mill content management system. The schema consists of articles, authors and categories where the articles are internationalized.
Start your Project
First you need to initialize a brand new symfony 1.2 project and initialize a backend application. Make sure you are using the latest code from svn as beta1 did not include this Doctrine functionality.
Generate your project
$ mkdir cms
$ cd cms
$ symfony generate:project cms
Configure your database
Open config/databases.yml
and replace the contents with a configuration for Doctrine like the following:
all: doctrine: class: sfDoctrineDatabase param: dsn: mysql:host=localhost;dbname=dbname username: root password: secret
Generate backend application
$ symfony generate:app backend
Everybody get your Doctrine on
Now we need to enable Doctrine and disable Propel :) Edit your config/ProjectConfiguration.class.php
and add the following code to your setup()
function.
public function setup() { $this->enablePlugins(array('sfDoctrinePlugin')); $this->disablePlugins(array('sfPropelPlugin')); }
Now that Doctrine is enabled we can list the available Doctrine tasks:
$ ./symfony list doctrine
Available tasks for the "doctrine" namespace:
:build-all Generates Doctrine model, SQL and initializes the database (doctrine-build-all)
:build-all-load Generates Doctrine model, SQL, initializes database, and load data (doctrine-build-all-load)
:build-all-reload Generates Doctrine model, SQL, initializes database, and load data (doctrine-build-all-reload)
:build-all-reload-test-all Generates Doctrine model, SQL, initializes database, load data and run all test suites (doctrine-build-all-reload-test-all)
:build-db Creates database for current model (doctrine-build-db)
:build-filters Creates filter form classes for the current model
:build-forms Creates form classes for the current model (doctrine-build-forms)
:build-model Creates classes for the current model (doctrine-build-model)
:build-schema Creates a schema from an existing database (doctrine-build-schema)
:build-sql Creates SQL for the current model (doctrine-build-sql)
:data-dump Dumps data to the fixtures directory (doctrine-dump-data)
:data-load Loads data from fixtures directory (doctrine-load-data)
:dql Execute a DQL query and view the results (doctrine-dql)
:drop-db Drops database for current model (doctrine-drop-db)
:generate-admin Generates a Doctrine admin module
:generate-migration Generate migration class (doctrine-generate-migration)
:generate-migrations-db Generate migration classes from existing database connections (doctrine-generate-migrations-db, doctrine-gen-migrations-from-db)
:generate-migrations-models Generate migration classes from an existing set of models (doctrine-generate-migrations-models, doctrine-gen-migrations-from-models)
:generate-module Generates a Doctrine module (doctrine-generate-crud, doctrine:generate-crud)
:generate-module-for-route Generates a Doctrine module for a route definition
:insert-sql Inserts SQL for current model (doctrine-insert-sql)
:migrate Migrates database to current/specified version (doctrine-migrate)
:rebuild-db Creates database for current model (doctrine-rebuild-db)
The Schema
Now the fun begins. We have Doctrine enabled so the first thing we need to is define our schema for the CMS in config/doctrine/schema.yml
.
--- Article: actAs: Timestampable: I18n: fields: [title, content] columns: author_id: integer status: type: enum values: [Draft, Published] notnull: true title: type: string(255) notnull: true content: type: clob notnull: true is_on_homepage: boolean published_at: timestamp relations: Author: foreignAlias: Articles Categories: class: Category refClass: ArticleCategory foreignAlias: Articles Category: columns: name: type: string(255) notnull: true Author: columns: name: type: string(255) notnull: true about: string(1000) ArticleCategory: columns: article_id: integer category_id: integer relations: Article: foreignAlias: ArticleCategories Category: foreignAlias: ArticleCategories
Data Fixtures
We have our schema, now we need some data to test against so copy the following YAML in to data/fixtures/data.yml
--- Article: Article_1: Author: jwage status: Published is_on_homepage: true published_at: '<?php echo date("Y-m-d h:i:s"); ?>' Categories: [article, ontheedge] Translation: en: title: symfony 1.2 and Doctrine content: Article about the new Doctrine integration in symfony 1.2 fr: title: symfony 1.2 et doctrine content: Article sur l'intégration de Doctrine dans symfony 1.2 Author: jwage: name: Jonathan H. Wage about: Jonathan is the lead developer of the Doctrine project and is also a core contributor to the symfony project. Category: article: name: Article tutorial: name: Tutorial ontheedge: name: Living on the edge
Building and Testing
Now that we have our schema and data fixtures we have everything we need to initialize our database, models, forms, data, etc. This can all be done with the extremely simply command below:
$ ./symfony doctrine:build-all-reload --no-confirmation
>> doctrine dropping databases
>> doctrine creating databases
>> doctrine generating model classes
>> doctrine generating sql for models
>> doctrine generating form classes
>> doctrine generating filter form classes
>> doctrine created tables successfully
>> doctrine loading data fixtures from "/Us...ymfony12doctrine/data/fixtures"
That was too easy, when is this gonna get hard? Now lets do some inspecting with DQL to see that the data was loaded properly.
$ ./symfony doctrine:dql "FROM Article a, a.Author a2, a.Translation t"
>> doctrine executing dql query
DQL: FROM Article a, a.Author a2, a.Translation t
found 1 results
-
id: '1'
author_id: '1'
status: Published
is_on_homepage: true
published_at: '2008-11-06 04:37:11'
created_at: '2008-11-06 16:37:11'
updated_at: '2008-11-06 16:37:11'
Author:
id: '1'
name: 'Jonathan H. Wage'
about: 'Jonathan is the lead developer of the Doctrine project and is also a core contributor to the symfony project.'
Translation:
en:
id: '1'
title: 'symfony 1.2 and Doctrine'
content: 'Article about the new Doctrine integration in symfony 1.2'
lang: en
fr:
id: '1'
title: 'symfony 1.2 et doctrine'
content: 'Article sur l''intégration de Doctrine dans symfony 1.2'
lang: fr
That may be your first taste of the Doctrine Query Language, also known as DQL. Looks a lot like SQL huh? Close your mouth, you're drooling.
For those of you who don't wanna write raw DQL strings, don't worry we have a fully featured Doctrine_Query
object for building your queries.
$q = Doctrine_Query::create() ->from('Article a, a.Author a2, a.Translation t'); $articles = $q->execute();
Admin Generators
Now that we have everything built, we can start generating some magic with symfony. Lets start first by defining the route collections for managing our articles, authors and categories. Open apps/backend/config/routing.yml
in your editor and paste the following routes inside.
articles: class: sfDoctrineRouteCollection options: model: Article module: articles prefix_path: articles with_wildcard_routes: true categories: class: sfDoctrineRouteCollection options: model: Category module: categories prefix_path: categories with_wildcard_routes: true authors: class: sfDoctrineRouteCollection options: model: Author module: authors prefix_path: authors with_wildcard_routes: true
These routes will allow us to generate an admin generator module for managing the data through each of the Doctrine models. Run the following commands to generate the three modules.
$ ./symfony doctrine:generate-admin backend articles
$ ./symfony doctrine:generate-admin backend categories
$ ./symfony doctrine:generate-admin backend authors
Now when you access the categories
module from the backend you should see the following.
Now you may want to slightly customize the articles admin generators to display only a certain set of fields in the list and filters form. You can do so by editing the generator.yml
located in apps/backend/modules/articles/config/generator.yml
.
config: list: display: [title, published_at, is_on_homepage, status] filter: class: ArticleFormFilter display: [author_id, status, is_on_homepage, published_at, categories_list]
You can customize the other modules as well in the same way.
Now if you change the url to have ?sf_culture=fr
in your url the title will show the french version. You should see the following in your browser when you pull up the articles module.
Editing Translations
Now how do we go about editing those translations? If you remember this with the old admin generators, it was pretty much impossible. Now, it is a matter of adding one line of code to our ArticleForm
. All we need to do is embed the ArticleTranslation
form. This can be done by editing lib/form/doctrine/ArticleForm.class.php
and adding the following code to configure()
public function configure() { $this->embedI18n(array('en', 'fr')); }
Now when you edit an article you will see you have the ability to edit translations directly inside of the article.
Now that is pretty cool but what if you want to edit the Author
directly inside of the Article
as well and have it create a new Author
if it doesn't exist and use the existing Author
record if that name does exist. This is simple as well.
Edit/Add Author
In order to add the above described functionality we need to add a little bit of code in three difference places. First you need to embed the Author
form in to the Article
form by editing lib/form/doctrine/ArticleForm.class.php
and add the following code.
public function configure() { unset($this['author_id']); $authorForm = new AuthorForm($this->getObject()->getAuthor()); unset($authorForm['about']); $this->embedForm('Author', $authorForm); $this->embedI18n(array('en', 'fr')); }
Optionally Optimize Queries
Now we can modify the
articles
actions class to join in theAuthor
andTranslation
information so that the data is joined on the main query and not lazily loaded by Doctrine invoking additional queries to the database. This step is optional. Openapps/backend/modules/articles/actions/actions.class.php
and override theexecuteEdit()
function.public function executeEdit(sfWebRequest $request) { $this->article = Doctrine_Query::create() ->from('Article a') ->leftJoin('a.Author a2') ->leftJoin('a.Translation t') ->where('a.id = ?', $request->getParameter('id')) ->fetchOne(); $this->form = $this->configuration->getForm($this->article); }
Now when you view the form for editing and creating an Article you will see the embedded form for the Author with the existing information populated.
Now the last step is to tell Doctrine to look for existing Author
objects when setting the name so that duplicate Author
names are not created. Edit lib/model/doctrine/Author.class.php
and override the name
mutator by adding the following code.
public function setName($name) { $name = trim($name); $found = Doctrine_Query::create() ->select('a.id') ->from('Author a') ->where('a.name = ?', $name) ->fetchOne(array(), Doctrine::HYDRATE_ARRAY); if ($found) { $this->assignIdentifier($found['id']); } else { $this->_set('name', $name); } }
The above code will check if an Author
with the passed name already exists, if it does it will assign the identifier of the found record otherwise it does what it normally would do, set the name to the object.
The
_set()
and_get()
methods must be used to avoid a infinite loop when overriding accessors and mutators.
Wow, did we really just build an entire backend for managing the content of a simple content management system in under an hour? We sure did. Try it for yourself today and enjoy developing rich web based functionality using symfony and Doctrine.
THE END
Very nice. Thanks for the example.
Is the translations table missing from the schema? Or am I not understanding something? I never had to support multi-language web sites.
@Brett With Doctrine, when you enable the I18n actAs behavior, it dynamically creates the ArticleTranslation model which creates the article_translation table.
It looks nice, but I am having problems when I try to create or edit....I think it is a problem with the routing...any ideas
@GabrielI Are you getting any specific errors or can you describe the behavior you are seeing. I just double checked again to make sure and it is working for me.
hi jwage, when I try to edit an author with id=1, It show me a 404 exception...(Action "authors/1" does not exist.) when I try to create a new one it doesn't show me nothing..just goes back to the author list but nothing is created.
@Gabriel And you added the routes to routing.yml and cleared your cache?
yes, I did it....I copied from the example to the routing.yml and I kept the cache clear.
thanks anyway....maybe it's just bad luck. I'll be looking for all those new features...I'm really excited with the doctrine admin generator.
@Gabriel I just ran through it myself again with a fresh symfony 1.2 to make sure I didn't have any mistakes and it worked. The only thing I can think of is that you're not using the absolute latest version from svn because I literally commited the code that makes all this work hours before I wrote the article.
I would like to see how you can add virtual fields for it.
Doctrine and symfony 1.2 are absolutly awesome together. Thanks a lot for this nice tutorial Jon ;)
I'm really thrilled to have jumped ship from home-made crud interfaces to this awesome framework. Thanks for all the hard work, all of you! (See, I thank Fabien! :))
See you on Saturday!
Nice, i will try doctrine soon !
Hi, now everything is working, but now I am trying to change the categories_list's widget from sfWidgetFormDoctrineSelectMany to sfWidgetFormDoctrineChoiceMany; but it doesn't work. Any Ideas???
Don't mind...problem solved
Im still using 1.0 and have been waiting for 1.2 so I can switch to Doctrine, so forgive me if this is a silly question. Can you explain why you needed to add the routing instead of using the defaults? Where is the use of sfDoctrineRouteCollection documented?
Thanks Jonathan, nice tutorial! small correction though, the line that read: Now you may want to slightly customize the articles admin generators to display only a certain set of fields in the list and filters form. You can do so by editing the generator.yml located in apps/backend/modules/categories/config/generator.yml.
should have this path instead as we're trying to modify the article list. apps/backend/modules/articles/config/generator.yml
Oh, and for the absolute beginner, you still need to have a properly done config/database.yml file. This can be as simple than this:
all: doctrine: class: sfDoctrineDatabase param: dsn: sqlite:////cms.db
I tried the new admin gen with propel thanks to the GREAT WORK Fabian did.
One hour and I finished the tuto with everything working. This is just wonderful. I was waiting for Doctrine to switch to it due to all the standard behabiors it provides (I18N, versionable ...).
2 questions after going trough that tutorial :
Nice work to both of you Fabien and Johnatan.
I just post (fedlab - typo error) and ask a question regarding filters.
One hour ago, Fabian updated this ticked : http://trac.symfony-project.org/changeset/12758.
May be it explains why my link to action to articles from my authors page was not working. I am not good enought to understand changes Fabian made.
Am I right ?
Hi,
Few things, it would have been great to show the database.yml for newbies like me. I've spent time looking at Doctrine symfony 1.1 cookbook example.
Then in your doctrine website documentation, there are mistakes describing relations. You forget the 's' in the yaml samples in a One-To-Many or Many-to-One relationship chapter.
Anyway, I'm very happy about this tutorial, and I really thank you for being so reactive.
For those who are interested, here's another great Doctrine tutorial (simpler): http://tinyurl.com/5eflfn
When I save from admin it is not saving anything. I am using beta2
Error when submitting form after putting $this->embedI18n(array('en', 'fr')); Undefined index: en | fr in sfDoctrinePlugin\lib\form\sfFormDoctrine.class.php on line 214
It looks like some small bugs existed in the beta2 release for Doctrine but are already fixed in svn..
the title of this article should be prefixed with "Call the expert:", like all the other articles of this category.
No problem occurring anymore when submitting form but embed form is not updated
I'm using the latest code (http://svn.symfony-project.com/branches/1.2), with the same routes posted in the article. I get the following error when editing:
Action "articles/1" does not exist.
I had pasted the routes after the default ones thus making it not work as routes shortcircuit on the first match. yay for rtfm. ;)
I think it would be a good idea to mention that detail in the article so others don't fall in the same trap.
Hi Jon I'm currently using the 1.2RC1. The new/edit works fine but I'm having a problem accessing the list. It throws a 500 error. Do I missed something? Thanks.
Adding "with_wilcard_routes: true" on route solves the problem. :)
Anyone getting this error? 500 | Internal Server Error | sfConfigurationException The route "articles_collection" does not exist.
If I go to articles/new it works but not the lists?
@Andreas: I do, with the 1.2RC1, so I guess I will submit a ticket for that. I thought I am doing something wrong but I know I followed the instructions.
@Andreas Make sure your routes have "with_wilcard_routes: true" defined.
@jwage: I have such error too. I've defined 'with_wildcard_routes:true' in my routing.yml
@Andreas : same error for me, and I don't know how to fix it...
Those wondering why their admin modules don't look like the ones pictured above need to run 'symfony plugin:publish-assets'.
Also, note that the misspelling of 'with_wildcard_routes' as 'with_wilcard_routes' in RC1 has been fixed in the latest SVN - I assumed it was a typo and corrected in my routing.yml to 'with_wildcard_routes' got an error about the route
@Dylan Oliver: Thanks, it works now:)
I have another problem with symfony and Doctrine.
My admin page is generated about 2-3 secons each time. Is it normal?
I try to connect to IDS and run "symfony doctrine:build-all-reload", but it show me :SQLSTATE=HY000, SQLDriverConnect: -11060 [Informix][Informix ODBC Driver]General error.
I defined IDS Server ids11a in $INFORMIXDIR/etc/sqlhosts.My database.yml: ... dsn:inforimx:;database=cmsw; server=ids11a username:ifxuser password:ifxpwd