It’s really easy to find inspiration when you developing on CakePHP. For the current project I need to create global (for all models) behavior which stores creator and modifier as well as deleter and deleted (for soft-delete).
What are the requirements
1. For every table (or at least the most of them) there are fields 6 fields:
- creator (int) – user who create the the record
- created (datetime) – date when the record was created
- modifier (int) – user who modified the the record
- modified (datetime) – date when the record was modified
- deleter (int) – user who delete the the record
- deleted (datetime) – date when the record was modified
2. Some tables doesn’t have such fields (like aros, acos, settings etc), so it should update tables which has such fields.
I’ve seen Soft Deletable Behavior as well as recently added WhoDidIt behavior, but I don’t like the approach of using _SESSION. I’ve also decided to use datetime field to determine if the record is active (field deleted is NULL) or deleted (if the field is set with date).
So the result was – I’ve write my own behavior who serves this.
The code of behavior
Here it is:
/**
* Take care of fields in the tables:
* creator,
* created (overwrite default one),
* modifier,
* modified (overwrite default one),
* deleter,
* deleted
*
* Created by Nik Chankov
* http://nik.chankov.net
*/
class ActivityLoggerBehavior extends ModelBehavior {
/**
* fields used
*/
var $__fields = array(
'f_creator'=>'creator', //User ID of creator of the record
'f_created'=>'created', //Created Date
'f_modifier'=>'modifier', //User ID of the last modifier of the record
'f_modified'=>'modified', //Modified date
'f_deleter'=>'deleter', //User ID of deleter of the record
'f_deleted'=>'deleted' //Deleted date
);
/**
* Database datetime field format
*/
var $__dateformat = 'Y-m-d H:i:s';
/** User ID
* 0 mean not logged user
*/
var $user = 0;
/**
* Initiate behaviour for the model using settings.
*
* @param object $Model Model using the behaviour
* @param array $settings - fields and dateformat.
* @access public
*/
public function setup(&$Model, $settings = array()){
if (isset($settings['fields'])){
$this->__fields = $settings['fields'];
}
if (isset($settings['dateformat'])){
$this->__dateformat = $settings['dateformat'];
}
}
/**
* Function which remove nodes from $this->__fields if there is no such column in the database
* @param $Model reference of the model class
* return void
*/
private function checkForFields(&$Model){
$schema = array_keys($Model->schema());
foreach($this->__fields as $key=>$value){
if(!in_array($value, $schema)){
unset($this->__fields[$key]);
}
}
}
function beforeSave(&$Model){
//Get user variable for current model. It's set in app_controller or current controllers beforeFilter().
if (isset($Model->user)){
$this->user = $Model->user;
}
$this->checkForFields($Model);
//If it's a new record update created and creator fields
if($Model->id > 0){ //update record
if(isset($this->__fields['f_modifier'])){
$Model->data[$Model->alias][$this->__fields['f_modifier']] = $this->user;
}
if(isset($this->__fields['f_modified'])){
$Model->data[$Model->alias][$this->__fields['f_modified']] = date($this->__dateformat);
}
} else { //create record
if(isset($this->__fields['f_creator'])){
$Model->data[$Model->alias][$this->__fields['f_creator']] = $this->user;
}
if(isset($this->__fields['f_created'])){
$Model->data[$Model->alias][$this->__fields['f_created']] = date($this->__dateformat);
}
}
return true;
}
function beforeDelete(&$Model, $cascade = true){
//Check if there is deleted field
if(!isset($this->__fields['f_deleted'])){
return true;
}
if (isset($Model->user)){
$this->user = $Model->user;
}
$this->checkForFields($Model);
if(isset($this->__fields['f_deleter'])){
$data[$Model->alias][$this->__fields['f_deleter']] = $this->user;
}
if(isset($this->__fields['f_deleted'])){
$data[$Model->alias][$this->__fields['f_deleted']] = date($this->__dateformat);
}
//Save the soft delete behaviour
$deleted = $Model->save($data, false, array_keys($data[$Model->alias]));
//Recurse on child records
if ($deleted && $cascade){
$Model->_deleteDependent($id, $cascade);
$Model->_deleteLinks($id);
}
return false;
}
function beforeFind(&$Model, $queryData){
$this->checkForFields($Model);
if(isset($this->__fields['f_deleted']) && (isset($Model->softly) && $Model->softly == true)){
$Db =& ConnectionManager::getDataSource($Model->useDbConfig);
$include = false;
if (!empty($queryData['conditions']) && is_string($queryData['conditions'])){
$include = true;
$fields = array(
$Db->name($Model->alias) . '.' . $Db->name($this->__fields['f_deleted']),
$Db->name($this->__fields['f_deleted']),
$Model->alias . '.' . $this->__fields['f_deleted'],
$this->__fields['f_deleted']
);
foreach($fields as $field){
if (preg_match('/^' . preg_quote($field) . '[\s=!]+/i', $queryData['conditions']) || preg_match('/\\x20+' . preg_quote($field) . '[\s=!]+/i', $queryData['conditions'])){
$include = false;
break;
}
}
} else if (empty($queryData['conditions']) || (!in_array($this->__fields['f_deleted'], array_keys($queryData['conditions'])) && !in_array($Model->alias . '.' . $this->__fields['f_deleted'], array_keys($queryData['conditions'])))){
$include = true;
}
if ($include){
if (empty($queryData['conditions'])){
$queryData['conditions'] = array();
}
if (is_string($queryData['conditions'])){
$queryData['conditions'] = $Db->name($Model->alias) . '.' . $Db->name($this->__fields['f_deleted']) . ' is NULL AND ' . $queryData['conditions'];
} else {
$queryData['conditions'][$Model->alias . '.' . $this->__fields['f_deleted']] = NULL;
}
}
}
return $queryData;
}
}
?>
I’ve decided for consistency to overwrite created and modified as well, but probably it’s not critical if they are not present here.
How to use it
1. Save the behavior’s code to app/models/behaviors/activity_logger.php
2. In AppModel add variable $actsAs or if you have it just add the behavior in it:
3. In your AppController add a function setModelVars()
foreach($this->modelNames as $model){
if($this->Auth->user('id') > 0){
$this->{$model}->user = $this->Auth->user('id');
}
$this->{$model}->softly = true; //Set all models as soft deleted so hide records with 'deleted is not NULL'
}
}
This function passes a user id to each model used in the controller as well as defaults softly to true meaning – hide deleted records from visitors when find() is used.
4. After this add this function in beforeFilter():
function beforeFilter(){
....
$this->setModelAuth();
....
}
}
This is done because this way I’ve pass the $user to all Models which after this is used in the behavior.
5. When you use find() function records with field deleted is not null will be hidden.
Bear in mind that beforeFind() in the behavior is a modified version of Soft Deletable behavior.
The conclusion
Although it’s not so easy to be used as WhoDidIt, it’s an example how to pass variables from Controller to Behavior keeping MVC pattern strict.
This behavior is experimental, because I am still developing the application with it, but in test environment it’s working as expected.
I am opened for ideas or comments.
Hope this will help someone.
Update: This method doesn’t work properly with child relations, so I decided to use $_SESSION variable in the Bahavior. 🙁
Good job. Thank you Nik, I will try it. Have subscribe your site.
Although the codes are kind of complicated but I will try it. Thanks for the suggestions
You can control how soon the model is validated… For instance, you don’t have to load a view before you validate the model… Meaning that validation still happens as soon as possible (triggered from the controller as soon as the data enters) but is defined in the model. I like that…
I found this a little hard to get the hang of but you are right it really works well once you master it.