spatie / laravel-activitylog

Log activity inside your Laravel app
https://docs.spatie.be/laravel-activitylog
MIT License
5.35k stars 714 forks source link

Log many to many doesn't work #1281

Open Oui-Dev opened 8 months ago

Oui-Dev commented 8 months ago

I don't know if there is a way to log many to many relationship automaticaly, because it's really annoying to handle them by hand. I've searched everywhere, but I can't find any solutions. Is there a solution, how do you do it ?

Related issues/discussions/pr : https://github.com/spatie/laravel-activitylog/issues/487 https://github.com/spatie/laravel-activitylog/issues/386 https://github.com/spatie/laravel-activitylog/discussions/1269 https://github.com/spatie/laravel-activitylog/pull/1270

Versions

AmirKhan47 commented 5 months ago

any fix?

bardolf69 commented 2 months ago

You can extend LogsActivity with your own custom trait that logs the many-to-many actions. This is what I'm using

<?php

namespace App\Models\Traits;

use App\Exceptions\InvalidRelation;
use Illuminate\Support\Collection;
use Spatie\Activitylog\ActivityLogStatus;
use Spatie\Activitylog\Traits\LogsActivity as TraitsLogsActivity;

trait LogsActivity
{
    use TraitsLogsActivity {
        eventsToBeRecorded as parentEventsToBeRecorded;
        shouldLogEvent as parentShouldLogEvent;
        attributeValuesToBeLogged as parentAttributeValuesToBeLogged;
    }

    private $logRelationChanges = [];

    /**
     * Log the attachment of a related model.
     *
     * @param string $relationName
     * @param mixed $id
     * @param array $attributes
     * @param bool $touch
     * @param array $columns
     * @return void
     * @throws InvalidRelation
     */
    public function logAttach(string $relationName, $id, array $attributes = [], $touch = true, $columns = ['*'])
    {
        // Check if the relationship and attach method exist
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'attach')) {
            throw new InvalidRelation('Relationship ' . $relationName . ' was not found or does not support method attach');
        }

        // Get the current state before attaching the new related model
        $old = $this->{$relationName}()->get($columns);

        // Attach the new related model
        $this->{$relationName}()->attach($id, $attributes, $touch);

        // Get the updated state after attaching
        $new = $this->{$relationName}()->get($columns);

        // Dispatch the relation change event if there are differences
        if ($old->count() !== $new->count()) {
            // Dispatch the relation change event
            $this->dispatchRelationChanges($relationName, 'relationAttached', $old, $new);
        }
    }

    /**
     * Log the detachment of related models.
     *
     * @param string $relationName
     * @param mixed $ids
     * @param bool $touch
     * @param array $columns
     * @return int
     * @throws InvalidRelation
     */
    public function logDetach(string $relationName, $ids = null, $touch = true, $columns = ['*'])
    {
        // Check if the relationship and detach method exist
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'detach')) {
            throw new InvalidRelation('Relationship ' . $relationName . ' was not found or does not support method detach');
        }

        // Get the current state before detaching the related models
        $old = $this->{$relationName}()->get($columns);

        // Detach the related models
        $results = $this->{$relationName}()->detach($ids, $touch);

        // Get the updated state after detaching
        $new = $this->{$relationName}()->get($columns);

        // Dispatch the relation change event if there are differences
        if (!empty($results)) {
            // Dispatch the relation change event
            $this->dispatchRelationChanges($relationName, 'relationDetached', $old, $new);
        }

        return empty($results) ? 0 : $results;
    }

    /**
     * Log the syncing of related models.
     *
     * @param $relationName
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
     * @param bool $detaching
     * @param array $columns
     * @return array
     * @throws InvalidRelation
     */
    public function logSync($relationName, $ids, $detaching = true, $columns = ['*'])
    {
        // Check if the relationship and sync method exist
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'sync')) {
            throw new InvalidRelation('Relationship ' . $relationName . ' was not found or does not support method sync');
        }

        // Get the current state before syncing the related models
        $old = $this->{$relationName}()->get($columns);

        // Perform the sync operation
        $changes = $this->{$relationName}()->sync($ids, $detaching);

        // Determine old and new states based on changes
        if (collect($changes)->flatten()->isEmpty()) {
            $old = $new = collect([]);
        } else {
            $new = $this->{$relationName}()->get($columns);
        }

        // Dispatch the relation change event if there are differences
        if ($old->count() > 0 || $new->count() > 0) {
            $this->dispatchRelationChanges($relationName, 'relationSynced', $old, $new);
        }

        return $changes;
    }

    /**
     * Log the syncing of related models without detaching.
     *
     * @param string $relationName
     * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids
     * @param array $columns
     * @return array
     * @throws InvalidRelation
     */
    public function logSyncWithoutDetaching(string $relationName, $ids, $columns = ['*'])
    {
        // Check if the relationship and syncWithoutDetaching method exist
        if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), 'syncWithoutDetaching')) {
            throw new InvalidRelation('Relationship ' . $relationName . ' was not found or does not support method syncWithoutDetaching');
        }

        return $this->logSync($relationName, $ids, false, $columns);
    }

    /**
     * Dispatch the changes made to a related model.
     *
     * @param string $relationName
     * @param string $eventName
     * @param Collection $old
     * @param Collection $new
     * @return void
     */
    protected function dispatchRelationChanges($relationName, $eventName, $old, $new)
    {
        // Store the changes for logging purposes
        $this->logRelationChanges = [
            'old' => [
                $relationName => $old->toArray()
            ],
            'attributes' => [
                $relationName => $new->toArray()
            ]
        ];

        // Fire the model event to notify listeners about the relation change
        $this->fireModelEvent($eventName);

        // Clear the changes to avoid interference with subsequent events
        $this->logRelationChanges = [];
    }

    /**
     * Determine if the specified event should be logged.
     *
     * @param string $eventName
     * @return bool
     */
    protected function shouldLogEvent(string $eventName): bool
    {
        // Check the global activity log status and model-specific logging settings
        $logStatus = app(ActivityLogStatus::class);

        if (!$this->enableLoggingModelsEvents || $logStatus->disabled()) {
            return false;
        }

        // Log only relation events, delegate other events to parent method
        if (!in_array($eventName, ['relationAttached', 'relationDetached', 'relationSynced'])) {
            return true;
        }

        return $this->parentShouldLogEvent($eventName);
    }

    /**
     * Get the values to be logged based on the specified event.
     *
     * @param string $processingEvent
     * @return array
     */
    public function attributeValuesToBeLogged(string $processingEvent): array
    {
        // Return relation changes for specific events, delegate others to parent method
        if (in_array($processingEvent, ['relationAttached', 'relationDetached', 'relationSynced'])) {
            return $this->logRelationChanges;
        }

        return $this->parentAttributeValuesToBeLogged($processingEvent);
    }

    /**
     * Get the events that should be recorded, including custom relation events.
     *
     * @return Collection
     */
    protected static function eventsToBeRecorded(): Collection
    {
        // Include parent events and custom relation events
        return self::parentEventsToBeRecorded()->concat(['relationAttached', 'relationDetached', 'relationSynced']);
    }

    /**
     * Register a model event for when a related model is attached.
     *
     * @param  \Illuminate\Events\QueuedClosure|\Closure|string|array  $callback
     * @return void
     */
    public static function relationAttached($callback)
    {
        static::registerModelEvent('relationAttached', $callback);
    }

    /**
     * Register a model event for when a related model is detached.
     *
     * @param  \Illuminate\Events\QueuedClosure|\Closure|string|array  $callback
     * @return void
     */
    public static function relationDetached($callback)
    {
        static::registerModelEvent('relationDetached', $callback);
    }

    /**
     * Register a model event for when a related model is synced.
     *
     * @param  \Illuminate\Events\QueuedClosure|\Closure|string|array  $callback
     * @return void
     */
    public static function relationSynced($callback)
    {
        static::registerModelEvent('relationSynced', $callback);
    }
}