Skip to content

Automatic auditing of attach/detach for BelongsToMany #869

@gisostallenberg

Description

@gisostallenberg
Q A
Bug? no
New Feature? yes
Framework Laravel
Framework version 10
Package version 13
PHP version 8.1

To audit attach and detach at the moment you'd have to call Auditable::auditAttach or Auditable::auditDetach. I have a solution to audit the attach/detach while the normal methods are called.
Would you be interested into a PR with this solution?

It works as follows:

<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
use OwenIt\Auditing\Database\Relations\BelongsToManyAudit;

final class MyModel extends Model implements AuditableContract
{
    use Auditable;

    public function otherModels(): BelongsToMany
    {
        return BelongsToManyAudit::audit($this->belongsToMany(OtherModel::class));
    }

}
<?php

declare(strict_types=1);

namespace OwenIt\Auditing\Database\Relations;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Facades\Event;
use OwenIt\Auditing\Contracts\Auditable;
use OwenIt\Auditing\Events\AuditCustom;

/**
 * @property Model&Auditable $parent
 *
 * @method Collection getResults()
 *
 * @mixin BelongsToMany
 */
final class BelongsToManyAudit extends BelongsToMany
{
    protected function __construct()
    {
        parent::__construct(...\func_get_args());
    }

    public static function audit(BelongsToMany $belongsToMany): static
    {
        return new self(
            $belongsToMany->related->newQueryWithoutRelationships(),
            $belongsToMany->parent,
            $belongsToMany->table,
            $belongsToMany->foreignPivotKey,
            $belongsToMany->relatedPivotKey,
            $belongsToMany->parentKey,
            $belongsToMany->relatedKey,
            $belongsToMany->relationName,
        );
    }

    /**
     * Audit attach to the parent of the relation.
     *
     * {@inheritdoc}
     *
     * @param array<string, mixed> $attributes
     */
    public function attach($id, array $attributes = [], $touch = true): void
    {
        $old = $this->getResults()->toArray();

        parent::attach($id, $attributes, $touch);

        $this->auditEvent('attached', $old, $this->getResults()->toArray());
    }

    /**
     * Audit detach from the parent of the relation.
     *
     * {@inheritdoc}
     */
    public function detach($ids = null, $touch = true): int
    {
        $old = $this->getResults()->toArray();

        $result = parent::detach($ids, $touch);

        $this->auditEvent('detached', $old, $this->getResults()->toArray());

        return $result;
    }

    /**
     * @param array<string, mixed> $old
     * @param array<string, mixed> $new
     */
    private function auditEvent(string $eventName, array $old, array $new): void
    {
        /** @var Auditable $model */
        $model = clone $this->parent;

        if (!\property_exists($model, 'auditEvent') || !\property_exists($model, 'isCustomEvent') || !\property_exists($model, 'auditCustomOld') || !\property_exists($model, 'auditCustomNew')) {
            return;
        }
        $model->auditEvent = $eventName;
        $model->isCustomEvent = true;
        $model->auditCustomOld = [$this->relationName => $old];
        $model->auditCustomNew = [$this->relationName => $new];

        Event::dispatch(AuditCustom::class, [$model]);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementAn improvement or new feature

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions