mirror of
https://gitlab.com/signalytic/client-external/streamline/streamline-emr.git
synced 2026-09-12 19:21:33 +00:00
873 lines
24 KiB
PHP
873 lines
24 KiB
PHP
<?php
|
|
|
|
namespace OwenIt\Auditing;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\App;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Illuminate\Support\Facades\Event;
|
|
use OwenIt\Auditing\Contracts\AttributeEncoder;
|
|
use OwenIt\Auditing\Contracts\AttributeRedactor;
|
|
use OwenIt\Auditing\Contracts\Resolver;
|
|
use OwenIt\Auditing\Events\AuditCustom;
|
|
use OwenIt\Auditing\Exceptions\AuditableTransitionException;
|
|
use OwenIt\Auditing\Exceptions\AuditingException;
|
|
|
|
trait Auditable
|
|
{
|
|
/**
|
|
* Auditable attributes excluded from the Audit.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $excludedAttributes = [];
|
|
|
|
/**
|
|
* Audit event name.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $auditEvent;
|
|
|
|
/**
|
|
* Is auditing disabled?
|
|
*
|
|
* @var bool
|
|
*/
|
|
public static $auditingDisabled = false;
|
|
|
|
/**
|
|
* Property may set custom event data to register
|
|
* @var null|array
|
|
*/
|
|
public $auditCustomOld = null;
|
|
|
|
/**
|
|
* Property may set custom event data to register
|
|
* @var null|array
|
|
*/
|
|
public $auditCustomNew = null;
|
|
|
|
/**
|
|
* If this is a custom event (as opposed to an eloquent event
|
|
* @var bool
|
|
*/
|
|
public $isCustomEvent = false;
|
|
|
|
/**
|
|
* @var array Preloaded data to be used by resolvers
|
|
*/
|
|
public $preloadedResolverData = [];
|
|
|
|
/**
|
|
* Auditable boot logic.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function bootAuditable()
|
|
{
|
|
if (static::isAuditingEnabled()) {
|
|
static::observe(new AuditableObserver());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function audits(): MorphMany
|
|
{
|
|
return $this->morphMany(
|
|
Config::get('audit.implementation', Models\Audit::class),
|
|
'auditable'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolve the Auditable attributes to exclude from the Audit.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function resolveAuditExclusions()
|
|
{
|
|
$this->excludedAttributes = $this->getAuditExclude();
|
|
|
|
// When in strict mode, hidden and non visible attributes are excluded
|
|
if ($this->getAuditStrict()) {
|
|
// Hidden attributes
|
|
$this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden);
|
|
|
|
// Non visible attributes
|
|
if ($this->visible) {
|
|
$invisible = array_diff(array_keys($this->attributes), $this->visible);
|
|
|
|
$this->excludedAttributes = array_merge($this->excludedAttributes, $invisible);
|
|
}
|
|
}
|
|
|
|
// Exclude Timestamps
|
|
if (!$this->getAuditTimestamps()) {
|
|
if ($this->getCreatedAtColumn()) {
|
|
$this->excludedAttributes[] = $this->getCreatedAtColumn();
|
|
}
|
|
if ($this->getUpdatedAtColumn()) {
|
|
$this->excludedAttributes[] = $this->getUpdatedAtColumn();
|
|
}
|
|
if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) {
|
|
$this->excludedAttributes[] = $this->getDeletedAtColumn();
|
|
}
|
|
}
|
|
|
|
// Valid attributes are all those that made it out of the exclusion array
|
|
$attributes = Arr::except($this->attributes, $this->excludedAttributes);
|
|
|
|
foreach ($attributes as $attribute => $value) {
|
|
// Apart from null, non scalar values will be excluded
|
|
if (
|
|
(is_array($value) && !Config::get('audit.allowed_array_values', false)) ||
|
|
(is_object($value) &&
|
|
!method_exists($value, '__toString') &&
|
|
!($value instanceof \UnitEnum))
|
|
) {
|
|
$this->excludedAttributes[] = $attribute;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getAuditExclude(): array
|
|
{
|
|
return $this->auditExclude ?? Config::get('audit.exclude', []);
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getAuditInclude(): array
|
|
{
|
|
return $this->auditInclude ?? [];
|
|
}
|
|
|
|
/**
|
|
* Get the old/new attributes of a retrieved event.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getRetrievedEventAttributes(): array
|
|
{
|
|
// This is a read event with no attribute changes,
|
|
// only metadata will be stored in the Audit
|
|
|
|
return [
|
|
[],
|
|
[],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get the old/new attributes of a created event.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getCreatedEventAttributes(): array
|
|
{
|
|
$new = [];
|
|
|
|
foreach ($this->attributes as $attribute => $value) {
|
|
if ($this->isAttributeAuditable($attribute)) {
|
|
$new[$attribute] = $value;
|
|
}
|
|
}
|
|
|
|
return [
|
|
[],
|
|
$new,
|
|
];
|
|
}
|
|
|
|
protected function getCustomEventAttributes(): array
|
|
{
|
|
return [
|
|
$this->auditCustomOld,
|
|
$this->auditCustomNew
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get the old/new attributes of an updated event.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getUpdatedEventAttributes(): array
|
|
{
|
|
$old = [];
|
|
$new = [];
|
|
|
|
foreach ($this->getDirty() as $attribute => $value) {
|
|
if ($this->isAttributeAuditable($attribute)) {
|
|
$old[$attribute] = Arr::get($this->original, $attribute);
|
|
$new[$attribute] = Arr::get($this->attributes, $attribute);
|
|
}
|
|
}
|
|
|
|
return [
|
|
$old,
|
|
$new,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get the old/new attributes of a deleted event.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getDeletedEventAttributes(): array
|
|
{
|
|
$old = [];
|
|
|
|
foreach ($this->attributes as $attribute => $value) {
|
|
if ($this->isAttributeAuditable($attribute)) {
|
|
$old[$attribute] = $value;
|
|
}
|
|
}
|
|
|
|
return [
|
|
$old,
|
|
[],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get the old/new attributes of a restored event.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getRestoredEventAttributes(): array
|
|
{
|
|
// A restored event is just a deleted event in reverse
|
|
return array_reverse($this->getDeletedEventAttributes());
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function readyForAuditing(): bool
|
|
{
|
|
if (static::$auditingDisabled || Models\Audit::$auditingGloballyDisabled) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->isCustomEvent) {
|
|
return true;
|
|
}
|
|
|
|
return $this->isEventAuditable($this->auditEvent);
|
|
}
|
|
|
|
/**
|
|
* Modify attribute value.
|
|
*
|
|
* @param string $attribute
|
|
* @param mixed $value
|
|
*
|
|
* @return mixed
|
|
* @throws AuditingException
|
|
*
|
|
*/
|
|
protected function modifyAttributeValue(string $attribute, $value)
|
|
{
|
|
$attributeModifiers = $this->getAttributeModifiers();
|
|
|
|
if (!array_key_exists($attribute, $attributeModifiers)) {
|
|
return $value;
|
|
}
|
|
|
|
$attributeModifier = $attributeModifiers[$attribute];
|
|
|
|
if (is_subclass_of($attributeModifier, AttributeRedactor::class)) {
|
|
return call_user_func([$attributeModifier, 'redact'], $value);
|
|
}
|
|
|
|
if (is_subclass_of($attributeModifier, AttributeEncoder::class)) {
|
|
return call_user_func([$attributeModifier, 'encode'], $value);
|
|
}
|
|
|
|
throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier));
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function toAudit(): array
|
|
{
|
|
if (!$this->readyForAuditing()) {
|
|
throw new AuditingException('A valid audit event has not been set');
|
|
}
|
|
|
|
$attributeGetter = $this->resolveAttributeGetter($this->auditEvent);
|
|
|
|
if (!method_exists($this, $attributeGetter)) {
|
|
throw new AuditingException(sprintf(
|
|
'Unable to handle "%s" event, %s() method missing',
|
|
$this->auditEvent,
|
|
$attributeGetter
|
|
));
|
|
}
|
|
|
|
$this->resolveAuditExclusions();
|
|
|
|
list($old, $new) = $this->$attributeGetter();
|
|
|
|
if ($this->getAttributeModifiers() && !$this->isCustomEvent) {
|
|
foreach ($old as $attribute => $value) {
|
|
$old[$attribute] = $this->modifyAttributeValue($attribute, $value);
|
|
}
|
|
|
|
foreach ($new as $attribute => $value) {
|
|
$new[$attribute] = $this->modifyAttributeValue($attribute, $value);
|
|
}
|
|
}
|
|
|
|
$morphPrefix = Config::get('audit.user.morph_prefix', 'user');
|
|
|
|
$tags = implode(',', $this->generateTags());
|
|
|
|
$user = $this->resolveUser();
|
|
|
|
return $this->transformAudit(array_merge([
|
|
'old_values' => $old,
|
|
'new_values' => $new,
|
|
'event' => $this->auditEvent,
|
|
'auditable_id' => $this->getKey(),
|
|
'auditable_type' => $this->getMorphClass(),
|
|
$morphPrefix . '_id' => $user ? $user->getAuthIdentifier() : null,
|
|
$morphPrefix . '_type' => $user ? $user->getMorphClass() : null,
|
|
'tags' => empty($tags) ? null : $tags,
|
|
], $this->runResolvers()));
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function transformAudit(array $data): array
|
|
{
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Resolve the User.
|
|
*
|
|
* @return mixed|null
|
|
* @throws AuditingException
|
|
*
|
|
*/
|
|
protected function resolveUser()
|
|
{
|
|
if (!empty($this->preloadedResolverData['user'] ?? null)) {
|
|
return $this->preloadedResolverData['user'];
|
|
}
|
|
|
|
$userResolver = Config::get('audit.user.resolver');
|
|
|
|
if (is_null($userResolver) && Config::has('audit.resolver') && !Config::has('audit.user.resolver')) {
|
|
trigger_error(
|
|
'The config file audit.php is not updated to the new version 13.0. Please see https://laravel-auditing.com/guide/upgrading.html',
|
|
E_USER_DEPRECATED
|
|
);
|
|
$userResolver = Config::get('audit.resolver.user');
|
|
}
|
|
|
|
if (is_subclass_of($userResolver, \OwenIt\Auditing\Contracts\UserResolver::class)) {
|
|
return call_user_func([$userResolver, 'resolve'], $this);
|
|
}
|
|
|
|
throw new AuditingException('Invalid UserResolver implementation');
|
|
}
|
|
|
|
protected function runResolvers(): array
|
|
{
|
|
$resolved = [];
|
|
$resolvers = Config::get('audit.resolvers', []);
|
|
if (empty($resolvers) && Config::has('audit.resolver')) {
|
|
trigger_error(
|
|
'The config file audit.php is not updated to the new version 13.0. Please see https://laravel-auditing.com/guide/upgrading.html',
|
|
E_USER_DEPRECATED
|
|
);
|
|
$resolvers = Config::get('audit.resolver', []);
|
|
}
|
|
|
|
foreach ($resolvers as $name => $implementation) {
|
|
if (empty($implementation)) {
|
|
continue;
|
|
}
|
|
|
|
if (!is_subclass_of($implementation, Resolver::class)) {
|
|
throw new AuditingException('Invalid Resolver implementation for: ' . $name);
|
|
}
|
|
$resolved[$name] = call_user_func([$implementation, 'resolve'], $this);
|
|
}
|
|
return $resolved;
|
|
}
|
|
|
|
public function preloadResolverData()
|
|
{
|
|
$this->preloadedResolverData = $this->runResolvers();
|
|
|
|
$user = $this->resolveUser();
|
|
if (!empty($user)) {
|
|
$this->preloadedResolverData['user'] = $user;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Determine if an attribute is eligible for auditing.
|
|
*
|
|
* @param string $attribute
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function isAttributeAuditable(string $attribute): bool
|
|
{
|
|
// The attribute should not be audited
|
|
if (in_array($attribute, $this->excludedAttributes, true)) {
|
|
return false;
|
|
}
|
|
|
|
// The attribute is auditable when explicitly
|
|
// listed or when the include array is empty
|
|
$include = $this->getAuditInclude();
|
|
|
|
return empty($include) || in_array($attribute, $include, true);
|
|
}
|
|
|
|
/**
|
|
* Determine whether an event is auditable.
|
|
*
|
|
* @param string $event
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function isEventAuditable($event): bool
|
|
{
|
|
return is_string($this->resolveAttributeGetter($event));
|
|
}
|
|
|
|
/**
|
|
* Attribute getter method resolver.
|
|
*
|
|
* @param string $event
|
|
*
|
|
* @return string|null
|
|
*/
|
|
protected function resolveAttributeGetter($event)
|
|
{
|
|
if (empty($event)) {
|
|
return;
|
|
}
|
|
|
|
if ($this->isCustomEvent) {
|
|
return 'getCustomEventAttributes';
|
|
}
|
|
|
|
foreach ($this->getAuditEvents() as $key => $value) {
|
|
$auditableEvent = is_int($key) ? $value : $key;
|
|
|
|
$auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent));
|
|
|
|
if (preg_match($auditableEventRegex, $event)) {
|
|
return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function setAuditEvent(string $event): Contracts\Auditable
|
|
{
|
|
$this->auditEvent = $this->isEventAuditable($event) ? $event : null;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getAuditEvent()
|
|
{
|
|
return $this->auditEvent;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getAuditEvents(): array
|
|
{
|
|
return $this->auditEvents ?? Config::get('audit.events', [
|
|
'created',
|
|
'updated',
|
|
'deleted',
|
|
'restored',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Is Auditing disabled.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isAuditingDisabled(): bool
|
|
{
|
|
return static::$auditingDisabled || Models\Audit::$auditingGloballyDisabled;
|
|
}
|
|
|
|
/**
|
|
* Disable Auditing.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function disableAuditing()
|
|
{
|
|
static::$auditingDisabled = true;
|
|
}
|
|
|
|
/**
|
|
* Enable Auditing.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function enableAuditing()
|
|
{
|
|
static::$auditingDisabled = false;
|
|
}
|
|
|
|
/**
|
|
* Execute a callback while auditing is disabled.
|
|
*
|
|
* @param callable $callback
|
|
* @param bool $globally
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public static function withoutAuditing(callable $callback, bool $globally = false)
|
|
{
|
|
$auditingDisabled = static::$auditingDisabled;
|
|
|
|
static::disableAuditing();
|
|
Models\Audit::$auditingGloballyDisabled = $globally;
|
|
|
|
try {
|
|
return $callback();
|
|
} finally {
|
|
Models\Audit::$auditingGloballyDisabled = false;
|
|
static::$auditingDisabled = $auditingDisabled;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine whether auditing is enabled.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isAuditingEnabled(): bool
|
|
{
|
|
if (App::runningInConsole()) {
|
|
return Config::get('audit.enabled', true) && Config::get('audit.console', false);
|
|
}
|
|
|
|
return Config::get('audit.enabled', true);
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getAuditStrict(): bool
|
|
{
|
|
return $this->auditStrict ?? Config::get('audit.strict', false);
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getAuditTimestamps(): bool
|
|
{
|
|
return $this->auditTimestamps ?? Config::get('audit.timestamps', false);
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getAuditDriver()
|
|
{
|
|
return $this->auditDriver ?? Config::get('audit.driver', 'database');
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getAuditThreshold(): int
|
|
{
|
|
return $this->auditThreshold ?? Config::get('audit.threshold', 0);
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getAttributeModifiers(): array
|
|
{
|
|
return $this->attributeModifiers ?? [];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function generateTags(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function transitionTo(Contracts\Audit $audit, bool $old = false): Contracts\Auditable
|
|
{
|
|
// The Audit must be for an Auditable model of this type
|
|
if ($this->getMorphClass() !== $audit->auditable_type) {
|
|
throw new AuditableTransitionException(sprintf(
|
|
'Expected Auditable type %s, got %s instead',
|
|
$this->getMorphClass(),
|
|
$audit->auditable_type
|
|
));
|
|
}
|
|
|
|
// The Audit must be for this specific Auditable model
|
|
if ($this->getKey() !== $audit->auditable_id) {
|
|
throw new AuditableTransitionException(sprintf(
|
|
'Expected Auditable id (%s)%s, got (%s)%s instead',
|
|
gettype($this->getKey()),
|
|
$this->getKey(),
|
|
gettype($audit->auditable_id),
|
|
$audit->auditable_id
|
|
));
|
|
}
|
|
|
|
// Redacted data should not be used when transitioning states
|
|
foreach ($this->getAttributeModifiers() as $attribute => $modifier) {
|
|
if (is_subclass_of($modifier, AttributeRedactor::class)) {
|
|
throw new AuditableTransitionException('Cannot transition states when an AttributeRedactor is set');
|
|
}
|
|
}
|
|
|
|
// The attribute compatibility between the Audit and the Auditable model must be met
|
|
$modified = $audit->getModified();
|
|
|
|
if ($incompatibilities = array_diff_key($modified, $this->getAttributes())) {
|
|
throw new AuditableTransitionException(sprintf(
|
|
'Incompatibility between [%s:%s] and [%s:%s]',
|
|
$this->getMorphClass(),
|
|
$this->getKey(),
|
|
get_class($audit),
|
|
$audit->getKey()
|
|
), array_keys($incompatibilities));
|
|
}
|
|
|
|
$key = $old ? 'old' : 'new';
|
|
|
|
foreach ($modified as $attribute => $value) {
|
|
if (array_key_exists($key, $value)) {
|
|
$this->setAttribute($attribute, $value[$key]);
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Pivot help methods
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Methods for auditing pivot actions
|
|
|
|
|
*/
|
|
|
|
/**
|
|
* @param string $relationName
|
|
* @param mixed $id
|
|
* @param array $attributes
|
|
* @param bool $touch
|
|
* @param array $columns
|
|
* @param \Closure|null $callback
|
|
* @return void
|
|
* @throws AuditingException
|
|
*/
|
|
public function auditAttach(string $relationName, $id, array $attributes = [], $touch = true, $columns = ['*'], $callback = null)
|
|
{
|
|
$this->validateRelationshipMethodExistence($relationName, 'attach');
|
|
|
|
$relationCall = $this->{$relationName}();
|
|
|
|
if ($callback instanceof \Closure) {
|
|
$this->applyClosureToRelationship($relationCall, $callback);
|
|
}
|
|
|
|
$old = $relationCall->get($columns);
|
|
$relationCall->attach($id, $attributes, $touch);
|
|
$new = $relationCall->get($columns);
|
|
|
|
$this->dispatchRelationAuditEvent($relationName, 'attach', $old, $new);
|
|
}
|
|
|
|
/**
|
|
* @param string $relationName
|
|
* @param mixed $ids
|
|
* @param bool $touch
|
|
* @param array $columns
|
|
* @param \Closure|null $callback
|
|
* @return int
|
|
* @throws AuditingException
|
|
*/
|
|
public function auditDetach(string $relationName, $ids = null, $touch = true, $columns = ['*'], $callback = null)
|
|
{
|
|
$this->validateRelationshipMethodExistence($relationName, 'detach');
|
|
|
|
$relationCall = $this->{$relationName}();
|
|
|
|
if ($callback instanceof \Closure) {
|
|
$this->applyClosureToRelationship($relationCall, $callback);
|
|
}
|
|
|
|
$old = $relationCall->get($columns);
|
|
$results = $relationCall->detach($ids, $touch);
|
|
$new = $relationCall->get($columns);
|
|
|
|
$this->dispatchRelationAuditEvent($relationName, 'detach', $old, $new);
|
|
|
|
return empty($results) ? 0 : $results;
|
|
}
|
|
|
|
/**
|
|
* @param string $relationName
|
|
* @param Collection|Model|array $ids
|
|
* @param bool $detaching
|
|
* @param array $columns
|
|
* @param \Closure|null $callback
|
|
* @return array
|
|
* @throws AuditingException
|
|
*/
|
|
public function auditSync(string $relationName, $ids, $detaching = true, $columns = ['*'], $callback = null)
|
|
{
|
|
$this->validateRelationshipMethodExistence($relationName, 'sync');
|
|
|
|
$relationCall = $this->{$relationName}();
|
|
|
|
if ($callback instanceof \Closure) {
|
|
$this->applyClosureToRelationship($relationCall, $callback);
|
|
}
|
|
|
|
$old = $relationCall->get($columns);
|
|
$changes = $relationCall->sync($ids, $detaching);
|
|
|
|
if (collect($changes)->flatten()->isEmpty()) {
|
|
$old = $new = collect([]);
|
|
} else {
|
|
$new = $relationCall->get($columns);
|
|
}
|
|
|
|
$this->dispatchRelationAuditEvent($relationName, 'sync', $old, $new);
|
|
|
|
return $changes;
|
|
}
|
|
|
|
/**
|
|
* @param string $relationName
|
|
* @param Collection|Model|array $ids
|
|
* @param array $columns
|
|
* @param \Closure|null $callback
|
|
* @return array
|
|
* @throws AuditingException
|
|
*/
|
|
public function auditSyncWithoutDetaching(string $relationName, $ids, $columns = ['*'], $callback = null)
|
|
{
|
|
$this->validateRelationshipMethodExistence($relationName, 'syncWithoutDetaching');
|
|
|
|
return $this->auditSync($relationName, $ids, false, $columns, $callback);
|
|
}
|
|
|
|
/**
|
|
* @param string $relationName
|
|
* @param Collection|Model|array $ids
|
|
* @param array $values
|
|
* @param bool $detaching
|
|
* @param array $columns
|
|
* @param \Closure|null $callback
|
|
* @return array
|
|
*/
|
|
public function auditSyncWithPivotValues(string $relationName, $ids, array $values, bool $detaching = true, $columns = ['*'], $callback = null)
|
|
{
|
|
$this->validateRelationshipMethodExistence($relationName, 'syncWithPivotValues');
|
|
|
|
if ($ids instanceof Model) {
|
|
$ids = $ids->getKey();
|
|
} elseif ($ids instanceof \Illuminate\Database\Eloquent\Collection) {
|
|
$ids = $ids->isEmpty() ? [] : $ids->pluck($ids->first()->getKeyName())->toArray();
|
|
} elseif ($ids instanceof Collection) {
|
|
$ids = $ids->toArray();
|
|
}
|
|
|
|
return $this->auditSync($relationName, collect(Arr::wrap($ids))->mapWithKeys(function ($id) use ($values) {
|
|
return [$id => $values];
|
|
}), $detaching, $columns, $callback);
|
|
}
|
|
|
|
/**
|
|
* @param string $relationName
|
|
* @param string $event
|
|
* @param Collection $old
|
|
* @param Collection $new
|
|
* @return void
|
|
*/
|
|
private function dispatchRelationAuditEvent($relationName, $event, $old, $new)
|
|
{
|
|
$this->auditCustomOld[$relationName] = $old->diff($new)->toArray();
|
|
$this->auditCustomNew[$relationName] = $new->diff($old)->toArray();
|
|
|
|
if (
|
|
empty($this->auditCustomOld[$relationName]) &&
|
|
empty($this->auditCustomNew[$relationName])
|
|
) {
|
|
$this->auditCustomOld = $this->auditCustomNew = [];
|
|
}
|
|
|
|
$this->auditEvent = $event;
|
|
$this->isCustomEvent = true;
|
|
Event::dispatch(AuditCustom::class, [$this]);
|
|
$this->auditCustomOld = $this->auditCustomNew = [];
|
|
$this->isCustomEvent = false;
|
|
}
|
|
|
|
private function validateRelationshipMethodExistence(string $relationName, string $methodName): void
|
|
{
|
|
if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), $methodName)) {
|
|
throw new AuditingException("Relationship $relationName was not found or does not support method $methodName");
|
|
}
|
|
}
|
|
|
|
private function applyClosureToRelationship(BelongsToMany $relation, \Closure $closure): void
|
|
{
|
|
try {
|
|
$closure($relation);
|
|
} catch (\Throwable $exception) {
|
|
throw new AuditingException("Invalid Closure for {$relation->getRelationName()} Relationship");
|
|
}
|
|
}
|
|
}
|