feature/audit-logger #7
@ -7,6 +7,7 @@ const ROOT = __DIR__;
 | 
			
		||||
class Container
 | 
			
		||||
{
 | 
			
		||||
    static SokoWeb\Interfaces\Database\IConnection $dbConnection;
 | 
			
		||||
    static SokoWeb\Interfaces\Database\IAuditLogger $auditLogger;
 | 
			
		||||
    static SokoWeb\Routing\RouteCollection $routeCollection;
 | 
			
		||||
    static SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler;
 | 
			
		||||
    static SokoWeb\Interfaces\Request\IRequest $request;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										75
									
								
								src/Database/AuditLoggerBase.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/Database/AuditLoggerBase.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,75 @@
 | 
			
		||||
<?php namespace SokoWeb\Database;
 | 
			
		||||
 | 
			
		||||
use SokoWeb\Interfaces\Database\IAuditLogger;
 | 
			
		||||
use SokoWeb\Interfaces\Database\IConnection;
 | 
			
		||||
 | 
			
		||||
abstract class AuditLoggerBase implements IAuditLogger
 | 
			
		||||
{
 | 
			
		||||
    const LOG_TYPE_INSERT = 'insert';
 | 
			
		||||
    const LOG_TYPE_UPDATE = 'update';
 | 
			
		||||
    const LOG_TYPE_DELETE = 'delete';
 | 
			
		||||
 | 
			
		||||
    private IConnection $connection;
 | 
			
		||||
 | 
			
		||||
    private string $logTable;
 | 
			
		||||
 | 
			
		||||
    public function __construct(IConnection $connection, string $logTable)
 | 
			
		||||
    {
 | 
			
		||||
        $this->connection = $connection;
 | 
			
		||||
        $this->logTable = $logTable;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function logInsert(string $localTable, $localId): void
 | 
			
		||||
    {
 | 
			
		||||
        $data = [
 | 
			
		||||
            'local_table' => $localTable,
 | 
			
		||||
            'local_id' => $localId,
 | 
			
		||||
            'type' => static::LOG_TYPE_INSERT,
 | 
			
		||||
            'modifier_id' => $this->getModifierId(),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $query = 'INSERT INTO ' . Utils::backtick($this->logTable) . ' SET ' . Utils::generateColumnsWithBinding(array_keys($data));
 | 
			
		||||
        $stmt = $this->connection->prepare($query);
 | 
			
		||||
        $stmt->execute($data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function logUpdate(string $localTable, $localId, array $diff): void
 | 
			
		||||
    {
 | 
			
		||||
        $data = [
 | 
			
		||||
            'local_table' => $localTable,
 | 
			
		||||
            'local_id' => $localId,
 | 
			
		||||
            'type' => static::LOG_TYPE_UPDATE,
 | 
			
		||||
            'modifier_id' => $this->getModifierId(),
 | 
			
		||||
            'column' => null,
 | 
			
		||||
            'old' => null,
 | 
			
		||||
            'new' => null,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $query = 'INSERT INTO ' . Utils::backtick($this->logTable) . ' SET ' . Utils::generateColumnsWithBinding(array_keys($data));
 | 
			
		||||
        $stmt = $this->connection->prepare($query);
 | 
			
		||||
 | 
			
		||||
        foreach ($diff as $name => $values) {
 | 
			
		||||
            $data['column'] = $name;
 | 
			
		||||
            $data['old'] = $values['old'];
 | 
			
		||||
            $data['new'] = $values['new'];
 | 
			
		||||
            $stmt->execute($data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function logDelete(string $localTable, $localId, array $attributes): void
 | 
			
		||||
    {
 | 
			
		||||
        $data = [
 | 
			
		||||
            'local_table' => $localTable,
 | 
			
		||||
            'local_id' => $localId,
 | 
			
		||||
            'type' => static::LOG_TYPE_DELETE,
 | 
			
		||||
            'modifier_id' => $this->getModifierId(),
 | 
			
		||||
            'old' => $attributes,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $query = 'INSERT INTO ' . Utils::backtick($this->logTable) . ' SET ' . Utils::generateColumnsWithBinding(array_keys($data));
 | 
			
		||||
        $stmt = $this->connection->prepare($query);
 | 
			
		||||
        $stmt->execute($data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract protected function getModifierId();
 | 
			
		||||
}
 | 
			
		||||
@ -2,6 +2,8 @@
 | 
			
		||||
 | 
			
		||||
use SokoWeb\Interfaces\Database\IConnection;
 | 
			
		||||
use SokoWeb\Database\Utils;
 | 
			
		||||
use SokoWeb\Interfaces\Database\IAuditLogger;
 | 
			
		||||
use SokoWeb\Interfaces\Database\IResultSet;
 | 
			
		||||
 | 
			
		||||
class Modify
 | 
			
		||||
{
 | 
			
		||||
@ -9,6 +11,8 @@ class Modify
 | 
			
		||||
 | 
			
		||||
    private string $table;
 | 
			
		||||
 | 
			
		||||
    private ?IAuditLogger $auditLogger;
 | 
			
		||||
 | 
			
		||||
    private string $idName = 'id';
 | 
			
		||||
 | 
			
		||||
    private array $attributes = [];
 | 
			
		||||
@ -17,10 +21,13 @@ class Modify
 | 
			
		||||
 | 
			
		||||
    private bool $autoIncrement = true;
 | 
			
		||||
 | 
			
		||||
    public function __construct(IConnection $connection, string $table)
 | 
			
		||||
    private ?array $diff = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(IConnection $connection, string $table, ?IAuditLogger $auditLogger = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->connection = $connection;
 | 
			
		||||
        $this->table = $table;
 | 
			
		||||
        $this->auditLogger = $auditLogger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setIdName(string $idName): Modify
 | 
			
		||||
@ -65,6 +72,13 @@ class Modify
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setDiff(array $diff): Modify
 | 
			
		||||
    {
 | 
			
		||||
        $this->diff = $diff;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->attributes[$this->idName];
 | 
			
		||||
@ -89,6 +103,10 @@ class Modify
 | 
			
		||||
 | 
			
		||||
        $stmt = $this->connection->prepare($query);
 | 
			
		||||
        $stmt->execute([$this->idName => $this->attributes[$this->idName]]);
 | 
			
		||||
 | 
			
		||||
        if ($this->auditLogger !== null) {
 | 
			
		||||
            $this->auditLogger->logDelete($this->table, $this->attributes[$this->idName], $this->attributes);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function insert(): void
 | 
			
		||||
@ -99,7 +117,7 @@ class Modify
 | 
			
		||||
            $this->attributes[$this->idName] = $this->generateKey();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $set = $this->generateColumnsWithBinding(array_keys($this->attributes));
 | 
			
		||||
        $set = Utils::generateColumnsWithBinding(array_keys($this->attributes));
 | 
			
		||||
 | 
			
		||||
        $query = 'INSERT INTO ' . Utils::backtick($this->table) . ' SET ' . $set;
 | 
			
		||||
 | 
			
		||||
@ -109,32 +127,62 @@ class Modify
 | 
			
		||||
        if ($this->autoIncrement) {
 | 
			
		||||
            $this->attributes[$this->idName] = $this->connection->lastId();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($this->auditLogger !== null) {
 | 
			
		||||
            $this->auditLogger->logInsert($this->table, $this->attributes[$this->idName]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function update(): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->auditLogger !== null) {
 | 
			
		||||
            $this->generateDiff();
 | 
			
		||||
            if (count($this->diff) === 0) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $attributes = $this->attributes;
 | 
			
		||||
        unset($attributes[$this->idName]);
 | 
			
		||||
 | 
			
		||||
        $set = $this->generateColumnsWithBinding(array_keys($attributes));
 | 
			
		||||
        $set = Utils::generateColumnsWithBinding(array_keys($attributes));
 | 
			
		||||
 | 
			
		||||
        $query = 'UPDATE ' . Utils::backtick($this->table) . ' SET ' . $set . ' WHERE ' . Utils::backtick($this->idName) . '=?';
 | 
			
		||||
 | 
			
		||||
        $stmt = $this->connection->prepare($query);
 | 
			
		||||
        $stmt->execute(array_merge($attributes, [$this->idName => $this->attributes[$this->idName]]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function generateColumnsWithBinding(array $columns): string
 | 
			
		||||
    {
 | 
			
		||||
        array_walk($columns, function(&$value, $key) {
 | 
			
		||||
            $value = Utils::backtick($value) . '=?';
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return implode(',', $columns);
 | 
			
		||||
        if ($this->auditLogger !== null) {
 | 
			
		||||
            $this->auditLogger->logUpdate($this->table, $this->attributes[$this->idName], $this->diff);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function generateKey(): string
 | 
			
		||||
    {
 | 
			
		||||
        return substr(hash('sha256', serialize($this->attributes) . random_bytes(5) . microtime()), 0, 7);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function generateDiff(): void
 | 
			
		||||
    {
 | 
			
		||||
        if (isset($this->diff)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->diff = [];
 | 
			
		||||
        $original = $this->readFromDb(array_keys($this->attributes));
 | 
			
		||||
        foreach ($original as $key => $value) {
 | 
			
		||||
            if ($value !== $this->attributes[$key]) {
 | 
			
		||||
                $this->diff[$key] = ['old' => $value, 'new' => $this->attributes[$key]];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function readFromDb($columns): array
 | 
			
		||||
    {
 | 
			
		||||
        return (new Select($this->connection, $this->table))
 | 
			
		||||
            ->columns($columns)
 | 
			
		||||
            ->where($this->idName, '=', $this->attributes[$this->idName])
 | 
			
		||||
            ->execute()
 | 
			
		||||
            ->fetch(IResultSet::FETCH_ASSOC);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,4 +5,13 @@ class Utils {
 | 
			
		||||
    {
 | 
			
		||||
        return '`' . $name . '`';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function generateColumnsWithBinding(array $columns): string
 | 
			
		||||
    {
 | 
			
		||||
        array_walk($columns, function(&$value, $key) {
 | 
			
		||||
            $value = static::backtick($value) . '=?';
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return implode(',', $columns);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								src/Interfaces/Database/IAuditLogger.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/Interfaces/Database/IAuditLogger.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
<?php namespace SokoWeb\Interfaces\Database;
 | 
			
		||||
 | 
			
		||||
interface IAuditLogger
 | 
			
		||||
{
 | 
			
		||||
    public function logInsert(string $localTable, $localId): void;
 | 
			
		||||
 | 
			
		||||
    public function logUpdate(string $localTable, $localId, array $diff): void;
 | 
			
		||||
 | 
			
		||||
    public function logDelete(string $localTable, $localId, array $attributes): void;
 | 
			
		||||
}
 | 
			
		||||
@ -117,19 +117,23 @@ class PersistentDataManager
 | 
			
		||||
        $modified = $model->toArray();
 | 
			
		||||
        $id = $model->getId();
 | 
			
		||||
 | 
			
		||||
        $modify = new Modify(\Container::$dbConnection, $model::getTable());
 | 
			
		||||
        $modify = new Modify(\Container::$dbConnection, $model::getTable(), \Container::$auditLogger);
 | 
			
		||||
 | 
			
		||||
        if ($id !== null) {
 | 
			
		||||
            $original = $model->getSnapshot();
 | 
			
		||||
            $diff = [];
 | 
			
		||||
 | 
			
		||||
            foreach ($original as $key => $value) {
 | 
			
		||||
                if ($value === $modified[$key]) {
 | 
			
		||||
                    unset($modified[$key]);
 | 
			
		||||
                } else {
 | 
			
		||||
                    $diff[$key] = ['old' => $value, 'new' => $modified[$key]];
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (count($modified) > 0) {
 | 
			
		||||
                $modify->setId($id);
 | 
			
		||||
                $modify->setDiff($diff);
 | 
			
		||||
                $modify->fill($modified);
 | 
			
		||||
                $modify->save();
 | 
			
		||||
            }
 | 
			
		||||
@ -145,8 +149,9 @@ class PersistentDataManager
 | 
			
		||||
 | 
			
		||||
    public function deleteFromDb(Model $model): void
 | 
			
		||||
    {
 | 
			
		||||
        $modify = new Modify(\Container::$dbConnection, $model::getTable());
 | 
			
		||||
        $modify = new Modify(\Container::$dbConnection, $model::getTable(), \Container::$auditLogger);
 | 
			
		||||
        $modify->setId($model->getId());
 | 
			
		||||
        $modify->fill($model->toArray());
 | 
			
		||||
        $modify->delete();
 | 
			
		||||
 | 
			
		||||
        $model->setId(null);
 | 
			
		||||
 | 
			
		||||
@ -15,9 +15,11 @@ $dotenv->load();
 | 
			
		||||
class Container
 | 
			
		||||
{
 | 
			
		||||
    static SokoWeb\Interfaces\Database\IConnection $dbConnection;
 | 
			
		||||
    static SokoWeb\Interfaces\Database\IAuditLogger $auditLogger;
 | 
			
		||||
    static SokoWeb\Routing\RouteCollection $routeCollection;
 | 
			
		||||
    static SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler;
 | 
			
		||||
    static SokoWeb\Interfaces\Request\IRequest $request;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Container::$dbConnection = new SokoWeb\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
 | 
			
		||||
Container::$auditLogger = new {app}\Database\AuditLogger(Container::$dbConnection, 'audit_log');
 | 
			
		||||
 | 
			
		||||
@ -13,3 +13,16 @@ CREATE TABLE `users` (
 | 
			
		||||
  UNIQUE KEY `email` (`email`),
 | 
			
		||||
  UNIQUE KEY `google_sub` (`google_sub`)
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
 | 
			
		||||
 | 
			
		||||
CREATE TABLE `audit_log` (
 | 
			
		||||
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 | 
			
		||||
  `local_table` varchar(255) NOT NULL,
 | 
			
		||||
  `local_id` int(10) unsigned NOT NULL,
 | 
			
		||||
  `type` enum('insert','update','delete') NOT NULL,
 | 
			
		||||
  `date` timestamp NOT NULL DEFAULT current_timestamp(),
 | 
			
		||||
  `modifier_id` int(10) unsigned NULL,
 | 
			
		||||
  `column` varchar(255) NULL,
 | 
			
		||||
  `old` text NULL,
 | 
			
		||||
  `new` text NULL,
 | 
			
		||||
  PRIMARY KEY (`id`)
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								templates/src/Database/AuditLogger.php.tpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								templates/src/Database/AuditLogger.php.tpl
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
<?php namespace {app}\Database;
 | 
			
		||||
 | 
			
		||||
use SokoWeb\Database\AuditLoggerBase;
 | 
			
		||||
 | 
			
		||||
class AuditLogger extends AuditLoggerBase
 | 
			
		||||
{
 | 
			
		||||
    protected function getModifierId()
 | 
			
		||||
    {
 | 
			
		||||
        \Container::$request->user()->getUniqueId();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user