3 Trigger templates
A trigger template contains PHP methods that camindo CMS calls on events of the records of a content area – when creating, saving, deleting, restoring, and for the actions of a deletion policy. In this way, contents can be prepared on saving (combine several fields into one, check values), linked data can be updated, or personal data can be anonymized according to your own rules.
The methods run regardless of where the change comes from: input form, frontend (CMS:SAVEDATA,
CMS:USER_…), API, import, batch processing, the trash cleanup job, and deletion policies.
Assignment
A trigger template is an ordinary template in a template area. You assign it in the settings of the content area under “Trigger templates” (see Handbook for administrators). An area can have several trigger templates, and the same template may be assigned to several areas. In this way, reusable building blocks can be formed – for example a template that maintains slugs in all areas, and alongside it one with the rules of a single area.
Order
The templates of an area run one after another in the displayed order for each event; you change the order with the arrow buttons next to each entry.
- A template without the method of the event is skipped.
- All of them receive the same variables: what one template changes in
$dataor$affectedFields, the next one already sees. - If a template throws an exception, the following ones no longer run, and the process is cancelled (see Cancelling).
- Each template is an object of its own: properties and helper methods with the same name in
different templates do not interfere with each other. Exchange data between templates via
$data.
Structure
The template contains method definitions only – no output, no statements outside the methods. camindo CMS places the compiled code into the body of a class; each method thus becomes a method of an object:
<?php
// Trigger template "members_trigger" for the area Members
function onSave(array &$affectedFields, array &$data, array &$previousData): void
{
// Display name from first and last name
$data['name'] = trim(($data['first_name'] ?? '') . ' ' . ($data['last_name'] ?? ''));
if (!in_array('name', $affectedFields, true)) $affectedFields[] = 'name';
}
private function normalize(string $text): string
{
return trim(preg_replace('/\s+/', ' ', $text));
}
- All event methods are optional. If one is missing, nothing happens for that event.
- Your own helper methods (
private function …), properties and constants are allowed. - CMS:SELECT is allowed to determine contents of linked records (note the remarks below!).
- Address classes by their full name (
\Camindo\Core\Cms,\DateTimeImmutable) –usedoes not work, because the code sits in the middle of a class. - A template that cannot be loaded (syntax error, output outside the methods) aborts the process with an error message. Saving records without the intended processing would be worse.
What is available in the template
$this->cms |
Cms – snippet(), error(), select(), fetch() … |
$this->db |
Database – your own queries |
$this->record |
Record – the record that triggers the event (load(), save() …) |
$this->areaId |
Area for which the template was loaded. If it is assigned to several areas, take the area from $data['area_id'] |
$this->temporary |
true when a working copy is saved temporarily (tab change in the input form) |
$this->email, $this->pagination … |
the services that templates also know |
$cms['record_id'] |
number of the triggering record – only this, for the CMS commands; in onSave() that of its working copy (see Querying linked contents) |
Events
| Method | When |
|---|---|
onCreate(array &$data): void |
a new record is prefilled – values in $data appear in the empty mask |
onLoaded(array &$data): void |
after each loading of a record (input form, state before saving …) – not with CMS:SELECT in the frontend |
onSave(array &$affectedFields, array &$data, array &$previousData): void |
before the check and the writing. A field that the method sets anew must be added to $affectedFields |
onSaved(array &$affectedFields, array &$data, array &$previousData): void |
after writing, before the transaction is completed |
onSoftDelete(array $data): void |
the record goes into the trash |
onRestore(array $data): void |
the record comes back from the trash |
onDelete(array $data): void |
permanent deletion, before the record is deleted in the database |
onDeleted(array $data): void |
permanent deletion, after it |
onPrivacyAction(string $action, array &$data, array $context): void |
action of a deletion policy – see below |
$previousData is the saved state before the change (empty for a new record).
Cancelling
All events except onCreate and onLoaded run in the transaction of the process. An exception
cancels it and reverts it completely; the message appears in the input form or as an error of the
API:
function onSave(array &$affectedFields, array &$data, array &$previousData): void
{
if (($data['end'] ?? '') !== '' && $data['end'] < ($data['start'] ?? '')) {
throw new \Exception('The end is before the start.');
}
}
Temporary saving
If an editor switches tabs in the input form, camindo CMS saves the interim state of the working
copy – onSave and onSaved also run then, with $this->temporary = true. Time-consuming or
outward-facing steps (emails, interfaces) belong after if ($this->temporary) return;.
Moving
If a record is moved to another area, the trigger templates of the target area apply.
$data already contains the fields of the target area there, $previousData the state in the
source area with its field names.
Re-entry
If a template saves the same record again in onSaved(), it would run endlessly. An event is
therefore not nested for the same record within the same template: when saving again, the other
trigger templates of the area run, but the one currently running does not. Other records – also of
the same area – trigger their triggers normally.
Querying linked contents
The contents of the triggering record are in $data (new state) and $previousData (previous
state) – multilingual fields with all languages. Read and change them there, not via CMS:TEXT.
For other records, such as the linked ones, you can use CMS:SELECT as in any template.
While a method runs, $cms contains only record_id, the number of the triggering record. A
CMS:SELECT with relation therefore needs no src_id – and should not have one (see below):
function onSave(array &$affectedFields, array &$data, array &$previousData): void
{
$isVariant = false;
?>
(CMS:SELECT
relation=variant_of
state=any archive=true future=true
resulttemplate=shop.trigger_variant_found
)
<?php
$data['is_variant'] = $isVariant ? 'yes' : '';
if (!in_array('is_variant', $affectedFields, true)) $affectedFields[] = 'is_variant';
}
The result template (shop.trigger_variant_found) runs in the body of the method: it sees the
variables of the method and $this, and here only sets $isVariant = true;. It must not output
anything.
Observe the limits:
-
New state of the links via
$cms['record_id']. While an existing record is being edited, its links – outgoing as well as incoming – hang on a working copy; only after saving do they pass over to the record. InonSave()– and inonSaved()during temporary saving –$cms['record_id']therefore contains the number of the working copy:CMS:SELECT relation=…and CMS:RELATION without a source specification already see the changed links, and so does the reverse direction (“Who refers to this record?”) withdest_id="{$cms['record_id']}"orfilter[field]="{$cms['record_id']}".$data['record_id'], on the other hand, is always the number of the record itself – with it you get the previous state inonSave(). InonSaved()after the final saving, and in all other methods, both numbers are the same. -
Embedded contents during temporary saving: Links of a field with embedded contents point, during temporary saving, to their working copies, which
CMS:SELECTdoes not find. On final saving these contents are already taken over and are found. -
Publication:
CMS:SELECTfinds here only what the website would show – released, publication date reached, not expired. If the question concerns the structure (“Is any record linked at all?”), setstate=any archive=true future=true; otherwise blocked, pre-dated and expired records are missing.CMS:SELECTnever finds records in the trash. -
Rights: The query sees what the triggering user may see. The result can therefore differ depending on who saves:
Trigger What CMS:SELECTfindsUser in the back end or via the API only contents of the instances, areas and languages that their user group may read (superusers: all) Website ( CMS:SAVEDATA,CMS:USER_…)contents of the instance and its readable instances Scheduler (also due blocks of a deletion policy later on) all -
Language: Multilingual fields of the records found appear in one language – on the website in the language of the page, in the back end in the content language of the user. All languages are returned by
$this->record->load($id).
onPrivacyAction: deletion policies
A deletion policy determines what happens when a record is deleted. What cannot be expressed with “Overwrite individual fields” – linked records, texts that are only partly personal, multilingual contents per language – is handled by the trigger template with two actions:
| Action of the policy | Call |
|---|---|
| “Apply trigger template to the record” | once, $data is the record |
| “Apply trigger template to the audit log” | once for each entry in the audit log of the record, $data is the entry |
Each such block has an identifier (lowercase letters, digits, _). It arrives as $action –
the template uses it to tell the blocks apart, for example an immediate step from one after a year.
function onPrivacyAction(string $action, array &$data, array $context): void
$context |
|
|---|---|
target |
record or audit |
record_id |
the deleted record |
user_id |
the website user to whom it belongs (record.user_id), otherwise 0 |
area_id |
its area |
source |
trigger: record (record is being deleted), profile (UGC: profile was deleted) or area (area was deleted) |
policy_id, job_id |
policy and job |
deleted |
only with record: true if the record lies in the trash at that time |
On the record
$data contains all fields of the record – multilingual fields as an array language_id => value,
file fields with their metadata. camindo CMS then saves what the method changes in $data:
- in policy mode: without required-field and format checks and without the rights of the user group – “unique” still applies;
- not file fields,
record_id,area_id,user_idand the other identifiers. To remove a file, use “Overwrite individual fields” (image = "") in the same or a later block; - in the audit log as the action “adjusted by deletion policy” (
privacy), without the previous values.
If the record lies in the trash ($context['deleted']), the method runs as well – for example to tidy up
linked data – but nothing is saved on the record then. Other records the method processes itself via
$this->record or $this->db.
On the audit log
For each entry in the history of the record, the method is called with the entry:
| Key | |
|---|---|
id, t |
number and time – remain unchanged |
action |
create, update, delete, restore, purge, privacy … |
actor |
type (admin, user, system), admin_id, user_id, ip, origin – acting persons only as numbers, without names |
label |
designation of the record at that time |
changes |
list of the changes: k field name (multilingual field@en_US), l label, t field type, g language, b before, a after |
camindo CMS writes changed entries back – into the file under var/audit/ with a new checkpoint of the
checksum chain, and label also into the database. Entries that arise only later (for example during
permanent deletion) do not pass through the method; plan a later block for them.
If the area has several trigger templates with onPrivacyAction(), they run one after another with the
same identifier; each checks itself whether $action concerns it.
Errors
If none of the trigger templates of the area has onPrivacyAction(), or if the method throws an exception,
the job fails: it appears in the log privacy with the message and is retried at the next run of the
scheduler task “Apply deletion policies” – at most 20 times. If a block that is due immediately fails,
the deletion does not abort: the remaining blocks run, and the failed one follows with the next run.
As of camindo CMS 9.1.3