Fields
Declaring fields
fields() returns typed field objects. Each field names a column of the model and picks the type that matches it:
use Lomkit\Rest\Fields\Boolean;
use Lomkit\Rest\Fields\Date;
use Lomkit\Rest\Fields\DateTime;
use Lomkit\Rest\Fields\Enum;
use Lomkit\Rest\Fields\ID;
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Fields\Json;
use Lomkit\Rest\Fields\Number;
use Lomkit\Rest\Fields\Text;
public function fields(): array
{
return [
ID::make('id'),
Text::make('title'),
Text::make('summary')->nullable(),
Integer::make('views'),
Number::make('price'),
Boolean::make('featured'),
Enum::make('status'),
Date::make('published_on')->nullable(),
DateTime::make('published_at')->nullable(),
Json::make('metadata')->nullable(),
];
}
| Column | Field | Filter operators | Base rule |
|---|---|---|---|
| primary key | ID::make('id') | =, !=, in, not in | the key type: integer, string, uuid or ulid |
| string, text | Text::make('name') | =, !=, like, not like, ilike, not ilike, in, not in | string |
| integer | Integer::make('views') | =, !=, >, >=, <, <=, between, not between, in, not in | integer |
| decimal, float | Number::make('price') | same as Integer | numeric |
| boolean | Boolean::make('featured') | =, != | boolean |
| date | Date::make('published_on') | =, !=, >, >=, <, <=, between, not between | date |
| datetime, timestamp | DateTime::make('published_at') | same as Date | date |
| enum cast | Enum::make('status') | =, !=, in, not in | Rule::enum() of the cast |
| json, jsonb | Json::make('metadata') | none | array |
- Add
->nullable()to a nullable column. A nullable field acceptsnullin writes, and adds theis nullandis not nulloperators. - A string in
fields(), or a name declared twice, throwsLomkit\Rest\Exceptions\InvalidFieldException, which names the resource. - A name holds no dot.
Lomkit\Rest\Fields\DateTimehas the same short name as PHP's\DateTime. In a file that uses both, alias one of them.
ID
ID reads its key type from the model: uuid or ulid when the model generates the column with HasUuids or HasUlids, int for an integer key type, string otherwise. Declare it when the model says otherwise:
ID::make('code')->keyType('string'),
keyType() takes int, string, uuid or ulid. The type of the ID field types every key that clients send: key in mutate, resources on the deletion endpoints and on actions. A key of another type gives a 422, such as The mutate.0.key field must be an integer.
Enum
Enum reads its enum class from the casts of the model. A field that is not bound to a model, such as the input of a scope or an action, names it:
Enum::make('status', PostStatus::class),
The details and the OpenAPI document list the backing values of the enum.
Offered cases
offered() withdraws some cases from writes, with a list of cases or a closure. A case left out stays readable, filterable and sortable, so the rows that hold it keep working:
Enum::make('status')->offered([ShowStatus::Planned, ShowStatus::OnSale]),
Enum::make('status')->offered(fn (ShowStatus $status): bool => $status !== ShowStatus::SoldOut),
{"mutate": [{"operation": "update", "key": 2, "attributes": {"status": "sold_out"}}]}
{
"message": "The selected mutate.0.attributes.status is invalid.",
"errors": {
"mutate.0.attributes.status": ["The selected mutate.0.attributes.status is invalid."]
}
}
- Without
offered(), every case is offered. - Writes refuse a withdrawn case on its key: the attributes of
mutateat every level, pivot fields, the fields of actions and instructions, the parameters of scopes, and Precognition requests. - An update that leaves the field out keeps the stored case, withdrawn or not.
- Filters, sorts and rows keep every case:
{"field": "status", "operator": "in", "value": ["sold_out"]}still finds the sold-out shows. - The details give
offerednext tovalueswhen cases are left out:"values": ["planned", "on_sale", "sold_out"], "offered": ["planned", "on_sale"]. The OpenAPI document lists the offered cases in the create, update, pivot and input schemas, and every case in the records and the filters. - A case of another enum, or a list or closure that offers no case, throws
InvalidFieldExceptionwhen the offered cases are first read:The enum field [status] offers no case. Offer at least one case, or declare the field readonly().
Json
Json works with or without a JSON cast on the model. When the model writes the attribute as it is, with no cast or a string, encrypted or AsStringable cast, and no set mutator, the field encodes the value before Eloquent writes it, and the response returns the decoded value. Any other cast, such as array, json, collection or AsArrayObject, and a set mutator receive the value as sent.
A Json value accepts at most 64 levels of nested arrays and objects, the top level included. A Json field cannot be filtered, and is sorted only when it declares sortable().
Reading fields
By default, a field is selectable, sortable and filterable with the operators of its type. These methods change what a search can do with it:
| Declaration | selects and rows | sorts | filters |
|---|---|---|---|
| default | yes | yes, but not Json | yes, but not Json |
selectable(false) | no | only with sortable() | only with filterable() |
writeOnly() | never returned | only with sortUsing() | only with filterUsing() |
virtual() | only with resolveUsing() | only with sortUsing() | only with filterUsing() |
sortable(false) | yes | no | yes |
filterable(false) | yes | yes | no |
filterable(['=', 'like']) | yes | yes | these operators only |
A field that a search cannot use gives a 422, as an undeclared field does: The selected search.filters.0.field is invalid.
Value types in rows
A row returns each value in the type that the OpenAPI document gives its field:
NumberandIntegerturn a numeric string into a number. Adecimal:2cast reads"25000.50"from the database, and the row returns25000.5.Jsonreturns an empty array as{}. A list stays a list, and an object an object.- The other types,
null, a string that is not numeric and a list on aNumberfield are returned as they are.
[
{"id": 1, "price": 25000.5, "seats": 300, "settings": {"lights": "warm"}},
{"id": 2, "price": null, "seats": 0, "settings": {}}
]
This covers the rows of a search, their includes and pivots, the records of the response block of a mutation, the values of resolveUsing() and the rows of Rest::search(). A custom map() receives the converted values. Read these values as numbers and objects in your clients.
Text::make('api_token')->writeOnly(), // written, never returned
DateTime::make('created_at')->selectable(false), // not returned, not sorted, not filtered
Text::make('slug')->filterable(false), // refused in filters
Text::make('title')->filterable(['=', 'like']), // = and like only
Text::make('summary')->nullable(), // the Text operators, plus is null and is not null
filterable([...]) narrows the operators of the field. An operator that its type does not have throws InvalidFieldException: Boolean::make('featured')->filterable(['>']).
Virtual fields
A field that is not a column of the table, such as an accessor, is declared virtual(). resolveUsing() gives its value, and requires() lists the columns that the callback reads:
use Illuminate\Database\Eloquent\Model;
Text::make('display_name')
->virtual()
->requires(['first_name', 'last_name'])
->resolveUsing(fn (Model $author): string => $author->first_name.' '.$author->last_name),
- The callback receives the model of each returned row, and runs once per row.
- The value is sent as the callback returns it, in the type of the field: a
Jsonfield does not decode a string, but returns an empty array as{}, and aNumberorIntegerfield turns a numeric string into a number. See Value types in rows. - A field that is not virtual can declare
resolveUsing()too: the callback then replaces the column value. - The appended attributes of a model (
$appends) are never returned. Return an appended value throughresolveUsing().
Custom sorts and filters
sortUsing() and filterUsing() replace the column of a field in sorts and filters. They make a virtual or write-only field sortable or filterable:
use Illuminate\Contracts\Database\Eloquent\Builder;
Text::make('full_name')
->virtual()
->sortUsing(fn (Builder $query, string $direction) => $query
->orderBy('last_name', $direction)
->orderBy('first_name', $direction))
->filterable(['=', 'like'])
->filterUsing(fn (Builder $query, string $operator, mixed $value) => $query
->where('first_name', $operator, $value)
->orWhere('last_name', $operator, $value)),
- The sort callback receives the Eloquent query and
ascordesc. - The filter callback receives the Eloquent query, the operator as a string and the value as sent. It can also be an object that implements
Lomkit\Rest\Contracts\Filter. Its conditions stay in their own group, so anorWhere()does not leave the filter. - The callbacks also run at the end of a relation path, such as
author.full_name.
See Filters and Sorts and selects.
Columns a search loads
A search loads only the columns that its rows need: the returned fields, the model key, the requires() of the returned fields, the keys of its includes, and the columns of alwaysSelect(). List there the columns that a policy, an accessor, an observer or a custom response reads without returning them:
public function alwaysSelect(): array
{
return ['status', 'team_id'];
}
A gate policy, a resolveUsing() callback, a custom map(), the searched() event or searchMeta() that reads a column that the search did not load throws Lomkit\Rest\Exceptions\MissingColumnException, which names the resource, the column and the model. Set search.select_columns to false in config/rest.php to load every column. See Configuration.
Writing fields
A field is creatable and updatable by default. ID fields are not. These methods change what mutate accepts in attributes:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
ID::make('id'), // refused on create and update
ID::make('uuid')->creatable(), // a key that clients send on create
DateTime::make('created_at')->readonly(), // refused on create and update
Text::make('slug')->updatable(false), // written on create only
Text::make('reference')->creatable(false), // written on update only
Text::make('api_token')->writeOnly(), // written, never returned
Text::make('full_name')
->virtual()
->fillUsing(function (Model $author, mixed $value): void {
$author->forceFill([
'first_name' => Str::before((string) $value, ' '),
'last_name' => Str::after((string) $value, ' '),
]);
}),
createaccepts the creatable fields, andupdate,syncandtogglethe updatable fields.- A
virtual()field is refused unless it declaresfillUsing(). The callback receives the model and the value, and runs after the other fields of the entry are filled. - An attribute that the operation cannot write gives a
422under theattributeskey of the entry:The created_at key of the mutate.0.attributes field is not writable on create.
Validating fields
Each field validates its value with the base rule of its type, nullable, then its own rules:
Text::make('title')
->rules('max:255') // on every write
->creationRules('required') // on create
->updateRules('filled'), // on update, sync and toggle
label() names the field in validation messages, and messages() replaces Laravel's text for its rules:
Text::make('email')
->label(__('email address'))
->creationRules('required', 'unique:users,email')
->messages(['unique' => __('This :attribute is already registered.')]),
See Validation for the resource rules, the rules that depend on the entry, and the order of the messages.
Text search
searchable() marks a Text field that the Contains and FullText text searches read:
Text::make('name')->searchable(),
Only Text fields, and field types that extend Text, can be searchable. A virtual or write-only field cannot. See Text search.
Documentation
Each field gives its OpenAPI type. examples() lists example values, which the request examples of the document use, and meta() adds keys that the details return with ?with=fields.meta and the document returns under x-lomkit-meta:
Text::make('priority')
->examples('low', 'normal', 'urgent')
->meta(['placeholder' => 'normal']),
Custom field types
A field type of your application extends a field class. Extend the closest type, so that the field keeps its operators, its base rule and its aggregates:
use Dedoc\Scramble\Support\Generator\Types\NumberType;
use Dedoc\Scramble\Support\Generator\Types\Type;
use Lomkit\Rest\Fields\Number;
class Money extends Number
{
public function toSchema(): Type
{
return (new NumberType)->format('decimal');
}
}
toSchema()returns the Scramble type of the field. A type that extendsFielddirectly documents astringuntil it overrides it.toWriteSchema()returns the type of the values that writes accept. It returnstoSchema()unless the type overrides it.render(mixed $value): mixedgives the response form of a value, read from the model or returned byresolveUsing(). A type that documents another type intoSchema()overridesrender()to return that type. A type that already defines a method namedrender()renames it or gives it this signature.avgandsumaggregates need a field that extendsNumber.- A search engine declaration refuses the app types that extend a field type. See Search engines.
Reserved names
A resource cannot declare a field named like a key that the package adds to its rows. The resource throws InvalidResourceException when its fields are first read:
- the gates key,
gatesby default (gates.keyinconfig/rest.php); - the more key,
@moreby default (search.more_key); - the morph type key,
@typeby default (morph.type_key), on a resource that aMorphTorelation loads. This one throws when a request resolves the target.
Input fields
Scopes, instructions and actions declare their inputs with the same field classes. Input fields take rules() and default(). creationRules() and updateRules() describe writes, and throw InvalidFieldException on an input field:
Integer::make('min_views')->default(100),
Boolean::make('notify')->rules('required'),
See Scopes and instructions and Actions.
Resources
Define a resource for an Eloquent model, bind it to a controller, register its routes and generate it from the table of the model.
Relationships
Declare the Eloquent relations of a resource, their targets, pivot fields, constraints, link abilities and the operations a mutation accepts on them.