定制 wezlo/filament-record-watcher 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

wezlo/filament-record-watcher

Composer 安装命令:

composer require wezlo/filament-record-watcher

包简介

Subscribe to Eloquent records and receive in-panel Filament notifications when they change, with per-watch condition rules and field diffs.

README 文档

README

Subscribe to individual Eloquent records and receive in-panel Filament notifications whenever they change — with the actor (who) and a field-level diff (what). Watches can carry conditions ("only if status changes", "only if amount > 10K"), can be paused, and live on a personal My Watches page scoped to the authenticated user. Every fan-out is also persisted to a permanent event log, so users can review the full change history even after dismissing notifications.

Highlights

  • One-click Watch / Unwatch actions for any Filament Resource record.
  • Conditions DSL — JSON rules with changed, =, !=, >, <, >=, <=, contains. ANDed. Empty = notify on any change.
  • In-panel notifications delivered through Filament's built-in databaseNotifications() channel — they appear in the bell dropdown.
  • Persistent event history — every fan-out writes a WatchEvent row keeping the actor + diff, so users can see what they missed even after clearing notifications.
  • My Watches page — auth-scoped, shows event count, supports pause / resume / unwatch / history.
  • Per-action permission gates via ->can() (boolean or closure).
  • Curated field selector — the condition modal renders a Select populated from a getWatchableFields() hook (auto-introspected by default).
  • Actor capture for queues / commands through WatchEngine::actingAs($user, fn () => …).
  • Extensible via the RecordWatchedChange Laravel event for email / Slack / audit pipelines.

Requirements

  • PHP 8.2+
  • Laravel 11+ / Laravel 13
  • Filament v4+
  • A panel with ->databaseNotifications() enabled (the package writes to Filament's database notifications channel)

Installation

The package is path-linked inside this monorepo. From the project root:

composer require wezlo/filament-record-watcher
php artisan migrate

The migrate step creates two tables: watches and watch_events.

Tailwind v4 — register the package's views

The package ships a Blade view (the History modal) that uses Tailwind utility classes. Tailwind v4 only scans paths it has been told about, so you must add an @source directive to your panel's theme CSS file (typically resources/css/filament/admin/theme.css):

@source '../../../../vendor/wezlo/filament-record-watcher/resources/views/**/*';

After editing the theme, rebuild your assets (npm run build or npm run dev). Without this line the History modal will render unstyled.

Register the plugin on each panel where you want the My Watches page:

use Wezlo\FilamentRecordWatcher\FilamentRecordWatcherPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->databaseNotifications() // required — watchers receive db notifications
        ->plugins([
            FilamentRecordWatcherPlugin::make(),
        ]);
}

Make a model watchable

Add the HasWatchers trait to any Eloquent model:

use Wezlo\FilamentRecordWatcher\Concerns\HasWatchers;

class Order extends Model
{
    use HasWatchers;
}

You can now:

$order->watchFor($user);                                  // subscribe with no conditions
$order->watchFor($user, [                                 // subscribe with rules
    ['field' => 'status', 'operator' => 'changed', 'value' => null],
    ['field' => 'amount', 'operator' => '>',       'value' => 10_000],
]);

$order->isWatchedBy($user);    // bool
$order->watches;               // MorphMany of Watch rows
$order->watchers();            // collection of distinct subscribed users

$order->unwatchFor($user);     // delete the user's subscription (cascades to events)

$order->update(['status' => 'paid']);   // → fans out notifications + events to all matching watchers

watchFor() is idempotent — calling it twice for the same (record, user) pair updates the existing row instead of creating a duplicate (enforced by a unique index).

Curating which fields users can build conditions on

The Field input in the condition modal is rendered as a searchable Select, populated from getWatchableFields(). The trait's default introspects the model's table and returns every column minus the primary key, deleted_at, and any plugin-level ignored columns (default updated_at, created_at).

Override the method on your model to expose a curated list with friendlier semantics:

class Order extends Model
{
    use HasWatchers;

    public function getWatchableFields(): array
    {
        return ['status', 'total', 'client_name', 'address'];
    }
}

Filament integration — WatchAction / UnwatchAction

Drop the actions into any Resource that uses a watchable model. They are two separate actions:

  • WatchAction — always visible. Label flips between "Watch" and "Edit watch" depending on whether the current user is already subscribed. Opens a modal with a Repeater of condition rows.
  • UnwatchAction — visible only when the current user is already subscribed. One-click confirmation modal.
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Wezlo\FilamentRecordWatcher\Actions\UnwatchAction;
use Wezlo\FilamentRecordWatcher\Actions\WatchAction;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('reference'),
            TextColumn::make('status')->badge(),
        ])
        ->recordActions([
            ViewAction::make(),
            EditAction::make(),
            WatchAction::make(),
            UnwatchAction::make(),
        ]);
}

Both actions also work in page header arrays (headerActions()) on view / edit pages.

Permissions

Both actions expose a ->can() method that accepts a boolean or a closure receiving (Model $record, ?Authenticatable $user) and returning bool. Returning false hides the action for that user / row. The check is ANDed with the action's built-in visibility rules.

WatchAction::make()
    ->can(fn (Order $record, ?Authenticatable $user) => $user?->can('view', $record) ?? false),

UnwatchAction::make()
    ->can(fn (Order $record) => auth()->user()->hasPermissionTo('manage-watches')),

Conditions DSL

Conditions are stored as a JSON array of rules. Empty / null conditions are fail-open — every non-ignored change notifies. Multiple rules are ANDed together.

[
  {"field": "status", "operator": "changed", "value": null},
  {"field": "amount", "operator": ">",       "value": 10000}
]
Operator Semantics
changed Passes if the field appears in the diff for this update.
= / != Loose comparison against the new attribute value.
> / < Numeric / lexical comparison against the new attribute value.
>= / <= Numeric / lexical comparison against the new attribute value.
contains True if the new attribute value is a string and contains the rule's value.

The Repeater in WatchAction's modal builds this JSON for end users — they pick a field, an operator, and (optionally) a value, then save.

"My Watches" page

The plugin registers a custom Filament Page at /{panel}/my-watches. It's auth-scoped at the query level — users only ever see their own watches.

Columns: model type, record id, conditions ("Any change" or "N rule(s)"), event count badge, paused indicator, watching since.

Row actions:

  • History — opens a modal listing every recorded change event for this watch (timestamp, actor name, per-field old → new diff). Survives notification cleanup. Limited to the latest 100 events per click.
  • Pause — sets paused_at. Paused watches stop receiving notifications without losing their conditions.
  • Resume — clears paused_at.
  • Unwatch — deletes the row (and its event history via cascade).

Disable the page from a specific panel:

FilamentRecordWatcherPlugin::make()->registerMyWatchesPage(false)

Or change its navigation:

FilamentRecordWatcherPlugin::make()
    ->navigationGroup('Notifications')
    ->navigationIcon('heroicon-o-bell-alert')

Persistent event history

Notifications can be dismissed, cleared, or simply missed. To make sure users never lose track of changes, every successful fan-out also persists a row in the watch_events table. The history retains the actor and the full field-level diff and is exposed on the My Watches page through the History action.

$watch->events;                                     // HasMany WatchEvent, latest first
$watch->events()->latest()->limit(50)->get();
$watch->events->first()->actor;                     // morphTo — the user who made the change
$watch->events->first()->diff;                      // ['status' => ['old' => 'pending', 'new' => 'paid'], ...]
$watch->events->first()->created_at;

Schema:

Column Purpose
id Primary key
watch_id FK to watches (cascade on delete)
actor_type, actor_id Polymorphic actor (nullable for unattributed writes)
diff JSON {field: {old, new}} for the changed columns
created_at When the event was recorded

Indexed (watch_id, created_at) for fast per-watch history queries. Unwatching cascades — deleting a watch removes its event log automatically.

Actor capture (queues, jobs, commands)

By default, WatchEngine reads auth()->user() to attribute the change. In a queued job, scheduled command, or any context where there is no authenticated user, wrap the write with actingAs():

use Wezlo\FilamentRecordWatcher\Services\WatchEngine;

WatchEngine::actingAs($user, function () use ($order): void {
    $order->update(['status' => 'paid']);
});

The actor is included in the notification body (By {name}), persisted on the WatchEvent row, and dispatched on the event payload.

Self-notify is suppressed

By design, the engine does not notify the actor on their own changes. If user A is watching an order and user A edits it, no notification is delivered (otherwise watching anything you ever touched would spam your bell). The event is also not recorded for the actor's own watches. To verify the system end-to-end, log in as a second user and edit a record the first user is watching.

Events

Every notification fan-out also dispatches a plain Laravel event:

Wezlo\FilamentRecordWatcher\Events\RecordWatchedChange

The event carries the Watch model, the changed Model, the diff array, and the ?Authenticatable $actor. Subscribe to it for email / Slack delivery, audit logging, analytics, or anything else you need beyond the in-panel notification.

use Illuminate\Support\Facades\Event;
use Wezlo\FilamentRecordWatcher\Events\RecordWatchedChange;

Event::listen(function (RecordWatchedChange $event): void {
    // $event->watch, $event->record, $event->diff, $event->actor
});

How change detection works

HasWatchers::bootHasWatchers() registers a static updated model hook that:

  1. Builds a field-level diff with DiffBuilder::build($model, $ignored) using the model's getOriginal() + getChanges().
  2. Skips ignored columns (updated_at, created_at by default — configurable).
  3. If the filtered diff is non-empty, calls WatchEngine::fanOut($model, $diff).

WatchEngine::fanOut():

  1. Eager-loads $model->watches()->active()->with('user') (active = paused_at IS NULL).
  2. Skips the actor's own watch.
  3. For each remaining watch, evaluates conditions via ConditionEvaluator::passes().
  4. For each passing watch: persists a WatchEvent, sends a Filament database notification, dispatches RecordWatchedChange.

Database schema

watches

Column Purpose
id Primary key
watchable_type, watchable_id Polymorphic target
user_id Subscriber (FK to users, cascade on delete)
conditions JSON rules array (nullable = notify on any change)
paused_at When the watch was paused (nullable)
created_at, updated_at Timestamps

Indexes:

  • Unique (watchable_type, watchable_id, user_id) — one watch per user per record.
  • (user_id, paused_at) — fast lookup for the My Watches page.

watch_events

See the Persistent event history section above.

Configuration reference

Publish with php artisan vendor:publish --tag="filament-record-watcher-config".

Key Default Description
user_model App\Models\User Model used for the user_id relationship.
table_name watches Name of the polymorphic subscriptions table.
events_table_name watch_events Name of the persistent event history table.
ignored_columns ['updated_at', 'created_at'] Column changes that should NOT trigger watcher notifications.
max_diff_lines 8 Max diff lines included in a notification body.
my_watches.enabled true Whether the My Watches page registers itself.
my_watches.navigation_group null Navigation group for the page.
my_watches.navigation_icon heroicon-o-bell Navigation icon.
my_watches.navigation_sort 95 Navigation sort order.

You can also configure most of these fluently on the plugin (->ignoredColumns([...]), ->navigationGroup(...), ->navigationIcon(...), ->userModel(...), ->registerMyWatchesPage(false)) — the plugin values win over config values when both are set.

Low-level service API

use Wezlo\FilamentRecordWatcher\Services\WatchEngine;
use Wezlo\FilamentRecordWatcher\Services\ConditionEvaluator;

// Manually fan out (rarely needed — the observer handles this for you)
app(WatchEngine::class)->fanOut($order, [
    'status' => ['old' => 'pending', 'new' => 'paid'],
]);

// Evaluate a rule set against a model + diff
$passes = app(ConditionEvaluator::class)->passes($order, $diff, $watch->conditions);

// Run a write under an explicit actor (queues, console, system jobs)
WatchEngine::actingAs($systemUser, fn () => $order->update(['status' => 'paid']));

The engine performs no authorization of its own — it's a low-level primitive. Enforce permissions at the caller (action, job, command) before invoking it.

Translations

The package ships English translations under filament-record-watcher::watcher.*. Every user-visible string — action labels, modal headings, notifications, table columns, page title, history modal — is routed through __(). Add additional locales by publishing:

php artisan vendor:publish --tag="filament-record-watcher-translations"

Testing

The package is exercised by 20 Pest tests covering the condition evaluator, the trait surface (watchFor / unwatchFor / getWatchableFields), the observer + engine notification path, the persistent event log (survives notification deletion), the diff builder's ignored-columns behaviour, and the auth-scoped My Watches page. Run them from the host app:

php artisan test --compact tests/Feature/FilamentRecordWatcher

License

MIT.

wezlo/filament-record-watcher 适用场景与选型建议

wezlo/filament-record-watcher 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 114 次下载、GitHub Stars 达 2, 最近一次更新时间为 2026 年 04 月 11 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「notifications」 「Audit」 「laravel」 「watch」 「subscribe」 「filament」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 wezlo/filament-record-watcher 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 wezlo/filament-record-watcher 我们能提供哪些服务?
定制开发 / 二次开发

基于 wezlo/filament-record-watcher 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 114
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 2
  • 点击次数: 37
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 2
  • Watchers: 0
  • Forks: 1
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-04-11