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.

New Admin Generators

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.

New Admin Generators

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.

New Admin Generators I18n

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'));
}
 

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.

New Admin Generators Embedded Author

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