›
byrcsc/laravel-custom-fields · 1.x
Keep a listing that reads custom fields to a fixed number of queries regardless of how many records it shows.
Reading custom fields costs two things: the value rows for the records on the page, and the definitions for the model and tenant. The package handles the second for you and needs one line from you for the first.
Customer::query()->with('customFieldValues')->paginate();Reads go through the customFieldValues relation rather than a query, so eager
loading it makes the whole page cost one extra query instead of one per record.
Leave it out and every getCustomField() or customFields read on a record
loads that record's values on its own, which is the usual lazy-loading N+1.
With the relation eager loaded, a page of five records and a page of twenty-five cost the same number of queries: one for the records, one for their values, and at most one for the definitions.
The manager holds the definitions it has resolved, keyed by model class and tenant, so reading a page of records asks for the same set once rather than once per record.
Without it a listing would cost one definitions query per row, which is an N+1 on the definitions table rather than the values one, and harder to spot because the eager-loaded values look fine.
The binding is scoped rather than a singleton, so in practice the lifetime is one request or one queued job. A singleton would survive between Octane requests and between jobs, serving one tenant's definitions to the next.
Saving or deleting a definition through its model clears what the manager holds.
That covers every write the facade makes and every change you make through
CustomFieldDefinition.
Four kinds of write fire no model event, so none of them reaches the manager:
saveQuietly()withoutEvents()CustomFieldDefinition::query()->update() or ->delete()Say so by hand after one of those:
use ByRcsc\LaravelCustomFields\Facades\CustomFields;
CustomFieldDefinition::query()->where('model_type', $type)->update(['sort_order' => 0]);
CustomFields::flush();flush() forgets every definition resolved so far, so the next read goes to the
database. It is cheap, and calling it when nothing had changed costs one extra
query.
The same blind spot applies to the type-change guard, for the same reason.
The values table indexes (model_type, model_id), which is the lookup an
eager-loaded page runs, and each of the four filterable typed columns alongside
the definition id. The definitions table indexes (model_type, tenant), which
is the lookup every read starts from.
text_value and json_value carry no index, so filtering a textarea or a
multi_select scans the value rows for that field. See which filters use an
index.
A job that reads custom fields needs the tenant to be current inside the job, not only at dispatch. The package asks its resolver at the moment of the read, and a resolver reading from the request has nothing to read from in a worker.
Make the tenant current the way your tenancy setup does, then read. Under
spatie/laravel-multitenancy that is its own queue behaviour, and under a
resolver of your own it is whatever you put the tenant on.
The manager's cache is scoped, so each job starts with nothing resolved and cannot inherit the previous job's tenant.