Blog

What’s so special about ORM anyway?

January 16th, 2007 by Ted Kulp

One of the really neat features about CMSMS 2.0 is the inclusion of the Object Relational Mapping system. This isn’t a new concept by any stretch of the imagination. I’ve been using ORM systems on and off for about 4 years now.

However, ORM really started to take a whole different direction with the introduction of Ruby on Rails. The ORM implementation (known as ActiveRecord) is absolutely wonderful. It does a few things that seems like strong decisions at first, but make life SO much easier. No XML configuration files… in fact, nothing to really configure at all. You just setup your database table and it “knows” what is in it. All you need to do is setup some associations and it’s good to go.

Anyway, I’ve basically built an implementation of the Rails style of ORM, with making changes as necessary to fit into PHP5. While it’s not nearly as complete as the rails version, it is definitely working… and better than expected given the lack of tuning that has happened so far.

So, here is an example of how it works…

I’ve created a way to “register” an ORM object within modules. News is the first module that I’ve been converting to sort of help me work out what needs to be done. Here is the entire code for the NewsArticle class

class NewsArticle extends CmsObjectRelationalMapping
{
  var $table = 'module_news';
  var $sequence = 'module_news_seq';
  var $field_maps = array('news_id' => 'id', 'news_category_id' => 'category_id', 
    'news_title' => 'title', 'news_data' => 'content', 'news_date' => 'post_time');
  var $params = array('status' => 'draft', 'use_expiraiton' => true);
  
  public function __construct()
  {
    parent::__construct();
    $this->post_time = time();
    $this->start_time = $this->post_time;
    $this->end_time = strtotime('+6 months', $this->post_time);
    $this->create_belongs_to_association('author', 'user', 'author_id');
    $this->create_belongs_to_association('category', 'news_category', 'category_id');
  }
  
  function validate()
  {
    $this->validate_not_blank('title', lang('nofieldgiven',array(lang('title'))));
    $this->validate_not_blank('content', lang('nofieldgiven',array(lang('content'))));
  }
  
  protected function after_save()
  {
    //Update search index
    $module = CmsModule::GetModuleInstance('Search');
    if ($module != FALSE)
    {
      $module->AddWords($module->GetName(), $this->id, 'article', 
        $this->content . ' ' . $this->summary . ' ' . $this->title . ' ' . $this->title, 
        $this->use_expiration == 1 ? $this->end_time : NULL);
    }
    
    @CmsEvents::send_event(($this->id == -1 ? 'NewsArticleAdded' : 'NewsArticleEdited'), 
      array('news_id' => $this->id, 'category_id' => $this->category_id, 'title' => $this->title, 'content' => $this->content, 
      'summary' => $this->summary, 'status' => $this->status, 'start_time' => $this->start_time, 'end_time' => $this->end_time, 
      'useexp' => $this->use_expiration));
  }
  
  protected function after_delete()
  {
    $module = CmsModule::GetModuleInstance('Search');
    if ($module != FALSE)
    {
      $module->DeleteWords($module->GetName(), $this->id, 'article');
    }
    
    @CmsEvents::send_event('NewsArticleDeleted', array('news_id' => $this->id));
  }
}

That’s it. This class can do all database functions without anything else.

Let’s go over a couple of key points in this file.

var $table = 'module_news';
var $sequence = 'module_news_seq';
var $field_maps = array('news_id' => 'id', 'news_category_id' => 'category_id', 'news_title' => 'title', 
  'news_data' => 'content', 'news_date' => 'post_time');
var $params = array('status' => 'draft', 'use_expiraiton' => true);

In here, we define what table to use. I also have the name of the sequence in here as well, though it’s not required. If no sequence is defined, then we just use a regular automatic incrementing primary key field. The field maps represent any difference between the actual database field and a property name. I didn’t want to alter the news table, but I did want more consistent names for my properties, so I mapped about half of them to better names. The $params array can define any defaults, calculated fields or parameters that might not be in the table definition.

$this->create_belongs_to_association('author', 'user', 'author_id');
$this->create_belongs_to_association('category', 'news_category', 'category_id');

Here, we define some on-the-fly associations. Basically, a news article can belong to a category and also belong to a user, though that user is called an author. Now, we can do a $somearticle->category->name and get back the name of the category we’re in. Associations are lazy-loaded (on the fly and only once per object) dynamically when you use them. They also only work in a select context at the moment. Meaning, that if you do any “set”s to an association, they will not be saved or handled appropriately. This will probably change in the future.

function validate()
{
  $this->validate_not_blank('title', lang('nofieldgiven',array(lang('title'))));
  $this->validate_not_blank('content', lang('nofieldgiven',array(lang('content'))));
}

This is some neat stuff. The object will actually handle it’s own validation. Instead of having to write the same validation logic in multiple places (once for editing, once for adding, etc), we do it in one place. Validation is automatically called before a save() call and will only get saved if the validation is successful. In this case, we’re just checking to make sure title and content are filled in.

protected function after_save()
{
  //Update search index
  $module = CmsModule::GetModuleInstance('Search');
  if ($module != FALSE)
  {
    $module->AddWords($module->GetName(), $this->id, 'article', $this->content . ' ' . 
      $this->summary . ' ' . $this->title . ' ' . $this->title, $this->use_expiration == 1 ? $this->end_time : NULL);
  }
    
  @CmsEvents::send_event(($this->id == -1 ? 'NewsArticleAdded' : 'NewsArticleEdited'), 
      array('news_id' => $this->id, 'category_id' => $this->category_id, 'title' => $this->title, 
      'content' => $this->content, 'summary' => $this->summary, 'status' => $this->status, 
      'start_time' => $this->start_time, 'end_time' => $this->end_time, 'useexp' => $this->use_expiration));
}

The ORM also has callbacks for different parts of an object’s lifecycle. In this particular case, we want to run a few things after an article is saved. Instead of having to call them after every save in our various web interfaces, we add them in one place and know that they’re called EVERY time there is a save(). In this case, we’re updating the search module’s index if it’s installed and sending out our events. There are also callbacks before and after an object is deleted, and before and after an object is loaded.

So, after you create this object that associates to a table in the database, your module just has to register it. I just call this in the SetParameters method of the News module.

$this->register_data_object('NewsArticle', cms_join_path(dirname(__FILE__), 'class.news_article.php'));

Now I can use this anywhere else in the system to grab objects from the database. For example, let’s say I want to get all of the articles. I don’t care about category or ordering.

$articles = cmsms()->news_article->find_all();

That’s it! I’ll have a collection class of all of the articles in the system. I can use a foreach() or get an $articles->count() or grab the first one ($articles[0]). How many lines of code did you just get saved? The nice thing is that you can just push that collection directly to smarty and loop over them in the smarty template. No more having to assign fields to empty objects, adding them to an array, and passing the array…

Ok, but I want them ordered by the date they were posted.

$articles = cmsms()->news_article->find_all(array('order' => 'post_time ASC'));

(Interesting note here: Notice that we’re ordering by post_time, even though the database field is news_date. Because we set the field_maps to have news_date point to post_time, we automatically honor that in the order clause)

Maybe you want to get a particular one by the id?

$article = cmsms()->news_article->find_by_id(1);

What about id AND title and order them by post_time?

$article = cmsms()->news_article->find_by_id_and_title(1, 'Hello World', array('order' => 'post_time ASC'));

Getting the idea?

So, now you’ve made some changes to your article and want to save that back to the database. What then?

$article->save();

Validation will automatically go into effect, and any callbacks will be performed. That’s it.

Same goes for deletion…

$article->delete();

This only scratches the surface of the power that’s contained in the ORM system. In future articles, I’ll discuss how to do some more advanced functionality. Keep in mind, this isn’t an end-all solution for all database access. Somethings are still better to do with straight SQL queries. But, if it’s basic CRUD operations, this should remove MANY lines of code from your modules. Enjoy!

2 Responses to “What’s so special about ORM anyway?”

  1. newclear Says:

    Wow. This kind of development will be really amazing and fast.
    It looks like the modules for cmsms 1.0 needs to be rewritten to work on the new cmsms 2.0 module API.

  2. Ted Says:

    So far there won’t be much rewriting necessary. However, I’d encourage everyone to take advantage of these features since they really will make maintenance a lot easier because of the serious reduction in lines of code.

Leave a Reply