Skip to content

Search

The search package adds a global application search to Lattice: a command-menu interface backed by search providers the application registers with the #[AsSearchProvider] attribute. Providers are picked up by Lattice’s discovery, results are grouped by category, and an optional SearchHistoryRecorder binding surfaces recent selections.

Terminal window
composer require lattice-php/search

That is the whole integration: the package ships its React renderer as source, and the lattice() Vite plugin compiles it into your app’s bundle via virtual:lattice/plugins (see Component packages). The PHP classes are picked up by Lattice’s discovery and TypeScript generation automatically. No-build apps use the precompiled module the package also ships: run php artisan lattice:assets after installation.

SearchBox::make() renders a trigger button that opens the search dialog (also reachable via ⌘K/Ctrl+K). The dialog opens compact — just the input, plus recent selections when a SearchHistoryRecorder is bound — and expands into the three-pane layout (categories, results, preview) once a query is typed. Inside a collapsed sidebar the trigger shrinks to an icon-only button; the keyboard shortcut keeps working.

Pass your own ->schema([...]) of SearchInput, SearchResults, SearchCategories, SearchRecent, and SearchPreview components to replace the default composition entirely.

The same pieces are props-based clients exported from @lattice-php/search, so an app can mount the trigger outside a Lattice page or compose its own palette:

import { SearchBox, SearchInput, SearchResults } from "@lattice-php/search";
<SearchBox endpoint="/lattice/search" placeholder="Find anything" shortcut>
<SearchInput />
<SearchResults />
</SearchBox>;

SearchBox opens the palette through the nearest ModalProvider; the slot components read the active search from SearchProvider, which SearchPalette mounts around its children.

EloquentSearchProvider covers the common case: give it the base query, the columns a term matches against, and how a record becomes a result. Counting, paging and re-resolving a recorded selection are the same for every such provider and come from the base class.

/**
* @extends EloquentSearchProvider<Product>
*/
#[AsSearchProvider('products', can: 'products.view')]
final class ProductSearchProvider extends EloquentSearchProvider
{
public function category(): SearchCategory
{
return new SearchCategory('products', __('Products'), 'package');
}
protected function query(): Builder
{
return Product::query()->orderBy('name');
}
protected function searchColumns(): array
{
return ['name', 'sku'];
}
protected function result(Model $model): SearchResult
{
return SearchResult::make('products', new SearchResultItem($model->id, $model->name, route('products.show', $model)));
}
}

query() is where scopes, ordering and the eager loads result() reads belong — it is called per request, never memoized. An empty term matches everything, which is what the palette shows before anything is typed; a % or _ in the term is escaped so it matches literally. Override matching() for anything the column list cannot express, such as a relation or a full-text index.

A provider is gated exactly like a definition: declare the ability on its attribute, and use authorize() only for what can cannot express. A provider that fails is dropped from the results, the category list and the counts, and its endpoint refuses a selection recorded against it.

#[AsSearchProvider('products', can: 'products.view')]
final class ProductSearchProvider implements SearchResultProvider { /* … */ }

A provider has no sealed context of its own, so if the ability needs a subject the provider supplies it — declare on and implement ResolvesGateSubject:

#[AsSearchProvider('products', can: 'products.view', on: 'workspace')]
final class ProductSearchProvider implements ResolvesGateSubject, SearchResultProvider
{
public function gateSubject(string $key): ?object
{
return $key === 'workspace' ? Workspace::current() : null;
}
}

Declaring on without that contract throws at registration rather than silently denying every request. A subject that resolves to nothing denies, the same as anywhere else in Lattice.

SearchResultProvider requires an authorize() because it is an interface and cannot default one. When the declaration is the whole gate, take it from Lattice\Core\Concerns\AuthorizesByDeclaration rather than writing return true by hand.

The component’s strings ship with inline English defaults. With laravel-i18next enabled, the plugin’s search namespace is loaded automatically and serves the bundled en/de translations (override them like any Laravel package translation — see Internationalization).

The breakdown below measures the package’s built plugin — React and the framework runtime are external, so this is exactly what the search package adds on top of an app that already ships Lattice. It is regenerated on every docs build.

5.6 KB gzipped · 19.0 KB raw JavaScript

DependencyRawGzipShare
@lattice-php/search15.1 KB4.9 KB88.3%
Bundler runtime3.9 KB0.6 KB11.7%

Emitted JavaScript files

FileRawGzip
plugin.js19.0 KB5.6 KB

Open the full interactive treemap ↗ · Generated by Sonda 0.14.0 on Sat, 05 Sep 2026 21:22:18 GMT.