Events
DGS supports a flexible event system that allows developers to customize and extend platform behavior by reacting to key actions during data processing (e.g., insert, update, list rendering).
How Events Work
Events in DGS are triggered at various points in the lifecycle of a store or action (e.g., before inserting data, after updating, while rendering lists, etc.). When an event is fired, the system passes an event object (like FestiEvent or StoreActionEvent) containing contextual data. Developers can intercept these events to inspect or modify behavior.
Supported Approaches to Handle Events
DGS supports three ways to subscribe to events, depending on your project structure and flexibility needs.
Subscription in Definition Scheme (XML/JSON/Array)
Recommended for plugin-based or declarative module setups.
<listeners>
<listener event="<?php echo Store::EVENT_BEFORE_INSERT; ?>"
plugin="Manager"
method="onCreateNewProduct" />
<listener event="<?php echo Store::EVENT_INSERT; ?>"
plugin="Manager"
method="onInsertNewProduct" />
</listeners>
Runtime Subscription
Use addEventListener() to attach listeners dynamically in controller logic or store setup.
/**
* @urlRule ~^/companies/new/$~
* @area admin
*/
public function onAjaxCreateForm(Response &$response): bool
{
$store = $this->createStoreInstance("company_create");
$method = [&$this, 'onCreateNewCompany'];
$store->addEventListener(Store::EVENT_BEFORE_INSERT, $method);
$store->onStart($response);
return true;
} // end onAjaxCreateForm
Interface-Based Event Handling
Best for clean, structured, and reusable logic. Implement event-specific interfaces directly in your Store class.
class MyStore extends Store implements IBeforeInsertListener
{
public function onBeforeInsertHandler(StoreActionEvent &$event): void
{
// Custom logic here
}
}
StoreActionEvent and FestiEvent Classes
In the event handler method,
you can use the event classes FestiEvent and StoreActionEvent.
When using FestiEvent, all data will be accessible through the method $event->getTarget()
or the attribute $event->target.
It is recommended to use StoreActionEvent, especially in systems with custom logic.
StoreActionEvent allows for better compatibility with different core versions.
public function onSave(StoreActionEvent &$event)
{
$values = &$event->getValues();
// ...
}
Subscribable Events for Store Runtime Listeners
Core::EVENT_ON_CREATE_STORE
This event is triggered every time a store is created in the system. It is useful for adding interceptors to view or modify all stores in the system.
Store::EVENT_BEFORE_INSERT (core\store\event\IBeforeInsertListener) and Store::EVENT_BEFORE_UPDATE (core\store\event\IBeforeUpdateListener)
This event is triggered before performing data updates. It is suitable for preparing additional fields, modifying data, or performing additional operations before updating data in the store.
The transaction is active at the time this event is executed.
Target:
id- primary key valueinstance- reference to theactionvalues- reference to the array of valuesaction- name of theactionresponse- reference to the systemresponseisUpdated- flag indicating manual data update. If set to true, the system will not physically update the data and assumes that the event listener has performed the update.
$target = [
'instance' => &$this,
'action' => $this->store->getAction(),
'id' => $this->primaryKeyValue,
'values' => &$values,
'data' => $this->data,
'isUpdated' => false
'response' => &$response
];
Store::EVENT_INSERT (core\store\event\IInsertListener) and Store::EVENT_UPDATE (core\store\event\IUpdateListener)
This event is triggered after adding or editing data. It is useful for updating additional data in the system.
The transaction is active at the time this event is executed.
Not all field data is saved yet. Listeners run in the order they were added.
many2manyandfilefields save their data in their ownEVENT_INSERT/EVENT_UPDATElisteners, which they add while the request is being processed, after the listeners from the scheme or from your plugin. A listener here therefore does not see the link rows or the uploaded file of the current record. UseStore::EVENT_UPDATE_VALUESwhen you need them.
Target:
id- primary key valueinstance- reference to theactionvalues- reference to the array of valuesaction- name of theactionresponse- reference to the systemresponse
$target = [
'instance' => &$this,
'action' => $this->store->getAction(),
'id' => $this->primaryKeyValue,
'values' => &$values,
'data' => $this->data,
'isUpdated' => false
'response' => &$response
];
Store::EVENT_UPDATE_VALUES
Occurs after adding/editing data.
This event is triggered immediately after the Store::EVENT_INSERT or Store::EVENT_UPDATE events.
This is the recommended place for logic that needs the complete saved record:
- every field is saved, including
many2manylinks and uploaded files; - the transaction is still active, so you can read that data with your queries;
- if a listener throws, the whole change is rolled back: the record, its links and your own writes.
Use it instead of Store::EVENT_AFTER_UPDATE whenever the work must succeed or
fail together with the record.
$store->addEventListener(
Store::EVENT_UPDATE_VALUES,
function (StoreActionEvent &$event) {
$idProduct = $event->getPrimaryKeyValue();
// The product categories (many2many) are already saved here.
$this->object->recalculateCategoryCounters($idProduct);
}
);
Target:
id- primary key valueinstance- reference to theactionvalues- reference to the array of valuesaction- name of theactionresponse- reference to the systemresponse
$target = [
'instance' => &$this,
'action' => $this->store->getAction(),
'id' => $this->primaryKeyValue,
'values' => &$values,
'data' => $this->data,
'isUpdated' => false
'response' => &$response
];
Store::EVENT_AFTER_UPDATE
This event is triggered after adding/editing data and after the data is saved. It is used to implement After Commit logic, such as making requests to a remote API that cannot be executed within a transaction.
This event should be used only in rare cases.
- The data is already committed. A failing listener cannot roll it back. The error is reported to the user, but the record stays saved.
- When the action runs inside a transaction that someone else started (a parent transaction), the event fires before that transaction is committed.
- Listeners must be safe to run again (idempotent). If the side effect must be
undone when the record is not saved, do it in
Store::EVENT_UPDATE_VALUESand clean up inStore::EVENT_ROLLBACK.
Store::EVENT_ROLLBACK
This event is triggered when the transaction that saved or removed the record is really rolled back. Use it to undo side effects that the database cannot roll back: files, cache entries, messages to external systems.
- It fires for insert, edit, batch insert and remove actions.
- It fires only on a real rollback. A successful action, or a commit of the parent transaction, never fires it.
- When the action runs inside a parent transaction, the event waits for that transaction. It fires only if the parent rolls back, even when the action itself succeeded.
- The database changes are already rolled back when the event fires. Do not write to the database in a listener.
- A failing listener does not replace the error that caused the rollback. Its
exception goes to
FestiUtils::doHandleCoreException(). - It needs a connection that supports rollback callbacks
(
ITransactionCallbacksfromfesti-team/festi-framework-database, see itsdocs/Transactions.md).
file fields use the same connection callbacks directly (rollback and commit),
to keep the files on disk consistent with the record:
- A file uploaded by a save that is rolled back is removed.
- When an upload replaces an existing file (an edit with the default
<id>_<field>name writes to the same path), the old file is copied aside first. If the save is rolled back, the old file is put back. If it is committed, the copy is deleted.
So after a failed edit the record and its file are both the old ones, and after
a successful edit no backup is left behind. Without a transaction (or with a
connection that does not support ITransactionCallbacks), files are written
directly, as before.
The copy is kept beside the original as <file>.rollback-backup-<id>. If you
find one in an uploads folder, it is from a save that was interrupted before
its transaction ended (fatal error, timeout, redeploy) and is safe to delete.
The next upload to the same file removes such copies once they are older than
an hour. A restore that fails does not stop the others, and each failure,
naming the file and its backup, goes to the rollback callback errors and the
PHP error log.
Target:
id- primary key valueinstance- reference to theactionvalues- the values the action tried to save (falsefor remove)data- the record data passed to the actionaction- name of theaction
$store->addEventListener(
Store::EVENT_UPDATE_VALUES,
function (StoreActionEvent &$event) {
$pdfPath = $this->generateInvoicePdf($event->getPrimaryKeyValue());
// Keep the path, so the PDF can be removed if the invoice is not saved.
$values = &$event->getValues();
$values['pdf_path'] = $pdfPath;
}
);
$store->addEventListener(
Store::EVENT_ROLLBACK,
function (StoreActionEvent &$event) {
$values = $event->getValues();
$pdfPath = $values['pdf_path'] ?? null;
if ($pdfPath && is_file($pdfPath)) {
unlink($pdfPath);
}
}
);
Store::EVENT_BEFORE_REMOVE (core\store\event\IBeforeRemoveListener) and Store::EVENT_REMOVE (core\store\event\IRemoveListener)
This event is triggered after removing or removing data.
The transaction is active at the time this event is executed.
Target:
id- primary key valueinstance- reference to theactionvalues-falsedata- array of valuesaction- name of theactionresponse- reference to the systemresponse
$target = [
'instance' => &$this,
'action' => $this->store->getAction(),
'id' => $this->primaryKeyValue,
'values' => false,
'data' => $this->data,
'isUpdated' => false,
'response' => &$response,
];
Store::EVENT_PREPARE_ACTION_REQUEST
Event for preparing request data. Called before Store::EVENT_PREPARE_VALUES.
Convenient to use for adding custom data to prepare values.
Target:
instance- reference to theactionrequest- reference to the array with request dataaction- name of theaction
$target = [
'instance' => &$this,
'request' => &$request,
'action' => $this->store->getAction()
];
Store::EVENT_PREPARE_VALUES
Event that is called after processing request data and before updating data in the storage.
Target:
instance- reference to theactionvalues- reference to the array of valuesaction- name of theaction
$target = [
'instance' => &$this,
'values' => &$result,
'action' => $this->store->getAction()
];
Store::EVENT_ON_FETCH_LIST
Event for preparing content of the data list. Can be used to add content to the list.
<listeners>
<listener event="<?php echo Store::EVENT_ON_FETCH_LIST; ?>"
plugin="Deals"
method="onFetchListContent" />
</listeners>
public function onFetchListContent(FestiEvent &$event)
{
$event->target['content'] .= $this->fetch('form_create_deal.phtml');
}
Store::EVENT_ON_LIST_GENERAL_ACTIONS
Event for preparing general action list for the list. Displayed above the list.
Store::EVENT_ON_LOAD_LIST_DATA
Event for preparing data for displaying the data list.
Target:
info- reference to the table formation optionsdata- reference to the array with data used for displaytableData- reference to the datafilters- filtersstore- reference to thestore
Store::EVENT_ON_FIELDS_PREFILL_VALUES
Called when preparing default values for adding a new record.
Target
instance- reference to theactionaction- name of theactionfields- reference on list of thestorefieldsvalues- reference to the values arraystore- reference to thestore
$storage->addEventListener(
Store::EVENT_ON_FIELDS_PREFILL_VALUES,
[&$this, 'onPrepareFormFields']
);
...
public function onPrepareFormFields(\StoreActionEvent &$event): void
{
}
Store::EVENT_ON_FETCH_LIST_CELL_VALUE_WITH_LINK
This event is triggered when preparing a link in the list of records and cells.
Target
url- linkvalue- cell valuefield- reference to thefieldprimaryKeyValue- primary key value for the rowrow- array of row valuestarget- link's target attribute(default_self)
$storage->addEventListener(
Store::EVENT_ON_FETCH_LIST_CELL_VALUE_WITH_LINK,
[&$this, 'onPrepareListCellValueLink']
);
Store::EVENT_ON_REMOVE_INTEGRITY
When deleting data, there are often cases where we cannot delete it directly as it would violate data integrity.
Usually, such data has additional deletion logic or requires a status change.
For such cases, the Store::EVENT_ON_REMOVE_INTEGRITY event should be used:
<listeners>
<listener event="<?php echo Store::EVENT_ON_REMOVE_INTEGRITY; ?>"
plugin="Companies"
method="onRemoveUserWithIntegrityError" />
</listeners>
public function onRemoveUserWithIntegrityError(StoreActionEvent &$event)
{
$idUser = (int) $event->getPrimaryKeyValue();
$values = [
'status' => UserValuesObject::STATUS_DELETED
];
$this->object->changeUser($values, $idUser);
$event->setUpdated();
return true;
}
Store::EVENT_ON_LIST_ACTIONS (core\store\event\IListActionsListener)
Sometimes, you may need to have different actions on different rows depending on the data in the record.
Let's consider an example where the delete button should not be displayed on rows where the status is set to created:
-
Subscribe to the
Store::EVENT_ON_LIST_ACTIONSevent. -
Write the handler:
public function onListActions(FestiEvent &$event) { $status = $event->target['values']['status']; if ($status != "created") { return false; } $actions = &$event->target['actions']; $index = array_search(Store::ACTION_REMOVE, $actions); unset($actions[$index]); return true; }
This event is triggered when displaying a value for a field filter.
Target
instance- reference to thefieldfield- name of thefieldaction- name of theactionvalue- filter valuevalues- filter value forForeignKey,Many2many, andDatetimefields
Store::EVENT_ON_FETCH_FIELD_FILTER | core\store\event\IPrepareListItem
This event is triggered before data is formatted for a cell in ListAction.
Event class: core\store\event\ListItemEvent
Example usage of the event to modify data for display in a cell.
ListRowStyleEvent::EVENT_ON_PREPARE_LIST_ROW_STYLE (Festi\Store\Event\IListRowStyleListener)
Fired for every row a list draws, and for the row the grid editor sends back after a save, just before the row's css classes are joined into rowCss. The classes start from what the schema declares: e-db-table-row-<primary key> and the cssClass of every highlight rule the record matches. Listeners add or remove classes, and the row is drawn with whatever is left. Store listeners run first, then the store itself if it implements IListRowStyleListener.
Event class: Festi\Store\Event\ListRowStyleEvent
Target
store- the DGSvalues- the row's values as the storage returned them, including columns the table does not drawclassNames- the row's classes, changed in place; preferaddClassName()/removeClassName()
Mark rows from a plugin:
$store->addEventListener(
ListRowStyleEvent::EVENT_ON_PREPARE_LIST_ROW_STYLE,
function (ListRowStyleEvent &$event) {
$values = $event->getValues();
if ($values['status'] === 'overdue') {
$event->addClassName('b-row-overdue');
}
}
);
Or let the store decide how its own rows look:
class InvoicesStore extends Store implements IListRowStyleListener
{
public function onListRowStyleHandler(ListRowStyleEvent &$event): void
{
$values = $event->getValues();
if ($values['is_archived']) {
$event->removeClassName('b-row-overdue');
}
}
}
To style the rows of every DGS, attach the listener from Core::EVENT_ON_CREATE_STORE.
To replace how an action decides its rows' classes altogether, implement Festi\Store\View\IListRowStyleResolver and hand it over with setListRowStyleResolver(), or override getListRowStyleResolver() in the action. ListRowStyleResolver is the default, and it is the one that fires this event.
Store::EVENT_PREPARE_REPOSITORY_VALUES
This event is triggered when preparing values for updating data in the database.
Target
general- reference to values for tablescallback- reference to values for tables with dynamic logicstore- reference to theStore
$store->addEventListener(Store::EVENT_PREPARE_REPOSITORY_VALUES, function (FestiEvent &$event) {
$tablesValues = &$event->getTargetValueByKey('general');
$store = $event->getTargetValueByKey('store');
assert($store instanceof Store);
if ($store->getAction() == Store::ACTION_INSERT) {
$tablesValues['employees']['cdate'] = date('Y-m-d');
} else {
$tablesValues['employees']['mdate'] = date('Y-m-d H:i:s');
}
});
Store::EVENT_ON_LIST_GROUPED_ACTIONS (core\store\event\IListGropedActionsListener)
This event is triggered when append groped actions to the list. Dispatch StoreActionEvent.
Target:
instance- reference to theListActioninstanceactions- reference to the array of the grouped actions
Example:
class MyStore extends Store implements IListGropedActionsListener
{
public function onListGropedActionsHandler(StoreActionEvent &$event): void
{
$actions = &$event->getTargetValueByKey('actions');
$systemPlugin = Core::getInstance()->getSystemPlugin();
// Remove a grouped action based on a permission section
if (!$systemPlugin->hasUserPermissionToSection("my_store_admin")) {
unset($actions['export']);
}
}
}
StoreFieldCryptoEvent::EVENT_ON_ENCRYPT_VALUE (core\store\event\IFieldEncryptValueListener)
This event is triggered before a crypt="true" field value is encrypted.
Dispatch core\store\event\StoreFieldCryptoEvent. The event builds its
target internally; the keys are exposed as TARGET_*_KEY_NAME constants.
Target:
TARGET_FIELD_KEY_NAME- reference to the field instance, read viagetField()TARGET_VALUE_KEY_NAME- plain value to encrypt, read viagetValue()TARGET_RESULT_VALUE_KEY_NAME- set viasetResultValue()to replace the framework RSA-OAEP encryption
If a listener sets the result value, the framework openssl logic is skipped and the listener result is stored as-is.
Example:
use core\store\event\StoreFieldCryptoEvent;
$store->addEventListener(
StoreFieldCryptoEvent::EVENT_ON_ENCRYPT_VALUE,
function (StoreFieldCryptoEvent &$event): bool {
$event->setResultValue(MyVault::encrypt($event->getValue()));
return true;
}
);
StoreFieldCryptoEvent::EVENT_ON_DECRYPT_VALUE (core\store\event\IFieldDecryptValueListener)
This event is triggered before a crypt="true" field value is decrypted.
Dispatch core\store\event\StoreFieldCryptoEvent. The event builds its
target internally; the keys are exposed as TARGET_*_KEY_NAME constants.
Target:
TARGET_FIELD_KEY_NAME- reference to the field instance, read viagetField()TARGET_VALUE_KEY_NAME- encrypted (base64) value from the database, read viagetValue()TARGET_RESULT_VALUE_KEY_NAME- set viasetResultValue()to replace the framework RSA-OAEP decryption
Use this event to read values encrypted with a custom scheme or to
implement a fallback for data encrypted with the legacy PKCS#1 v1.5
padding (see the Crypt section in Fields).
Example:
class OffersStore extends Store implements IFieldDecryptValueListener
{
public function onFieldDecryptValueHandler(
StoreFieldCryptoEvent &$event
): void
{
$event->setResultValue(MyVault::decrypt($event->getValue()));
}
}
core\store\event\IRowPermissionListener
The framework's default row-access check (hasPermissionToLoadRowByPrimaryKey()) only verifies that the primary key was previously shown to the current session by ListAction — it does not verify real ownership, and it never applies to API/token-based callers. Implementing IRowPermissionListener on a Store subclass replaces that check with a real per-row ownership check for that table. Once implemented, the check is always enforced — a caller cannot bypass it by passing $isCheckPermission = false.
namespace core\store\event;
interface IRowPermissionListener
{
public function hasPermissionToAccessRowHandler(mixed $primaryKeyValue): bool;
}
Behavior:
hasPermissionToAccessRowHandler($primaryKeyValue)decides access by checking the row against real ownership data (e.g. anid_usercolumn) for the table.- Stores that do not implement the interface keep the existing session-based
DB_ALLOWED_IDSbehavior unchanged.
Example:
class GradesStore extends Store implements IRowPermissionListener
{
public function hasPermissionToAccessRowHandler(mixed $primaryKeyValue): bool
{
$idUser = $this->getCore()->user->getID();
if (!$idUser) {
return false;
}
$row = $this->proxy->loadRowByPrimaryKey($primaryKeyValue);
return $row && (int) $row['id_user'] === (int) $idUser;
}
}
Interface-Based Event Handling
In addition to subscribing to events via scheme or dynamically through code, DGS also supports a more structured and performant method: implementing a listener interface in the Store or related class. This approach is especially useful for organizing event logic directly inside classes related to a specific data entity.
Benefits: - Strongly typed and IDE-friendly - Easier to test and refactor - Avoids dynamic event binding overhead - Promotes clean separation of concerns
Example:
class ModuleOptionsStore extends Store implements IBeforeInsertListener
{
public function onBeforeInsertHandler(StoreActionEvent &$event): void
{
$rows = $event->getValues();
foreach ($rows as $key => $value) {
$values = ['value' => $value];
$search = ['name' => $key];
$this->_object->change($values, $search);
}
$event->setUpdated(true);
}
}
Store::EVENT_ACTION_ITEMS (core\store\event\IActionItemsListener)
This event is triggered when preparing the list of form items (fields) for a store action form. It allows you to modify, add, or remove items (fields) before the form is rendered to the user.
Use this event to customize the form fields dynamically based on context, user permissions, or other logic before the form is displayed.
Target:
- instance — reference to the current action instance
- data — reference to the array of form items (fields)
- action — name of the current action
Example:
$store->addEventListener(Store::EVENT_ACTION_ITEMS, function (StoreActionEvent &$event) {
$items = &$event->getTargetValueByKey('data');
// Add a custom field
$items['custom_field'] = [
'caption' => 'Custom Field',
'name' => 'custom_field',
'input' => '<input ... />',
// ... other options
];
});
For a more structured approach, implement the IActionItemsListener interface in your Store class:
class MyStore extends Store implements IActionItemsListener
{
public function onActionItemsHandler(StoreActionEvent &$event): void
{
$items = &$event->getTargetValueByKey('data');
// Modify or add items as needed
}
}
Store::EVENT_ON_FETCH_FORM (core\store\event\IPrepareActionFormListener)
This event is triggered when preparing the variables and structure for rendering a store action form. It allows you to modify the form's fields, sections, captions, and other parameters before the form is displayed to the user.
Use this event to customize the form layout, add or remove fields, adjust captions, or inject additional data into the form rendering process.
Target:
action— array of action optionsitems— reference to the array of form items (fields)sections— reference to the array of form sectionsinfo— reference to the form info (caption, action, token, etc.)what— action namevalues— array of current valuesstore— reference to the store instancetemplateName— template file nameisTabsMode— whether the form uses tabbed sectionsappendContent— additional content to append to the form, such as custom HTML or JavaScript
Example:
$store->addEventListener(Store::EVENT_ON_FETCH_FORM, function (FestiEvent &$event) {
$info = &$event->getTargetValueByKey('info');
// Change the form caption dynamically
$info['caption'] = 'Custom Form Caption';
});
class MyStore extends Store implements IPrepareActionFormListener
{
public function onPrepareActionFormHandler(FestiEvent &$event): void
{
$items = &$event->getTargetValueByKey('items');
// Modify or add items as needed
}
}
Example of DGS when additional columns need to be added, not retrieved from the database, but populated from a plugin:
<?xml version="1.0" encoding="UTF-8" ?>
<table charset="UTF-8"
name="cloud_services"
primaryKey="id"
defaultOrderField="id"
defaultOrderDirection="ASC"
rowsForPage="20"
emptyMessage="<?php echo __('Not found Services...')?>">
<fields>
<field type="text"
name="caption"
caption="<?php echo __('Service'); ?>"
width="30%" />
<field type="text"
caption="<?php echo __('Host'); ?>"
name="host"
width="30%"
isCustom="true" />
<field type="text"
caption="<?php echo __('Status'); ?>"
name="status"
width="30%"
isCustom="true" />
</fields>
<listeners>
<listener event="<?php echo Store::EVENT_ON_LOAD_LIST_DATA; ?>"
plugin="FestiCloudServicesManager"
method="onLoadListData" />
</listeners>
<actions>
<action type="list" caption="<?php echo __('Services'); ?>" />
<action type="toggle"
caption="<?php echo __l('Enable'); ?>"
confirmDialog="true"
dialogTitle="<?php echo __('Confirmation'); ?>"
dialogMessage="<?php echo __('Are you sure?'); ?>"
link="<?php echo Core::getInstance()->getUrl('/app/%s/services/%%id%%/toggle/', App::getID()); ?>" />
</actions>
</table>
public function onLoadListData(FestiEvent &$event)
{
$tableData = &$event->target['data'];
$indexes = &$event->target['info']['indexes'];
$idApp = App::getID();
$statusIndex = $indexes['status'];
$hostIndex = $indexes['host'];
$services = $this->_loadServicesByAppID($idApp);
foreach ($tableData as &$row) {
$this->_prepareServiceRow(
$row,
$services,
$statusIndex,
$hostIndex
);
}
} // end onLoadListData
private function _prepareServiceRow(
&$row, $services, $statusIndex, $hostIndex
)
{
$idService = $row['id'];
$status = &$row['data'][$statusIndex]['value'];
$host = &$row['data'][$hostIndex]['value'];
if (empty($services[$idService])) {
$status = '—';
$host = '—';
} else {
$service = $services[$idService];
$status = $service['status'];
$host = $service['host'].":".$service['port'];
}
return true;
} // end _prepareServiceRow
Store::EVENT_ON_LOAD_ACTION_ROWS
When it is necessary to modify a value for display in a list or populate values for custom fields:
$store = $this->createStoreInstance('report_offer_acceptance_rate');
$store->addEventListener(Store::EVENT_ON_LOAD_ACTION_ROWS, function (FestiEvent &$event) {
$rows = &$event->getTargetValueByKey('values');
foreach ($rows as &$row) {
$row['rate'] = $row['offer_cnt'] ? ($row['completed_cnt'] * 100) / $row['offer_cnt'] : 0;
$row['rate'] = number_format($row['rate'], 2).'%';
}
});
Store::EVENT_ON_LOAD_CHILD_FIELD_VALUES
This event is triggered when it is necessary to load child field values based on a parent field value. It is typically used in scenarios where a field's options depend on the selection of another field, such as in cascading dropdowns.
AbstractAction Event
AbstractAction::EVENT_PREPARE_FOREIGN_FIELD_VALUES
This event is triggered when preparing values for a foreign field.
Target
ajaxChild- child field nameajaxChildValues- values of theajaxChildajaxParent- parent field namefieldName- current field nameterm- search stringvalue- current field valueexclude- excluded fieldsresults- array of items for the field, as a rawidentifier => labelmap bound by reference. When several child fields are requested it is re-bound for each one in turn, so a listener always sees exactly one field's values. The response payload is built from it after the listener runs, so edits made here reach the client - a listener works withidentifier => label, never with the{"key": ..., "value": ...}wire format.
Usage
- Subscribe to the Event: To use this event, you need to subscribe to it in your store class.
$store->addEventListener(AbstractAction::EVENT_PREPARE_FOREIGN_FIELD_VALUES, function (FestiEvent &$event) {
// Your event handling logic here
});
- Event Handler: Implement the event handler to process the event. The handler can modify the target values or perform additional logic.
public function onLoadChildValuesHandler(FestiEvent &$event)
{
// Access the target values
$parentField = &$event->getTargetValueByKey('parentField');
$childField = &$event->getTargetValueByKey('childField');
$options = &$event->getTargetValueByKey('options');
$values = &$event->getValues();
// Custom logic to load child field values
$values = $this->loadCustomChildValues($parentField, $childField, $options);
}
This example demonstrates how to handle the event to load child field values based on the parent field's value.
ListItemEvent::EVENT_ON_PREPARE_LIST_ITEM
Override DGS
class ModuleOptionsStore extends Store implements IBeforeInsertListener
{
public function onBeforeInsertHandler(StoreActionEvent &$event): void
{
$rows = $event->getValues();
foreach ($rows as $key => $value) {
$values = [
'value' => $value
];
$search = [
'name' => $key
];
$this->_object->change($values, $search);
}
$event->setUpdated(true);
}
}
Creating a New Event in DGS
- Create a class for the event of a specific entity (if it doesn't already exist):
namespace core\store\event; use FestiEvent; class ListItemEvent extends FestiEvent { } - Add a constant with the event name to the event class:
php class ListItemEvent extends FestiEvent { public const EVENT_ON_PREPARE_LIST_ITEM = "on_prepare_list_item"; } - Initialize the event class:
$target = [ 'field' => &$field, 'row' => &$row, 'value' => &$value ]; $event = new ListItemEvent(ListItemEvent::EVENT_ON_PREPARE_LIST_ITEM); -
Create an interface for the event listener (its name should be similar to the event constant). It's recommended to prefix the method name with
onand postfix it withHandler. The method can accept either the event class or you can pass the necessary parameters directly to the method:
5. Trigger the event in DGS:namespace core\store\event; interface IPrepareListItem { public function onPrepareListItemHandler(IStoreField &$field, mixed &$value, array &$row): void; }
6. Add documentation for the event.$this->store->dispatchEvent($event); if ($this->store instanceof IPrepareListItem) { $this->store->onPrepareListItemHandler($field, $value, $row); }
Frontend Events in DGS
Frontend Events provide a bridge between frontend interactions and backend processing in the DGS. They allow you to execute server-side code in response to frontend actions without full page reloads.
The DGS uses FrontendEventAction to handle the communication.
Firing Frontend Events
Use the DataGridStore.fireBackendEvent() method to trigger backend events:
// Get store instance
let store = Jimbo.getDataGridStoreInstance('your_store_ident');
// Fire an event with data
store.fireBackendEvent('EVENT_NAME', {
param1: 'value1',
param2: 'value2'
}, function(response) {
// Optional callback function
console.log('Event completed', response);
});
Implementing Frontend Event Listeners
Your store class must implement IFrontendEventListener:
use core\store\event\IFrontendEventListener;
use core\store\event\FrontendStoreActionEvent;
class YourStore extends Store implements IFrontendEventListener
{
public function onStoreFrontendEventHandler(FrontendStoreActionEvent &$event): void
{
$eventName = $event->getName();
$data = $event->getData();
$response = &$event->getResponse();
// Handle different events
switch ($eventName) {
case 'EVENT_YOUR_CUSTOM_EVENT':
$this->handleCustomEvent($data, $response);
break;
}
}
}
Access event data and manipulate responses:
public function onStoreFrontendEventHandler(FrontendStoreActionEvent &$event): void
{
// Get event data sent from frontend
$data = $event->getData();
// Get primary key if available
$primaryKey = $event->getPrimaryKey();
// Get the response object to send data back
$response = &$event->getResponse();
// Set response data
$response->data = ['result' => 'success'];
$response->message = 'Operation completed';
$response->setAction(\Response::ACTION_ALERT);
}
Error Handling
If a frontend event listener throws, FrontendEventAction catches the exception and returns it to the frontend as a single alert message — the handler does not need to build the alert itself:
- A
SystemExceptionwith a display message surfaces that message. - Any other
Throwable(or aSystemExceptionwithout a display message) falls back to the store's default error message.
public function onStoreFrontendEventHandler(FrontendStoreActionEvent &$event): void
{
$data = $event->getData();
if (!$this->isValidPayload($data)) {
// Surfaced to the user as an alert; the response action is set automatically.
throw new \SystemException(
'Invalid payload.',
displayMessage: __('Please check the values you entered and try again.')
);
}
// ...
}
The same handling applies to listeners registered directly on the store with $store->addEventListener('EVENT_NAME', $callback), not only to onStoreFrontendEventHandler. The event is dispatched to both.
Built-in Frontend Events
EVENT_JIMBO_WIZARD_STEP_CHANGE
Automatically fired during wizard navigation. The event includes:
- Current form data from the active step
_currentStepparameter with the current step number
For wizard step changes, implement the specific interface:
use Festi\Theme\Store\Event\IFrontendWizardStepChangeEventListener;
class YourStore extends Store implements IFrontendWizardStepChangeEventListener
{
public function onStoreFrontendWizardStepChangeEventHandler(FrontendStoreActionEvent &$event): void
{
$data = $event->getData();
$response = &$event->getResponse();
$currentStep = $data['_currentStep'] ?? 0;
// Process wizard step data
$response->message = 'Step ' . $currentStep . ' processed';
$response->setAction(\Response::ACTION_ALERT);
}
}