WhoDidIt behavior: automagic created_by and modified_by fields

By Daniel Vecchiato (danfreak)
WhoDidIt behavior is useful for tracking who has created and modified records: automagically!
Handles created_by, modified_by fields for a given Model, if they exist in the Model DB table.
It's similar to the created, modified automagic, but it stores the logged User id
in the models that actsAs = array('WhoDidIt')
This is useful to track who created records, and the last user that has changed them.

The DB table must have a created_by or a modified_by field (the names of these fields can be overriden during behavihor inizialisation).

more info: 4webby.com
Git repository: http://github.com/danfreak/4cakephp/
Thanks to the guys of the #IRC channel (poLK, AD7six, etc) for their help!

Download code
<?php
/**
 * WhoDidIt Model Behavior for CakePHP
 *
 * Handles created_by, modified_by fields for a given Model, if they exist in the Model DB table.
 * It's similar to the created, modified automagic, but it stores the logged User id
 * in the models that actsAs = array('WhoDidIt')
 * 
 * This is useful to track who created records, and the last user that has changed them
 *
 * @package behaviors
 * @author Daniel Vecchiato
 * @version 1.2
 * @date 01/03/2009
 * @copyright http://www.4webby.com
 * @licence MIT
 * @repository  https://github.com/danfreak/4cakephp/tree
 **/
class WhoDidItBehavior extends ModelBehavior {
/**
   * Default settings for a model that has this behavior attached.
   *
   * @var array
   * @access protected
   */
  
protected $_defaults = array(
    
'auth_session' => 'Auth',  //name of Auth session key
    
'user_model' => 'User',    //name of User model
    
'created_by_field' => 'created_by',    //the name of the "created_by" field in DB (default 'created_by')
    
'modified_by_field' => 'modified_by',  //the name of the "modified_by" field in DB (default 'modified_by')
    
'auto_bind' => true     //automatically bind the model to the User model (default true)
  
);
/**
 * Initiate WhoMadeIt Behavior
 *
 * @param object $model
 * @param array $config  behavior settings you would like to override
 * @return void
 * @access public
 */
    
function setup(&$model$config = array()) {
        
//assigne default settings
        
$this->settings[$model->alias] = $this->_defaults;
        
        
//merge custom config with default settings
        
$this->settings[$model->alias] = array_merge($this->settings[$model->alias], (array)$config);
        
        
$hasFieldCreatedBy $model->hasField($this->settings[$model->alias]['created_by_field']);
        
$hasFieldModifiedBy $model->hasField($this->settings[$model->alias]['modified_by_field']);
        
        
$this->settings[$model->alias]['has_created_by'] = $hasFieldCreatedBy;
        
$this->settings[$model->alias]['has_modified_by'] = $hasFieldModifiedBy;
        
        
//handles model binding to the User model
        //according to the auto_bind settings (default true)
        
if($this->settings[$model->alias]['auto_bind'])
        {
            if (
$hasFieldCreatedBy) {
                
$commonBelongsTo = array(
                    
'CreatedBy' => array('className' => $this->settings[$model->alias]['user_model'],
                                        
'foreignKey' => $this->settings[$model->alias]['created_by_field'])
                                        );
                
$model->bindModel(array('belongsTo' => $commonBelongsTo), false);
            }

            if (
$hasFieldModifiedBy) {
                
$commonBelongsTo = array(
                    
'ModifiedBy' => array('className' => $this->settings[$model->alias]['user_model'],
                                        
'foreignKey' => $this->settings[$model->alias]['modified_by_field']));
                
$model->bindModel(array('belongsTo' => $commonBelongsTo), false);
            }
        }
    }
/**
 * Before save callback
 *
 * @param object $model Model using this behavior
 * @return boolean True if the operation should continue, false if it should abort
 * @access public
 */
    
function beforeSave(&$model) {
        if (
$this->settings[$model->alias]['has_created_by'] || $this->settings[$model->alias]['has_modified_by']) {
            
$AuthSession $this->settings[$model->alias]['auth_session'];
            
$UserSession $this->settings[$model->alias]['user_model'];
            
$userId Set::extract($_SESSION$AuthSession.'.'.$UserSession.'.'.'id');
            if (
$userId) {
                
$data = array($this->settings[$model->alias]['modified_by_field'] => $userId);
                if (!
$model->exists()) {
                    
$data[$this->settings[$model->alias]['created_by_field']] = $userId;
                }
                
$model->set($data);
            }
        }
        return 
true;
    }
}
?>

 

Comments 954

CakePHP Team Comments Author Comments
 

Comment

1 more info?

I'll not comment on the fact that I don't like this direct $_SESSION request, since we discussed that in IRC, but I have some other comments.

First, I did not find any more info at 4webby.com

Secondly, it seems to me that binding associations to the model goes beyond the scope of this behavior, also I do not see any configuration for it.
Posted Mar 1, 2009 by Alexander Morland
 

Comment

2 Replay to Alexander

Hey Alexander, thanks for your comments!

First, I did not find any more info at 4webby.com

Sorry I was in late with publishing, check this out Secondly, it seems to me that binding associations to the model goes beyond the scope of this behavior
Well you might be right, even if I find it handy to retrieve the username, for example of you has saved/modified a record in an admin_index view for example. But I added a further config variable


class Post extends AppModel { 
     var $name = 'Post';
     var $actsAs = array('WhoDidIt'=>array('auto_bind'=>false));
 
 }

So that it is now possible to disable autobinding

also I do not see any configuration for it

Here they are:
  1. 'auth_session' the name of the Auth session default Auth
  2. 'user_model' the name of the User model (default User)
  3. 'created_by_field' the name of the "created_by" field in DB (default 'created_by')
  4. 'modified_by_field' the name of the "modified_by" field in DB (default 'modified_by')
  5. 'auto_bind' automatically bind the model to the User model (see example below)

And you can use them as follows:


class Post extends AppModel { 
     var $name = 'Post'; 
     var $actsAs = array('WhoDidIt'=>array('user_model'=>'MyUser',
                                           'created_by_field'=>'built_by',
                                            'modified_by_field'=>'affected_by'));
 
 }
 



Posted Mar 1, 2009 by Daniel Vecchiato
 

Comment

3 Useful

Daniel: thank you for sharing your work. I find this
code really useful.
I will give it a try in one of my projects.
Posted Mar 1, 2009 by Tomás Laureano Peralta Tormey
 

Comment

4 ;o)

Your welcome Tomas ;o)
Posted Mar 1, 2009 by Daniel Vecchiato
 

Comment

5 Very nice!

I made something similar before:http://code.google.com/p/chieftain/source/browse/trunk/chieftain/models/behaviors/traceable.php, but I had to invoke this in the controller upon add and edit:

Controller Class:

<?php 
$this
->Model->trace($this->Auth->user('id'));
?>
E.g.: http://code.google.com/p/chieftain/source/browse/trunk/chieftain/controllers/books_controller.php#35
+1 on my references for behaviors.
Thanks for your work. :)
Posted Mar 5, 2009 by Sonny Gauran
 

Question

6 Doesn't seem to be working.

Daniel,
I updated my database to include the two fields of created_by and modified_by however if I do a debug($this->Dealership->save($this->data)); I should be able to see the created_by and modified_by added to the save function however I don't see anything extra being added. I also it doesn't appear that any value is being passed into the database.

Just to make sure I have everything set up correctly, I want to add

var $actAs = array('WhoDidIt');
to my model for the controller I am working with correct?
Then I also want to save the Who Did It Behavior to the app/models/behavior folder as well?

Sorry for all the questions but this is the first behavior that I have made use of.
Posted Apr 5, 2009 by Jon S
 

Comment

7 Answer to Jon

Hey Jon,

you should use


var $actsAs = array('WhoDidIt');

and not


var $actAs = array('WhoDidIt');

Dan
Posted Apr 7, 2009 by Daniel Vecchiato
 

Comment

8 Whodunit

I like this. Thanks for sharing. I am using it in my project. Although I thought "Whodunit" was a bit more clever as far as naming. Have you considered it?

See also: http://en.wikipedia.org/wiki/Whodunit
It's like Sherlock Holmes. Murder mysteries. Like when good database table rows are bludgeoned to death by ill-mannered users. This behavior helps us solve the case.
Posted Jul 9, 2009 by Mike Smullin
 

Question

9 Can you use UUID?

Daniel,

I'm new to Cake and this is my first app using Cake. This is one part of my app that I have been trying to figure out over the last couple of weeks. If this works I will be in hog heaven! My question is that I believe you said that the created_by field has to be int. Is that correct? I'm using cakes UUID and wondering will I still be able to use the behavior.

Posted Jul 18, 2009 by Eddie Lovett
 

Bug

10 Problem with whodidit and joiner tables

Hi.

Thanks for sharing this useful behaviour. I'm having trouble using it. If anyone could help, I'd appreciate it. I have a hABTM relationship using a joiner table, and for some reason, whodidit tries to look for a created_by column in that table, even though it doesn't have its own model. It's trying a left join with this non-existant column when CakePHP tries to delete from the joiner table, causing the deletes to fail, giving me duplicate rows.

Is there a way around this? Is it a bug in whodidit, or am I just using it incorrectly somehow?

Thank you for your time,
Zoe.
Posted Aug 25, 2009 by Zoe Blade
 

Comment

11 Re: Whodunit

I like this. Thanks for sharing. I am using it in my project. Although I thought "Whodunit" was a bit more clever as far as naming. Have you considered it?

See also: http://en.wikipedia.org/wiki/Whodunit
It's like Sherlock Holmes. Murder mysteries. Like when good database table rows are bludgeoned to death by ill-mannered users. This behavior helps us solve the case.

Cheers for the suggestion Mike! I'm not a native English speaker.... therefore I didn't have probably the best naming idea!
Posted Aug 27, 2009 by Daniel Vecchiato
 

Comment

12 Re: Can you use UUID?

Daniel,

I'm new to Cake and this is my first app using Cake. This is one part of my app that I have been trying to figure out over the last couple of weeks. If this works I will be in hog heaven! My question is that I believe you said that the created_by field has to be int. Is that correct? I'm using cakes UUID and wondering will I still be able to use the behavior.


I didn't try to use UUID, but I guess it should work! Let me know!

Dan
Posted Aug 27, 2009 by Daniel Vecchiato
 

Comment

13 Re: Problem with whodidit and joiner tables

Hi.

Thanks for sharing this useful behaviour. I'm having trouble using it. If anyone could help, I'd appreciate it. I have a hABTM relationship using a joiner table, and for some reason, whodidit tries to look for a created_by column in that table, even though it doesn't have its own model. It's trying a left join with this non-existant column when CakePHP tries to delete from the joiner table, causing the deletes to fail, giving me duplicate rows.

Is there a way around this? Is it a bug in whodidit, or am I just using it incorrectly somehow?

Thank you for your time,
Zoe.

Hey Zoe,

which version of CakePHP are you using?
Can you post your model relationship?
Do you use custom queries?

Dan
Posted Aug 27, 2009 by Daniel Vecchiato
 

Comment

14 Re: Problem with whodidit and joiner tables

Hi Dan!

Thanks for the reply! I'm using CakePHP version 1.2.3.8166.

I'm having this problem on pages which involve has-and-belongs-to-many relationships. For example, each product hABTM categories. In the back office, I have the following in the products' form so it draws a tickbox for each category:


echo $form->input('Category.Category', array(
  'multiple' => 'checkbox'
));

Using CakePHP's standard save() method on Product, it tries to delete and insert any rows into the joiner table as necessary. The products and categories both act as WhoDidIt. CakePHP seems to be trying to link in the users table to the actual joiner table itself, namely categories_products. It tries to perform a left join using a non-existent created_by column while performing a delete:


DELETE `CategoriesProduct` FROM `categories_products` AS `CategoriesProduct` LEFT JOIN `users` AS `CreatedBy` ON (`CategoriesProduct`.`created_by` = `CreatedBy`.`id`) LEFT JOIN `users` AS `ModifiedBy` ON (`CategoriesProduct`.`modified_by` = `ModifiedBy`.`id`) WHERE `CategoriesProduct`.`product_id` = 56 AND `CategoriesProduct`.`category_id` IN (30, 31, 30)

This gives an error, so the delete isn't executed, resulting in duplicate rows in the joiner table.

Thank you for your help,
Zoe.
Posted Aug 28, 2009 by Zoe Blade
 

Comment

15 Bug not WhoDidIt's fault

Hi again!

My apologies. I took out WhoDidIt from my app and wrote similar code in app_model's beforeSave, and experienced the exact same problem still. It turns out the culprit was that I had the belongsTo in app_model. This still shouldn't have affected the joiner table, but whatever's to blame, it's clearly not WhoDidIt, which seems to be working just fine.

Thanks again for your help!
Zoe.
Posted Sep 2, 2009 by Zoe Blade
 

Comment

16 Awesome!

Works great!

A note to beginners: Remember that the file name convention is underscored lowercase. That is, the file needs to be named who_did_it.php and be placed in the /app/models/behaviors folder.

Thanks!
H
Posted Oct 25, 2009 by Hernán Calabrese