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:
-
function add()
-
{
-
$this->Task->set($this->data);
-
-
if ($this->Task->validates())
-
{
-
$this->Task->save()
-
// Display a success message
-
}
-
else
-
{
-
// Display an error message that validation failed
-
}
-
}
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:
-
function add()
-
{
-
$this->Task->set($this->data);
-
-
if ($this->Task->validates())
-
{
-
if ($this->Task->save())
-
{
-
// Display a success message
-
}
-
else
-
{
-
// Display an error message that there was a save error
-
}
-
}
-
else
-
{
-
// Display an error message that validation failed
-
}
-
}
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 ; ).
-
function add()
-
{
-
$result = $this->saveData();
-
-
switch ($result)
-
{
-
case 'success':
-
// Display a success message
-
break;
-
case 'validation_failed':
-
// Display an error message that validation failed
-
break;
-
case 'save_failed':
-
// Display an error message that validation failed
-
break;
-
}
-
}
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:
-
// Make sure the Common (fake) namespace is available throughout the entire project
-
loadComponent('Common');
-
-
class AppController extends Controller
-
{
-
/**
-
* AppController::saveData is a generic save function that performs validations and saving for any given $data
-
* on a given $model. It returns a string which indicates the result of the operation. Possible result values
-
* are:
-
*
-
* 'missing_model', 'save_failed', 'validation_failed', 'success'
-
*
-
* @param mixed $data If skipped, Controller::data will be used
-
* @param mixed $model If skipped, Controller::modelClass will be used
-
* @return string
-
*/
-
function saveData($data = null, $model = null, $cleanUpFields = false)
-
{
-
// If $this->action has the name of this function, it was requested by the Dispatcher
-
if ($this->action=='saveData')
-
{
-
// Make sure this does not work
-
return false;
-
}
-
-
// The $data parameter defaults to $this->data if none was provided
-
Common::defaultTo($data, $this->data);
-
-
// The $model parameter defaults to $this->modelClass if none was provided
-
Common::defaultTo($model, $this->modelClass);
-
-
// If our $cleanUpFields variable is set to true
-
if ($cleanUpFields===true)
-
{
-
// Clean up this controllers fields
-
$this->cleanUpFields($model);
-
}
-
-
// If no $model parameter was given, but Controller::modelClass is available
-
{
-
// Try to use the Model instance from our Controller
-
$Model =& $this->{$model};
-
}
-
else
-
{
-
// Try to load this Model using Common::getModel
-
$Model =& Common::getModel($model);
-
}
-
-
// If our $Model variable is no object
-
{
-
// Return the 'missing_model' result value
-
return 'missing_model';
-
}
-
-
// Set our $Model::data
-
$Model->set($data);
-
-
// See if our $Model validates
-
if ($Model->validates())
-
{
-
// If we couldn't save our $Model $data
-
if (!$Model->save($data))
-
{
-
// Return the 'save_failed' result value
-
return 'save_failed';
-
}
-
}
-
else
-
{
-
// If it didn't validate, return the 'validation_failed' result value
-
return 'validation_failed';
-
}
-
-
// If everything worked out, return the 'success' result value
-
return 'success';
-
-
}
-
}
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)
-
/**
-
* This class serves as a namespace for functions that need to be globally available within this application.
-
* All of it's functions can be called statically, i.e. Common::defaultTo(...), etc.
-
*
-
* Warning: It's name violates against the CakePHP naming conventions which demand it to be named CommonComponent.
-
* For the sake of convenience I decided against the conventions in this case (also because it's not a component in
-
* the classic / CakePHP sense of things)
-
*/
-
class Common extends Object
-
{
-
/**
-
* Tries a couple of approaches to return an instance of a given $model. If none of them succeed,
-
* 'false' is returned instead.
-
*
-
* @param string $model
-
* @return mixed
-
*/
-
function &getModel($model)
-
{
-
// Make sure our $modelClass name is camelized
-
$modelClass = Inflector::camelize($model);
-
-
// If the Model class does not exist and we cannot load it
-
{
-
// Can't pass false directly because only variables can be passed via reference
-
$tmp = false;
-
-
// Return false
-
return $tmp;
-
}
-
-
// The $modelKey is the underscored $modelClass name for the ClassRegistry
-
$modelKey = Inflector::underscore($modelClass);
-
-
// If the ClassRegistry holds a reference to our Model
-
if (ClassRegistry::isKeySet($modelKey))
-
{
-
// Then make this our $ModelObj
-
$ModelObj =& ClassRegistry::getObject($modelKey);
-
}
-
else
-
{
-
// If no reference to our Model was found in trhe ClassRegistry, create our own one
-
$ModelObj =& new $modelClass();
-
-
// And add it to the class registry for the next time
-
ClassRegistry::addObject($modelKey, $ModelObj);
-
}
-
-
// Return the reference to our Model object
-
return $ModelObj;
-
}
-
-
/**
-
* Simple, yet very convenient function to set a given $variable to it's $defaultValue if it is empty
-
*
-
* @param mixed $variable
-
* @param mixed $defaultValue
-
*/
-
function defaultTo(&$variable, $defaultValue)
-
{
-
// If our $variable is empty
-
{
-
// Overwrite it with it's $defaultValue
-
$variable = $defaultValue;
-
}
-
}
-
}
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:
-
function add()
-
{
-
$success = false;
-
-
$user_saved = $this->saveData(null, 'User');
-
-
if ($user_saved=='success')
-
{
-
$profile_saved = $this->saveData(null, 'Profile');
-
-
if ($profile_saved=='success')
-
{
-
$success == true;
-
}
-
else
-
{
-
// Roll back our first DB operation
-
$this->User->delete();
-
}
-
}
-
-
if ($success==true)
-
{
-
// Display success message
-
}
-
else
-
{
-
// Use the contents of $user_saved and $profile_saved to display the appropiate error
-
}
-
}
Therefor I'd like to end this post by pleading innocent regarding eventual accusations regarding my sanity ; ).
-- Felix Geisendörfer aka the_undefined