Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log many to many doesn't work #1281

Open
Oui-Dev opened this issue Feb 28, 2024 · 2 comments
Open

Log many to many doesn't work #1281

Oui-Dev opened this issue Feb 28, 2024 · 2 comments
Labels

Comments

@Oui-Dev
Copy link

Oui-Dev commented Feb 28, 2024

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 :
#487
#386
#1269
#1270

Versions

  • PHP: 8.2
  • Database: Mysql 8
  • Laravel: 10.39
  • Package: 4.7.3
@Oui-Dev Oui-Dev added the bug label Feb 28, 2024
@AmirKhan47
Copy link

any fix?

@bardolf69
Copy link

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);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants