Search

Searching outside HTTP

Replay a client search payload from a job, a command or an export with Rest::search(), with the validation, perimeter and policies of the endpoint.

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 the search key of the endpoint body. It is validated with the rules and limits of the endpoint. A failure throws Illuminate\Validation\ValidationException with the keys and messages of the endpoint, such as search.filters.0.field. Without payload(), the search runs as an endpoint body without search.
  • as() is required. Without it, the search throws SearchRunException: 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 in perimeterQuery(), 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.
  • viewAny is checked before the search, as on the endpoint: a denial throws Lomkit\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 the Illuminate\Contracts\Pagination\LengthAwarePaginator of 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, meta included.
  • The objects that stay {} when empty, such as meta, pivot and gates, are stdClass instances in that array.
  • page(page: 2) and render(page: 2) read that page instead of the page of the payload. It is validated as search.page.
  • page() and render() run searching() before the query and searched() after it, as on the endpoint.
  • page() and render() are bounded by rest.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 Number value is a number, and an empty Json value 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:

app/Http/Controllers/TicketExportController.php
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');
}
POST /tickets/export
{
  "search": {
    "filters": [{"field": "status", "value": "open"}],
    "selects": [{"field": "id"}, {"field": "title"}, {"field": "status"}],
    "sorts": [{"field": "id"}]
  }
}
  • lazy(chunk: 500, max: null) returns an Illuminate\Support\LazyCollection of rendered rows. Each row is the row that the endpoint returns in data: its fields, its includes cut to their limits with @more, and the gates that 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, checks viewAny, runs searching() 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 chunk keys 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.
  • max reads the first max rows only. lazy() ignores the limit and the page of the payload.
  • searched() does not run, and an override of paginate() in the resource does not apply to lazy().
To export many rows in the background, in the columns that the client picks, use a queued, batched action whose jobs each write a chunk of rows, and let the client download the file. See Exports.

lazy() errors

All of them throw Lomkit\Rest\Exceptions\SearchRunException:

CaseMessage
A text search on a search engineThe 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 1The chunk of lazy() must be at least 1, [0] given.
A negative maxThe 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.