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

Lattice Laravel Package

lattice-php/lattice

Lattice is a Laravel/PHP package for building clean, maintainable, data-driven interfaces with reusable components and a fluent API. It streamlines common UI patterns so you can ship structured admin screens and workflows faster without repetitive boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Lattice is a server-driven UI layer for Laravel + React: you describe pages, tables, forms, and actions in PHP, and the React client renders them. The practical entry point is not the package root but the skills documentation (skills/lattice-actions, skills/lattice-closures, and the referenced lattice-tables), because that is where the day-to-day conventions live.

Minimal path to first value:

  1. Install the package and wire up its React/Inertia frontend as described in the repository README.
  2. Publish any config/assets the package requires.
  3. Define one page or layout that includes Outlet::make() — this is where component trees render.
  4. Build one table or form-backed page. The fastest meaningful use case is a table with a row action: it exercises discovery, signed context, server-side mutation, and client effects in one vertical slice.
  5. Add a layout Callouts::make() slot between the header and the outlet if you plan to use callout effects.

A good first action is an archive/delete/send flow:

#[AsAction('app.products.archive')]
class ArchiveProductAction extends ActionDefinition
{
    public function definition(Action $action): Action
    {
        return $action
            ->label('Archive')
            ->variant(ButtonVariant::Destructive)
            ->confirm('Archive product?', 'This hides it from the catalogue.');
    }

    public function handle(Request $request): ActionResult
    {
        Product::findOrFail($this->context('product_id'))
            ->update(['status' => 'archived']);

        return ActionResult::success()
            ->toast('Product archived.')
            ->reloadComponent('app.products');
    }
}

Then attach it to a table row with Action::use(ArchiveProductAction::class)->context(['product_id' => $row['id']]).

Look first at:

  • ActionDefinition / BulkActionDefinition for server-side interactions.
  • #[AsAction], #[AsBulkAction], and table attributes for discovery.
  • The closure evaluation rules, because almost every dynamic value in Lattice — computed fields, dependent fields, searchable selects, row labels, filters — goes through the same evaluator.

The package is conceptually strong for admin panels, internal tools, and CRUD-heavy back offices where the team is PHP-first and wants to avoid duplicating validation and business rules in React. With a small ecosystem (62 stars, no listed dependents), treat it as a framework you will need to learn deeply rather than a drop-in UI kit.

Implementation Patterns

Actions as the primary interaction primitive. The strongest pattern is to treat every non-trivial click as an ActionDefinition: it owns authorization, validation, mutation, and the UI effects that follow. Do not scatter redirect/toast logic across controllers. An action returns a pipeline of effects, executed in order:

return ActionResult::success()
    ->toast('Saved.')
    ->reloadComponent('app.products')
    ->closeModal();

This makes the post-click experience declarative and testable server-side.

Signed row context instead of trusting the request. For row actions, pass identifiers through ->context([...]) and read them in handle() with $this->context('product_id'). The reference is signed, so it is safe to place record IDs in the component tree. For repeated lookups, register a key once with Lattice::context('product', Product::class) or a closure resolver, then use contextModel('product'). This gives you memoization per request and 404s on missing records, and registered keys cascade into child components and forms.

CRUD flow with forms in modals. The productive pattern is:

  • Table row exposes Action::use(EditProductAction::class).
  • The action uses ->form([...]) or ->lazyForm()->form([...]) to render a modal form.
  • handle() calls $this->validate($request), mutates, then returns success()->toast(...)->reloadComponent('app.products')->closeModal().
  • Server-side validation is precognitive, so field errors appear without duplicating rules in React.

For per-record prefill, lazyForm() is the one to reach for — it loads the row into the form when the modal opens.

Bulk actions over table selections. Use BulkActionDefinition for selection-based operations. The selected records arrive as a Collection, resolved by the table's data source; for an EloquentTableDefinition they are models, so you can update them directly. Return a count in the success payload and a toast, then reload the table component.

Authorization belongs on the action. Override authorize(Request $request): bool for custom gates, or declare subject abilities on the attribute:

#[AsAction('app.products.archive', can: 'update', on: 'product')]

Prefer the attribute form for standard policy checks, because a missing subject denies outright rather than silently falling back to a subject-less check.

Flash effects outside actions. Controllers, listeners, and middleware can deliver effects with the next Inertia response:

Effects::flash(
    Effects::toast('Settings saved.', Variant::Success),
    Callout::make('Export queued.', Variant::Info)->title('Export')
);

return redirect('/dashboard');

This replaces the older CreatesToastMessages trait pattern ($this->toast(...)), so plan migrations accordingly.

Closures for dependent and computed fields. The closure evaluator is the key to keeping forms dynamic without client-side logic. Use named parameters deliberately:

TextInput::make('total', 'Total')->dependsOn(
    ['qty', 'price'],
    fn (TextInput $field, FormData $state) => $field->value(
        $state->float('qty') * $state->float('price')
    ),
);

Available utilities include $state/FormData, $get, $value, $component, Request $request, plus hook-specific $search, $values, $row, and $form. Select::searchable() uses $search; repeater/builder row hooks use $row and $form; filter closures get a typed Eloquent builder.

Component composition. Action::use(...) is itself a component, so actions can appear anywhere in a page tree, not just tables. Use ActionGroup::make('row')->actions([...]) to group related triggers behind one button, and Link::make('Edit')->href(...) for plain navigation.

Layout discipline. Place Callouts::make() in the layout schema between the header and Outlet::make(). If the layout lacks that slot, callout effects are silently dropped — no error, no banner. Establish the layout early so effects have somewhere to land.

Gotchas and Tips

  • Attribute vs component confusion is the most common mistake. #[AsAction] and #[AsBulkAction] live in Lattice\Core\Attributes; Action::use() and BulkAction::use() are components in Lattice\Actions\Components. Mixing them up produces confusing discovery or rendering failures.
  • An action without #[AsAction('id')] is not discovered and has no endpoint. If a click 404s, check the attribute before debugging routing. The same applies to bulk actions.
  • reloadComponent() takes the target component/table id, not the action id. This is easy to get wrong because both are string identifiers. Use the #[AsTable]/component id that renders the data you want refreshed.
  • Never read action context from the raw request. $this->context($key) reads the signed, trusted copy. Reading request input bypasses the signature guarantee and reintroduces tampering risk.
  • Signalling failure with a 2xx is a trap. ActionResult::failure('...') returns HTTP 422, keeps confirm dialogs and modals open, and auto-adds an error toast. A success() with an error toast still reads as success to the client and will close the modal.
  • Callouts need both the layout slot and, for persistent banners, a key. Without ->unique($key) a callout is a one-off event that stays until dismissed. With a key it is a projection of server state: re-emit it on every request where it still applies and retract it with ->retractCallout($key) or Callout::retract($key) when it no longer does.
  • Closure parameter names drive resolution, not type hints. fn ($state) and fn (FormData $state) both receive the same object. Name parameters after the utility you want. An unmatched, non-nullable parameter throws an exception listing the available utilities — read that exception before guessing.
  • Field parameters are never autowired. A typed field parameter in a closure must match the live field instance; a mismatched type throws rather than constructing a fresh field. This is a deliberate safety behaviour but can surprise developers used to container resolution.
  • Registered context resolvers are memoized per request. A resolver runs at most once however many times it is read. Do not rely on it re-querying after a mutation within the same request.
  • Subject-less fallback does not happen for can/on. If the context key is absent or its resolver finds nothing, the action is denied. Make sure the context key is always present on the component that triggers the action.
  • Bulk action record types depend on the table data source. Do not assume Eloquent models unless the table is an EloquentTableDefinition; type-hint defensively or verify the data source.
  • Debugging order for a broken interaction: confirm discovery attribute → confirm endpoint exists → confirm component id used by reloadComponent() → confirm layout has Callouts::make() if effects are missing → inspect the closure exception for parameter-resolution failures.
  • Extension points to keep in mind: custom effect types via ActionResult, custom callout configuration (title, dismissible, link, action), custom context resolvers, and layout slots. These are the places to invest when the built-ins stop fitting.
  • Maturity risk: with a small ecosystem, expect to read source and skills documentation rather than rely on community answers. Pin the package version, and verify the React/Inertia frontend setup against your Laravel version before committing to it for a large surface area.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin