Searching outside HTTP
Rest::search() runs the search of a resource from PHP, for a given user, as the search endpoint runs it. Use it to replay a search payload that a client built: a saved search that a scheduled job sends each morning, a console command that reruns it, or an export of what a user sees. To read rows that your own code chooses, keep using Eloquent.
use Lomkit\Rest\Facades\Rest;
$search = Rest::search(TicketResource::class)
->as($user)
->payload($savedSearch);
$page = $search->page(); // LengthAwarePaginator of the models
$body = $search->render(page: 2); // the endpoint body, as an array
$rows = $search->lazy(); // every rendered row, by chunks
The builder
payload()takes the value of thesearchkey of the endpoint body. It is validated with the rules and limits of the endpoint. A failure throwsIlluminate\Validation\ValidationExceptionwith the keys and messages of the endpoint, such assearch.filters.0.field. Withoutpayload(), the search runs as an endpoint body withoutsearch.as()is required. Without it, the search throwsSearchRunException:The search of the resource [App\Rest\Resources\TicketResource] has no user. Call as() with the user, or as(null) to search as a guest.as(null)searches as a guest, also when a user is logged in.as($user, 'api')names the guard of the user; the default guard applies otherwise.- The given user is the one that
$this->request()->user()returns inperimeterQuery(), in the declarations of the resources and in their hooks, at every include level. The policies and the decision cache use that user and guard. The logged-in user and the default guard of the application do not change. viewAnyis checked before the search, as on the endpoint: a denial throwsLomkit\Rest\Exceptions\AuthorizationDenied.
page() and render()
use Illuminate\Support\Facades\Mail;
use Lomkit\Rest\Facades\Rest;
public function handle(): void
{
$page = Rest::search(TicketResource::class)
->as($this->user)
->payload($this->savedSearch->payload)
->page();
Mail::to($this->user)->send(new OpenTickets($page->items(), $page->total()));
}
page()returns theIlluminate\Contracts\Pagination\LengthAwarePaginatorof the models of the page, with their includes cut to their limits.render()returns the body of the endpoint as an array:json_encode($body)is the content that the endpoint answers for the same user and payload,metaincluded.- The objects that stay
{}when empty, such asmeta,pivotandgates, arestdClassinstances in that array. page(page: 2)andrender(page: 2)read that page instead of thepageof the payload. It is validated assearch.page.page()andrender()runsearching()before the query andsearched()after it, as on the endpoint.page()andrender()are bounded byrest.limits.sizes.rows, as a page of the endpoint:The search field takes the response over the limit of 49 rows: it can render up to 50 rows.for a page of 50 rows with the limit set to 49.lazy()does not count rows. See Response rows.- The values of a row follow the type of their field, as on the endpoint: a
Numbervalue is a number, and an emptyJsonvalue is an object. See Value types in rows.
lazy() and exports
lazy() reads every row of the search. Here, an export of the tickets that the user sees, with the filters of the client, streamed in the request:
use Illuminate\Http\Request;
use Lomkit\Rest\Facades\Rest;
use Symfony\Component\HttpFoundation\StreamedResponse;
public function __invoke(Request $request): StreamedResponse
{
$rows = Rest::search(TicketResource::class)
->as($request->user())
->payload($request->input('search', []))
->lazy();
return response()->streamDownload(function () use ($rows): void {
$file = fopen('php://output', 'w');
foreach ($rows as $row) {
fputcsv($file, [$row['id'], $row['title'], $row['status']]);
}
fclose($file);
}, 'tickets.csv');
}
{
"search": {
"filters": [{"field": "status", "value": "open"}],
"selects": [{"field": "id"}, {"field": "title"}, {"field": "status"}],
"sorts": [{"field": "id"}]
}
}
lazy(chunk: 500, max: null)returns anIlluminate\Support\LazyCollectionof rendered rows. Each row is the row that the endpoint returns indata: its fields, its includes cut to their limits with@more, and thegatesthat the payload asks for.- In order,
lazy()returns the rows of every page of the endpoint, each row once. - When
lazy()is called, it validates the payload, checksviewAny, runssearching()and reads the keys of the rows in the order of the search. A validation error or a denial throws there, before the streamed response starts. - The rows load and render by chunks of
chunkkeys while you iterate, with their includes and aggregates. The query of the keys selects the model key only, so only the keys of all rows stay in memory. - A row deleted after its key was read is skipped. Each iteration loads the rows again.
maxreads the firstmaxrows only.lazy()ignores thelimitand thepageof the payload.searched()does not run, and an override ofpaginate()in the resource does not apply tolazy().
lazy() errors
All of them throw Lomkit\Rest\Exceptions\SearchRunException:
| Case | Message |
|---|---|
| A text search on a search engine | The text search of the resource [App\Rest\Resources\TicketResource] runs on a search engine, which returns at most its first 10000 results. lazy() reads every row: search without text, or read the pages with page(). |
A chunk below 1 | The chunk of lazy() must be at least 1, [0] given. |
A negative max | The max of lazy() must be at least 0, [-1] given. |
The engine message names the result window of the engine, here the 10000 results of Turbopuffer. An engine without a window, such as Meilisearch or Typesense, says which returns its results page by page. instead.