Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

fabricecw's avatar

Resolve attributes for activity log

We need to track model changes and output the changes in a user-friendly way. But I'm messing around with relationship and numeric-based attributes which are outputted with accessors.

I get the differences from the Model with a trait named Auditable:

// ...

public function trackChanges() {
        $this->track = true;
        $this->oldAttributes = $this->getOriginal();
        $this->newAttributes = $this->getAttributes();

        return $this;
    }


/**
     * The model's changes.
     *
     * @return array|null
     */
    public function changes()
    {
        $before = array_diff_assoc($this->oldAttributes, $this->newAttributes);
        $after = array_diff_assoc($this->newAttributes, $this->oldAttributes);

        if($this->trackChanges()) {
            return [
                'before' => array_except(
                    $before, self::$withoutTracking
                ),
                'after' => array_except(
                    $after, self::$withoutTracking
                )
            ];
        }

        return null;
    }

// ..

But for example, a field "priority" which is stored numeric and resolved via an accessor is unusable in an activity log.

E.g

$activity->changes['before']['priority'] // -> "1", but the user expects a "Standard" or "Express"

So I implemented an accessor for the changes which access a resolver function on the model itself:

Activity Model:

protected $casts = [
        'changes' => 'array'
    ];

 /**
     * Resolve the changed values to user-friendly values.
     *
     * @param $changes
     * @return mixed
     */
    public function getChangesAttribute($changes)
    {
        $changes = $this->castAttribute('changes', $changes);

        if($changes['before']) {
            foreach($changes['before'] as $key => $value) {
                $changes['before'][$key] = $this->trackable->resolveAttribute($key, $value);
            }
        }

        if($changes['after']) {
            foreach($changes['after'] as $key => $value) {
                $changes['after'][$key] = $this->trackable->resolveAttribute($key, $value);
            }
        }

        return $changes;
    }

Models with activities:

public function resolveAttribute($key, $value) {
        $mappings = [
            'priority' => $this->getPriorityNameAttribute($value),
        ];

        if(array_key_exists($key, $mappings)) {
            return $mappings[$key];
        }

        return $value;
    }

But this solution doesn't feels right... Someone may have a better approach doing this?

0 likes
8 replies
fabricecw's avatar

Thanks for the hint, I already checked this package. Unfortunately, the case is a little bit more complex, since the accessors cannot be auto-resolved.

Snapey's avatar

I assume only the model knows which fields need resolving? Im wondering if the model can have some static methods to a) provide a list of fields to resolve, and then b) individual methods like resolveField()

I assume you already have accessors for these fields? perhaps the functionality could be combined in some way?

1 like
fabricecw's avatar

Exactly, so I don't came around to map the fields to the right resolving on each model, since there is no logic that a field priority is resolved by a getPriorityNameAttributemethod.

What actually disturbs me is that the resolveAttribute() function fires the passed value through each anonymous function in the mappings array.

Snapey's avatar

but you could not use the accessors that exist on the model because you need an instance of the model for that.

I was thinking like

$model = `App\Activity`;

$resolvers = $model::getResolvers();  // returns collection of resolved attribute names

Then for each of your audited fields, see if they are in this collection, and if so call something like$model::resolve{name}($attribute)

fabricecw's avatar

I can use the accessors by passing the value:

    /**
     * Returns the name of the priority (stored numeric)
     * @param $value
     * @return string
     */
    public function getPriorityNameAttribute($value)
    {
        switch ($value ?: $this->priority) {
            case 1:
                $name = 'Standard';
                break;
            case 2:
                $name = 'Express';
                break;
            default:
                $name = 'NO PRIORITY';
                break;
        }

        return $name;
    }

And so I don't have to write extra resolvers since I can reuse the accessors. But the execution of all resolver methods inside the resolvers array isn't the fine way...

Snapey's avatar

if Priority is the only one (on this model) that needs resolving, where's the issue? Other models might have 2 or three, or four.... whatever... you are going to have to process those attributes somehow?

fabricecw's avatar

Mostly there are more than one attribute which needs a resolving. That's the reason why I impletemented the resolveAttribute function. And if I can't use an existing accessor, I can use a dedicated resover-method. And if it's a relationship field, I use Eloquent:

public function resolveAttribute($key, $value) {
        $mappings = [
            'priority' => $this->getPriorityNameAttribute($value),
            'very_special_field' => self::verySpecialResolveMethod($value),
            'area' => Area::find($value)->name
        ];

        if(array_key_exists($key, $mappings)) {
            return $mappings[$key];
        }

        return $value;
    }

But this way, all methods are called with $value instead of only the one from the key (or no one, if there is no mapping).

Please or to participate in this conversation.