Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Single Record Resource Laravel Package

coringawc/filament-single-record-resource

Filament resource pattern that replaces list pages with a single resolved record per user, ideal for My Profile, My Wallet, or My Settings pages. Redirects index navigation to view, supports nested resources, and works with Filament 4 and 5.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require coringawc/filament-single-record-resource

Requires Filament ^4.0 || ^5.0. No service provider, no config file, no migration — it ships two traits and one interface.

Where to look first

  1. README "Core Concepts" and "Step-by-Step Implementation" — the whole API is documented there.
  2. SingleRecordResolvableResource (added v1.1.0) — implement it on every resource; it is what makes the page trait resolve the record via the Resource contract instead of duck-typing.
  3. Changelog v1.0.4 — the authorization fallback (viewAny denied but view allowed) landed there. If you are on 1.0.0–1.0.3, upgrade before debugging permissions.
  4. The workbench/ and tests/ folders in the repo for a runnable example (MyWallet → Companies → Products).

Your first use case: "My Wallet"

  1. Resource uses HasSingleRecordResource, implements SingleRecordResolvableResource, and registers only view (optionally edit) — no index.
class MyWalletResource extends Resource implements SingleRecordResolvableResource
{
    use HasSingleRecordResource;

    public static function getPages(): array
    {
        return [
            'view' => ViewMyWallet::route('/'),
            'edit' => EditMyWallet::route('/edit'),
        ];
    }
}
  1. Both ViewMyWallet and EditMyWallet use HasSingleRecord.
  2. Model relationship is 1:1 with the auth user (User hasOne Wallet, Wallet belongsTo User) — the default builder does whereBelongsTo(filament()->auth()->user()).

That's it: sidebar item appears, clicking it lands directly on the wallet view instead of a list.

Implementation Patterns

Default 1:1 resolution (do nothing). For belongsTo(User::class)-shaped models the package resolves the record for you. This is the case you should optimize for; only deviate when business rules force it.

Pattern A — narrow the builder on the Resource (recommended for filters). Keeps authorization and page loading on the same query:

public static function resolveSingleRecordBuilder(Builder $query): Builder
{
    return parent::resolveSingleRecordBuilder($query)->where('active', true);
}

Use for: soft "current" selection, where('tenant_id', ...), status filters, subscription expiry checks.

Pattern B — full resolver on the Resource (firstOrCreate / business rules).

public static function resolveSingleRecord(): ?Model
{
    $user = filament()->auth()->user();
    return $user?->wallet()->firstOrCreate([]);
}

Use for: lazy-provisioning records ("My Settings" row created on first open), tenant-aware resolution, data coming from a non-user() relation (override owner()/account() by replacing the builder with whereBelongsTo($user, 'owner')).

Pattern C — page-level override. Legitimate but rare: only when the Edit page needs a different record than the View page. Keep in mind the Resource-level resolver still drives the viewAny fallback, so a divergent page override can produce a page that loads but whose navigation/permission check disagrees. Prefer A or B.

Nested chains (MyWallet → Companies → Products).

  • Keep HasSingleRecordResource on every resource in the chain that follows the single-record flow.
  • Keep HasSingleRecord on the deep ViewRecord/EditRecord pages.
  • The package normalizes breadcrumbs and resolves root URL/slug so a deep page still says "My Wallet / Acme / Widget".
  • If you drop parent IDs from nested URLs, you own the scoping. Enforce where('company_id', $parent->id) in the child resource's getEloquentQuery() or use Filament's getParentResource/relation-manager scoping. The plugin will happily render a record the user should not see if your query is unscoped.

Policies that deny listing. Explicitly viewAny(): false + view(): true is now a supported stance — the Resource stays in the sidebar and is reachable. This is the pattern for "users may open their own resource but there is no collection to browse."

Multiple single-record resources per panel. Nothing special: one trait-bearing resource each (MyWallet, MySettings, CurrentSubscription). Group them under a navigation group and set $navigationLabel to the possessive form.

Explicit contract everywhere. Implement SingleRecordResolvableResource on every resource using the trait. It costs one implements clause and keeps PHPStan/Larastan from complaining about the static calls.

Gotchas and Tips

viewAny() denial is a real trap. Filament gates navigation and resource access on viewAny(). On versions before 1.0.4, a policy with viewAny() = false makes the whole thing invisible or 403s. The package now falls back to view() on the resolved record — but only if resolution actually returns a record, so a null-returning resolveSingleRecord() will silently hide your menu item. Test with a fresh user who has no wallet row yet.

Edit page save redirect. EditRecord redirects to the resource index by default. There is no index here. Override getRedirectUrl() on the edit page (point at the view page or null) and smoke-test "Save changes" as part of setup — this is the most common first-day bug with this pattern.

Breadcrumbs and "back to list" links. Filament still generates index-facing URLs in some places (breadcrumbs, page headers, global search result URLs). If you add ->breadcrumb() overrides or use getGlobalSearchResultUrl(), make sure they resolve to the view route.

Resolution runs per request. There is no caching. If your resolver hits an expensive query or an external API, wrap it or cache the result — otherwise every sidebar render plus every page load fires it. The authorization fallback triggers resolution on navigation rendering too, so lazy-provisioning firstOrCreate runs on panel boot, not just on page visit.

Resolver returning null. Decide deliberately: let the page 404, or return a freshly created record. Returning null from a full resolver yields confusing behaviour (empty page / hidden nav), so prefer firstOrCreate when a record is required.

Tenancy. The default whereBelongsTo(auth()->user()) ignores filament()->getTenant(). In a tenant panel, override the builder with a tenant-scoped query — otherwise a user switching tenants may see a record from another tenant.

Custom relation names. The default assumes a user() relation exists on the model. A model with owner() or account() needs resolveSingleRecordBuilder() with whereBelongsTo($user, 'owner').

Don't register index. Some generators scaffold it by default. If you leave it, the trait redirects it, but you now have a dead route and a confusing page class. Delete ListXxx and its page registration.

Static analysis. Calling MyWalletResource::resolveSingleRecord() from a page without the interface implemented will fail Larastan. The interface exists precisely for this — use it.

Package maturity. v1.x, ~12 stars, zero dependents, small surface (two traits, one interface, README + changelog + CI on Filament 4 and 5). Vendor risk is low because everything is trait-composed inside your own resource classes — if the package stalls, copying the two traits into your app is a viable fallback. Pin a minor version anyway and re-run your panel smoke tests when crossing a Filament major.

Testing tip from the maintainers' own release notes (v1.1.1): Testbench-based tests need a persistent SQLite DB_DATABASE path; the in-memory testing connection breaks HTTP-level feature tests. If you write your own tests around these pages, mirror that setup.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr