Skip to content

Support trait-named lifecycle hooks on pages and importers - #20189

Open
oddvalue wants to merge 1 commit into
filamentphp:4.xfrom
oddvalue:feature/trait-named-lifecycle-hooks
Open

Support trait-named lifecycle hooks on pages and importers#20189
oddvalue wants to merge 1 commit into
filamentphp:4.xfrom
oddvalue:feature/trait-named-lifecycle-hooks

Conversation

@oddvalue

Copy link
Copy Markdown
Contributor

Description

Lifecycle hooks (beforeFill, afterCreate, afterSave, etc.) can currently only be defined directly on the page class. A trait that defines one of these hooks is silently overridden the moment the page declares its own, which makes it impossible for reusable traits and plugins to reliably hook into the page lifecycle.

This PR makes callHook() also invoke {hook}{TraitName}() for every trait used by the class (recursively), mirroring how Eloquent boots model traits via boot{TraitName}() and how Livewire supports mount{TraitName}() / boot{TraitName}() trait hooks:

trait HandlesDrafts
{
    protected function afterSaveHandlesDrafts(): void
    {
        // Runs in addition to the page's own afterSave().
    }
}

class EditUser extends EditRecord
{
    use HandlesDrafts;

    protected function afterSave(): void
    {
        // Both hooks are called; this one first.
    }
}

Behaviour

  • The class's own hook runs first, then each trait hook (in class_uses_recursive() order).
  • Duplicate method names (two traits sharing a basename) are only called once.
  • Applies everywhere callHook() is used: panel pages (BasePage — so all CreateRecord / EditRecord / ViewRecord, tenancy and auth pages) and importers (Importer).
  • Fully backwards compatible: existing hooks behave exactly as before; the trait convention is purely additive.

Changes

  • packages/panels/src/Pages/BasePage.phpcallHook() calls trait-named hooks after the class hook.
  • packages/actions/src/Imports/Importer.php — same change for importer hooks.
  • docs/03-resources/03-creating-records.md, docs/03-resources/04-editing-records.md — new "Defining lifecycle hooks in traits" sections.
  • Tests — new fixtures (TracksLifecycleHooks trait, CreatePostWithTraitHooks, EditPostWithTraitHooks) and cases in CreateRecordTest / EditRecordTest asserting the page hook and the trait hooks all fire, in order.

Visual changes

None — no UI changes.

Functional changes

  • Code style has been fixed by running the composer cs command. (Rector and Pint run clean on all touched files; Prettier ignores *.md, and no JS/Blade files are touched.)
  • Changes have been tested to not break existing functionality. (tests/src/Panels/Resources/Pages/CreateRecordTest.php + EditRecordTest.php: 65 passed, 194 assertions, including the two new cases. tests/src/Actions/ImportActionTest.php + tests/src/Actions/Imports/ImporterTest.php: 35 passed, 54 assertions.)
  • Documentation is up-to-date. (Creating- and editing-records lifecycle hook docs extended.)

Lifecycle hooks such as afterCreate() and afterSave() can currently only
be defined directly on the page class. A trait that defines one of these
hooks is silently overridden as soon as the page declares its own,
which makes it impossible for reusable traits or plugins to reliably
hook into the page lifecycle.

callHook() now also invokes {hook}{TraitName}() for every trait used by
the class (recursively), mirroring how Eloquent boots model traits via
boot{TraitName}(). The class's own hook runs first, followed by each
trait hook. Applies to panel pages (BasePage) and importers (Importer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@danharrin danharrin added enhancement New feature or request pending review labels Jul 16, 2026
@danharrin danharrin added this to the v4 milestone Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request pending review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants