Making error handling for Model::save more beautiful in CakePHP

Posted by Felix Geisendörfer, on Feb 03, 2007 - in PHP & CakePHP » DataSources, Models & Behaviors

Deprecated post

The authors of this post have marked it as deprecated. This means the information displayed is most likely outdated, inaccurate, boring or a combination of all three.

Policy: We never delete deprecated posts, but they are not listed in our categories or show up in the search anymore.

Comments: You can continue to leave comments on this post, but please consult Google or our search first if you want to get an answer ; ).

Hi folks,

when looking through other peoples code, I often see actions like this:

php
  1. function add()
  2. {
  3.     $this->Task->set($this->data);
  4.    
  5.     if ($this->Task->validates())
  6.     {
  7.         $this->Task->save()
  8.         // Display a success message
  9.     }
  10.     else
  11.     {
  12.         // Display an error message that validation failed
  13.     }
  14. }

Looks good, wouldn't you say? Well it is good code for most cases. However, it doesn't handle an important aspect of saving something to the database: checking if the actual DB operation succeeded. Now I've written actions like the one above in the past as well. It's just that I've not had many MySql errors since I've switched to CakePHP. The Model class usually handles all the DB operations flawlessly and it's probably been over a year that I've written a custom MySql statement in my code somewhere. However, even CakePHP or, what's more likely, the database can fail or deny operations.

So in order to not get unpleasant / surprising results, one should always use a structure like this:

php
  1. function add()
  2. {
  3.     $this->Task->set($this->data);
  4.    
  5.     if ($this->Task->validates())
  6.     {
  7.         if ($this->Task->save())
  8.         {
  9.             // Display a success message
  10.         }
  11.         else
  12.         {
  13.         // Display an error message that there was a save error
  14.         }
  15.     }
  16.     else
  17.     {
  18.         // Display an error message that validation failed
  19.     }
  20. }

I don't know about you, but to me there is beauty missing. The code above is readable and understandable, no question about it. However, I think it can be replaced with something better. Now, if you look at the following code and your reaction goes along the line "same difference, who cares, etc." then I can understand that. In this case you might still find an interesting sequence of code so you didn't waste your time reading this. But I hope some of you can confirm my inner feeling regarding the beauty of the code.

Alright, here comes a code that does exactly what the code above does, just with what I'd call the beauty-factor ; ).

php
  1. function add()
  2. {
  3.     $result = $this->saveData();
  4.    
  5.     switch ($result)
  6.     {
  7.         case 'success':
  8.             // Display a success message
  9.             break;
  10.         case 'validation_failed':
  11.             // Display an error message that validation failed
  12.             break;
  13.         case 'save_failed':
  14.             // Display an error message that validation failed
  15.             break;
  16.     }
  17. }

So before I'll ask if anybody can confirm that I've not turned crazy, here comes the code that is required to make the one above work. First of all, you need an AppController function like the one below:

php
  1. // Make sure the Common (fake) namespace is available throughout the entire project
  2. loadComponent('Common');
  3.  
  4. class AppController extends Controller
  5. {
  6.     /**
  7.      * AppController::saveData is a generic save function that performs validations and saving for any given $data
  8.      * on a given $model. It returns a string which indicates the result of the operation. Possible result values
  9.      * are:
  10.      *
  11.      * 'missing_model', 'save_failed', 'validation_failed', 'success'
  12.      *
  13.      * @param mixed $data If skipped, Controller::data will be used
  14.      * @param mixed $model If skipped, Controller::modelClass will be used
  15.      * @return string
  16.      */
  17.     function saveData($data = null, $model = null, $cleanUpFields = false)
  18.     {
  19.         // If $this->action has the name of this function, it was requested by the Dispatcher
  20.         if ($this->action=='saveData')
  21.         {
  22.             // Make sure this does not work
  23.             return false;
  24.         }
  25.        
  26.         // The $data parameter defaults to $this->data if none was provided
  27.         Common::defaultTo($data, $this->data);
  28.        
  29.         // The $model parameter defaults to $this->modelClass if none was provided
  30.         Common::defaultTo($model, $this->modelClass);
  31.            
  32.         // If our $cleanUpFields variable is set to true
  33.         if ($cleanUpFields===true)
  34.         {
  35.             // Clean up this controllers fields
  36.             $this->cleanUpFields($model);
  37.         }
  38.        
  39.         // If no $model parameter was given, but Controller::modelClass is available
  40.         if (isset($this->{$model}))
  41.         {
  42.             // Try to use the Model instance from our Controller
  43.             $Model =& $this->{$model};
  44.         }
  45.         else
  46.         {
  47.             // Try to load this Model using Common::getModel
  48.             $Model =& Common::getModel($model);
  49.         }
  50.        
  51.         // If our $Model variable is no object
  52.         if (!is_object($Model))
  53.         {
  54.             // Return the 'missing_model' result value
  55.             return 'missing_model';
  56.         }
  57.        
  58.         // Set our $Model::data
  59.         $Model->set($data);
  60.        
  61.         // See if our $Model validates
  62.         if ($Model->validates())
  63.         {
  64.             // If we couldn't save our $Model $data
  65.             if (!$Model->save($data))
  66.             {
  67.                 // Return the 'save_failed' result value
  68.                 return 'save_failed';
  69.             }
  70.         }
  71.         else
  72.         {
  73.             // If it didn't validate, return the 'validation_failed' result value
  74.             return 'validation_failed';
  75.         }
  76.        
  77.         // If everything worked out, return the 'success' result value
  78.         return 'success';
  79.    
  80.     }
  81. }

Ok, you might have noticed the usage of a class called Common in here. I was thinking to write about this in another post, but it fits in really well with this one, so here is what I use it for. One thing I often feel I don't know the right place for in CakePHP are generic functions that modify dates, restructure arrays or do similar things. In the past they usually ended up as "private" functions (the ones starting with '__' in CakePHP) in some controller. What I didn't like about this however, was that I couldn't share those across the application easily and that they just didn't seem to belong there in first place. So my recent solution to this problem was to create a class called 'Common' and place it in /app/components. Now technically it's not a real CakePHP component as it mainly serves as a name space and it's also not named 'CommonComponent'. However, I still think it's a good place to put this class, at least I couldn't think of a better one.

Alright, before I start to bore you, here comes the code for it. (You might remember my old friend the getModel function)

php
  1. /**
  2.  * This class serves as a namespace for functions that need to be globally available within this application.
  3.  * All of it's functions can be called statically, i.e. Common::defaultTo(...), etc.
  4.  *
  5.  * Warning: It's name violates against the CakePHP naming conventions which demand it to be named CommonComponent.
  6.  * For the sake of convenience I decided against the conventions in this case (also because it's not a component in
  7.  * the classic / CakePHP sense of things)
  8.  */
  9. class Common extends Object
  10. {
  11.     /**
  12.      * Tries a couple of approaches to return an instance of a given $model. If none of them succeed,
  13.      * 'false' is returned instead.
  14.      *
  15.      * @param string $model
  16.      * @return mixed
  17.      */
  18.     function &getModel($model)
  19.     {
  20.         // Make sure our $modelClass name is camelized
  21.         $modelClass = Inflector::camelize($model);
  22.    
  23.         // If the Model class does not exist and we cannot load it
  24.         if (!class_exists($modelClass) && !loadModel($modelClass))
  25.         {
  26.             // Can't pass false directly because only variables can be passed via reference
  27.             $tmp = false;
  28.            
  29.             // Return false
  30.             return $tmp;
  31.         }
  32.        
  33.         // The $modelKey is the underscored $modelClass name for the ClassRegistry
  34.         $modelKey = Inflector::underscore($modelClass);
  35.        
  36.         // If the ClassRegistry holds a reference to our Model
  37.         if (ClassRegistry::isKeySet($modelKey))
  38.         {
  39.             // Then make this our $ModelObj
  40.             $ModelObj =& ClassRegistry::getObject($modelKey);
  41.         }
  42.         else
  43.         {
  44.             // If no reference to our Model was found in trhe ClassRegistry, create our own one
  45.             $ModelObj =& new $modelClass();
  46.            
  47.             // And add it to the class registry for the next time
  48.             ClassRegistry::addObject($modelKey, $ModelObj);        
  49.         }
  50.    
  51.         // Return the reference to our Model object
  52.         return $ModelObj;
  53.     }
  54.    
  55.     /**
  56.      * Simple, yet very convenient function to set a given $variable to it's $defaultValue if it is empty
  57.      *
  58.      * @param mixed $variable
  59.      * @param mixed $defaultValue
  60.      */
  61.     function defaultTo(&$variable, $defaultValue)
  62.     {
  63.         // If our $variable is empty
  64.         if (empty($variable))
  65.         {
  66.             // Overwrite it with it's $defaultValue
  67.             $variable = $defaultValue;
  68.         }
  69.     }
  70. }

Alright, now you got all the code to get my initial sample from above running and the opportunity to question the state of my sanity ; ). For those of you who'd like to send me to a mental institute I got some rational arguments left that might be able to save me:

AppController::saveData ...

  • ... makes it possible to save data for any given Model (even those not included in the current Controller) easily.
  • ... always makes sure Model::save was successful
  • ... makes the code even more readable (imho)

And last but not least it also provides you with an easier way to only save ModelB if saving ModelA (really) succeeded:

php
  1. function add()
  2. {
  3.     $success = false;
  4.    
  5.     $user_saved = $this->saveData(null, 'User');
  6.    
  7.     if ($user_saved=='success')
  8.     {
  9.         $profile_saved = $this->saveData(null, 'Profile');
  10.        
  11.         if ($profile_saved=='success')
  12.         {
  13.             $success == true;
  14.         }
  15.         else
  16.         {
  17.             // Roll back our first DB operation
  18.             $this->User->delete();
  19.         }
  20.     }
  21.    
  22.     if ($success==true)
  23.     {
  24.         // Display success message
  25.     }
  26.     else
  27.     {
  28.         // Use the contents of $user_saved and $profile_saved to display the appropiate error
  29.     }
  30. }

Therefor I'd like to end this post by pleading innocent regarding eventual accusations regarding my sanity ; ).

-- Felix Geisendörfer aka the_undefined