Loading and saving
A resource loads and saves records through its model. Override these
methods to change how. Saving methods run inside the transaction of the
endpoint request: a failure rolls back the record, its audit entries and
everything afterSave() did.
| Method | Use it to |
|---|---|
scopeQuery(Model $model): void |
restrict all queries, e.g. join and filter related data |
formValues(array|object $record): array |
provide values of virtual fields for the form |
createRecord(array $data, array $virtual): Uuid |
create records differently, e.g. through a service |
updateRecord(array|object $record, array $data, array $virtual): void |
update records differently |
deleteRecord(array|object $record): void |
e.g. archive instead of delete |
afterSave(Uuid $uuid, array $virtual, string $operation): void |
save virtual fields, e.g. relations (protected) |
scopeQuery()
Section titled “scopeQuery()”public function scopeQuery(Model $model): void{ $model->select('invoices.*, c.name AS customer_name') ->join('customers c', 'c.id = invoices.customer_id') ->where('invoices.archived_at', null);}Records are always loaded through query() and findRecord($uuid), which
apply the tenant scope of the model and scopeQuery(). A record outside
the scope results in 404, as if it did not exist: users cannot even
learn that it exists. The scope applies to the list, the detail, edit and
delete, actions, exports, the global search, selects and relationship
columns that use the resource, related lists and the API.
- Joins are not tenant scoped by Anvil: join through keys of records the main query already restricts, or add the tenant condition yourself.
- Select
table.*when you add columns, so the record keeps its own columns (id,uuid). - Columns from joins can be shown, sorted and searched with
queryColumn(), see Computed values.
What is saved
Section titled “What is saved”- The form validates the input and converts it; only fields writable in the operation are included, see What is saved.
- Values of normal fields are
$data(column => value), values of virtual fields$virtual. createRecord()inserts$data(UUID, tenant and audit entry are added by the model) and callsafterSave(…, 'create');updateRecord()updates the changed record and callsafterSave(…, 'edit').- Validation errors of the model (
$validationRules) are shown at the fields (422).
Virtual fields: formValues() and afterSave()
Section titled “Virtual fields: formValues() and afterSave()”Example: the items of a business role, stored in a link table.
public function form(Form $form): Form{ return $form->schema([ TextInput::make('name')->required()->maxLength(150), CheckboxList::make('items')->groupedOptions($this->itemOptions())->columns(3)->virtual(), ]);}
public function formValues(array|object $record): array{ return ['items' => service('roleItems')->uuidsOf((int) $this->recordKey($record))];}
protected function afterSave(Uuid $uuid, array $virtual, string $operation): void{ service('roleItems')->sync($uuid, $virtual['items'] ?? []);}formValues() is also used for the detail page and the API
representation. $operation is FormContext::CREATE or FormContext::EDIT.
Own create, update and delete
Section titled “Own create, update and delete”public function createRecord(array $data, array $virtual): Uuid{ return service('orders')->place($data, $virtual['items'] ?? []);}
public function deleteRecord(array|object $record): void{ // Archive instead of delete. $this->model()->update($this->recordKey($record), ['archived_at' => date('Y-m-d H:i:s')]);}createRecord()returns the UUID of the new record; the user is sent to its detail page (or back to the parent record when it was created from a relation).- The default
deleteRecord()deletes the record and its files (unless the model uses soft deletes, so a restore keeps them). - Permissions and policies are checked before these methods are called; don’t check them again, but do check business rules.
Validation errors from code
Section titled “Validation errors from code”use Anvil\Endpoint\EndpointException;
throw EndpointException::forValidation(['amount' => 'The amount exceeds the budget.']);The message is shown at the field (422) and the transaction is rolled back. A key that is not a field of the form is shown as an error message (toast) instead.
Keys and UUIDs
Section titled “Keys and UUIDs”recordKey($record) returns the internal primary key for joins and
services, recordUuid($record) the UUID. Never send the key to the
browser: URLs, forms, tokens and API answers use the UUID.