Tuesday, October 20, 2009

Making cakephp Models and Components user friendly

So cakephp is a handy cake website development platform, but the way models and components work can make refactoring an existing site painful.

Making Models Nice
This is what a basic cakephp controller looks like
class MyController extends AppController
{
  var $uses=array('MyModel');

  do_something()
  {
    $this->MyModel->findById($id);
    ...
    $this->MyModel->save($data);
  } 
}
My problem with this is as your code grows you add more and more models to the uses array. Then when you want to duplicate this functionality in another controller you need to work out which models each function uses. This becomes even more tortuous once parts of these functions have been refactored into components. There has to be a better way, a way that puts the definition of the models used closer to the usage of the models.

So lets dig into how models can be loaded in cakephp. There are a few options...
  1. You can place it in a $uses=array('Foo') variable at the start of your controller
  2. You can do a $this->loadModel('Foo') at the point of usage
  3. you can use the ClassRegistry. which I wont talk about here...
These are all pretty ugly. IMHO the nicest solution is 2. But this leads to a lot of code like this

class MyController extends AppController
{
  function do_something()
  {
    $this->loadModel('Foo');
    $this->Foo->findById( $id );
    ...
    $this->Foo->save( $data );
  }
}
But I think this is better but still kind of ugly. The separation between the initial loadModel and the final save can lead to cases where refactoring is difficult.

So I add the followig function to my AppController class.
function m($modelName )
{
  $this->loadModel($modelName);
  return$this->$modelName;
}
Now the above code becomes
class MyController extends AppController
{
  function do_something()
  {
    $this->m('Foo')->finById( $id );
    ...
    $this->m('Foo')->save( $data );
  }
}
Which I think is much neater. Its now impossible to have the Foo model not instantiated when we need it.

Making Components Nice
So using a component is much like using a model
class MyController extends AppController
{
   var $components=array('MyComponent');
  
   function do_something()
   {
     $this->MyComponent->foo();
   }
}
However there is no loadComponent that can be used to bring the defining of which components we will use and the actual use of the component closer together. So lets write one in app_controller.php
function loadComponent( $name )
{
  if( isset($this->$name) ) return;

  $class_name = $name.'Component';
  if( !class_exists( $class_name ) && !App::import('Component', $name ) )
  {
    throw new Exception('Loading component failed');
  }
  
  $this->$name = new $class_name;
  $this->$name->initialize($this);
}
This laodComponent function is pretty hacky and will not work for all components that are supported by cakephp, nor will it configure all components correctly, but it works for all the components that I have written. You may need to adjust it if you're using funky components.

So now our example would be
class MyController extends AppController
{ 
  function do_something()
  {
     $this->loadComponent('MyComponent');
     $this->MyComponent->foo();
   }
}
Much nicer. However we can apply the same trick we used on models to make it neater again.
Add the followig function to AppController
function c($componentName )
{
  $this->loadComponent($componentName);
  return$this->$componentName;
}
And our example is
class MyController extends AppController
{ 
  function do_something()
  {
     $this->c('MyComponent')->foo();
  }
}

No comments:

Post a Comment