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.
Architecture fit. Lattice is a server-driven UI layer that sits on top of Laravel + Inertia + React. The PHP side owns the component tree ("schemas"), the action endpoints, and the effect protocol; React is a renderer for a vocabulary Lattice defines (Action, Table, Callout, Outlet, Callouts, field builders, etc.). Three architectural patterns carry the package:
#[AsAction('app.products.archive')], #[AsBulkAction(...)], #[AsTable] register definition types that are auto-mounted at fixed routes (lattice/actions/{action}). This is a Laravel-native, container-friendly pattern and fits teams already comfortable with attribute-driven registration.fn (FormData $state) => ..., $get, $value, $search, $row, $form resolve positionally by name, falling back to container resolution. This is the package's most distinctive and most opinionated design decision.$this->context('product_id'), with a Lattice::context() registry, memoized resolvers, and cascade into child components.For a Laravel/Inertia/React shop, the fit is genuinely good: no new runtime, no separate BFF, no GraphQL layer, authorization reuses Laravel policies (can: 'update', on: 'product'), and validation is standard Laravel precognition. For a Blade/Livewire shop or a Vue/Nuxt front end, the fit is close to zero — this is not an incremental library, it's a UI paradigm.
Integration feasibility. Feasible but broad-surface. Adoption touches routing, layout structure, the form layer, the table layer, the authorization layer, the front-end build, and the design system simultaneously. There is a genuine low-cost wedge, though: Effects::flash() lets you deliver toasts and callouts from ordinary controllers, listeners, and middleware on existing Inertia pages without adopting the schema model at all. That gives a legitimate two-phase evaluation (effects-only → full SDUI) rather than a big-bang commitment.
Technical risk. High, and concentrated in three places:
Effects::flash() "replaces the old CreatesToastMessages trait" — meaning breaking API changes are shipping and being documented in onboarding material. I could not verify the current version number or a semver/stability commitment from the supplied sources; the most recent release date is recent, but cadence and breaking-change policy are unknown.$state to $formData changes behavior; an unmatched non-nullable parameter throws at request time. A missing Callouts::make() slot silently drops the effect. A missing #[AsAction] attribute means no endpoint. These are cheap to write and expensive to statically verify — static analysis and IDE support will be weak exactly where the framework is most expressive.Key questions for the maintainer / spike.
ActionResult's effect list in a unit test without a browser?localeChange), and SSR posture of the shipped components.Stack fit. Target a Laravel app on Inertia + React + TypeScript, with Tailwind-style design tokens (the ButtonVariant/Variant enums imply a themed component set). Verify peer constraints before committing: PHP and Laravel minor versions, Inertia version, React 18 vs 19, Node/build toolchain. With zero dependents there is no compatibility matrix to inherit — you build it yourself in the spike.
Migration path. Strangler pattern, route-group by route-group. Do not retrofit existing Blade or Livewire pages, and do not attempt a whole-app cutover.
Effects::flash() on existing Inertia pages for toasts and callouts. No schema adoption. This validates the effects protocol against your real layouts and gives an early read on churn.->confirm() and ->toast(), then ->form() with server-side precognitive validation.#[AsAction(can:, on:)] policy checks, reloadComponent() targeting.Compatibility and sequencing. Two sequencing constraints matter more than the rest. First, establish the layout contract early — Callouts::make() between the header and Outlet::make() — because the failure mode is silent, and a missing slot will burn a day of debugging. Second, establish context registration (Lattice::context('product', Product::class)) as a team convention before action count grows; ad-hoc ->context([...]) passing and registry-based cascade will otherwise diverge across the codebase.
Maintenance. Assume you may need to fork. Budget for reading source rather than docs for edge cases. The documentation set here is oriented toward AI agents (SKILL.md files with "Common mistakes" sections) — excellent for assisted ramp-up and for keeping conventions consistent, but it is not a substitute for API reference or a changelog you can diff. Assign a named internal owner; a package with this surface area and this little external signal should not be "everyone's responsibility."
Support. No vendor, no SLA, no paid tier. Escalation path is: source, then upstream issue, then fork. Treat upstream responsiveness as unverified and plan a fork-and-patch capability (CI that builds the package, a pinned commit strategy) before the second surface ships.
Scaling. The runtime model is stateless per request and scales horizontally like any Laravel app. Two scaling considerations are specific to SDUI: (a) prop payload size and query count on table pages — reloadComponent() is the mitigation and should be used deliberately rather than defaulting to reloadPage(); (b) closure evaluation runs per request, so cached/computed field values on large forms need measuring rather than assuming.
Failure modes to instrument.
| Failure | Symptom | Mitigation |
|---|---|---|
Missing #[AsAction] |
Endpoint 404s, no error at build | Lint/test that every action class has the attribute |
| Callout without layout slot | Effect silently dropped | Layout conformance test |
| Closure param renamed | Runtime exception listing utilities | Convention doc + code review checklist |
reloadComponent() wrong id |
Stale UI after successful mutation | Assert component ids in tests |
success() + error toast |
Client treats as success, modal closes | Standard: failure() → 422 for rejections |
| PHP/JS version skew | Unknown component renders nothing | Pin both, upgrade together, smoke test |
| Long-lived page + signed context | Stale or rejected action refs | Define page-lifetime expectations |
Ramp-up. A mid-level Laravel developer should reach productivity on read-only surfaces in roughly a week; actions, closures, forms, and the effects contract add another one to two weeks. The two recurring mental hurdles are documented as "common mistakes" and are worth putting in your own onboarding note: the attribute-vs-component distinction (#[AsAction] vs Action::use()), and reading context via $this->context() rather than off the request. The named-parameter closure model is the single highest-value thing to teach explicitly, because it is unlike anything else in the Laravel ecosystem and fails at runtime rather than at review.
Recommendation. Worth a bounded, time-boxed evaluation on a net-new admin surface — the architecture is a strong conceptual fit for Laravel + Inertia + React, and the effects-flash wedge makes the first step cheap. Do not place it on a revenue-critical or deadline-critical path until you have personally validated upgrade cost across one minor release and confirmed fork viability.
How can I help you explore Laravel packages today?