diff --git a/dummy.php b/dummy.php index f3e0693..0437e18 100644 --- a/dummy.php +++ b/dummy.php @@ -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; diff --git a/src/Database/AuditLoggerBase.php b/src/Database/AuditLoggerBase.php new file mode 100644 index 0000000..e50f43c --- /dev/null +++ b/src/Database/AuditLoggerBase.php @@ -0,0 +1,75 @@ +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(); +} diff --git a/src/Database/Query/Modify.php b/src/Database/Query/Modify.php index 1f554c8..abb1927 100755 --- a/src/Database/Query/Modify.php +++ b/src/Database/Query/Modify.php @@ -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); + } } diff --git a/src/Database/Utils.php b/src/Database/Utils.php index 658b13c..c9c4885 100644 --- a/src/Database/Utils.php +++ b/src/Database/Utils.php @@ -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); + } } diff --git a/src/Interfaces/Database/IAuditLogger.php b/src/Interfaces/Database/IAuditLogger.php new file mode 100644 index 0000000..1bb8d6d --- /dev/null +++ b/src/Interfaces/Database/IAuditLogger.php @@ -0,0 +1,10 @@ +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);