Dealing with dates… reporting, etc.

Once in a while I run into a situation, where some sort of reporting is required for an application…
(It happens.)

An example?.. We need to get a list of all active users in the DB.

Obviously CakePHP makes it overly easy for us.
However, it also happens, that once in a while we need to filter the list by using a “from” and “to” date range.

I really wanted to keep things simple and re-usable (yeah, lazy) and one solution came to mind…

It fits nicely within an MVC concept, and since I’ve got nothing else to write about, perhaps it might be worth sharing.

To get things started, we’ll consider a view:

<?php
  echo $form->create('User', array('action' => 'report_to_me'));
  
  echo $form->input('active_from');
  echo $form->input('active_to');
  
  echo $form->end('Set date');
?>

<?php if(!empty($allActiveUsers)) : ?>
  
  <?php foreach($allActiveUsers as $user) : ?>
    
    <div>
      <?php echo $user&#91;'User'&#93;&#91;'name'&#93;; ?>
    </div>
  
  <?php endforeach; ?>
  
<?php else : ?>

  <div>
    Ain't got no active users.
  </div>  
  
<?php endif; ?>

Pretty straight-forward… We display a user’s name for all active users from the DB.
Also, there is a way for someone to input a date range and send it off to the controller.

Let’s take a look at the action in the controller:

public function report_to_me() {
      if(!empty($this->data)) {
        $this->User->fromDate = $this->data['User']['active_from'];
        $this->User->toDate = $this->data['User']['active_to'];
      } 
    
      $this->set('allActiveUsers', $this->User->getActive());
    }

If no date is supplied (i.e. $this->data is empty) we show all active users from the DB.
Yet, if the date is supplied we set a few properties in our User model…
You might notice that I like fat models (eh?), therefore the goal is, once again, to avoid any logic in the controller.

With that being said, we’ll take a look at the User model:

<?php
    class User extends AppModel {
      
      public $fromDate = NULL;
      public $toDate = NULL;
      
      public function getActive() {
      
        return $this->find('all', array('conditions' => array('User.is_active' => 1,
                                                              $this->getDateConditions()),
                                 'recursive' => -1));
      }
    
      private function getDateConditions() {
        if(!empty($this->fromDate) && !empty($this->toDate)) {
          return array($this->alias . '.' . 'created BETWEEN ? AND ?' => 
                 array(date('Y-m-d', strtotime($this->fromDate)), 
                       date('Y-m-d', strtotime($this->toDate))));
        }
        
        return FALSE;
      }     
    }
?>

The only interesting piece of code here is the getDateConditions() method.
Once we set the $fromDate and $toDate properties, the method gives us a nicely formatted condition for the date range. Otherwise, as promised, we get a list of all active users (no date condition is applied).

A little tip about counterScope

One of many cool CakePHP’s features is the Counter Cache.

From the manual: “This function helps you cache the count of related data. Instead of counting the records manually via find(‘count’), the model itself tracks any addition/deleting towards the associated $hasMany model and increases/decreases a dedicated integer field within the parent model table.”

So if you have Forum with Posts, you can keep track of how many posts are there in a given forum by tracking this data in the “forums” table, which certainly speeds things up and takes a little load off your DB.

One feature that is not (yet) very well documented is the ability to provide a counterScope. It essentially allows you to specify a simple condition, which tells the model when to update (or when not to, depending on how you look at it) the counter value.

Using our Post model example, we can specify it like so:

class Post extends AppModel {
        var $name = 'Post';
        var $belongsTo = array('Forum'=>array(
                                              'counterCache'=>true,
                                              'counterScope'=>array('Post.deleted'=>0)));

    }

This means that counter will only be updated for posts that are active: 'deleted'=>0.

Also, it is implied in this example that you have some admin tool, which lets you do soft deletes of your posts (i.e. set the “deleted” field to 1)…

Something simple, like this:

function delete($id = null) {
   if($id) {
        $this->Post->id = $id;
        $this->Post->saveField('deleted', 1);
   }
}

Guess what? Cake is going to update your counter field in the forums table, when you deactivate a post in this manner.

Brilliant stuff, I tell ya…

Blacklist your model fields for save()…

You’ve probably heard that in order to make your save() more secure, you can pass-in a third parameter of only those fields that you’d like to save (all others will be ignored).

However, in some cases it would make more sense to “blacklist” one or two fields, rather than “whitelist” a whole bunch of required fields.

An awesome tip from Nate, shows just how to accomplish this…


$blackList = array('protected', 'fields', 'here');
$this->Model->save($this->data, true, array_diff(array_keys($this-> Model->schema()), $blackList);

Quite clever indeed.

How to build a “dashboard” for your application in CakePHP

As an addition to my recent post, I wanted to share a technique, which should allow you to pretty easily build a “dashboard” for your app.
The dashboard would basically grab some information from a few different models and display it all on one page.

First, let’s go over some assumptions…

  1. We are not going to use requestAction(), because it’s the “worst” (and absolutely the last-resort approach). It has it’s purposes, but we want to avoid it as much as possible. (I’m not going into detail as to why, because that has been covered many times in many many places, so you can just fire-up your trusty search engine to learn more).
  2. The models from which we’ll gather the information are absolutely not related to one another, if they are related… this is not the best approach. Remember, we don’t want to reload any models (objects), which CakePHP has already loaded for us.

So, keeping both of those things in mind, here’s what we’ve got:

Post model, with a method getTop();
News model, with a method getRecent();
Employee model, with a method getTopPerformers();
Product model, with a method getTopSellers();

(You are welcome to come up with your own methods or use your imagination as to what they might return).
Even though it’s hard to believe that we have no relationship between any of the above models, we’ll assume that to be true for the sake of this example…

We are going to build a Dashboard Controller, with a single action (for now), called index() it’s only purpose is to grab the info from all of the above models and “set” it for the view.

Here’s what it might look like:

<?php
    class DashboardController extends AppController {
          
          var $name = 'Dashboard';
          var $uses = array();
          
          function index () {
               $this->set('topPosts', ClassRegistry::init('Post')->getTop());
               $this->set('recentNews', ClassRegistry::init('News')->getRecent());
               $this->set('topEmployees', ClassRegistry::init('Employee')->getTopPerformers());
               $this->set('topSellingProducts', ClassRegistry::init('Product')->getTopSellers());
          }
    }
?>

And that’s really it. Now we have all of our Models’ info available for the view, and we can display it any way we’d like.

Just a few things to point out:

  • We are using ClassRegistry::init('ModelName') to load the relevant models, only once we need them. The init() method will automagically instantiate the model object.
  • We are avoiding the use of the $uses array(), although it could have been used just as well, but this way the models are loaded only when required, and if you have additional actions that might need some other set of models, there is no need to “overburden” your app by loading something extra, before it’s actually needed.
  • If you’d like to make this new dashboard into your actual homepage of the site, please see my other post on how to setup the appropriate route.

Controller makeover (from ugly to beautiful)

Let’s take a controller ridden with problems and see how we can improve it. Hopefully this little experiment can help you beautify your code and optimize your app…

This is our ugly Users controller:

class UsersController extends AppController {

var $name = 'Users';
var $uses = array('User', 'Profile');

function add() {
   if(!empty($_POST)) {
      $this->User->create();
      $this->User->set($this->data);

      if($this->User->validates()) {
            //create the process_type hash
            $this->data['User']['process_type_hash'] = md5($this->data['User']['process_type']);

           //prepare user code messages
          switch($this->data['User']['error_code']) {

               case('00'):
                   $this->data['User']['error_message'] = 'Success';
               break;

               case('01'):
                   $this->data['User']['error_message'] = 'User suspended';
               break;

               case('02'):
                   $this->data['User']['error_message'] = 'User account cancelled';
               break;

               case('03'):
                   $this->data['User']['error_message'] = 'User not verified';
               break;
         }

         $this->User->save($this->data);
         $userId = $this->User->getLastInsertId();

         $this->Profile->create();
         
         if($this->Profile->validates()) {
                 $this->data['Profile']['user_id'] = $userId;
                 $this->data['Profile']['temp_id'] = $this->generateTempId();
                 
                 $this->Profile->save($this->data);
         }
     }
  }
}

function view($id=null) {
        $this->set('users', $this->User->findAll('id='.$id));
        $this->set('profiles', $this->Profile->findAll('user_id ='.$id));
}
}

Let’s analyze the problems line by line and see what we can do it make it better…

Line 4
The $uses array is not needed (as in the vast majority of cases). Our models are related, therefore both User and Profile are already available to your controller

Line 7
When checking for POST’ed data, it’s best to rely on $this->data rather than $_POST

Line 8
No need to create() a model in this case, save() will handle that for you later on.

Line 9
This line really depends on the validates() method. Generally speaking there is no need to call this method separately, since it is called by save() for you, therefore setting the model’s data is no longer necessary.

Line 11
Based on the point above, we will not call validates() method, but will rely on save() instead.
Well, what about the data we need to prepare before saving?…

Lines 12 – 33
This work should never be done in the controller, and definitely not in this manner. Considering our points above, whenever we have any data, which has to be processed/prepared prior to save(), we should build a beforeSave() method in a given model to accomplish that.

Line 35
Calling save() without a false param, (as in save($data, false)) will trigger validation again. Either we get rid of the validates() method and rely on save() to handle validation for us, or we accept that data is already validated and we use ‘false’. Regardless of the approach, certainly there is no need to validate the same data twice.

Line 36
There is no need to call getLastInsertId(); After saving model’s id is automatically set to the last inserted record’s id. So in our case $this->User->id is already set.

Lines 38 – 45
Related models can be easily saved in one shot with saveAll(), there is simply no need of all this extra code.

Line 42
Looks like we’ve made some utility function in App Controller to generate our temp id. There are a few problems with this approach… First of all, the function is not protected, so we could access it by a URL, which is probably not desired. An easy way to avoid this would be to rename the function to function _generateTempId(), then you’d call it like $this->_generateTempId();. Still, this type of processing is better done in the model’s beforeSave() method.

Lines 51-52
We’ve got a few problems with our view() action. First of all our models are related, so we can easily retrieve the information for both by using $this->User->find(‘all’); Secondly, findAll() is deprecated so it’s best to use find(‘all’) instead. Third, the conditions, while working, are written poorly. A better syntax would be: find(‘all’, array(‘conditions’=>array(‘User.id’=>$id)));
And last, but not least, we are not checking for the value of $id prior to find(). If $id is indeed ‘null’, we should not call find() at all (a simple if() statement should handle it for us).

So what would a fixed-up controller look like?


class UsersController extends AppController {

    var $name = 'Users';

    function add() {
        if(!empty($this->data)) {
           $this->User->saveAll($this->data, array('validate'=>'first'));	
        }
    }

    function view($id=null) {
        if(!empty($id)) {
           $this->set('users', $this->User->find('all', array('conditions'=>array('User.id'=>$id))));
        }
    }

}

That’s all folks. A lot cleaner and prettier.

Well, what happened with all the code?

saveAll() will handle validation and saving of our User and Profile models, so all that code above can be easily condensed into a single line of code.

The data preparation i.e. the hash and error message creation will be moved into the beforeSave() of the User model.
And the preparation of temporary id will be moved to beforeSave() of the Profile model.

One note here, is that placing such a switch statement directly into your beforeSave() method is ugly. Instead, make a little utility function that you could use to process your data, for example:
$this->data = $this->_processErrorMsgs($this->data); … and move your switch code block into that utility function, it will make your code more flexible in the long run.

The main point here is that if you feel/see that your controllers are getting a little fat and your actions seems to have all sorts of ‘if’ statements, consider taking a step back and evaluating whether some of the functionality should be offloaded to models, utility methods or plain old simplified by using cake’s built-in tools.

Practical use of saveAll() (part 1, working with multiple models)

(Part 2 is here)

I would like to provide some insight on how to use the saveAll() method, with a few real-world examples. First, I’m going to cover how to use saveAll() when working with two related models.

Imagine we are building a CRM of some sort. We have a Company model, which holds some generic Company information. Company hasMany Account, which would hold the information about various users who can access the CRM.

Let’s go ahead and create our tables:

CREATE TABLE `companies` (
  `id` int(11) NOT NULL auto_increment PRIMARY KEY,
  `name` varchar(200) NOT NULL,
  `description` varchar(200) NOT NULL,
  `location` varchar(200) NOT NULL,
  `created` datetime NOT NULL
)
 CREATE TABLE `accounts` (
`id` INT NOT NULL auto_increment PRIMARY KEY,
`company_id` int(11) NOT NULL,
`name` VARCHAR( 200 ) NOT NULL,
`username` VARCHAR( 200 ) NOT NULL,
`email` VARCHAR( 200 ) NOT NULL,
`created` DATETIME NOT NULL
)

Then we can setup the Company and Account models as follows:

class Company extends AppModel {
	var $name = 'Company';	

	var $hasMany= array('Account');

	var $validate = array(
            'name' => array('rule' => array('notEmpty')),
            'description' => array('rule' => array('notEmpty'))
    );
}
class Account extends AppModel {
	var $name = 'Account';	

	var $belongsTo = array('Company');

	var $validate = array(
            'name' => array('rule' => 'notEmpty'),
            'username' => array('rule' => 'notEmpty'),
	    'email' => array('rule' => 'email')

    );
}

As you can see, the models are pretty simplistic, for example the Account model is missing an obvious password field, but I’m leaving it up to you to extend the models as you wish. At least we’ve added some validation and all in all that should suffice for the purposes of this example.
Please note, that I’m using a new (at the time of writing) validation rule ‘notEmpty’ if you do not have the latest version of cake (nightly build of 07/31/2008 or later), this rule may not be available in your core.

Next, let’s create a Companies controller, we’ll leave it empty for now:

class CompaniesController extends AppController {
 var $name = 'Companies';
}

Now that we have our models and a controller, our goal is to build a form where some CRM user would setup a company and a first default account at the same time. This is where saveAll() comes in very handy, because it allows us to save both models without any effort.

So, let’s build a form, which would allow us to create a Company and an Account (create a file called /companies/add.ctp):


echo $form->create();
echo $form->input('Company.name', array('label'=>'Company name'));
echo $form->input('Company.description');
echo $form->input('Company.location');

echo $form->input('Account.0.name', array('label'=>'Account name'));
echo $form->input('Account.0.username');
echo $form->input('Account.0.email');

echo $form->end('Add');

Let’s take a look at what’s going on here. We consider Company to be our main model, therefore the form by default will post to Companies controller’s add action (i.e. /companies/add/).
Take a look at the way I named the form fields for the Account model. If Company is our main model saveAll() will expect the related model’s (Account) data to arrive in a specific format. And having Account.0.fieldName is exactly what we need (this is only true for hasMany relationship, for hasOne the fields would follow Account.fieldName format… I hope it makes sense as to why).
Having the label for the two fields allows us to be more descriptive, otherwise CakePHP would label both fields as just “Name” by default, which would be confusing to the user.

Now, in our Companies controller we can create an add() action:

function add() {
   if(!empty($this->data)) {
      $this->Company->saveAll($this->data, array('validate'=>'first'));
   }
}

Is that easy or what?

A quick thing to point out here, is the use of array(‘validate’=>’first’), this option will ensure that both of our models are validated. You can refer to the API for other options that saveAll() accepts, but this is good enough for the current example (and most similar cases).

Now let’s take our tiny app for a test drive and try to submit the form with empty data. If all goes well, the validation should take place for both models and you should see our relevant fields being invalidated.

Go ahead, and try to save some data. Looking at your SQL debug, you’ll see that cake saved both models and established the correct relationship by saving company_id field with the correct id into our accounts table. Ah, the wonders of automagic…

Well, we are not done just yet. Let’s now build a form and a related action for editing our Company and Account.

For the purposes of our example let’s do something like this for the edit action:

function edit() {
   if(!empty($this->data)) {
      $this->Company->saveAll($this->data, array('validate'=>'first'));
   }
   else {
      $this->Session->write('AccountId', 2);
      $this->data = $this->Company->Account->find('first', array(
						  'conditions'=>array(
						  	'Account.id'=> $this->Session->read('AccountId');
									)));
   }
}

We’ll imagine that at this point the user (or technically Account) is logged into our application and we’ve stored the Account.id into the session by using: $this->Session->write(‘AccountId’, 2);

In your case this id might be different, so please ensure that it’s a valid one by looking at your DB.

Of course in reality you would rely on CakePHP’s auth or some other log-in method, but that would be beyond the scope of this article.

Lastly let’s build our edit form:


echo $form->create('Company', array('controller'=>'companies',
								    'action'=>'edit'));

echo $form->input('Company.name');
echo $form->input('Company.description');
echo $form->input('Company.location');

echo $form->input('Account.'.$session->read('AccountId').'.name', array('label'=>'Account name'));
echo $form->input('Account.'.$session->read('AccountId').'.username');
echo $form->input('Account.'.$session->read('AccountId').'.email');

echo $form->input ('Company.id');
echo $form->input('Account.'.$session->read('AccountId').'.id');

echo $form->end('Edit');

————————-

Update: In CakePHP 1.2 RC3+ you can name your edit form fields with any key (not necessarily the model ID).

Although using model ID for the key name still works, it is no longer necessary to correctly display the validation errors. The code above and text below refers to the older way of doing things, but it should still give you an idea of how to approach the edit form.
————————-

There are a couple of things to point out here, first you’ll notice that in this form I’ve named the Account model fields by using $session->read(‘AccountId’) as the array key. This is needed in order to properly display the validation errors.
If you are unsure about what I mean pr($this->validationErrors) in your view and pay attention to the value of the key, which holds the errors for the Account model. You’ll notice that it will be matching the id of the model we are currently editing.

If you think back to our add() action, the Account model had no data (and therefore no id) and we were just creating one and only account, therefore Account.0.fieldName would work just fine.

In the case of edit() we already know the id of the Account model, therefore the field names must match the actual id from the database. Of course, we know that in this example it was 2, but we do have to make our form dynamic, so by using $session->read(‘AccountId’) we ensure that our fields will always be named correctly regardless of whether the actual value of the id is 2 or 76.

Next thing to notice is that we’ve added two inputs, which hold the id’s of our models. This is needed to ensure that an UPDATE query is triggered, rather than an INSERT. And yes, cake is smart enough to make those inputs as hidden fields.

Well, if you’ve managed to follow all along, congratulations we are done. I hope that now you can see how saveAll() can make your life so much easier with so little effort.

Notes on CakePHP HABTM (Part 1, the basics)

Part 1. The basics

HABTM seems to give a lot of people trouble, so I wanted to cover a few points that may or may not be in the manual. And I will assume here that you have basic understanding or some knowledge of HABTM, otherwise you should really go and read the manual first… (By the way, do read the manual if you are having trouble. It’s constantly evolving and some points you’ve missed before may very well be covered).

OK, so let’s begin with a good ol’ Post HABTM Tag.

Here’s the simplest definition of the Post model:

class Post extends AppModel {
     var $name = 'Post';	
     var $hasAndBelongsToMany = array('Tag');	
}

(In PHP5 you won’t even need $name, but let’s leave it in for a few years).
To make HABTM work you have to have a very similar definition of the Tag model:

class Tag extends AppModel {	
     var $name = 'Tag';		
     var $hasAndBelongsToMany = array('Post');
}

And the magic has happened already…

However, unless you know a thing or two about magic, it can be hard to figure out what’s going on because CakePHP makes a lot of assumptions if you do not manually override the defaults.

I’m going to cover here with and joinTable as I find these to be the most often overlooked/misunderstood HABTM basics.

with
The with key specifies an auto-model, which cake creates to represent the join table (joinTable in CakePHP syntax). If we follow the defaults the joinTable model is named PostsTag and you can use it like any other regular CakePHP model. Yes, it is automagically created for you.

joinTable
Still following conventions, CakePHP will assume that the joinTable is named posts_tags. Table name consists of plural model names involved in HABTM and is always in alphabetical order. So, P goes before T, therefore posts_tags.

Well, let’s say you don’t like the name posts_tags and instead would like to rename your joinTable to my_cool_join_table…

Off you go and modify your Post model like so:

class Post extends AppModel {
     var $name = 'Post';	
     var $hasAndBelongsToMany = array('Tag'=>array(
                                               'joinTable'=>
                                               'my_cool_join_table'));
}

Guess what? You’ve just messed with the magic.

First of all CakePHP will now rename your auto-model to MyCoolJoinTable, so if you had some references in the code to PostsTag (the default with model) you have to go and change them.
Secondly, and maybe more importantly, you’ve probably forgot about your Tag model. If you haven’t made any changes, Tag will still expect all the defaults. This can create a lot of mess and bizarre, unexpected results. So the point here is that since HABTM goes in both directions any changes you apply to one model definition should most likely be manually applied to another.

Now, what if you don’t like PostsTag name and would like to rename your join (with) model to PostTagJoin?

class Post extends AppModel {
      var $name = 'Post';	
      var $hasAndBelongsToMany = array('Tag'=>array('with'=>'PostTagJoin'));
}


Changing the with key will not affect the joinTable value (i.e. CakePHP will not assume posts_tags_joins or something) so if you don’t change the default, CakePHP will still expect posts_tags.
In other words, with only changes the name of the auto-model, so in reality it’s not something one would or should bother to do.

Now onto some good stuff…

Having this auto-model is quite handy if you are interested in the joinTable data and becomes even more powerful when you have some additional fields in the joinTable, which you need queried or saved.

In the Posts controller you could do:

$this->Post->PostsTag->find(‘all’);

Or you could even apply some conditions to find the most popular tag ID’s:

$this->Post->PostsTag->find('all', array('fields'=>array('tag_id','COUNT(tag_id) AS total'), 
                                                      'group'=>'tag_id')));

One caveat to note here is that PostsTag is not automagically associated with neither Post nor Tag. So in order to fetch some associated data, you’ll have to manually do something like:

$this->Post->PostsTag->bindModel(array(‘belongsTo’=>array(‘Tag’)));

Now, using the above example, you can even get the most popular Tag names by using Containable in the above query or simply increasing the recursive of PostsTag.

Part 2, saving HABTM data