Skip to content

Commit

Permalink
Merge pull request #53 from sourcetoad/switch-to-owner-concept
Browse files Browse the repository at this point in the history
Switch to Owner Concept
  • Loading branch information
rbondoc96 authored Nov 19, 2024
2 parents a88fd81 + 0aab714 commit 605173b
Show file tree
Hide file tree
Showing 18 changed files with 693 additions and 94 deletions.
88 changes: 73 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Sourcetoad Logger

This package allows you to log important events, including what data is viewed by who. Including the following:
This package allows you to log important events, including what data is viewed or updated by whom. Events include:

* Login Events
* Logout Events
Expand All @@ -19,7 +19,7 @@ This helps answer questions such as:
* What IP was responsible for these changes?

This plugin is additionally very slim, which makes installation more difficult than a regular plugin. This is because it is expected for perhaps legal reasons, that these logs may not be allowed to be removed for years.
This means that the method for storage must be efficient, unlike other packages which may specialize in auditing/logging but bloat database with information.
This means that the method for storage must be efficient, unlike other packages which may specialize in auditing/logging, but bloat the database with information.

### Laravel

Expand All @@ -29,7 +29,7 @@ You are reading the documentation for 11.x.
* If you're using Laravel 6, 7 or 8 please see the docs for [3.x](https://github.com/sourcetoad/Logger/releases/tag/v3.0.1).
* If you're using Laravel 5 or below please see docs for [1.x](https://github.com/sourcetoad/Logger/releases/tag/v1.3.0)

The currently supported database(s): `mysql`
The currently supported database(s): `mysql`, `pgsql`

You can install the package via composer:

Expand All @@ -54,32 +54,90 @@ This will contain model maps, which can be read about below.
Due to the large amount of records anticipated to be created, you must create an integer mapping to models in your system. We give an example as follows:

```php
use Sourcetoad\Logger\Enums\ModelMapping;
// In config/logger.php...

'morphs' => [
ModelMapping::USER => 'App\User'
return [

'morphs' => [
0 => App\Models\User::class
],

];
```

This points our `App\User::class` to an enum (integer). This means our database is created with small integers vs large fully qualified namespaces.
This points our `App\Models\User::class` to an enum (integer). This means our database is created with small integers vs large fully qualified namespaces.

Recommended action is creating an enum class to describe all models in your system. If an integer mapping is not detected. The system will error out with an `/InvalidArgumentException`.
Recommended action is creating an enum class to describe all models in your system. If an integer mapping is not detected. The system will error out with an `\InvalidArgumentException`.

This enforces the user to create shorthand notation for all models. To cut down on database size. If a numeric morph is not found, the system will fail out. Due to issues with blindly overwriting and applying these morphs globally, they are manually applied. This means that morphs in your application are left untouched.
This enforces the user to create shorthand notation for all models to cut down on database size. If a numeric morph is not found, the system will fail out. Due to issues with blindly overwriting and applying these morphs globally, they are manually applied. This means that morphs in your application are left untouched.

#### Trackable Trait
For models that may contain information that you wish to be notified was access/retrieved. You may add the Trackable trait to these models. The issue with this, is a record without a user association is frankly useless. However, you may access a record that has no foreign key or relation to a user model. Tracking that makes auditing quite useless.
For models that may contain information that you wish to be notified was accessed or mutated. You may add the `Trackable` trait and contract to these models:

```php
use Illuminate\Database\Eloquent\Model;
use Sourcetoad\Logger\Contracts\Trackable as TrackableContract;
use Sourcetoad\Logger\Traits\Trackable;

class Model {
class TrackedModel extends Model implements TrackableContract {
use Trackable;

// Tracked models must implement this function
public function trackableOwnerResolver(): ?Model
{
//
}
}
```

The issue with this, is a record without an owner association is frankly useless. However, you may access a record that has no foreign key or relation to its owner model. Tracking that makes auditing quite useless.

For this reason, we've developed the notion of custom resolvers. They must be implemented for each Entity tracked, which (if tracked) must be in the `morphs` table in the above settings.

#### HasLoggerRelationships Trait

For models with the `Trackable` trait, you may add the `HasLoggerRelationships` trait to them to query retrieval and mutation audit records:

```php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
use Sourcetoad\Logger\Contracts\Trackable as TrackableContract;
use Sourcetoad\Logger\Models\AuditChange;
use Sourcetoad\Logger\Models\AuditModel;
use Sourcetoad\Logger\Models\Relations\LoggerMorphMany;
use Sourcetoad\Logger\Traits\Trackable;
use Sourcetoad\Logger\Traits\HasLoggerRelationships;

/**
* @property-read Collection<int, AuditChange> $auditChanges
* @property-read Collection<int, AuditModel> $auditModels
*/
class TrackedModel extends Model implements TrackableContract {
use Trackable;
use HasLoggerRelationships;

/**
* Collection of mutation logs of this model
*
* @return LoggerMorphMany<AuditChange>
*/
public function auditChanges(): LoggerMorphMany
{
return $this->loggerMorphMany(AuditChange::class, 'entity');
}

/**
* Collection of accesses of this model
*
* @return LoggerMorphMany<AuditModel>
*/
public function auditModels(): LoggerMorphMany
{
return $this->loggerMorphMany(AuditModel::class, 'entity');
}
}
```

#### Custom Resolvers

#### Cron
Expand All @@ -91,13 +149,13 @@ Schedule::command('logger:audit-resolver')
->withoutOverlapping();
```

This will run taking 200 items of both changes and retrieved models. It will identify the user associated with them. The functions for each individual model should be easy.
This will run taking 200 items of both changes and retrieved models. It will identify the owner associated with the model through the `Trackable` contract. The functions for each individual model should be easy.

```php
public function trackableUserResolver()
public function trackableOwnerResolver(): Owner
{
return $this->object->relation->user_id;
return $this->object->relation->owner;
}
```

As you can see, we have to traverse whatever relation/property we need in order to relate the model at hand to a user. If there is no match, you probably shouldn't be logging it.
As you can see, we have to traverse whatever relation/property we need in order to relate the model at hand to an owner. If there is no match, you probably shouldn't be logging it.
9 changes: 4 additions & 5 deletions config/logger.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<?php

use Sourcetoad\Logger\Enums\ModelMapping;

return [
/*
|--------------------------------------------------------------------------
Expand All @@ -13,7 +11,7 @@
*/

'user' => [
'table' => 'users',
'table' => 'users',
'foreign_key' => 'id',
'foreign_key_type' => 'bigInteger',
],
Expand All @@ -28,6 +26,7 @@
|
*/
'morphs' => [
ModelMapping::USER => 'App\User'
]
0 => App\Models\User::class,
],

];
34 changes: 4 additions & 30 deletions database/migrations/2019_03_35_000001_create_logger_tables.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,43 +71,23 @@ public function up(): void
$table->bigInteger('activity_id', false, true);
$table->mediumInteger('entity_type', false, true);
$table->integer('entity_id', false, true);
$table->addColumn(
config('activity-logger.user.foreign_key_type', 'bigInteger'),
'user_id',
[
'autoIncrement' => false,
'unsigned' => true,
]
)
->nullable();
$table->integer('owner_type')->nullable();
$table->integer('owner_id', false, true)->nullable();

$table
->foreign('activity_id')
->references('id')
->on('audit_activities')
->onDelete('RESTRICT');

$table
->foreign('user_id')
->references(config('activity-logger.user.foreign_key', 'id'))
->on(config('activity-logger.user.table', 'users'))
->onDelete('RESTRICT');
});

Schema::create('audit_changes', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('activity_id', false, true);
$table->mediumInteger('entity_type', false, true);
$table->integer('entity_id', false, true);
$table->addColumn(
config('activity-logger.user.foreign_key_type', 'bigInteger'),
'user_id',
[
'autoIncrement' => false,
'unsigned' => true,
]
)
->nullable();
$table->integer('owner_type')->nullable();
$table->integer('owner_id', false, true)->nullable();

$table->json('fields');

Expand All @@ -116,12 +96,6 @@ public function up(): void
->references('id')
->on('audit_activities')
->onDelete('RESTRICT');

$table
->foreign('user_id')
->references(config('activity-logger.user.foreign_key', 'id'))
->on(config('activity-logger.user.table', 'users'))
->onDelete('RESTRICT');
});

Schema::table('audit_activities', function (Blueprint $table) {
Expand Down
20 changes: 12 additions & 8 deletions src/Commands/AuditModelResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,26 @@ class AuditModelResolver extends Command

public function handle(): void
{
AuditChange::query()->where('processed', false)->chunkById(200, function ($items) {
/** @var AuditChange $item */
AuditChange::query()->where('processed', false)->with('entity')->chunkById(200, function ($items) {
/** @var AuditChange[] $items */
foreach ($items as $item) {
$id = AuditResolver::findUserId($item->entity);
$owner = AuditResolver::findOwner($item->entity);

$item->processed = true;
$item->user_id = $id;
$item->owner_id = $owner?->getKey();
$item->owner_type = $owner?->getMorphClass();
$item->saveOrFail();
}
});

AuditModel::query()->where('processed', false)->chunkById(200, function ($items) {
/** @var AuditModel $item */
AuditModel::query()->where('processed', false)->with('entity')->chunkById(200, function ($items) {
/** @var AuditModel[] $items */
foreach ($items as $item) {
$id = AuditResolver::findUserId($item->entity);
$owner = AuditResolver::findOwner($item->entity);

$item->processed = true;
$item->user_id = $id;
$item->owner_id = $owner?->getKey();
$item->owner_type = $owner?->getMorphClass();
$item->saveOrFail();
}
});
Expand Down
12 changes: 12 additions & 0 deletions src/Contracts/Trackable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Sourcetoad\Logger\Contracts;

use Illuminate\Database\Eloquent\Model;

interface Trackable
{
public function trackableOwnerResolver(): ?Model;
}
9 changes: 0 additions & 9 deletions src/Enums/ModelMapping.php

This file was deleted.

10 changes: 6 additions & 4 deletions src/Helpers/AuditResolver.php
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
<?php

declare(strict_types = 1);

namespace Sourcetoad\Logger\Helpers;

use Illuminate\Database\Eloquent\Model;
use Sourcetoad\Logger\Contracts\Trackable;

class AuditResolver
{
public static function findUserId(?Model $model): ?int
public static function findOwner(?Trackable $model): ?Model
{
if (empty($model)) {
return null;
}

$id = $model->trackableUserResolver();
$owner = $model->trackableOwnerResolver();

if (empty($id)) {
if (empty($owner)) {
return null;
}

return $id;
return $owner;
}
}
2 changes: 1 addition & 1 deletion src/Logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Logger
private static array $retrievedModels = [];
private static array $changedModels = [];

public static string $userModel = 'App/User';
public static string $userModel = App\Models\User::class;

public function logSuccessfulLogin(): AuditActivity
{
Expand Down
4 changes: 2 additions & 2 deletions src/Models/AuditActivity.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Sourcetoad\Logger\Models;

use App\User;
use App\Models\User;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand Down Expand Up @@ -103,7 +103,7 @@ public function getHumanActivityAttribute(): string
default => throw new Exception('Unknown enum type: ' . $this->type),
};
}

//--------------------------------------------------------------------------------------------------------------
// Relations
//--------------------------------------------------------------------------------------------------------------
Expand Down
16 changes: 9 additions & 7 deletions src/Models/AuditChange.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@

namespace Sourcetoad\Logger\Models;

use App\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Sourcetoad\Logger\Logger;
use Sourcetoad\Logger\Contracts\Trackable;
use Sourcetoad\Logger\Traits\Immutable;

/**
Expand All @@ -17,12 +16,13 @@
* @property int $activity_id
* @property int $entity_type
* @property int $entity_id
* @property int|null $user_id
* @property int|null $owner_type
* @property int|null $owner_id
* @property int|null $key_id
* @property bool $processed
* @property-read AuditKey $key
* @property-read User|null $user
* @property-read Model $entity
* @property-read Model|null $owner
* @property-read Trackable $entity
*/
class AuditChange extends BaseModel
{
Expand Down Expand Up @@ -50,16 +50,18 @@ class AuditChange extends BaseModel
// Relations
//--------------------------------------------------------------------------------------------------------------

public function user(): BelongsTo
/** @return MorphTo<Model, self> */
public function owner(): MorphTo
{
return $this->belongsTo(Logger::$userModel);
return $this->morphTo();
}

public function key(): BelongsTo
{
return $this->belongsTo(AuditKey::class);
}

/** @return MorphTo<Trackable, self> */
public function entity(): MorphTo
{
return $this->morphTo();
Expand Down
Loading

0 comments on commit 605173b

Please sign in to comment.