mapik / audit-log
CakePHP的灵活且稳固的审计日志跟踪插件
Requires
- php: >=8.1
- cakephp/orm: ^5.0
Requires (Dev)
- cakephp/cakephp: ^5.0
- cakephp/cakephp-codesniffer: ^5.0
- cakephp/migrations: ^4.0
- friendsofcake/process-mq: dev-master
- phpstan/phpstan: ^1.8
- phpunit/phpunit: ^10.1.0
- psalm/phar: ^5.12
Suggests
- friendsofcake/process-mq: Use this package if you prefer sending the audit events to RabbitMQ
README
此插件是从 lorenzo/audit-stash 分支出来的。
尽管上面的插件非常适合存储变更历史,但它存在以下问题或没有我需要的所有功能。
-
删除事件时没有记录原始数据。
- 如果你在添加一些数据后添加该插件,这将很有用。
-
关联表记录没有正确保存(在这种情况下,我考虑了两个具有“hasMany”关系的模型)
- 目前,在CakePHP(4.x)ORM中,当存在“hasMany”关系(例如,考虑两个数据库表:items和item_attributes)时,
EntityTrait::extractOriginal(array $fields)
不会返回关联表(item_attributes)的原始数据,而是返回修改后的值。 - 还有另一个CakePHP(4.x)ORM的bug,它将关联的('hasMany')实体标记为脏的,即使关联表数据没有更改。
- 无法仅记录关联表的更改数据列
- 目前,在CakePHP(4.x)ORM中,当存在“hasMany”关系(例如,考虑两个数据库表:items和item_attributes)时,
-
当调用saveMany()保存多个实体时,无法记录审计日志
-
无法通过app.php设置AuditLog行为和/或Table Persister的一些常用配置
-
无法记录来自外键的人类友好数据字段
-
创建事件将相同的数据添加到'original'和'changed'列
-
'id'(主键)字段添加到'original'和'changed'数据中,除非你在每个模型类中将其列入黑名单。(主键作为单独的字段记录)
-
无法在数据库中存储用户友好的字段值。这有助于轻松识别记录;特别是当数据库只存储更改数据时。因此,我使用了CakePHP Model::setDisplayField()来检索用户友好的字段值。
因此,我决定从原始项目分支出来,并改进它以支持上述缺失的功能。
安装
您可以使用 composer 将此插件安装到您的CakePHP应用程序中,并在应用程序根目录中执行以下命令。
composer require mapik/audit-log
bin/cake plugin load AuditLog
如果您计划使用ElasticSearch作为存储引擎,请参阅 lorenzo/audit-stash
配置
表 / 常规数据库
如果您想使用常规数据库,即可以通过CakePHP ORM API使用的引擎,那么您可以使用此插件附带的自带表持久化器。
为此,您需要相应地配置AuditLog.persister
选项。在您的config/app.php
文件中添加以下配置
'AuditLog' => [ 'persister' => 'AuditLog\Persister\TablePersister' ]
然后,默认情况下,插件将尝试将日志存储在名为audit_logs
的表中,通过具有别名AuditLogs
的表类,您可以在需要时创建/覆盖它。
您可以在本插件的 config/migration
文件夹中找到一个迁移文件,您可以将其应用到您的数据库中,这将添加一个名为 audit_logs
的表,其中包含所有默认列。或者,您也可以自己创建迁移来创建表。之后,您可以迁移相应的表类。
如果您使用的是插件的默认迁移,可以使用以下命令创建表和模型类。
bin/cake migrations migrate -p AuditLog -t 20171018185609
bin/cake bake model AuditLogs
表持久化配置
表持久化支持各种配置选项,请参阅其API文档以获取更多信息。通常,配置可以通过其 config()
(或 setConfig()
)方法应用。
$this->addBehavior('AuditLog.AuditLog'); $this->behaviors()->get('AuditLog')->persister()->config([ 'extractMetaFields' => [ 'user.name' => 'username' ] ]);
此外,您还可以通过 app.php
设置一些常用配置。目前,插件支持 'extractMetaFields' 和 'blacklist'。
... 'AuditLog' => [ 'persister' => 'AuditLog\Persister\TablePersister', 'extractMetaFields' => [ 'user.username' => 'username', 'user.customer_id' => 'customer_id', ], 'blacklist' => ['customer_id'], ],
使用 AuditLog
在您的任何表类中启用审计日志,只需在 initialize()
函数中添加一个行为即可。
class ArticlesTable extends Table { public function initialize(array $config = []) { $this->setDisplayField('article_name'); ... $this->addBehavior('AuditLog.AuditLog'); } }
配置行为
默认情况下,AuditLog
行为会忽略您表中的某些字段,它默认忽略 id
、created
和 modified
字段。
class ArticlesTable extends Table { public function initialize(array $config = []) { $this->setDisplayField('article_name'); ... $this->addBehavior('AuditLog.AuditLog', [ 'blacklist' => ['created', 'modified', 'another_field_name'] ]); } }
如果您愿意,可以使用 whitelist
代替。这意味着只有该数组中列出的字段会被行为跟踪。
class ArticlesTable extends Table { public function initialize(array $config = []) { $this->setDisplayField('article_name'); ... $this->addBehavior('AuditLog.AuditLog', [ 'whitelist' => ['title', 'description', 'author_id'] ]); } }
如果您需要从相关表(即具有外键的表)中检索人类友好的数据字段,您可以设置 foreignKeys
如下。
public function initialize(array $config = []) { $this->setDisplayField('article_name'); ... $this->addBehavior('AuditLog.AuditLog', [ 'blacklist' => ['customer_id', 'product_id'], 'foreignKeys' => [ 'Categories' => 'name', // foreign key Model => human-friendly field name 'ProductStatuses' => 'status', ], 'unsetAssociatedEntityFieldsNotDirtyByFieldName' => [ 'associated_table_name' => 'field_name_in_associated_table' ] ]); }
如项目描述中所述,CakePHP (4.x) ORM即使在关联数据没有更改的情况下也会返回所有关联数据。因此,如果您需要从关联实体中删除未更改的数据,您需要设置 unsetAssociatedEntityFieldsNotDirtyByFieldName
,如上述示例所示。
存储登录用户
将触发更改的特定表的用户的标识符存储起来通常很有用。为此目的,AuditLog
提供了 RequestMetadata
监听器类,该类可以存储当前URL、IP和登录用户。您需要在应用程序中的 AppController::beforeFilter()
方法中添加此监听器。
use AuditLog\Meta\RequestMetadata; ... class AppController extends Controller { public function beforeFilter(Event $event) { ... $eventManager = $this->loadModel()->eventManager(); $identity = $this->request->getAttribute('identity'); if ($identity != null) { $eventManager->on( new RequestMetadata($this->request, [ 'username' => $identity['username'], 'customer_id' => $identity['customer_id'], ]) ); } } }
上述代码假设您将从控制器触发表操作,使用默认的Table类。如果您计划在同一控制器中为保存或删除使用其他Table类,建议您全局绑定监听器。
use AuditLog\Meta\RequestMetadata; use Cake\Event\EventManager; ... class AppController extends Controller { public function beforeFilter(Event $event) { ... $identity = $this->request->getAttribute('identity'); if ($identity != null) { EventManager::instance()->on( new RequestMetadata($this->request, [ 'username' => $identity['username'], 'customer_id' => $identity['customer_id'], ]) ); } } }
在日志中存储额外信息
AuditLog
还能够为每个已记录的事件存储任意数据。您可以使用 ApplicationMetadata
监听器或创建自己的。如果您选择使用 ApplicationMetadata
,则您的日志将包含存储的 app_name
键以及您可能提供的任何额外信息。您可以在应用程序中的任何位置配置此监听器,例如 bootstrap.php
文件或直接在您的 AppController 中。
use AuditLog\Meta\ApplicationMetadata; use Cake\Event\EventManager; EventManager::instance()->on(new ApplicationMetadata('my_blog_app', [ 'server' => $theServerID, 'extra' => $somExtraInformation, 'moon_phase' => $currentMoonPhase ]));
实现自己的元数据监听器非常简单,只需将其绑定到 AuditLog.beforeLog
事件。例如
EventManager::instance()->on('AuditLog.beforeLog', function ($event, array $logs) { foreach ($logs as $log) { $log->setMetaInfo($log->getMetaInfo() + ['extra' => 'This is extra data to be stored']); } });
实现您自己的持久化策略
使用不同的持久化引擎来存储审计日志有合理的理由。幸运的是,此插件允许您实现自己的存储引擎。这只需要实现 PersisterInterface
接口。
use AuditLog\PersisterInterface; class MyPersister implements PersisterInterface { public function logEvents(array $auditLogs) { foreach ($auditLogs as $log) { $eventType = $log->getEventType(); $data = [ 'timestamp' => $log->getTimestamp(), 'transaction' => $log->getTransactionId(), 'type' => $log->getEventType(), 'primary_key' => $log->getId(), 'display_value' => $event->getDisplayValue(), 'source' => $log->getSourceName(), 'parent_source' => $log->getParentSourceName(), 'original' => json_encode($log->getOriginal()), 'changed' => $eventType === 'delete' ? null : json_encode($log->getChanged()), 'meta' => json_encode($log->getMetaInfo()) ]; $storage = new MyStorage(); $storage->save($data); } } }
最后,您需要配置 AuditLog
以使用您的新持久化器。在 config/app.php
文件中添加以下行
'AuditLog' => [ 'persister' => 'App\Namespace\For\Your\Persister' ]
或者如果您使用的是独立版本
\Cake\Core\Configure::write('AuditLog.persister', 'App\Namespace\For\Your\DatabasePersister');
配置包含您持久化器的完全命名空间类名。
与事务查询一起工作
有时,您可能需要将多个数据库更改封装在一个事务中,以便在过程的一部分失败时可以回滚。为了在事务期间创建审计日志,需要一些额外的配置。首先创建文件 src/Model/Audit/AuditLog.php
,内容如下:
<?php namespace App\Model\Audit; use Cake\Utility\Text; use SplObjectStorage; class AuditLog { protected $_auditQueue; protected $_auditTransaction; public function __construct() { $this->_auditQueue = new SplObjectStorage; $this->_auditTransaction = Text::uuid(); } public function toSaveOptions() { return [ '_auditQueue' => $this->_auditQueue, '_auditTransaction' => $this->_auditTransaction ]; } }
在您想要使用 Connection::transactional()
的任何地方,您需要首先在文件顶部包含以下内容:
use App\Model\Audit\AuditLog; use Cake\Event\Event;
然后,您的交易应该类似于以下 BookmarksController 的示例:
$auditLog = new AuditLog(); $success = $this->Bookmarks->connection()->transactional(function () use ($trail) { $bookmark = $this->Bookmarks->newEntity(); $bookmark1->save($data1, $trail->toSaveOptions()); $bookmark2 = $this->Bookmarks->newEntity(); $bookmark2->save($data2, $trail->toSaveOptions()); ... $bookmarkN = $this->Bookmarks->newEntity(); $bookmarkN->save($dataN, $trail->toSaveOptions()); return true; }); if ($success) { $event = new Event('Model.afterCommit', $this->Bookmarks); $table->behaviors()->get('AuditLog')->afterCommit($event, $result, $auditLog->toSaveOptions()); }
这将保存您的对象的全部审计信息,以及任何相关数据的审计。请注意,$result
必须是一个对象的实例。不要更改文本 "Model.afterCommit"。
保存多个实体
按照上述部分创建文件 src/Model/Audit/AuditLog.php
... $auditLog = new AuditLog(); if ($this->Bookmarks->saveMany($entities, $auditLog->toSaveOptions())) { ... }