1 | <?php |
||||||
2 | |||||||
3 | namespace OwenIt\Auditing; |
||||||
4 | |||||||
5 | use Illuminate\Database\Eloquent\Model; |
||||||
6 | use Illuminate\Database\Eloquent\Relations\BelongsToMany; |
||||||
7 | use Illuminate\Database\Eloquent\Relations\MorphMany; |
||||||
8 | use Illuminate\Database\Eloquent\SoftDeletes; |
||||||
9 | use Illuminate\Support\Arr; |
||||||
10 | use Illuminate\Support\Collection; |
||||||
11 | use Illuminate\Support\Facades\App; |
||||||
12 | use Illuminate\Support\Facades\Config; |
||||||
13 | use Illuminate\Support\Facades\Event; |
||||||
14 | use OwenIt\Auditing\Contracts\AttributeEncoder; |
||||||
15 | use OwenIt\Auditing\Contracts\AttributeRedactor; |
||||||
16 | use OwenIt\Auditing\Contracts\Resolver; |
||||||
17 | use OwenIt\Auditing\Events\AuditCustom; |
||||||
18 | use OwenIt\Auditing\Exceptions\AuditableTransitionException; |
||||||
19 | use OwenIt\Auditing\Exceptions\AuditingException; |
||||||
20 | |||||||
21 | // @phpstan-ignore trait.unused |
||||||
22 | trait Auditable |
||||||
23 | { |
||||||
24 | /** |
||||||
25 | * Auditable attributes excluded from the Audit. |
||||||
26 | * |
||||||
27 | * @var array |
||||||
28 | */ |
||||||
29 | protected $excludedAttributes = []; |
||||||
30 | |||||||
31 | /** |
||||||
32 | * Audit event name. |
||||||
33 | * |
||||||
34 | * @var string |
||||||
35 | */ |
||||||
36 | public $auditEvent; |
||||||
37 | |||||||
38 | /** |
||||||
39 | * Is auditing disabled? |
||||||
40 | * |
||||||
41 | * @var bool |
||||||
42 | */ |
||||||
43 | public static $auditingDisabled = false; |
||||||
44 | |||||||
45 | /** |
||||||
46 | * Property may set custom event data to register |
||||||
47 | * @var null|array |
||||||
48 | */ |
||||||
49 | public $auditCustomOld = null; |
||||||
50 | |||||||
51 | /** |
||||||
52 | * Property may set custom event data to register |
||||||
53 | * @var null|array |
||||||
54 | */ |
||||||
55 | public $auditCustomNew = null; |
||||||
56 | |||||||
57 | /** |
||||||
58 | * If this is a custom event (as opposed to an eloquent event |
||||||
59 | * @var bool |
||||||
60 | */ |
||||||
61 | public $isCustomEvent = false; |
||||||
62 | |||||||
63 | /** |
||||||
64 | * @var array Preloaded data to be used by resolvers |
||||||
65 | */ |
||||||
66 | public $preloadedResolverData = []; |
||||||
67 | |||||||
68 | /** |
||||||
69 | * Auditable boot logic. |
||||||
70 | * |
||||||
71 | * @return void |
||||||
72 | 17 | */ |
|||||
73 | public static function bootAuditable() |
||||||
74 | 17 | { |
|||||
75 | 15 | if (static::isAuditingEnabled()) { |
|||||
76 | static::observe(new AuditableObserver()); |
||||||
77 | } |
||||||
78 | } |
||||||
79 | |||||||
80 | /** |
||||||
81 | * {@inheritdoc} |
||||||
82 | 15 | */ |
|||||
83 | public function audits(): MorphMany |
||||||
84 | 15 | { |
|||||
85 | 15 | return $this->morphMany( |
|||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||||
86 | 15 | Config::get('audit.implementation', Models\Audit::class), |
|||||
87 | 15 | 'auditable' |
|||||
88 | ); |
||||||
89 | } |
||||||
90 | |||||||
91 | /** |
||||||
92 | * Resolve the Auditable attributes to exclude from the Audit. |
||||||
93 | * |
||||||
94 | * @return void |
||||||
95 | 15 | */ |
|||||
96 | protected function resolveAuditExclusions() |
||||||
97 | 15 | { |
|||||
98 | $this->excludedAttributes = $this->getAuditExclude(); |
||||||
99 | |||||||
100 | 15 | // When in strict mode, hidden and non visible attributes are excluded |
|||||
101 | if ($this->getAuditStrict()) { |
||||||
102 | // Hidden attributes |
||||||
103 | $this->excludedAttributes = array_merge($this->excludedAttributes, $this->hidden); |
||||||
104 | |||||||
105 | // Non visible attributes |
||||||
106 | if ($this->visible) { |
||||||
107 | $invisible = array_diff(array_keys($this->attributes), $this->visible); |
||||||
108 | |||||||
109 | $this->excludedAttributes = array_merge($this->excludedAttributes, $invisible); |
||||||
110 | } |
||||||
111 | } |
||||||
112 | |||||||
113 | 15 | // Exclude Timestamps |
|||||
114 | 15 | if (!$this->getAuditTimestamps()) { |
|||||
115 | 15 | if ($this->getCreatedAtColumn()) { |
|||||
0 ignored issues
–
show
It seems like
getCreatedAtColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
116 | $this->excludedAttributes[] = $this->getCreatedAtColumn(); |
||||||
117 | 15 | } |
|||||
118 | 15 | if ($this->getUpdatedAtColumn()) { |
|||||
0 ignored issues
–
show
It seems like
getUpdatedAtColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
119 | $this->excludedAttributes[] = $this->getUpdatedAtColumn(); |
||||||
120 | 15 | } |
|||||
121 | 7 | if (in_array(SoftDeletes::class, class_uses_recursive(get_class($this)))) { |
|||||
122 | $this->excludedAttributes[] = $this->getDeletedAtColumn(); |
||||||
0 ignored issues
–
show
It seems like
getDeletedAtColumn() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
123 | } |
||||||
124 | } |
||||||
125 | |||||||
126 | 15 | // Valid attributes are all those that made it out of the exclusion array |
|||||
127 | $attributes = Arr::except($this->attributes, $this->excludedAttributes); |
||||||
128 | 15 | ||||||
129 | foreach ($attributes as $attribute => $value) { |
||||||
130 | // Apart from null, non scalar values will be excluded |
||||||
131 | 15 | if ( |
|||||
132 | 15 | (is_array($value) && !Config::get('audit.allowed_array_values', false)) || |
|||||
133 | 15 | (is_object($value) && |
|||||
134 | 15 | !method_exists($value, '__toString') && |
|||||
135 | !($value instanceof \UnitEnum)) |
||||||
0 ignored issues
–
show
The type
UnitEnum was not found. Maybe you did not declare it correctly or list all dependencies?
The issue could also be caused by a filter entry in the build configuration.
If the path has been excluded in your configuration, e.g. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||||||
136 | ) { |
||||||
137 | $this->excludedAttributes[] = $attribute; |
||||||
138 | } |
||||||
139 | } |
||||||
140 | } |
||||||
141 | |||||||
142 | /** |
||||||
143 | * @return array |
||||||
144 | 15 | */ |
|||||
145 | public function getAuditExclude(): array |
||||||
146 | 15 | { |
|||||
147 | return $this->auditExclude ?? Config::get('audit.exclude', []); |
||||||
148 | } |
||||||
149 | |||||||
150 | /** |
||||||
151 | * @return array |
||||||
152 | 14 | */ |
|||||
153 | public function getAuditInclude(): array |
||||||
154 | 14 | { |
|||||
155 | return $this->auditInclude ?? []; |
||||||
156 | } |
||||||
157 | |||||||
158 | /** |
||||||
159 | * Get the old/new attributes of a retrieved event. |
||||||
160 | * |
||||||
161 | * @return array |
||||||
162 | 3 | */ |
|||||
163 | protected function getRetrievedEventAttributes(): array |
||||||
164 | { |
||||||
165 | // This is a read event with no attribute changes, |
||||||
166 | // only metadata will be stored in the Audit |
||||||
167 | 3 | ||||||
168 | 3 | return [ |
|||||
169 | 3 | [], |
|||||
170 | 3 | [], |
|||||
171 | ]; |
||||||
172 | } |
||||||
173 | |||||||
174 | /** |
||||||
175 | * Get the old/new attributes of a created event. |
||||||
176 | * |
||||||
177 | * @return array |
||||||
178 | 9 | */ |
|||||
179 | protected function getCreatedEventAttributes(): array |
||||||
180 | 9 | { |
|||||
181 | $new = []; |
||||||
182 | 9 | ||||||
183 | 9 | foreach ($this->attributes as $attribute => $value) { |
|||||
184 | 9 | if ($this->isAttributeAuditable($attribute)) { |
|||||
185 | $new[$attribute] = $value; |
||||||
186 | } |
||||||
187 | } |
||||||
188 | 9 | ||||||
189 | 9 | return [ |
|||||
190 | 9 | [], |
|||||
191 | 9 | $new, |
|||||
192 | ]; |
||||||
193 | } |
||||||
194 | |||||||
195 | protected function getCustomEventAttributes(): array |
||||||
196 | { |
||||||
197 | return [ |
||||||
198 | $this->auditCustomOld, |
||||||
199 | $this->auditCustomNew |
||||||
200 | ]; |
||||||
201 | } |
||||||
202 | |||||||
203 | /** |
||||||
204 | * Get the old/new attributes of an updated event. |
||||||
205 | * |
||||||
206 | * @return array |
||||||
207 | 3 | */ |
|||||
208 | protected function getUpdatedEventAttributes(): array |
||||||
209 | 3 | { |
|||||
210 | 3 | $old = []; |
|||||
211 | $new = []; |
||||||
212 | 3 | ||||||
213 | 3 | foreach ($this->getDirty() as $attribute => $value) { |
|||||
0 ignored issues
–
show
It seems like
getDirty() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
214 | 3 | if ($this->isAttributeAuditable($attribute)) { |
|||||
215 | 3 | $old[$attribute] = Arr::get($this->original, $attribute); |
|||||
216 | $new[$attribute] = Arr::get($this->attributes, $attribute); |
||||||
217 | } |
||||||
218 | } |
||||||
219 | 3 | ||||||
220 | 3 | return [ |
|||||
221 | 3 | $old, |
|||||
222 | 3 | $new, |
|||||
223 | ]; |
||||||
224 | } |
||||||
225 | |||||||
226 | /** |
||||||
227 | * Get the old/new attributes of a deleted event. |
||||||
228 | * |
||||||
229 | * @return array |
||||||
230 | 2 | */ |
|||||
231 | protected function getDeletedEventAttributes(): array |
||||||
232 | 2 | { |
|||||
233 | $old = []; |
||||||
234 | 2 | ||||||
235 | 2 | foreach ($this->attributes as $attribute => $value) { |
|||||
236 | 2 | if ($this->isAttributeAuditable($attribute)) { |
|||||
237 | $old[$attribute] = $value; |
||||||
238 | } |
||||||
239 | } |
||||||
240 | 2 | ||||||
241 | 2 | return [ |
|||||
242 | 2 | $old, |
|||||
243 | 2 | [], |
|||||
244 | ]; |
||||||
245 | } |
||||||
246 | |||||||
247 | /** |
||||||
248 | * Get the old/new attributes of a restored event. |
||||||
249 | * |
||||||
250 | * @return array |
||||||
251 | 1 | */ |
|||||
252 | protected function getRestoredEventAttributes(): array |
||||||
253 | { |
||||||
254 | 1 | // A restored event is just a deleted event in reverse |
|||||
255 | return array_reverse($this->getDeletedEventAttributes()); |
||||||
256 | } |
||||||
257 | |||||||
258 | /** |
||||||
259 | * {@inheritdoc} |
||||||
260 | 15 | */ |
|||||
261 | public function readyForAuditing(): bool |
||||||
262 | 15 | { |
|||||
263 | if (static::$auditingDisabled || Models\Audit::$auditingGloballyDisabled) { |
||||||
264 | return false; |
||||||
265 | } |
||||||
266 | 15 | ||||||
267 | if ($this->isCustomEvent) { |
||||||
268 | return true; |
||||||
269 | } |
||||||
270 | 15 | ||||||
271 | return $this->isEventAuditable($this->auditEvent); |
||||||
272 | } |
||||||
273 | |||||||
274 | /** |
||||||
275 | * Modify attribute value. |
||||||
276 | * |
||||||
277 | * @param string $attribute |
||||||
278 | * @param mixed $value |
||||||
279 | * |
||||||
280 | * @return mixed |
||||||
281 | * @throws AuditingException |
||||||
282 | * |
||||||
283 | */ |
||||||
284 | protected function modifyAttributeValue(string $attribute, $value) |
||||||
285 | { |
||||||
286 | $attributeModifiers = $this->getAttributeModifiers(); |
||||||
287 | |||||||
288 | if (!array_key_exists($attribute, $attributeModifiers)) { |
||||||
289 | return $value; |
||||||
290 | } |
||||||
291 | |||||||
292 | $attributeModifier = $attributeModifiers[$attribute]; |
||||||
293 | |||||||
294 | if (is_subclass_of($attributeModifier, AttributeRedactor::class)) { |
||||||
295 | return call_user_func([$attributeModifier, 'redact'], $value); |
||||||
296 | } |
||||||
297 | |||||||
298 | if (is_subclass_of($attributeModifier, AttributeEncoder::class)) { |
||||||
299 | return call_user_func([$attributeModifier, 'encode'], $value); |
||||||
300 | } |
||||||
301 | |||||||
302 | throw new AuditingException(sprintf('Invalid AttributeModifier implementation: %s', $attributeModifier)); |
||||||
303 | } |
||||||
304 | |||||||
305 | /** |
||||||
306 | * {@inheritdoc} |
||||||
307 | 15 | */ |
|||||
308 | public function toAudit(): array |
||||||
309 | 15 | { |
|||||
310 | if (!$this->readyForAuditing()) { |
||||||
311 | throw new AuditingException('A valid audit event has not been set'); |
||||||
312 | } |
||||||
313 | 15 | ||||||
314 | $attributeGetter = $this->resolveAttributeGetter($this->auditEvent); |
||||||
315 | 15 | ||||||
316 | if (!method_exists($this, $attributeGetter)) { |
||||||
0 ignored issues
–
show
It seems like
$attributeGetter can also be of type null ; however, parameter $method of method_exists() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
317 | throw new AuditingException(sprintf( |
||||||
318 | 'Unable to handle "%s" event, %s() method missing', |
||||||
319 | $this->auditEvent, |
||||||
320 | $attributeGetter |
||||||
321 | )); |
||||||
322 | } |
||||||
323 | 15 | ||||||
324 | $this->resolveAuditExclusions(); |
||||||
325 | 15 | ||||||
326 | list($old, $new) = $this->$attributeGetter(); |
||||||
327 | 15 | ||||||
328 | if ($this->getAttributeModifiers() && !$this->isCustomEvent) { |
||||||
329 | foreach ($old as $attribute => $value) { |
||||||
330 | $old[$attribute] = $this->modifyAttributeValue($attribute, $value); |
||||||
331 | } |
||||||
332 | |||||||
333 | foreach ($new as $attribute => $value) { |
||||||
334 | $new[$attribute] = $this->modifyAttributeValue($attribute, $value); |
||||||
335 | } |
||||||
336 | } |
||||||
337 | 15 | ||||||
338 | $morphPrefix = Config::get('audit.user.morph_prefix', 'user'); |
||||||
339 | 15 | ||||||
340 | $tags = implode(',', $this->generateTags()); |
||||||
341 | 15 | ||||||
342 | $user = $this->resolveUser(); |
||||||
343 | 15 | ||||||
344 | 15 | return $this->transformAudit(array_merge([ |
|||||
345 | 15 | 'old_values' => $old, |
|||||
346 | 15 | 'new_values' => $new, |
|||||
347 | 15 | 'event' => $this->auditEvent, |
|||||
348 | 15 | 'auditable_id' => $this->getKey(), |
|||||
0 ignored issues
–
show
It seems like
getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
349 | 15 | 'auditable_type' => $this->getMorphClass(), |
|||||
0 ignored issues
–
show
It seems like
getMorphClass() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
350 | 15 | $morphPrefix . '_id' => $user ? $user->getAuthIdentifier() : null, |
|||||
351 | 15 | $morphPrefix . '_type' => $user ? $user->getMorphClass() : null, |
|||||
352 | 15 | 'tags' => empty($tags) ? null : $tags, |
|||||
353 | ], $this->runResolvers())); |
||||||
354 | } |
||||||
355 | |||||||
356 | /** |
||||||
357 | * {@inheritdoc} |
||||||
358 | 15 | */ |
|||||
359 | public function transformAudit(array $data): array |
||||||
360 | 15 | { |
|||||
361 | return $data; |
||||||
362 | } |
||||||
363 | |||||||
364 | /** |
||||||
365 | * Resolve the User. |
||||||
366 | * |
||||||
367 | * @return mixed|null |
||||||
368 | * @throws AuditingException |
||||||
369 | * |
||||||
370 | 15 | */ |
|||||
371 | protected function resolveUser() |
||||||
372 | 15 | { |
|||||
373 | if (!empty($this->preloadedResolverData['user'] ?? null)) { |
||||||
374 | return $this->preloadedResolverData['user']; |
||||||
375 | } |
||||||
376 | 15 | ||||||
377 | $userResolver = Config::get('audit.user.resolver'); |
||||||
378 | 15 | ||||||
379 | if (is_null($userResolver) && Config::has('audit.resolver') && !Config::has('audit.user.resolver')) { |
||||||
380 | trigger_error( |
||||||
381 | 'The config file audit.php is not updated to the new version 13.0. Please see https://laravel-auditing.com/guide/upgrading.html', |
||||||
382 | E_USER_DEPRECATED |
||||||
383 | ); |
||||||
384 | $userResolver = Config::get('audit.resolver.user'); |
||||||
385 | } |
||||||
386 | 15 | ||||||
387 | 15 | if (is_subclass_of($userResolver, \OwenIt\Auditing\Contracts\UserResolver::class)) { |
|||||
388 | return call_user_func([$userResolver, 'resolve'], $this); |
||||||
389 | } |
||||||
390 | |||||||
391 | throw new AuditingException('Invalid UserResolver implementation'); |
||||||
392 | } |
||||||
393 | 15 | ||||||
394 | protected function runResolvers(): array |
||||||
395 | 15 | { |
|||||
396 | 15 | $resolved = []; |
|||||
397 | 15 | $resolvers = Config::get('audit.resolvers', []); |
|||||
398 | if (empty($resolvers) && Config::has('audit.resolver')) { |
||||||
399 | trigger_error( |
||||||
400 | 'The config file audit.php is not updated to the new version 13.0. Please see https://laravel-auditing.com/guide/upgrading.html', |
||||||
401 | E_USER_DEPRECATED |
||||||
402 | ); |
||||||
403 | $resolvers = Config::get('audit.resolver', []); |
||||||
404 | } |
||||||
405 | 15 | ||||||
406 | 15 | foreach ($resolvers as $name => $implementation) { |
|||||
407 | if (empty($implementation)) { |
||||||
408 | continue; |
||||||
409 | } |
||||||
410 | 15 | ||||||
411 | if (!is_subclass_of($implementation, Resolver::class)) { |
||||||
412 | throw new AuditingException('Invalid Resolver implementation for: ' . $name); |
||||||
413 | 15 | } |
|||||
414 | $resolved[$name] = call_user_func([$implementation, 'resolve'], $this); |
||||||
415 | 15 | } |
|||||
416 | return $resolved; |
||||||
417 | } |
||||||
418 | 15 | ||||||
419 | public function preloadResolverData() |
||||||
420 | 15 | { |
|||||
421 | $this->preloadedResolverData = $this->runResolvers(); |
||||||
422 | 15 | ||||||
423 | 15 | $user = $this->resolveUser(); |
|||||
424 | if (!empty($user)) { |
||||||
425 | $this->preloadedResolverData['user'] = $user; |
||||||
426 | } |
||||||
427 | 15 | ||||||
428 | return $this; |
||||||
429 | } |
||||||
430 | |||||||
431 | /** |
||||||
432 | * Determine if an attribute is eligible for auditing. |
||||||
433 | * |
||||||
434 | * @param string $attribute |
||||||
435 | * |
||||||
436 | * @return bool |
||||||
437 | 14 | */ |
|||||
438 | protected function isAttributeAuditable(string $attribute): bool |
||||||
439 | { |
||||||
440 | 14 | // The attribute should not be audited |
|||||
441 | 13 | if (in_array($attribute, $this->excludedAttributes, true)) { |
|||||
442 | return false; |
||||||
443 | } |
||||||
444 | |||||||
445 | // The attribute is auditable when explicitly |
||||||
446 | 14 | // listed or when the include array is empty |
|||||
447 | $include = $this->getAuditInclude(); |
||||||
448 | 14 | ||||||
449 | return empty($include) || in_array($attribute, $include, true); |
||||||
450 | } |
||||||
451 | |||||||
452 | /** |
||||||
453 | * Determine whether an event is auditable. |
||||||
454 | * |
||||||
455 | * @param string $event |
||||||
456 | * |
||||||
457 | * @return bool |
||||||
458 | 15 | */ |
|||||
459 | protected function isEventAuditable($event): bool |
||||||
460 | 15 | { |
|||||
461 | return is_string($this->resolveAttributeGetter($event)); |
||||||
462 | } |
||||||
463 | |||||||
464 | /** |
||||||
465 | * Attribute getter method resolver. |
||||||
466 | * |
||||||
467 | * @param string $event |
||||||
468 | * |
||||||
469 | * @return string|null |
||||||
470 | 15 | */ |
|||||
471 | protected function resolveAttributeGetter($event) |
||||||
472 | 15 | { |
|||||
473 | 8 | if (empty($event)) { |
|||||
474 | return; |
||||||
475 | } |
||||||
476 | 15 | ||||||
477 | if ($this->isCustomEvent) { |
||||||
478 | return 'getCustomEventAttributes'; |
||||||
479 | } |
||||||
480 | 15 | ||||||
481 | 15 | foreach ($this->getAuditEvents() as $key => $value) { |
|||||
482 | $auditableEvent = is_int($key) ? $value : $key; |
||||||
483 | 15 | ||||||
484 | $auditableEventRegex = sprintf('/%s/', preg_replace('/\*+/', '.*', $auditableEvent)); |
||||||
485 | 15 | ||||||
486 | 15 | if (preg_match($auditableEventRegex, $event)) { |
|||||
487 | return is_int($key) ? sprintf('get%sEventAttributes', ucfirst($event)) : $value; |
||||||
488 | } |
||||||
489 | } |
||||||
490 | } |
||||||
491 | |||||||
492 | /** |
||||||
493 | * {@inheritdoc} |
||||||
494 | 15 | */ |
|||||
495 | public function setAuditEvent(string $event): Contracts\Auditable |
||||||
496 | 15 | { |
|||||
497 | $this->auditEvent = $this->isEventAuditable($event) ? $event : null; |
||||||
498 | 15 | ||||||
499 | return $this; |
||||||
0 ignored issues
–
show
|
|||||||
500 | } |
||||||
501 | |||||||
502 | /** |
||||||
503 | * {@inheritdoc} |
||||||
504 | 15 | */ |
|||||
505 | public function getAuditEvent() |
||||||
506 | 15 | { |
|||||
507 | return $this->auditEvent; |
||||||
508 | } |
||||||
509 | |||||||
510 | /** |
||||||
511 | * {@inheritdoc} |
||||||
512 | 15 | */ |
|||||
513 | public function getAuditEvents(): array |
||||||
514 | 15 | { |
|||||
515 | 15 | return $this->auditEvents ?? Config::get('audit.events', [ |
|||||
0 ignored issues
–
show
|
|||||||
516 | 15 | 'created', |
|||||
517 | 15 | 'updated', |
|||||
518 | 15 | 'deleted', |
|||||
519 | 15 | 'restored', |
|||||
520 | ]); |
||||||
521 | } |
||||||
522 | |||||||
523 | /** |
||||||
524 | * Is Auditing disabled. |
||||||
525 | * |
||||||
526 | * @return bool |
||||||
527 | */ |
||||||
528 | public static function isAuditingDisabled(): bool |
||||||
529 | { |
||||||
530 | return static::$auditingDisabled || Models\Audit::$auditingGloballyDisabled; |
||||||
531 | } |
||||||
532 | |||||||
533 | /** |
||||||
534 | * Disable Auditing. |
||||||
535 | * |
||||||
536 | * @return void |
||||||
537 | */ |
||||||
538 | public static function disableAuditing() |
||||||
539 | { |
||||||
540 | static::$auditingDisabled = true; |
||||||
541 | } |
||||||
542 | |||||||
543 | /** |
||||||
544 | * Enable Auditing. |
||||||
545 | * |
||||||
546 | * @return void |
||||||
547 | */ |
||||||
548 | public static function enableAuditing() |
||||||
549 | { |
||||||
550 | static::$auditingDisabled = false; |
||||||
551 | } |
||||||
552 | |||||||
553 | /** |
||||||
554 | * Execute a callback while auditing is disabled. |
||||||
555 | * |
||||||
556 | * @param callable $callback |
||||||
557 | * @param bool $globally |
||||||
558 | * |
||||||
559 | * @return mixed |
||||||
560 | */ |
||||||
561 | public static function withoutAuditing(callable $callback, bool $globally = false) |
||||||
562 | { |
||||||
563 | $auditingDisabled = static::$auditingDisabled; |
||||||
564 | |||||||
565 | static::disableAuditing(); |
||||||
566 | Models\Audit::$auditingGloballyDisabled = $globally; |
||||||
567 | |||||||
568 | try { |
||||||
569 | return $callback(); |
||||||
570 | } finally { |
||||||
571 | Models\Audit::$auditingGloballyDisabled = false; |
||||||
572 | static::$auditingDisabled = $auditingDisabled; |
||||||
573 | } |
||||||
574 | } |
||||||
575 | |||||||
576 | /** |
||||||
577 | * Determine whether auditing is enabled. |
||||||
578 | * |
||||||
579 | * @return bool |
||||||
580 | 17 | */ |
|||||
581 | public static function isAuditingEnabled(): bool |
||||||
582 | 17 | { |
|||||
583 | 15 | if (App::runningInConsole()) { |
|||||
584 | return Config::get('audit.enabled', true) && Config::get('audit.console', false); |
||||||
585 | } |
||||||
586 | 2 | ||||||
587 | return Config::get('audit.enabled', true); |
||||||
588 | } |
||||||
589 | |||||||
590 | /** |
||||||
591 | * {@inheritdoc} |
||||||
592 | 15 | */ |
|||||
593 | public function getAuditStrict(): bool |
||||||
594 | 15 | { |
|||||
595 | return $this->auditStrict ?? Config::get('audit.strict', false); |
||||||
596 | } |
||||||
597 | |||||||
598 | /** |
||||||
599 | * {@inheritdoc} |
||||||
600 | 15 | */ |
|||||
601 | public function getAuditTimestamps(): bool |
||||||
602 | 15 | { |
|||||
603 | return $this->auditTimestamps ?? Config::get('audit.timestamps', false); |
||||||
604 | } |
||||||
605 | |||||||
606 | /** |
||||||
607 | * {@inheritdoc} |
||||||
608 | 15 | */ |
|||||
609 | public function getAuditDriver() |
||||||
610 | 15 | { |
|||||
611 | return $this->auditDriver ?? Config::get('audit.driver', 'database'); |
||||||
612 | } |
||||||
613 | |||||||
614 | /** |
||||||
615 | * {@inheritdoc} |
||||||
616 | 15 | */ |
|||||
617 | public function getAuditThreshold(): int |
||||||
618 | 15 | { |
|||||
619 | return $this->auditThreshold ?? Config::get('audit.threshold', 0); |
||||||
620 | } |
||||||
621 | |||||||
622 | /** |
||||||
623 | * {@inheritdoc} |
||||||
624 | 15 | */ |
|||||
625 | public function getAttributeModifiers(): array |
||||||
626 | 15 | { |
|||||
627 | return $this->attributeModifiers ?? []; |
||||||
628 | } |
||||||
629 | |||||||
630 | /** |
||||||
631 | * {@inheritdoc} |
||||||
632 | 15 | */ |
|||||
633 | public function generateTags(): array |
||||||
634 | 15 | { |
|||||
635 | return []; |
||||||
636 | } |
||||||
637 | |||||||
638 | /** |
||||||
639 | * {@inheritdoc} |
||||||
640 | */ |
||||||
641 | public function transitionTo(Contracts\Audit $audit, bool $old = false): Contracts\Auditable |
||||||
642 | { |
||||||
643 | // The Audit must be for an Auditable model of this type |
||||||
644 | if ($this->getMorphClass() !== $audit->auditable_type) { |
||||||
0 ignored issues
–
show
|
|||||||
645 | throw new AuditableTransitionException(sprintf( |
||||||
646 | 'Expected Auditable type %s, got %s instead', |
||||||
647 | $this->getMorphClass(), |
||||||
648 | $audit->auditable_type |
||||||
649 | )); |
||||||
650 | } |
||||||
651 | |||||||
652 | // The Audit must be for this specific Auditable model |
||||||
653 | if ($this->getKey() !== $audit->auditable_id) { |
||||||
0 ignored issues
–
show
|
|||||||
654 | throw new AuditableTransitionException(sprintf( |
||||||
655 | 'Expected Auditable id (%s)%s, got (%s)%s instead', |
||||||
656 | gettype($this->getKey()), |
||||||
657 | $this->getKey(), |
||||||
658 | gettype($audit->auditable_id), |
||||||
659 | $audit->auditable_id |
||||||
660 | )); |
||||||
661 | } |
||||||
662 | |||||||
663 | // Redacted data should not be used when transitioning states |
||||||
664 | foreach ($this->getAttributeModifiers() as $attribute => $modifier) { |
||||||
665 | if (is_subclass_of($modifier, AttributeRedactor::class)) { |
||||||
666 | throw new AuditableTransitionException('Cannot transition states when an AttributeRedactor is set'); |
||||||
667 | } |
||||||
668 | } |
||||||
669 | |||||||
670 | // The attribute compatibility between the Audit and the Auditable model must be met |
||||||
671 | $modified = $audit->getModified(); |
||||||
672 | |||||||
673 | if ($incompatibilities = array_diff_key($modified, $this->getAttributes())) { |
||||||
0 ignored issues
–
show
It seems like
$modified can also be of type string ; however, parameter $array1 of array_diff_key() does only seem to accept array , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() The method
getAttributes() does not exist on OwenIt\Auditing\Auditable . Did you maybe mean getAttributeModifiers() ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
674 | throw new AuditableTransitionException(sprintf( |
||||||
675 | 'Incompatibility between [%s:%s] and [%s:%s]', |
||||||
676 | $this->getMorphClass(), |
||||||
677 | $this->getKey(), |
||||||
678 | get_class($audit), |
||||||
679 | $audit->getKey() |
||||||
0 ignored issues
–
show
The method
getKey() does not exist on OwenIt\Auditing\Contracts\Audit . Since it exists in all sub-types, consider adding an abstract or default implementation to OwenIt\Auditing\Contracts\Audit .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
680 | ), array_keys($incompatibilities)); |
||||||
681 | } |
||||||
682 | |||||||
683 | $key = $old ? 'old' : 'new'; |
||||||
684 | |||||||
685 | foreach ($modified as $attribute => $value) { |
||||||
686 | if (array_key_exists($key, $value)) { |
||||||
687 | $this->setAttribute($attribute, $value[$key]); |
||||||
0 ignored issues
–
show
It seems like
setAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
688 | } |
||||||
689 | } |
||||||
690 | |||||||
691 | return $this; |
||||||
0 ignored issues
–
show
|
|||||||
692 | } |
||||||
693 | |||||||
694 | /* |
||||||
695 | |-------------------------------------------------------------------------- |
||||||
696 | | Pivot help methods |
||||||
697 | |-------------------------------------------------------------------------- |
||||||
698 | | |
||||||
699 | | Methods for auditing pivot actions |
||||||
700 | | |
||||||
701 | */ |
||||||
702 | |||||||
703 | /** |
||||||
704 | * @param string $relationName |
||||||
705 | * @param mixed $id |
||||||
706 | * @param array $attributes |
||||||
707 | * @param bool $touch |
||||||
708 | * @param array $columns |
||||||
709 | * @param \Closure|null $callback |
||||||
710 | * @return void |
||||||
711 | * @throws AuditingException |
||||||
712 | */ |
||||||
713 | public function auditAttach(string $relationName, $id, array $attributes = [], $touch = true, $columns = ['*'], $callback = null) |
||||||
714 | { |
||||||
715 | $this->validateRelationshipMethodExistence($relationName, 'attach'); |
||||||
716 | |||||||
717 | $relationCall = $this->{$relationName}(); |
||||||
718 | |||||||
719 | if ($callback instanceof \Closure) { |
||||||
720 | $this->applyClosureToRelationship($relationCall, $callback); |
||||||
721 | } |
||||||
722 | |||||||
723 | $old = $relationCall->get($columns); |
||||||
724 | $relationCall->attach($id, $attributes, $touch); |
||||||
725 | $new = $relationCall->get($columns); |
||||||
726 | |||||||
727 | $this->dispatchRelationAuditEvent($relationName, 'attach', $old, $new); |
||||||
728 | } |
||||||
729 | |||||||
730 | /** |
||||||
731 | * @param string $relationName |
||||||
732 | * @param mixed $ids |
||||||
733 | * @param bool $touch |
||||||
734 | * @param array $columns |
||||||
735 | * @param \Closure|null $callback |
||||||
736 | * @return int |
||||||
737 | * @throws AuditingException |
||||||
738 | */ |
||||||
739 | public function auditDetach(string $relationName, $ids = null, $touch = true, $columns = ['*'], $callback = null) |
||||||
740 | { |
||||||
741 | $this->validateRelationshipMethodExistence($relationName, 'detach'); |
||||||
742 | |||||||
743 | $relationCall = $this->{$relationName}(); |
||||||
744 | |||||||
745 | if ($callback instanceof \Closure) { |
||||||
746 | $this->applyClosureToRelationship($relationCall, $callback); |
||||||
747 | } |
||||||
748 | |||||||
749 | $old = $relationCall->get($columns); |
||||||
750 | $results = $relationCall->detach($ids, $touch); |
||||||
751 | $new = $relationCall->get($columns); |
||||||
752 | |||||||
753 | $this->dispatchRelationAuditEvent($relationName, 'detach', $old, $new); |
||||||
754 | |||||||
755 | return empty($results) ? 0 : $results; |
||||||
756 | } |
||||||
757 | |||||||
758 | /** |
||||||
759 | * @param string $relationName |
||||||
760 | * @param Collection|Model|array $ids |
||||||
761 | * @param bool $detaching |
||||||
762 | * @param array $columns |
||||||
763 | * @param \Closure|null $callback |
||||||
764 | * @return array |
||||||
765 | * @throws AuditingException |
||||||
766 | */ |
||||||
767 | public function auditSync(string $relationName, $ids, $detaching = true, $columns = ['*'], $callback = null) |
||||||
768 | { |
||||||
769 | $this->validateRelationshipMethodExistence($relationName, 'sync'); |
||||||
770 | |||||||
771 | $relationCall = $this->{$relationName}(); |
||||||
772 | |||||||
773 | if ($callback instanceof \Closure) { |
||||||
774 | $this->applyClosureToRelationship($relationCall, $callback); |
||||||
775 | } |
||||||
776 | |||||||
777 | $old = $relationCall->get($columns); |
||||||
778 | $changes = $relationCall->sync($ids, $detaching); |
||||||
779 | |||||||
780 | if (collect($changes)->flatten()->isEmpty()) { |
||||||
781 | $old = $new = collect([]); |
||||||
782 | } else { |
||||||
783 | $new = $relationCall->get($columns); |
||||||
784 | } |
||||||
785 | |||||||
786 | $this->dispatchRelationAuditEvent($relationName, 'sync', $old, $new); |
||||||
787 | |||||||
788 | return $changes; |
||||||
789 | } |
||||||
790 | |||||||
791 | /** |
||||||
792 | * @param string $relationName |
||||||
793 | * @param Collection|Model|array $ids |
||||||
794 | * @param array $columns |
||||||
795 | * @param \Closure|null $callback |
||||||
796 | * @return array |
||||||
797 | * @throws AuditingException |
||||||
798 | */ |
||||||
799 | public function auditSyncWithoutDetaching(string $relationName, $ids, $columns = ['*'], $callback = null) |
||||||
800 | { |
||||||
801 | $this->validateRelationshipMethodExistence($relationName, 'syncWithoutDetaching'); |
||||||
802 | |||||||
803 | return $this->auditSync($relationName, $ids, false, $columns, $callback); |
||||||
804 | } |
||||||
805 | |||||||
806 | /** |
||||||
807 | * @param string $relationName |
||||||
808 | * @param Collection|Model|array $ids |
||||||
809 | * @param array $values |
||||||
810 | * @param bool $detaching |
||||||
811 | * @param array $columns |
||||||
812 | * @param \Closure|null $callback |
||||||
813 | * @return array |
||||||
814 | */ |
||||||
815 | public function auditSyncWithPivotValues(string $relationName, $ids, array $values, bool $detaching = true, $columns = ['*'], $callback = null) |
||||||
816 | { |
||||||
817 | $this->validateRelationshipMethodExistence($relationName, 'syncWithPivotValues'); |
||||||
818 | |||||||
819 | if ($ids instanceof Model) { |
||||||
820 | $ids = $ids->getKey(); |
||||||
821 | } elseif ($ids instanceof \Illuminate\Database\Eloquent\Collection) { |
||||||
822 | $ids = $ids->isEmpty() ? [] : $ids->pluck($ids->first()->getKeyName())->toArray(); |
||||||
823 | } elseif ($ids instanceof Collection) { |
||||||
824 | $ids = $ids->toArray(); |
||||||
825 | } |
||||||
826 | |||||||
827 | return $this->auditSync($relationName, collect(Arr::wrap($ids))->mapWithKeys(function ($id) use ($values) { |
||||||
828 | return [$id => $values]; |
||||||
829 | }), $detaching, $columns, $callback); |
||||||
830 | } |
||||||
831 | |||||||
832 | /** |
||||||
833 | * @param string $relationName |
||||||
834 | * @param string $event |
||||||
835 | * @param Collection $old |
||||||
836 | * @param Collection $new |
||||||
837 | * @return void |
||||||
838 | */ |
||||||
839 | private function dispatchRelationAuditEvent($relationName, $event, $old, $new) |
||||||
840 | { |
||||||
841 | $this->auditCustomOld[$relationName] = $old->diff($new)->toArray(); |
||||||
842 | $this->auditCustomNew[$relationName] = $new->diff($old)->toArray(); |
||||||
843 | |||||||
844 | if ( |
||||||
845 | empty($this->auditCustomOld[$relationName]) && |
||||||
846 | empty($this->auditCustomNew[$relationName]) |
||||||
847 | ) { |
||||||
848 | $this->auditCustomOld = $this->auditCustomNew = []; |
||||||
849 | } |
||||||
850 | |||||||
851 | $this->auditEvent = $event; |
||||||
852 | $this->isCustomEvent = true; |
||||||
853 | Event::dispatch(new AuditCustom($this)); |
||||||
0 ignored issues
–
show
$this of type OwenIt\Auditing\Auditable is incompatible with the type OwenIt\Auditing\Contracts\Auditable expected by parameter $model of OwenIt\Auditing\Events\AuditCustom::__construct() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
854 | $this->auditCustomOld = $this->auditCustomNew = []; |
||||||
855 | $this->isCustomEvent = false; |
||||||
856 | } |
||||||
857 | |||||||
858 | private function validateRelationshipMethodExistence(string $relationName, string $methodName): void |
||||||
859 | { |
||||||
860 | if (!method_exists($this, $relationName) || !method_exists($this->{$relationName}(), $methodName)) { |
||||||
861 | throw new AuditingException("Relationship $relationName was not found or does not support method $methodName"); |
||||||
862 | } |
||||||
863 | } |
||||||
864 | |||||||
865 | private function applyClosureToRelationship(BelongsToMany $relation, \Closure $closure): void |
||||||
866 | { |
||||||
867 | try { |
||||||
868 | $closure($relation); |
||||||
869 | } catch (\Throwable $exception) { |
||||||
870 | throw new AuditingException("Invalid Closure for {$relation->getRelationName()} Relationship"); |
||||||
871 | } |
||||||
872 | } |
||||||
873 | } |
||||||
874 |