Plugin data access
Guarded Signals::read / update / operation facade — field allowlists, custom-field routing, whole-update rejection, audit metadata.plugin, and entity whitelist.
Facade surface
Signals::read(string $package, string $entity, int $id): array
Signals::update(string $package, string $entity, int $id, array $attributes): bool
Signals::operation(string $package, string $action, mixed ...$args): mixed
PluginBase helpers: read(), updateEntity(), operation() (lifecycle hook is onUpdate(), separate from writes).
Hook handlers should prefer PluginContext:
$context->read('accounts', $id);
$context->update('accounts', $id, ['custom_fields' => ['xero_contact_id' => $xeroId]]);
Field catalogue / domain meaning of each entity: Plugin data models.
Entity whitelist
App\Sdk\Data\PluginEntityMap:
| Entity key | Model |
|---|---|
accounts |
Account |
rentals |
Rental |
invoices |
Invoice |
catalogue_items |
CatalogueItem |
Unknown entity → deny + audit + PluginDataException.
read() semantics
- Single record by integer id.
- Returns only fields allowed by the effective read allowlist from the manifest (
read_fieldsoverride, elsefields[]). - Requires
readin the entity’soperationslist.
update() semantics
- Single record by integer id; transactional write of the submitted attributes.
- Requires
writeinoperations. - Effective write allowlist from
write_fields(override) orfields[]. - Whole-update rejection: any undeclared top-level or nested custom-field key fails the entire update — no partial apply.
- Successful updates fire
AuditableEventwithmetadata: ['plugin' => $package](stored onaction_logs.metadata). Action name is{singular}.updatedwhen that name exists inEventRegistry, otherwiseplugin.data_updated. - Denials fire
plugin.data_access_deniedwith the same metadata shape.
Custom-field routing
- Nested writes use attribute key
custom_fields(flat map of name → value). - Nested key
xis allowed when the write allowlist declarescustom_fieldsorcustom_fields.x. - Reads project allowed custom fields into the returned array under the same conventions.
Special case: accounts.email
email is a virtual field for the account’s primary email sync path. It is not a SchemaRegistry core column; declare it explicitly in data_access when needed (as Xero does).
operation() semantics
- Name must appear in manifest
operations[]and be mapped inPluginOperationRegistry. - Seeded mappings today:
activities.create,activities.complete,favourites.toggle. - Gate / authorisation still runs inside the target action class.
Contract: Plugin Operation Registry.
Asymmetric read / write fields
Xero declares separate lists so hooks can read contact fields but only write the Xero id custom field:
- entity: accounts
read_fields: [name, email, custom_fields.xero_contact_id]
write_fields: [custom_fields.xero_contact_id]
operations: [read, write]
What you cannot do
| Not available | Instead |
|---|---|
| Eloquent on core models from plugin code | Use this facade |
| Query builder / list / filter API | Single-id read / update only — see Coming soon |
| Partial updates with extra keys | Drop undeclared keys before calling update() |
| Core table migrations | plugin_ tables only |
Related
- Plugin data models — field tables per entity
- Plugin manifest —
data_accessvalidation rules - Plugin hooks — AuditableEvent re-entry guard via
metadata.plugin