Cake 4: Capture created user id and modified user ID on new/modified records like Timestamp behavior

In my old Cake 2.0 app, I had the following beforeSave() function in AppModel, which captured the ID of the user who either created or modified a record and wrote it to the table if the field(s) existed:

function beforeSave($options = array()) {
    // check to make sure the created_user_id and modified_user_id fields exist
    if($this->trackUpdates()) {
        $userId = AuthComponent::user('id');
        $this->set('modified_user_id', $userId);

        // new record, only update created_user_id
        if(!$this->exists()) {
            $this->set('created_user_id', $userId);
        }
    }
    $this->beforeSaveValues = null;
    if($this->exists()) {
        $this->recursive = 1;
        $this->beforeSaveValues = $this->findById($this->id);
    }
    return parent::beforeSave();
}

I’m now trying to do the same thing in Cake 4, using a behavior, and I’m having difficulty understanding the process. I want this behavior to be used throughout my application and for it to be triggered for any create or update event, so I’ve called it in Application.php:

$this->addBehavior('CreatedModified', [
        'events' => [
            'Model.beforeSave' => [
                'created_user_id' => 'new',
                'modified_user_id' => 'always'
            ]
        ]
    ]);

And here is the behavior I wrote:

namespace App\Model\Behavior;

use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;

class CreatedModifiedBehavior extends Behavior
{
    protected $_defaultConfig = [
        'field' => 'id',
        'created_id' => 'created_user_id',
        'modified_id' => 'modified_user_id',
    ];

    public function createdModifiedUser(EntityInterface $entity)
    {
        $config = $this->getConfig();
        $value = $entity->get($config['field']);
        if ($this->trackUpdates()) {
             if ($entity->isNew()) {
                $entity->set($config['created_id'], $this->Identity->get('id'));
            } else {
                $entity->set($config['modified_id'], $this->Identity->get('id'));
            }
        }
    }

    public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
    {
        $this->createdModifiedUser($entity);
    }

    public function trackUpdates() 
    {
        // Check for presence of created_user_id and modified_user_id fields
        return ($this->hasField('created_user_id') && $this->hasField('modified_user_id'));
    } 
}

Am I even on the right track? So far, my application is ignoring it (so at least it’s not breaking anything).

I made something similar to you, but with an easier step (let’s call it a workaround):

<?php 
namespace App\Model\Behavior;

use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;

class FootprintBehavior extends Behavior
{
    public function getFootprint(EntityInterface $entity)
    {   
        if($entity->isNew()) {
            $entity->set('created_by', $_SESSION['Auth']['id']);
            $entity->set('modified_by', $_SESSION['Auth']['id']);
        } else {
            $entity->set('modified_by', $_SESSION['Auth']['id']);
        }
    }

    public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
    {
        $this->getFootprint($entity);
    }
}

And then all I have to do is load the Behavior in each Table:

// src/Model/Table/ArticlesTable.php
public function initialize(array $config): void
{
    parent::initialize($config);

    $this->addBehavior('Footprint');

    // ... rest of your table init
}

To make it a step easier when loading for future Models, I added the $this->addBehavior() to my bake models, so I don’t have to add them manually.

There probably is a better way to do it. Mine works as long as I create created_by and modified_by fields to my tables-structure.

1 Like

Yeah, this is awesome. Worked like a charm and is much less complicated than my thing. (I’m great for overthinking this stuff.) Thanks!

Oh no big deal. Currently I am working on a solution to get the related entity to the created_by and modified_by for each entry. But still checking out if it wouldn’t be overkill (performancewise).

And if implementing, thinking about where to implement it. Thought at first to add it in the Behavior with afterFind(), but doesn’t seem to work at the first try.

If you got any idea how to relate the users table to the footprint-fields, I would be glad…

Kind regards

Hmm… I might be able to give you a nudge in the right direction. I modified your behavior a bit to check whether the fields I want to update actually exist. If they don’t, the behavior doesn’t execute. Here’s the new behavior, which uses a method called checkUpdates(), into which you’ll pass the $event. Within that method, you call getSubject(), which should yield the table name.

Check out the whole thing here:

class FootprintBehavior extends Behavior
 {   
    public function getFootprint(EntityInterface $entity, EventInterface $event)
    {   
        /*  
        *   Capture created_user_id and modified_user_id for every create/modify table event
        *   after ensuring the fields exist
        */
        if ($this->trackUpdates($event)) {
            if ($entity->isNew()) {
                $entity->set('created_user_id', $_SESSION['Auth']['id']);
                $entity->set('modified_user_id', $_SESSION['Auth']['id']);
            } else {
                $entity->set('modified_user_id', $_SESSION['Auth']['id']);
            }
        }
    }

    public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
    {
        $this->getFootprint($entity, $event);
    } 

    public function trackUpdates(EventInterface $event) {
        // Make sure the table actually has the created_user_id and modified_user_id fields
        $table = $event->getSubject();
        return ($table->hasField('created_user_id') && $table->hasField('modified_user_id'));
    }  
}

Make sure you add “use Cake\ORM\Table” at the top!

$_SESSION is not recommended to use directly. So, I searched around for an alternative. I found it here: 3.0 - $this->Auth->user('id') inside a model or view · Issue #3929 · cakephp/cakephp · GitHub

NOTE: I am using the new authentication plugin which provides “AuthenticationComponent”. Quick Start - 2.x

Turning into the following code for me:

    public function beforeSave(EventInterface $event, EntityInterface $entity, \ArrayObject $options)
    {
        $identity = Router::getRequest()->getAttribute('identity');
        $entity->user_id = $identity?->get('id') ?? null;
    }

FYI: There is also GitHub - UseMuffin/Footprint: CakePHP plugin to allow passing currently logged in user to model layer.