PHPackages                             wezlo/filament-record-freezer - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. wezlo/filament-record-freezer

ActiveLibrary[Database &amp; ORM](/categories/database)

wezlo/filament-record-freezer
=============================

Freeze Eloquent records against modification — audit holds, finalized contracts, legal holds — for Filament.

1.0.0(1mo ago)151MITPHPPHP ^8.2

Since Apr 11Pushed 1mo agoCompare

[ Source](https://github.com/mustafakhaleddev/filament-record-freezer)[ Packagist](https://packagist.org/packages/wezlo/filament-record-freezer)[ RSS](/packages/wezlo-filament-record-freezer/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (1)Dependencies (5)Versions (2)Used By (0)

Filament Record Freezer
=======================

[](#filament-record-freezer)

Freeze individual Eloquent records against modification — finalised contracts, audited financial periods, legal holds — for Filament.

When a record is frozen:

- Any attempt to `update()`, `save()` (with dirty attributes) or `delete()` it throws a `RecordFrozenException` — from the UI, a job, a policy, or `tinker`.
- Filament resources using the `HandlesFrozenRecords` trait have their `canEdit()` / `canDelete()` disabled automatically, and row-level edit/delete actions are hidden via `FreezableActionGroup`.
- A polymorphic audit trail records who froze it, when, and why. Unfreezing preserves full history (freeze → unfreeze → re-freeze creates a new row each time) with who / when / reason for both sides.

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11+ / Laravel 13
- Filament v4+

Installation
------------

[](#installation)

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

```
composer require wezlo/filament-record-freezer
php artisan migrate
```

Register the plugin on the panel(s) where you want the admin resource:

```
use Wezlo\FilamentRecordFreezer\FilamentRecordFreezerPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugins([
            FilamentRecordFreezerPlugin::make(),
        ]);
}
```

Make a model freezable
----------------------

[](#make-a-model-freezable)

Add the `HasFreezes` trait to any Eloquent model:

```
use Wezlo\FilamentRecordFreezer\Concerns\HasFreezes;

class Contract extends Model
{
    use HasFreezes;
}
```

You can now:

```
$contract->freeze('Legal hold — case #4412');   // throws if already frozen
$contract->isFrozen();                           // true
$contract->activeFreeze;                         // current Freeze row (or null)
$contract->freezes;                              // full history, newest first

$contract->unfreeze('Case closed — release');    // sets unfrozen_at, keeps history
$contract->freeze('Re-opened for audit');        // new Freeze row, history preserved

$contract->update(['amount' => 5000]);           // → RecordFrozenException
$contract->delete();                             // → RecordFrozenException
```

Observer coverage: `updating`, `deleting`, and (for models using `SoftDeletes`) `restoring`.

Filament integration — `FreezableActionGroup`
---------------------------------------------

[](#filament-integration--freezableactiongroup)

The row-level UX is driven by `FreezableActionGroup`, a dropdown action group that bundles Freeze + Unfreeze alongside your own row actions. The host's actions (edit, delete, custom workflows) are passed through `unfrozenActions()` and are automatically hidden when the record is frozen — so there's no path to modifying a frozen record from the table.

```
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Resources\Resource;
use Wezlo\FilamentRecordFreezer\Actions\FreezableActionGroup;
use Wezlo\FilamentRecordFreezer\Concerns\HandlesFrozenRecords;

class ContractResource extends Resource
{
    use HandlesFrozenRecords;

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('title'),
                static::freezableColumn(),          // lock icon + tooltip
            ])
            ->recordActions([
                ViewAction::make(),                 // always available
                FreezableActionGroup::make()
                    ->canFreeze(fn ($record) => auth()->user()->is_admin)
                    ->canUnfreeze(fn ($record) => auth()->user()->hasRole('compliance'))
                    ->unfrozenActions([
                        EditAction::make(),         // auto-hidden when frozen
                        DeleteAction::make(),       // auto-hidden when frozen
                    ]),
            ]);
    }
}
```

`canFreeze()` and `canUnfreeze()` each accept a `bool` or a closure `fn (Model $record, ?Authenticatable $user) => bool`. Return `false` to hide that action for the current user / row. Default: allow.

`HandlesFrozenRecords` is still recommended as an authorization backstop — it overrides the resource's `canEdit()` and `canDelete()` so frozen records can't be modified even if a user hits the edit/delete URL directly or via a bulk action.

Audit trail — who froze / unfroze
---------------------------------

[](#audit-trail--who-froze--unfroze)

Every freeze and unfreeze is recorded on the `freezes` table as an immutable row. Unfreezing **does not delete** the row — it sets `unfrozen_at`, `unfrozen_by`, and `unfreeze_reason` on the active row, preserving the full history.

```
$contract->freezes;                                // full history, newest first
$contract->freezes->first()->frozen_by;            // user id who froze
$contract->freezes->first()->unfrozenBy?->name;    // who released it (if released)
$contract->freezes->first()->unfreeze_reason;      // why it was released
$contract->activeFreeze;                           // current Freeze row (null if not frozen)
```

Table schema:

ColumnPurpose`freezable_type`, `freezable_id`Polymorphic target`frozen_by`, `frozen_at`, `reason`Who froze, when, why`unfrozen_by`, `unfrozen_at`, `unfreeze_reason`Who released, when, why (null while active)Central admin resource
----------------------

[](#central-admin-resource)

The plugin ships `FreezeResource` — a central list of every freeze, active or released, across every polymorphic model. Filters by status (active / released) and model type. Columns for both freeze and unfreeze metadata (reason, user, timestamps) are visible by default and toggleable. `UnfreezeAction` is available inline and only on active rows — released rows never show it, even when the underlying record has a newer active freeze.

Disable it from a specific panel:

```
FilamentRecordFreezerPlugin::make()->registerResource(false)
```

Or change its navigation:

```
FilamentRecordFreezerPlugin::make()
    ->navigationGroup('Audit & Compliance')
    ->navigationIcon('heroicon-o-shield-check')
```

Configuration reference
-----------------------

[](#configuration-reference)

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

KeyDefaultDescription`user_model``App\Models\User`Model used for `frozen_by` / `unfrozen_by` relationships.`table_name``freezes`Name of the polymorphic table.`ignored_columns``[]`Columns whose dirty writes are allowed even while frozen (e.g. `updated_at`).`require_reason``true`Enforce a non-empty reason on freeze / unfreeze.`min_reason_length``5`Minimum reason length when required.`resource.enabled``true`Whether the central `FreezeResource` registers itself.`resource.navigation_group``Compliance`Navigation group for the resource.`resource.navigation_icon``heroicon-o-lock-closed`Navigation icon.`resource.navigation_sort``90`Navigation sort order.Events
------

[](#events)

Subscribe to these events for notifications, mirroring, or analytics:

- `Wezlo\FilamentRecordFreezer\Events\RecordFrozen` — a record just became frozen.
- `Wezlo\FilamentRecordFreezer\Events\RecordUnfrozen` — a record was just released.

Both carry the `Freeze` model (including the `freezable` morph target).

Low-level engine API
--------------------

[](#low-level-engine-api)

```
use Wezlo\FilamentRecordFreezer\Services\FreezingEngine;

$engine = app(FreezingEngine::class);

$engine->freeze($contract, 'Audit hold', frozenBy: $userId);
$engine->unfreeze($contract, 'Released by CFO', unfrozenBy: $userId);
```

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
------------

[](#translations)

The package ships full translations for:

- `en` (English)
- `ar` (Arabic)

Every user-visible string — action labels, modal headings, notifications, table columns, filters, tooltips — is routed through `__('filament-record-freezer::freezer.…')`. Switch locales via `App::setLocale()` or your existing locale middleware and the entire freezer UI follows.

Publish the translation files to override them in your host app:

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

Developer-facing exception messages (`RecordFrozenException`, engine validation) stay in English intentionally — they go to logs and stack traces, not end users.

Testing
-------

[](#testing)

The package is exercised by 27 Pest tests covering the trait, observer, engine, action group, resource, and re-freeze history semantics. Run them from the host app:

```
php artisan test --compact tests/Feature/FilamentRecordFreezer
```

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance88

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

59d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/25182746?v=4)[Mustafa Khaled](/maintainers/mustafakhaleddev)[@mustafakhaleddev](https://github.com/mustafakhaleddev)

---

Top Contributors

[![mustafakhaleddev](https://avatars.githubusercontent.com/u/25182746?v=4)](https://github.com/mustafakhaleddev "mustafakhaleddev (1 commits)")

---

Tags

laravelAuditfilamentcompliancefreezelegal-hold

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/wezlo-filament-record-freezer/health.svg)

```
[![Health](https://phpackages.com/badges/wezlo-filament-record-freezer/health.svg)](https://phpackages.com/packages/wezlo-filament-record-freezer)
```

###  Alternatives

[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3913.7k](/packages/rawilk-profile-filament-plugin)[relaticle/custom-fields

User Defined Custom Fields for Laravel Filament

16345.8k](/packages/relaticle-custom-fields)[stephenjude/filament-jetstream

A Laravel starter kit built with Filament inspired by Jetstream.

17758.9k2](/packages/stephenjude-filament-jetstream)[stephenjude/filament-two-factor-authentication

Filament Two Factor Authentication: Google 2FA + Passkey Authentication

84192.9k7](/packages/stephenjude-filament-two-factor-authentication)[mradder/filament-logger

Audit logging, activity tracking, exports, alerts, and dashboards for Filament admin panels.

2210.5k](/packages/mradder-filament-logger)[marcelweidum/filament-passkeys

Use passkeys in your filamentphp app

6643.3k](/packages/marcelweidum-filament-passkeys)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
