SIGNALS Documentation
API Reference

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_fields override, else fields[]).
  • Requires read in the entity’s operations list.

update() semantics

  • Single record by integer id; transactional write of the submitted attributes.
  • Requires write in operations.
  • Effective write allowlist from write_fields (override) or fields[].
  • Whole-update rejection: any undeclared top-level or nested custom-field key fails the entire update — no partial apply.
  • Successful updates fire AuditableEvent with metadata: ['plugin' => $package] (stored on action_logs.metadata). Action name is {singular}.updated when that name exists in EventRegistry, otherwise plugin.data_updated.
  • Denials fire plugin.data_access_denied with the same metadata shape.

Custom-field routing

  • Nested writes use attribute key custom_fields (flat map of name → value).
  • Nested key x is allowed when the write allowlist declares custom_fields or custom_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 in PluginOperationRegistry.
  • 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