Blog

Archive for the 'Development' Category

2.0 Needs (Your?) Help

Thursday, February 8th, 2007

CMSMS 2.0 is the largest project I’ve ever taken on. Not only that, I’ve basically signed myself up to do this solo. Well, at least the first large bits of work to the undercarriage will be/were done by me.

Here is what is done so far:

  • ORM
  • Migrated javascript (mostly) to jquery
  • Versioning on an object level
  • Totally restructured API
  • Function caching
  • Full page caching
  • Started installer
  • Rewrite of content
  • Smarty tags for module api functions
  • Smarty tags for admin functions
  • Rewrote how admin themes work (smarty templates) and how menus are loaded (xml file)
  • Rewrote News to take advantage of module api changes

If I had to guess, I have about 250-300 hours invested in 2.0. It’s beyond vaporware at this point… it will happen. Whether or not some features get cut is a different story, but so far so good.

Now, to give you an idea of how much work there is to do, here is what is left:

  • Finish installer
  • Multilanguage
  • Versioning interface
  • Workflow
  • Permissions/ACLs
  • Overhaul of language handling — addition of the language manager to download translations
  • Total rewrite of translation center to be database centric and able to create language files for download on the fly
  • Admin interface overhaul — especially content and permissions
  • New block types, especially image

These are the major points. There are a lot of little things in there as well… like removing half of the config.php variables, among some other things.

I’m guessing that 2.0 will require somewhere in the area of 1500 hours to complete (For those of us keeping score, that’s $112,500 at my current consulting rate). It’s going to be impossible for me to finish this thing by myself, especially with the timeframes I’ve made. I estimate the fact that I can devote about 15 hours a week to CMSMS means that will take about 80 more weeks, which puts us into Summer of 2008. I can’t let that happen.

So, I’m asking for one little thing… HELP!!!

I need to start handing over pieces of 2.0 to other developers. I need people with design skills to help mock up what the admin should look like. I need javascript people to help me tie up the interface and make it totally usable.

There is a ton of work to be done and many of those pieces are totally independent of the rest of the system. If you’re interesting in looking at any of these pieces, please let me know on IRC (it’s the best place to have a long conversation). Any takers?

Seriously, if there is one thing I’ve learned in almost 3 years of leading an open source project, it’s how to delegate. You can either ask me for a piece to start looking at or toss your skills at me and I’ll come up with something… Any and all help is greatly appreciated! Let’s this thing out of vapor and into beta!

Thanks!

Number 41

Friday, January 19th, 2007

Ok, so I keep spouting off about the goals of CMSMS 2.0. At this point, there are like 40 goals and all are equally important. You’ve heard it all before…

Oh well, I’m bringing up #41

#41: Serious, concise, functional and documented API.

What does this mean? CMSMS 1.x has an API of sorts. The module creation parts of the API are probably the most organized of the bunch. Most other parts of the CMSMS code are scattered through out smarty plugins, global functions, poorly named classes that should be called staticly, etc.

One of the things I took on early in the 2.0 development cycle was the formulation of a consistent API to work from. And honestly, the lib/classes directory was on the right track. It just wasn’t implemented as well as it could. Live and learn…

1.0.x has too many global functions for doing random things. I wanted to cut all this out. Also, there are too many $gCms->GetSomethingOperations() methods. This is stuff that can all be moved to static methods in classes.

index.php and include.php were both WAY too messy. I wanted to offload a lot of that stuff into clearly marked methods, using as much DRY (don’t repeat yourself) development as I could.

And, I wanted to “namespace” all of the CMSMS classes so that they don’t get in the way of other classes that might be used for modules, addons, etc to the system. Since PHP doesn’t use real namespacing, every class starts with Cms (CmsTemplate, CmsApplication, etc).

Because of this consolidation, I could take advantage of the autoloading feature in php5. So I get two benefits with this… no require(_once) statements littering the code, and no files loaded into memory that aren’t needed.

So, at this point, 90% of the CMSMS code has been converted to this API setup. index.php and include.php are readable, and memory usage is way down. In fact, I’ve segmented it in such a way that it could almost be used as an API for other php applications. As an example, the new installer is a totally separate application. It doesn’t use anything really CMSMS specific, instead it just includes the bare minimum and pulls out what methods it needs for the database, smarty, etc.

I’ve also documented the code as I’m going. There is still a lot more to go, but it’s coming along. We’ll be dumping doxygen docs nightly so that people will have quick access to the classes and methods. I’ve already been dumping docs of the svn builds out, though it’s not guaranteed to not change drasticly before 2.0 is released.

http://cmsmadesimple.org/api

Maybe someday we can break out the API and have a framework for other apps to use. That would be pretty slick.

Ted

CMSMS 2.0 Brings Sane From Processing

Wednesday, January 17th, 2007

CMSMS 2.0 has a few main goals. One of them is adding some sorely needed features. Another is code cleanup. However, one of the major ones in my opinion is making it easier to develop modules.

One of the things that has annoyed me about the whole smarty thing with modules is that you’re writing EVERYTHING to smarty and then spitting it all back out from strings in your smarty template. It’s almost not worth it, especially when you have lines of code just to set up for translated strings. Yuck!

Anyone who has made a module or has at least looked at module code can see how tedious this is… well, it’s all about to change.

Firstly, let me show you an example of a smarty setup now. This comes directly from my News rewrite. It’s in the admin section where you edit a category. I’ve removed a few lines of code, but the premise is there..

$catid = coalesce_key($params, 'catid', '');
$category = cmsms()->news_category->find_by_id($catid);
 
$smarty->assign_by_ref('category', $category);
 
#Display template
$this->smarty->assign('parents', $this->CreateParentDropdown($id, $catid, $category->parent_id));
$this->smarty->assign('hidden', 
		      $this->CreateInputHidden($id, 'catid', $catid).
		      $this->CreateInputHidden($id,'origname',$name));
$this->smarty->assign('submit', $this->CreateInputSubmit($id, 'submit', lang('submit')));
$this->smarty->assign('cancel', $this->CreateInputSubmit($id, 'cancel', lang('cancel')));
 
$smarty->assign('action', 'editcategory');
 
echo $this->process_template('editcategory.tpl', $id, $returnid);

I’ve removed the code that saves the category that fits into the middle. I also have a CreateInputSubmit and CreateInputHidden things that will go away before the final release. However, I used this to prove a point…

Here is the associated smarty template

{validation_errors for=$category}
 
{mod_form action=$action}
	<div class="pageoverflow">
		<p class="pagetext">*{mod_label name='category[name]' value='name' translate=true}:</p>
		<p class="pageinput">{mod_textbox name='category[name]' value=$category->name size='20' maxlength='255'}</p>
	</div>
	<div class="pageoverflow">
		<p class="pagetext">{mod_label name='category[parent_id]' value='parent' translate=true}:</p>
		<p class="pageinput">{mod_dropdown name='category[parent_id]' selected_value=$category->parent_id items=$parents}</p>
	</div>
	<div class="pageoverflow">
		<p class="pagetext">&nbsp;</p>
		<p class="pageinput">{$hidden}{$submit}{$cancel}</p>
	</div>
{/mod_form}

(Again, pretend the $submit and $cancel aren’t there… that’s yet to come)

As you can see, we have smarty plugins to wrap various methods in the module API. {mod_form} for CreateFormStart, {mod_label} for CreateInputLabel, etc. They retain all of the available parameters, except that the beauty is that you don’t have to worry about passing around the $id and $return_id. It’s all handled behind the scenes now.

{validation_errors} is a slick plugin that displays errors that might occur during the validation before an ORMed object’s save() method. If save fails, validation_errors will pick up the errors and display them. This makes for very consistent error messages throughout the system. It also means that you don’t have to do any validation in your screens, just in the ORM object.

Also, notice another thing I’m doing here. category[name], category[parent_id], etc. This allows me to get all of the parameters destined to be for the category object into one hash after the form is submitted. In fact, I name them exactly the same as the parameters in the object itself. Why? Well, here is the part that saves the object… assume that I have $category already set with the find_by_id we saw earlier.

if (isset($params['category']))
{
	$category->update_parameters($params['category']);
	if ($category->save())
	{
		$this->UpdateHierarchyPositions();
 
		$params = array('tab_message'=> 'categoryupdated', 'active_tab' => 'categories');
		$this->Redirect($id, 'defaultadmin', $returnid, $params);
	}
}

Basically, we have 2 lines of code to do all of the updating of the object and saving it. update_parameters takes all of my category[] fields in the form and fills the corresponding parameters in the object, and save(), well, saves. If the save fails, then we continue on as normal, knowing that {validation_errors} will display the reason(s) why the save failed.

All in all, these changes easily reduce your code by 50%, if not more. In the case of edit_category, we went from 102 lines to 42. It’s less memory intensive, uses smarty in a more correct manner, and is just plain old faster… faster to develop and faster to run.

What’s so special about ORM anyway?

Tuesday, January 16th, 2007

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!