- 后端
【免费下载链接】laravel-activitylog
Log activity inside your Laravel app
本指南聚焦 laravel-activitylog 的进阶功能——活动批量日志(Activity Batch Logs)。一次用户操作往往引发一连串模型事件(如级联删除、队列任务、中间件链),默认情况下它们会被记录为彼此孤立的日志行;而批量日志用同一个batch_uuid把这一连串活动串成同一批次,从而在审计、追溯与回滚分析时能够一键聚合。读完本文,你将掌握LogBatch门面的全部五个方法(startBatch/endBatch/getUuid/setBatch/withinBatch/isOpen)、按批次查询的forBatch作用域、批次与数据库事务的对应语义,以及跨多个队列 Job 和多次请求共享同一批次 UUID 的完整实战方案,并了解底层实现(src/LogBatch.php)的真实工作机制。
为什么需要"批量"日志
laravel-activitylog 在默认配置下,每发生一次模型事件就写入一行activity_log记录。但在真实业务里,"一次操作"从来不是单条 SQL:
- 一个
User删除Author,级联软删除该Author拥有的所有Book; - 一个队列批次(Queue Batch)中多个 Job 依次执行,共同完成一个业务流程;
- 一个请求链路上经过多个中间件,每个中间件都可能触发日志。
如果没有批量机制,这些由同一初始动作引发的日志在数据库里互不关联,后续想"查出这次操作到底动了什么"只能靠时间戳猜测。批量日志通过给这批活动打上同一个batch UUID标签,使它们仍与同一个causer和同一个批次 UUID 绑定,实现语义上的"同一次操作"聚合。
这一设计直接体现在数据模型上:activity_log表新增一个可空的batch_uuid列(见迁移文件 add_batch_uuid_column_to_activity_log_table.php.stub,该列位于properties列之后),而 Activity 模型 的文档注释中也明确声明了batch_uuid属性。
基础用法:startBatch()与endBatch()
原文档给出的核心用法非常简洁:在活动发生前调用LogBatch::startBatch()开启批次,之后产生的所有活动都会通过 UUID 关联到该批次;活动结束后调用LogBatch::endBatch()关闭批次。
use Spatie\Activitylog\Facades\LogBatch; use Spatie\Activitylog\Models\Activity; LogBatch::startBatch(); $author = Author::create(['name' => 'Philip K. Dick']); $book = Book::create(['name' => 'A Scanner Brightly', 'author_id' => $author->id]); $book->update(['name' => 'A Scanner Darkly']); $author->delete(); LogBatch::endBatch();这段代码中Author与Book都实现了LogsActivity特质(src/Traits/LogsActivity.php),因此它们的created/updated/deleted事件都会自动写入活动日志。借助批次,像"删除 Author 时级联删除 Book"这类非显式动作所触发的日志,也会被统一归入同一个批次。
关键点在于:ActivityLogger在真正保存活动之前,会把当前批次的 UUID 写到活动实例上。查看 src/ActivityLogger.php 的getActivity()方法:
protected function getActivity(): ActivityContract { if (! $this->activity instanceof ActivityContract) { $this->activity = ActivitylogServiceProvider::getActivityModelInstance(); $this ->useLog($this->defaultLogName) ->withProperties([]) ->causedBy($this->causerResolver->resolve()); $this->activity->batch_uuid = $this->batch->getUuid(); } return $this->activity; }也就是说,写入batch_uuid的动作发生在日志落库之前:只要批次处于开启状态,任何通过activity()辅助函数(见 src/helpers.php)或LogsActivity特质触发的日志,都会自动带上当前批次 UUID;批次关闭(uuid为null)时,batch_uuid列为空。
按批次检索活动:getUuid()与forBatch()
批次关闭后,如果保存了批次的 UUID,就可以在任意时刻把该批次内的所有活动一次性取回。先在endBatch()之前调用LogBatch::getUuid()拿到批次 ID:
// ... started batch and other code $batchUuid = LogBatch::getUuid(); // save batch id to retrieve activities later LogBatch::endBatch(); $batchActivities = Activity::forBatch($batchUuid)->get();forBatch是 Activity 模型 提供的查询作用域(scope),实现是对batch_uuid列的等值过滤:
public function scopeForBatch(Builder $query, string $batchUuid): Builder { return $query->where('batch_uuid', $batchUuid); }模型还同时提供了hasBatch()作用域(src/Models/Activity.php),用于筛选所有"属于某个批次"的活动(whereNotNull('batch_uuid')),适合查询所有被批量记录的活动。
原文档给出了一个完整的运行示例,注意其中Author和Book都实现了LogsActivity特质:
use Spatie\Activitylog\Facades\LogBatch; use Spatie\Activitylog\Models\Activity; LogBatch::startBatch(); $author = Author::create(['name' => 'Philip K. Dick']); $book = Book::create(['name' => 'A Scanner Brightly', 'author_id' => $author->id]); $book->update(['name' => 'A Scanner Darkly']); $book2 = Book::create(['name' => 'Paycheck', 'author_id' => $author->id]); $author->delete(); $batchUuid = LogBatch::getUuid(); // save batch id to retrieve activities later LogBatch::endBatch(); $batchActivities = Activity::forBatch($batchUuid)->get(); var_dump($batchActivities); // A collection of Activity models... // They will be in order: Author;created, Book;created, Book;updated, // Book;created, Author;deleted, Book;deleted and Book;deleted值得注意的一点是:级联软删除产生的Book;deleted日志也被包含在批次内(示例末尾出现了两条Book;deleted)。这正是批量日志的核心价值——即使删除动作由数据库外键/应用逻辑触发、代码中没有显式调用activity()->log(),只要发生在批次开启期间,就会被归入同一 UUID。返回的是一个Activity模型集合,顺序即活动产生顺序。
批次开启的约束:与数据库事务同构的语义
原文档明确警告:同一请求内,在关闭当前批次之前不能开启新批次——在已有批次开启时再次调用startBatch()不会生成新 UUID,而是沿用当前批次的 UUID。
这一约束的语义与数据库事务完全同构:"开启的批次数量必须等于关闭的批次数量"。查看 src/LogBatch.php 的核心实现即可印证:
public function startBatch(): void { if (! $this->isOpen()) { $this->uuid = $this->generateUuid(); } $this->transactions++; } public function isOpen(): bool { return $this->transactions > 0; } public function endBatch(): void { $this->transactions = max(0, $this->transactions - 1); if ($this->transactions === 0) { $this->uuid = null; } }startBatch():仅当批次未开启时才生成新 UUID(generateUuid()使用Ramsey\Uuid\Uuid::uuid4()生成标准 UUID v4 字符串),随后无论是否已开启,transactions计数都加一;endBatch():计数减一(下限为 0),只有当计数归零时才把uuid清为null。
所以startBatch()与endBatch()必须成对出现:嵌套调用多次startBatch()后,需要同样次数的endBatch()才能真正关闭批次。这一点由 tests/LogBatchTest.php 的测试用例直接验证,例如:
- "will not generate new uuid if start already started batch":两次
startBatch()后两次getUuid()结果相等; - "will return null uuid if end batch that started twice properly":两次
startBatch()必须配合两次endBatch(),第二次endBatch()后getUuid()才返回null。
检查批次是否开启:isOpen()
在队列 Job 或中间件中,很容易在不知情的情况下"在一个已有批次内部再开新批次"。为避免破坏批次聚合,先确认当前状态再决定是否开启:
// in middleware LogBatch::startBatch(); //... Other middlewares if (LogBatch::isOpen()) { // do something }isOpen()返回transactions > 0的布尔值。结合上一节可以这样理解:只要历史上调用过startBatch()且尚未完全配对endBatch(),批次就处于开启状态。测试 tests/LogBatchTest.php 中的 "generates uuid after start and end batch properely" 等用例也验证了:正常配对后isOpen()为false,且getUuid()恢复为null。
跨多个 Job / 多次请求保持批次开启:setBatch()
默认的startBatch()生成的是随机 UUID,且批次状态随请求生命周期结束而释放。当遇到以下场景时就需要显式"指定"批次 ID:
- 一个队列批次(
Bus::batch)中的多个 Job 各自执行、各自记录活动,但你希望它们共享同一个LogBatch; - 希望跨多个请求持续累积同一批次的活动。
解决方案是LogBatch::setBatch($uuid):传入任意唯一值(通常是一个 UUID 字符串)作为批次标识。查看实现:
public function setBatch(string $uuid): void { $this->uuid = $uuid; $this->transactions = 1; }setBatch()不仅直接覆盖uuid,还把事务计数置为 1,使批次进入开启状态。原文档给出了一个完整的队列批次示例:
use Spatie\Activitylog\Facades\LogBatch; use Illuminate\Bus\Batch; use Illuminate\Support\Str; $uuid = Str::uuid(); Bus::batch([ // First job will open a batch new SomeJob('some value', $uuid), // pass uuid as a payload to the job new AnotherJob($uuid), // pass uuid as a payload to the job new WorkJob('work work work', $uuid), // pass uuid as a payload to the job ])->then(function (Batch $batch) { // All jobs completed successfully... })->catch(function (Batch $batch, Throwable $e) { // First batch job failure detected... })->finally(function (Batch $batch) use ($uuid) { // The batch has finished executing... LogBatch::getUuid() === $uuid // true LogBatch::endBatch(); })->dispatch(); // Later on.. Activity::forBatch($uuid)->get(); // all the activity that happend in the batch配套的 Job 实现(把 UUID 作为 Job 载荷传递,进入 Job 后开启批次并绑定指定 UUID):
class SomeJob { public function handle(string $value, ?string $batchUuid = null) { LogBatch::startBatch(); if ($batchUuid) LogBatch::setBatch($batchUuid); // other code .. } }这里建议startBatch()与setBatch()搭配使用:先开启批次(保证transactions计数正确),再覆盖为指定 UUID。队列批次的所有then/catch/finally回调都执行完毕后,在finally中调用endBatch()收尾,之后即可随时用Activity::forBatch($uuid)->get()拉取这批 Job 产生的全部活动。
回调式批次:withinBatch()
如果批量动作集中在某一段代码里,可以用LogBatch::withinBatch(Closure $callback)以闭包方式包裹,无需手动配对startBatch()/endBatch()。查看 src/LogBatch.php 的实现:
public function withinBatch(Closure $callback): mixed { $this->startBatch(); $result = $callback($this->getUuid()); $this->endBatch(); return $result; }它内部就是startBatch()→ 执行回调(并把 UUID 作为参数传给闭包)→endBatch()的封装,闭包返回值会被原样透传。原文档示例:
use Spatie\Activitylog\Facades\LogBatch; LogBatch::withinBatch(function (string $uuid) { $uuid; // 5cce9cb3-3144-4d35-9015-830cf0f20691 activity()->log('my message'); $item = NewsItem::create(['name' => 'new batch']); $item->update(['name' => 'updated']); $item->delete(); }); Activity::latest()->get(); // batch_uuid: 5cce9cb3-3144-4d35-9015-830cf0f20691闭包收到的$uuid即本次批次的 UUID(示例值为形如5cce9cb3-3144-4d35-9015-830cf0f20691的 UUID v4 字符串)。无论闭包中触发了多少次activity()->log()或多少模型事件,它们都会被盖上同一个batch_uuid。
底层原理:LogBatch 的注册与作用域
从源码结构看,LogBatch之所以能在"同一请求内"保持一致的批次状态,关键在 src/ActivitylogServiceProvider.php 的容器注册方式:
$this->app->bind(ActivityLogger::class); $this->app->scoped(LogBatch::class); $this->app->scoped(CauserResolver::class); $this->app->scoped(ActivityLogStatus::class);LogBatch以scoped 单例形式绑定到 Laravel 容器:在一个请求/队列 Job 的生命周期内,所有对LogBatch门面(src/Facades/LogBatch.php,门面访问器指向Spatie\Activitylog\LogBatch)的调用都共享同一个实例,因此uuid与transactions状态能在整个生命周期内持续累积。
门面提供的全部静态方法(与底层LogBatch类一一对应):
| 门面方法 | 签名 | 说明 |
|---|---|---|
startBatch() | void | 开启新批次;若批次已开启则仅递增事务计数,不生成新 UUID |
endBatch() | void | 关闭批次;计数归零时清空uuid |
getUuid() | ?string | 返回当前批次 UUID;批次关闭时为null |
setBatch(string $uuid) | void | 指定批次 UUID 并置事务计数为 1(用于跨 Job/请求复用批次) |
isOpen() | bool | 批次是否处于开启状态(transactions > 0) |
withinBatch(Closure $callback) | mixed | 在闭包内开启/关闭批次,并把 UUID 作为参数传入 |
需要注意的是:由于LogBatch是 scoped 绑定,跨请求持续累积同一批次的场景(如原文档"Keep LogBatch open during multiple jobs/requests")需要借助setBatch()显式传递 UUID——每个 Job/请求各自的 scoped 实例通过"指定同一 UUID"来维持语义上的同一批次。
数据库层面,批次列来自可选的迁移 add_batch_uuid_column_to_activity_log_table.php.stub,它会在activity_log表的properties列后新增一个可空的batch_uuid列(UUID 类型)。该迁移已由ActivitylogServiceProvider通过hasMigrations([...])注册,发布迁移后执行php artisan migrate即可应用。如果希望自定义表名或数据库连接,可在 config/activitylog.php 中通过table_name与database_connection配置项调整。
实战建议与注意事项
- 成对调用:把
startBatch()/endBatch()视为数据库事务一样成对维护,建议用try/finally或在withinBatch()闭包内包裹,确保异常路径下也能正确关闭批次(endBatch()内部有max(0, ...)下限保护,重复调用不会让计数变成负数)。 - 不要在已开启批次内盲目
startBatch():它不会生成新 UUID,会让新动作错误地混入当前批次;需要拆分批次时,先确认isOpen()为false。 - 跨 Job 共享批次:UUID 必须作为 Job 载荷显式传递,且建议在
handle()中先startBatch()再setBatch($uuid);整个Bus::batch结束后在finally回调里endBatch()。 - 查询聚合:用
Activity::forBatch($uuid)->get()取回同一批次全部活动,或用Activity::hasBatch()->get()取所有带批次标签的活动(见 Activity 模型)。 - 批次与事务的区别:批次只是日志数据的"逻辑分组标签",并不具备事务的原子性/回滚能力;它是与数据库事务"结构相似"的配对机制,但语义上仅用于日志聚合。
结合 tests/LogBatchTest.php 中的完整测试矩阵(UUID 生成、配对关闭、嵌套开启、setBatch覆盖等),以上规则均有源码级验证,可作为你编写自己业务代码时的行为参考。
- 后端
【免费下载链接】laravel-activitylog
Log activity inside your Laravel app
相关推荐
KubeEdge 边缘计算入门:从云端部署到设备状态同步的完整实践指南
KubeEdge 边缘计算入门:从云端部署到设备状态同步的完整实践指南 KubeEdge 是一个基于 Kubernetes 的边缘计算框架(CNCF 毕业项目)
云原生边缘计算物联网容器编排边缘网关Argos Translate 离线翻译库:完全离线的 40+ 语言对机器翻译,5 分钟跑通选型与落地
Argos Translate 离线翻译库:完全离线的 40+ 语言对机器翻译,5 分钟跑通选型与落地 —— 选型参考 × 落地手册 数据不出机房,这件事在很多
人工智能NLP本地部署Spatie Laravel Activitylog 高级用法:批量日志处理详解
Spatie Laravel Activitylog 高级用法:批量日志处理详解 引言:为什么需要批量日志处理? 在日常开发中,我们经常会遇到这样的场景:一个用
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考