-
Notifications
You must be signed in to change notification settings - Fork 6
feat: add PostgreSQL driver support #4
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,237 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace iamfarhad\LaravelAuditLog\Drivers; | ||
|
|
||
| use iamfarhad\LaravelAuditLog\Contracts\AuditDriverInterface; | ||
| use iamfarhad\LaravelAuditLog\Contracts\AuditLogInterface; | ||
| use iamfarhad\LaravelAuditLog\Models\EloquentAuditLog; | ||
| use Illuminate\Database\Schema\Blueprint; | ||
| use Illuminate\Support\Facades\Schema; | ||
| use Illuminate\Support\Str; | ||
|
|
||
| final class PostgreSQLDriver implements AuditDriverInterface | ||
| { | ||
| private string $tablePrefix; | ||
|
|
||
| private string $tableSuffix; | ||
|
|
||
| private array $config; | ||
|
|
||
| private string $connection; | ||
|
|
||
| /** | ||
| * Cache for table existence checks to avoid repeated schema queries. | ||
| */ | ||
| private static array $existingTables = []; | ||
|
|
||
| /** | ||
| * Cache for configuration values to avoid repeated config() calls. | ||
| */ | ||
| private static ?array $configCache = null; | ||
|
|
||
| public function __construct(?string $connection = null) | ||
| { | ||
| $this->config = self::getConfigCache(); | ||
| $this->connection = $connection ?? $this->config['drivers']['postgresql']['connection'] ?? config('database.default'); | ||
| $this->tablePrefix = $this->config['drivers']['postgresql']['table_prefix'] ?? 'audit_'; | ||
| $this->tableSuffix = $this->config['drivers']['postgresql']['table_suffix'] ?? '_logs'; | ||
| } | ||
|
|
||
| /** | ||
| * Get cached configuration to avoid repeated config() calls. | ||
| */ | ||
| private static function getConfigCache(): array | ||
| { | ||
| if (self::$configCache === null) { | ||
| self::$configCache = config('audit-logger'); | ||
| } | ||
|
|
||
| return self::$configCache; | ||
| } | ||
|
|
||
| /** | ||
| * Validate that the entity type is a valid class. | ||
| * In testing environment, we allow fake class names for flexibility. | ||
| */ | ||
| private function validateEntityType(string $entityType): void | ||
| { | ||
| // Skip validation in testing environment to allow fake class names | ||
| if (app()->environment('testing')) { | ||
| return; | ||
| } | ||
|
|
||
| if (! class_exists($entityType)) { | ||
| throw new \InvalidArgumentException("Entity type '{$entityType}' is not a valid class."); | ||
| } | ||
| } | ||
|
|
||
| public function store(AuditLogInterface $log): void | ||
| { | ||
| $this->validateEntityType($log->getEntityType()); | ||
| $tableName = $this->getTableName($log->getEntityType()); | ||
|
|
||
| $this->ensureStorageExists($log->getEntityType()); | ||
|
|
||
| try { | ||
| $model = EloquentAuditLog::forEntity(entityClass: $log->getEntityType()); | ||
| $model->setConnection($this->connection); | ||
| $model->fill([ | ||
| 'entity_id' => $log->getEntityId(), | ||
| 'action' => $log->getAction(), | ||
| 'old_values' => $log->getOldValues(), // Remove manual json_encode - let Eloquent handle it | ||
| 'new_values' => $log->getNewValues(), // Remove manual json_encode - let Eloquent handle it | ||
| 'causer_type' => $log->getCauserType(), | ||
| 'causer_id' => $log->getCauserId(), | ||
| 'metadata' => $log->getMetadata(), // Remove manual json_encode - let Eloquent handle it | ||
| 'created_at' => $log->getCreatedAt(), | ||
| 'source' => $log->getSource(), | ||
| ]); | ||
| $model->save(); | ||
| } catch (\Exception $e) { | ||
| throw $e; | ||
| } | ||
|
Comment on lines
+77
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The $model = EloquentAuditLog::forEntity(entityClass: $log->getEntityType());
$model->setConnection($this->connection);
$model->fill([
'entity_id' => $log->getEntityId(),
'action' => $log->getAction(),
'old_values' => $log->getOldValues(), // Remove manual json_encode - let Eloquent handle it
'new_values' => $log->getNewValues(), // Remove manual json_encode - let Eloquent handle it
'causer_type' => $log->getCauserType(),
'causer_id' => $log->getCauserId(),
'metadata' => $log->getMetadata(), // Remove manual json_encode - let Eloquent handle it
'created_at' => $log->getCreatedAt(),
'source' => $log->getSource(),
]);
$model->save(); |
||
| } | ||
|
|
||
| /** | ||
| * Store multiple audit logs using Eloquent models with proper casting. | ||
| * | ||
| * @param array<AuditLogInterface> $logs | ||
| */ | ||
| public function storeBatch(array $logs): void | ||
| { | ||
| if (empty($logs)) { | ||
| return; | ||
| } | ||
|
|
||
| // Group logs by entity type (and thus by table) | ||
| $groupedLogs = []; | ||
| foreach ($logs as $log) { | ||
| $this->validateEntityType($log->getEntityType()); | ||
| $entityType = $log->getEntityType(); | ||
| $groupedLogs[$entityType][] = $log; | ||
| } | ||
|
|
||
| // Process each entity type separately using Eloquent models to leverage casting | ||
| foreach ($groupedLogs as $entityType => $entityLogs) { | ||
| $this->ensureStorageExists($entityType); | ||
|
|
||
| // Use Eloquent models to leverage automatic JSON casting | ||
| foreach ($entityLogs as $log) { | ||
| $model = EloquentAuditLog::forEntity(entityClass: $entityType); | ||
| $model->setConnection($this->connection); | ||
| $model->fill([ | ||
| 'entity_id' => $log->getEntityId(), | ||
| 'action' => $log->getAction(), | ||
| 'old_values' => $log->getOldValues(), // Eloquent casting handles JSON encoding | ||
| 'new_values' => $log->getNewValues(), // Eloquent casting handles JSON encoding | ||
| 'causer_type' => $log->getCauserType(), | ||
| 'causer_id' => $log->getCauserId(), | ||
| 'metadata' => $log->getMetadata(), // Eloquent casting handles JSON encoding | ||
| 'created_at' => $log->getCreatedAt(), | ||
| 'source' => $log->getSource(), | ||
| ]); | ||
| $model->save(); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+102
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The For a true batch operation, you should consider collecting all log data into an array and using a single |
||
|
|
||
| public function createStorageForEntity(string $entityClass): void | ||
| { | ||
| $this->validateEntityType($entityClass); | ||
| $tableName = $this->getTableName($entityClass); | ||
|
|
||
| Schema::connection($this->connection)->create($tableName, function (Blueprint $table) { | ||
| $table->id(); | ||
| $table->string('entity_id'); | ||
| $table->string('action'); | ||
| // PostgreSQL supports both json and jsonb. Using jsonb for better performance | ||
| $table->jsonb('old_values')->nullable(); | ||
| $table->jsonb('new_values')->nullable(); | ||
| $table->string('causer_type')->nullable(); | ||
| $table->string('causer_id')->nullable(); | ||
| $table->jsonb('metadata')->nullable(); | ||
| $table->timestamp('created_at'); | ||
| $table->string('source')->nullable(); | ||
| $table->timestamp('anonymized_at')->nullable(); | ||
|
|
||
| // Basic indexes | ||
| $table->index('entity_id'); | ||
| $table->index('causer_id'); | ||
| $table->index('created_at'); | ||
| $table->index('action'); | ||
| $table->index('anonymized_at'); | ||
|
|
||
| // Composite indexes for common query patterns | ||
| $table->index(['entity_id', 'action']); | ||
| $table->index(['entity_id', 'created_at']); | ||
| $table->index(['causer_id', 'action']); | ||
| $table->index(['action', 'created_at']); | ||
| }); | ||
|
|
||
| // Cache the newly created table | ||
| self::$existingTables[$tableName] = true; | ||
| } | ||
|
|
||
| public function storageExistsForEntity(string $entityClass): bool | ||
| { | ||
| $tableName = $this->getTableName($entityClass); | ||
|
|
||
| // Check cache first to avoid repeated schema queries | ||
| if (isset(self::$existingTables[$tableName])) { | ||
| return self::$existingTables[$tableName]; | ||
| } | ||
|
|
||
| // Check database and cache the result | ||
| $exists = Schema::connection($this->connection)->hasTable($tableName); | ||
| self::$existingTables[$tableName] = $exists; | ||
|
|
||
| return $exists; | ||
| } | ||
|
|
||
| /** | ||
| * Ensures the audit storage exists for the entity if auto_migration is enabled. | ||
| */ | ||
| public function ensureStorageExists(string $entityClass): void | ||
| { | ||
| $autoMigration = $this->config['auto_migration'] ?? true; | ||
| if ($autoMigration === false) { | ||
| return; | ||
| } | ||
|
|
||
| if (! $this->storageExistsForEntity($entityClass)) { | ||
| $this->createStorageForEntity($entityClass); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Clear the table existence cache and config cache. | ||
| * Useful for testing or when tables are dropped/recreated. | ||
| */ | ||
| public static function clearCache(): void | ||
| { | ||
| self::$existingTables = []; | ||
| self::$configCache = null; | ||
| } | ||
|
|
||
| /** | ||
| * Clear only the table existence cache. | ||
| */ | ||
| public static function clearTableCache(): void | ||
| { | ||
| self::$existingTables = []; | ||
| } | ||
|
|
||
| private function getTableName(string $entityType): string | ||
| { | ||
| // Extract class name without namespace | ||
| $className = Str::snake(class_basename($entityType)); | ||
|
|
||
| // Handle pluralization | ||
| $tableName = Str::plural($className); | ||
|
|
||
| return "{$this->tablePrefix}{$tableName}{$this->tableSuffix}"; | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
EloquentAuditLog::forEntity()method appears to be hardcoded to use themysqldriver's configuration for determining table names (i.e.,table_prefixandtable_suffix). This will cause thePostgreSQLDriverto generate incorrect table names if its prefix/suffix configuration differs from themysqldriver's settings, breaking the driver's functionality. This issue also affects the call on line 122.To resolve this,
EloquentAuditLog::forEntity()should be updated to accept the driver name (e.g., 'postgresql') and use it to retrieve the correct configuration. TheMySQLDriverwould also need to be updated to pass its driver name.