Data Grid Store (DGS)

One of the most commonly used components in the system is DGS. It is used to describe display lists and editing forms for various data.

DGS Process

DGS Options

When creating a DGS, many options are used, which are passed as an array in the third parameter:

$options = [
    ...
];
$store = new Store($connection, $ident, $options);

Option model - Store::OPTION_MODEL

The data model in the context of DGS is the schema declaration that describes the structure of the store: its main table, fields, actions, relations, filters, listeners, etc. The framework supports four ways to declare a model — pick the one that best fits the use case.

Source When to use
XML (default) Simple CRUD pages, conventional layout next to the plugin.
Array Schemas generated at runtime from configuration data or external input.
PHP class (ClassSchema) IDE navigation, static analysis, dynamic configuration via if, reuse via inheritance/traits.
Custom StoreModel subclass Advanced loaders that don't fit any of the above.

Resolution priority for the model option (handled by StoreModelResolver, first match wins):

  1. model is an IStoreModelSchema instance → ClassStoreModel with that instance.
  2. model is an array → ArrayStoreModel.
  3. model is a string that names an IStoreModelSchema subclass → ClassStoreModel (the class is instantiated with the Store injected). If the named class exists but does not implement IStoreModelSchema, the resolver raises InvalidStoreSchemaException — silent fall-through to XML would mask the real misconfiguration.
  4. model is not supplied and the Store subclass implements IStoreSchemaProvider → ClassStoreModel using the IStoreModelSchema instance returned from getSchema().
  5. Otherwise → XmlStoreModel loads tblDefs/{store}.xml; if the XML file is missing, loading fails with Not found model file.
flowchart TD
    Start([createStoreInstance]) --> HasModel{model option<br/>supplied?}

    HasModel -->|"IStoreModelSchema<br/>instance"| Wrap[ClassStoreModel<br/>wrapping instance]
    HasModel -->|array| ArrayModel[ArrayStoreModel]
    HasModel -->|string| IsSchema{names an<br/>IStoreModelSchema<br/>subclass?}
    HasModel -->|"not supplied"| Provider{Store implements<br/>IStoreSchemaProvider?}

    IsSchema -->|yes| ClassFromName[ClassStoreModel<br/>store injected]
    IsSchema -->|"class exists,<br/>wrong interface"| Raise[/InvalidStoreSchemaException/]

    Provider -->|yes| ClassFromProvider[ClassStoreModel<br/>from getSchema instance]
    Provider -->|no| Xml{"tblDefs/{store}.xml<br/>exists?"}

    Xml -->|yes| XmlModel[XmlStoreModel]
    Xml -->|no| NotFound[/Not found model file/]

ClassSchema is the standard base class that satisfies IStoreModelSchema — virtually every class-based schema extends it. Implement IStoreModelSchema directly only when you need a custom assembly path and are prepared to construct the schema yourself before passing it through the model option.

1. XML

Default. Place tblDefs/{store}.xml next to the plugin and the framework picks it up automatically:

$store = $this->createStoreInstance('products');
// schema loaded from plugins/Products/tblDefs/products.xml

The full XML reference and examples follow further down on this page.

2. Array

Pass an associative array as the model option. Same shape as the XML form, just spelled in PHP:

$params = [
    Store::OPTION_MODEL => [
        'table'  => ['name' => 'settings', 'primaryKey' => 'id'],
        'fields' => [
            ['type' => 'readonly', 'name' => 'id', 'caption' => 'ID'],
            ['type' => 'text',     'name' => 'caption', 'caption' => 'Caption'],
        ],
        'actions' => [
            Store::ACTION_LIST => ['type' => Store::ACTION_LIST, 'caption' => 'List'],
            Store::ACTION_EDIT => ['type' => Store::ACTION_EDIT, 'caption' => 'Edit'],
        ],
    ],
]
$store = $this->createStoreInstance('settings', $params);

See the full reference in Table → Describing the Model Using an Array.

3. PHP class (ClassSchema)

Declare the schema as a class that extends ClassSchema and configures the store via fluent builders:

namespace Plugins\SchoolUsers\Store;

use Festi\Store\Schema\ClassSchema;
use Festi\Store\Schema\Builder\ITableBuilder;
use Festi\Store\Schema\Builder\IFieldsBuilder;
use Festi\Store\Schema\Builder\IActionsBuilder;
use Store;

class UsersSchema extends ClassSchema
{
    protected function table(ITableBuilder $table): ITableBuilder
    {
        return $table->name('users')->primaryKey('id')->rowsForPage(50);
    }

    protected function fields(IFieldsBuilder $fields): IFieldsBuilder
    {
        $fields->add('id')
            ->type(ReadonlyField::class)
            ->caption(__('ID'));
        $fields->add('email')
            ->type(TextField::class)
            ->caption(__('Email'))
            ->required();

        return $fields;
    }

    protected function actions(IActionsBuilder $actions): IActionsBuilder
    {
        $actions->add(Store::ACTION_LIST)->caption(__('Users'));
        $actions->add(Store::ACTION_EDIT)->caption(__('Edit'));

        return $actions;
    }
}

For the three forms of wiring the schema into createStoreInstance (schema-class shorthand, explicit by class name, explicit by instance) see DGS Class Schema → Quick Start.

Custom Store subclasses can also declare their schema on the class itself by implementing IStoreSchemaProvider — StoreModelResolver picks it up automatically when no explicit model option is supplied.

See the full reference, the section-method contract, and the XML↔Array↔Class mapping table in DGS Class Schema.

4. Custom StoreModel subclass

For advanced loaders that can't be expressed with any of the above, inherit from StoreModel and implement load(). The model option doesn't accept arbitrary StoreModel FQNs (the resolver throws InvalidStoreSchemaException for class names that don't implement IStoreModelSchema) — instantiate the custom model yourself and inject it through your own Store subclass, or expose it via IStoreSchemaProvider if it can be expressed as an IStoreModelSchema.

Option store_view - Store::OPTION_VIEW_MODEL

The display model for DGS describes how to render the UI. You can pass an instance of the StoreView class.

Option theme_template_path - Store::OPTION_THEME_TEMPLATE_PATH

The path to the templates for the theme depends on the type of display model. By default, the path from Core (Core::OPTION_THEME_TEMPLATE_PATH) is taken.

Declaration of the Store in XML Format

To display a list or editing forms for data, you can use several options for data description: XML, Database, or Array. This section describes the XML format; all others are compatible with it.

Example of description:

<?xml version="1.0" encoding="UTF-8" ?>
<table charset="UTF-8"
       name="products"
       primaryKey="id"
       defaultOrderField="id"
       defaultOrderDirection="ASC"
       rowsForPage="20"
       grideditor="true">

    <relations>
        <link type="child"
              field="id"
              foreignTable="prices"
              foreignField="id_company"
              treeCaption="caption" />

        <link type="child"
              field="id"
              foreignTable="categories"
              foreignField="id_company"
              treeCaption="caption" />

    </relations>

    <fields>
        <field  type="readonly"
                caption="ID"
                sorting="true"
                name="id"
                trim="60"
                width="10%"
                filter="text" />

        <field  type="text"
                clicable="true"
                name="caption"
                caption="<?php echo __('Caption')?>"
                width="30%"
                filter="text"
                sorting="yes"
                required="true" />

        <field  type="select"
                caption="<?php echo __('Status')?>"
                sorting="true"
                name="status"
                filter="select"
                width="10%"
                required="true">
            <option id="active"><?php echo __('Active')?></option>
            <option id="deleted"><?php echo __('Deleted')?></option>
        </field>

        <field type="file"
               name="logo"
               hide="true"
               caption="<?php echo __('Logo')?>"
               fileName="__ID__.__EXT__"
               thumb="120x80"
               isnull="true"
               uploadDirPath="<?php echo Core::getInstance()->config['paths']['logo']; ?>"
               httpPath="<?php echo Core::getInstance()->config['http_paths']['logo']; ?>" />

        <field  type="text"
                name="phone"
                caption="<?php echo __('Phone')?>"
                width="20%"
                required="true"
                disclaimer="<?php  echo __('Format: (555) 787-4567');?>"
                regexp="#^\([0-9]{3}\)\s[0-9]{3}-[0-9]{4}$#" />

        <field  type="text"
                name="state"
                caption="<?php echo __('State')?>"
                width="10%"
                filter="text"
                required="true" />

        <field  type="text"
                name="website"
                caption="<?php echo __('Website');?>"
                width="15%"
                filter="text"
                regexp="#^[a-z\.]+$#" />

        <field  type="checkbox"
                name="is_paying"
                caption="<?php echo __('Is Paying')?>"
                isnull="true"
                filter="yes"
                width="5%" />

        <field  type="many2many"
                caption="<?php echo __('Managers')?>"
                linkTable="managers2products"
                linkField="id_company"
                linkForeignField="id_manager"
                foreignTable="users"
                foreignKeyField="id"
                foreignValueField="name"
                hide="true" />
    </fields>

    <filters>
        <filter field="id_user"><?php echo Core::getInstance()->user->getID(); ?></filter>
    </filters>

    <externalValues>
        <value field="mdate"><?php echo date('Y-m-d'); ?></value>
    </externalValues>

    <actions>

        <action type="prices"
                caption="<?php echo __('Prices')?>"
                relation="prices"
                relationType="child"
                mode="new" />

        <action type="categories"
                caption="<?php echo __('Categories')?>"
                relation="categories"
                relationType="child"
                mode="new" />

        <action type="insert"
                mode="new"
                caption="<?php echo __('Add')?>"
                title="<?php  echo __('Add New'); ?>" />

        <action type="list"
                caption="<?php echo __('Products')?>" />

        <action type="edit"
                mode="new"
                caption="<?php echo __('Edit')?>"
                title="<?php echo __('Edit: %caption%'); ?>" />

        <action type="remove"
                caption="<?php echo __('Remove')?>" />
    </actions>

</table>

Displaying the Store

  1. The simplest option to display the store is to use the system URL: /festi/[XML_NAME]/[PLUGIN_NAME]/ /festi/products/

PLUGIN_NAME must be specified if the XML is in a plugin; in the example, this is the plugin Products.

  1. Call the display from the plugin method:

    class ProductsPlugin extends DisplayPlugin
    {
        public function onDisplayList(Response &$response)
        {
            $store = $this->createStoreInstance("products");
            $store->onRequest($response);
    
            return true;
        }
    }

The XML file should be located at: plugins/Products/tblDefs/products.xml.

  1. If you are using multiple databases and want to display data from another database:

    $store = $this->createStoreInstanceByConnection($db, "products");

The XML file should be located at: plugins/Products/tblDefs/products.xml.

  1. Through the core:

    $store = Core::getInstance()->createStoreInstanceByConnection(...);
    $store = Core::getInstance()->createStoreInstance(...);

Note that through the core, XML will be searched not in the plugin but in the root of the project in the tblDefs folder. If you need to search for XML in the plugin, pass the plugin name in the parameter array with the key plugin:

```php
$params = [
    'plugin' => 'Products'
];

$store = Core::getInstance()->createStoreInstance("products", $params);
```

Displaying the Editing Form

Sometimes you need to display only specific forms in the stores; here’s an example of how to select and process only the edit form:

$store = $this->createStoreInstance("invoices");
$action = $store->createActionInstance(Store::ACTION_EDIT);

$search = [
    'id' => $data['id']
];

$action->loadRow($search);

$action->onStart($response);

echo $response->getContent();

If you only need to display the form without processing the update request, use fetchForm instead of onStart:

echo $action->fetchForm();

Tag relations

Often, it is necessary to establish a connection between different XML files and merge them based on a composite key. This is done using the relations and action tags. Let's consider an example: suppose we have a list of companies (companies.xml), and each company has sub-sections of email templates (company_email_templates.xml). It is necessary to make a transition from companies to company_email_templates:

companies.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<table charset="UTF-8"
       name="companies"
       primaryKey="id"
       defaultOrderField="id"
       defaultOrderDirection="ASC"
       rowsForPage="20"
       grideditor="true">

    <relations>

        <link type="child"
              field="id"
              foreignTable="company_email_templates"
              foreignField="id_company"
              treeCaption="caption" />

    </relations>

    <fields>
        <field  type="readonly"
                caption="<?php echo __('ID'); ?>"
                sorting="true"
                name="id"
                trim="60"
                width="10%"
                filter="text" />

        <field  type="text"
                name="caption"
                caption="<?php echo __('Name'); ?>"
                width="30%"
                filter="text"
                sorting="yes"
                required="true" />
    </fields>

    <actions>

        <action type="email_templates"
                caption="<?php echo __('Email Templates'); ?>"
                relation="company_email_templates"
                relationType="child"
                mode="new" />

        <action type="insert"
                mode="new"
                caption="<?php echo __('Add')?>"
                title="<?php  echo __('Add New Company'); ?>" />

        <action type="list"
                caption="<?php echo __('Companies'); ?>" />

        <action type="edit"
                mode="new"
                caption="<?php echo __('Edit')?>"
                title="<?php echo __('Edit: %caption%'); ?>" />

        <action type="remove"
                caption="<?php echo __('Remove')?>" />
    </actions>
</table>

We have described a child dependency in relations and added an action of any random type, but it is important not to use reserved types such as insert, list, etc., and specified relation attributes that must match with relations foreignTable:

relation="company_email_templates"
relationType="child"

Now, for it to work correctly, it is necessary to specify the parent relation in company_email_templates.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<table charset="UTF-8"
       name="company_email_templates"
       primaryKey="id"
       defaultOrderField="id"
       defaultOrderDirection="ASC"
       rowsForPage="50">

    <relations>
        <link type="parent"
              field="id_company"
              foreignTable="companies"
              foreignField="id"
              treeCaption="caption" />
    </relations>

    <fields>
        <field  type="text"
                name="caption"
                caption="<?php echo __('Caption')?>"
                width="30%"
                filter="text"
                required="true"
                sorting="false" />

        <field  type="text"
                name="subject"
                caption="<?php echo __('Subject')?>"
                filter="text"
                width="30%"
                required="true"
                sorting="false" />

        <field  type="textarea"
                name="body"
                caption="<?php echo __('Body')?>"
                width="10%"
                required="true"
                hide="true" />
    </fields>

    <actions>
        <action type="insert"
                caption="<?php echo __('Add')?>"
                title="<?php echo __('Email Template'); ?>"
                mode="new" />

        <action type="parent"
                caption="<?php echo __('Companies')?>"
                relation="companies"
                relationType="parent" />

        <action type="list"
                caption="<?php echo __('Email Templates')?>" />

        <action type="edit"
                caption="<?php echo __('Edit')?>" />

        <action type="remove"
                caption="<?php echo __('Remove')?>" />
    </actions>

</table>

It is important that in action you must specify the type as parent.

If non-system URLs are used (/festi/), then you need to specify the url attribute in the relation:

<link type="child"
     field="ident"
     foreignTable="module_options"
     foreignField="module"
     url="<?php echo Core::getInstance()->getUrl('/manage/modules/options/');?>"
     treeCaption="caption" />

<link type="parent"
             field="module"
             foreignTable="modules"
             url="<?php echo Core::getInstance()->getUrl('/manage/modules/');?>"
             foreignField="ident"
             treeCaption="caption" />

Tag externalValues

Through externalValues, you can add values for columns in the store that are known in advance and should not be entered by the user.

<externalValues>
    <value field="mdate"><?php echo date('Y-m-d'); ?></value>
</externalValues>

Tag routers


To describe the relationship between tables, you need to use the routers tag.

<field type="text"
       caption="<?php echo __('Owner Email')?>"
       sorting="true"
       name="id_user"
       store="users"
       expression="email"
       filter="text"
       width="25%"
       readonly="true" />
<routers>
    <route store="advertisements"
           joinStore="users"
           type="INNER"
           on="advertisements.id_user = users.id" />
</routers>

Tag route

Describes the rule for joining tables.

  • joinStoreName - the real name of the table; if this attribute is specified, then joinStore will be used as an alias.

Tag highlights

It is often necessary to highlight or change something in the rows of the data list. For this, you can use highlights, which adds the specified CSS class to a row that matches the values specified in the rule tag. It is important to note that for foreignKey fields, if you want to set conditions on values rather than names, you need to add the _foreign_ prefix.

<highlights>
    <rule cssClass="m-row-green">
        <field name="_foreign_action">replay</field>
    </rule>

    <rule cssClass="m-row-yellow">
        <field name="action">Sent to Customer</field>
    </rule>

    <rule cssClass="CUSTOM_CSS_CLASS_NAME">
        <field name="action">Sent to Customer</field>
        <field name="status">active</field>
    </rule>
</highlights>

If you need to specify a rule for the value of a column that is not displayed, in the action Store::ACTION_INSERT, you need to set the attribute isLoadAllColumns="true".

Annotations

/**
 * @store festi_plugins
 * @event Store::EVENT_PREPARE_ACTION_REQUEST
 */
public function onUploadPlugin(FestiEvent &$event)
{
    ...
}

/**
 * @store festi_plugins | OTHERE_STORE
 * @event Store::EVENT_BEFORE_INSERT | Store::EVENT_BEFORE_UPDATE
 */
public function onInstallPlugin(FestiEvent &$event)
{
    ...
}

Examples

Removing an Action from the Store

public function onDisplayList(Response &$response)
{
    $store = $this->createStoreInstance("invoices");

    $store->getModel()->removeAction(Store::ACTION_EDIT);

    $store->onRequest($response);

    return true;
}

Retrieving Data from One Table by Multiple Fields

This task often occurs with a user table; for example, in the system, there are two types of users, buyers and managers. We need to store both the buyer and the manager in one table and display both in the list. For this, we need to join the user table twice. In the routers, the joinStoreName attribute should specify the actual name of the table, while the joinStore should be your chosen alias.

<?xml version="1.0" encoding="UTF-8" ?>
<table  charset="UTF-8"
        name="deals"
        primaryKey="id"
        defaultOrderField="mdate"
        defaultOrderDirection="DESC"
        rowsForPage="50"
        plugin="Deals">

    <search>
        <filter field="deals.status">new</filter>
    </search>

    <routers>
        <route store="deals"
               joinStore="advertisements"
               type="INNER"
               on="advertisements.id = deals.id_ad" />
        <route store="deals"
               joinStore="users"
               type="INNER"
               on="users.id = deals.id_user" />

        <route store="deals"
               joinStore="company_users"
               joinStoreName="users"
               type="LEFT"
               on="company_users.id = deals.id_company_user" />
    </routers>

    <fields>
        <field type="text"
               caption="<?php echo __('Deal ID'); ?>"
               name="id"
               width="7%"
               required="true"
               filter="text"
               clicable="true"
               sorting="true" />

        <field type="composite"
               caption="<?php echo __('Customer')?>"
               name="customer"
               width="10%">
            <option store="users">name</option>
            <option store="users">lastname</option>
        </field>

        <field type="composite"
               caption="<?php echo __('Contact Info')?>"
               name="customer_info"
               separator="&lt;br/&gt;"
               width="10%">
            <option  store="users">email</option>
            <option  store="users">phone</option>
        </field>

        <field type="datetime"
               caption="<?php echo __('Last Updated'); ?>"
               name="mdate"
               width="10%"
               sorting="true" />

        <field type="datetime"
               caption="<?php echo __('Expiration'); ?>"
               name="expire_date"
               width="10%"
               sorting="true" />

        <field type="foreignKey"
               name="action"
               caption="<?php echo __('Status'); ?>"
               width="15%"
               foreignTable="deal_actions"
               foreignKeyField="ident"
               foreignValueField="caption"
               filter="select"
               isnull="true" />

        <field type="composite"
               caption="<?php echo __('Manager')?>"
               name="company_user"
               width="10%">
            <option store="company_users">name</option>
            <option store="company_users">lastname</option>
        </field>

    </fields>

    <actions>
        <action type="list"
                caption="<?php echo __('Deals'); ?>" />
    </actions>

</table>

Retrieving Parent ID in Tables with Nested Inheritance

<?xml version="1.0" encoding="UTF-8" ?>
<table name="site_contents"
       charset="UTF-8"
       primaryKey="id"
       defaultOrderField="id"
       defaultOrderDirection="ASC"
       rowsForPage="200">

    <relations>
        <link type="parent"
              field="id_parent"
              foreignTable="site_contents"
              url="<?php echo '/site/'.$idSite.'/manager/'; ?>"
              foreignField="id" />

        <link type="child"
              field="id"
              url="<?php echo '/site/'.$idSite.'/manager/'; ?>"
              foreignTable="site_contents"
              foreignField="id_parent"
              cascade="true"
              treeCaption="name" />
    </relations>

    <filters>
        <filter field="status"><?php echo ContentValuesObject::STATUS_ACTIVE; ?></filter>
    </filters>

    <externalValues>
        <value field="id_site"><?php echo $idSite; ?></value>
    </externalValues>

    <fields>
        <field type="readonly"
               caption="<?php echo __l('ID'); ?>"
               name="id"
               width="5%"
               required="true"
               sorting="true"/>

        <field type="text"
               clicable="true"
               caption="<?php echo __l('Page Title'); ?>"
               name="title"
               width="30%" />

        <field type="text"
               clicable="true"
               caption="<?php echo __l('Name'); ?>"
               name="name"
               width="30%"
               required="true" />

        <field type="select"
               caption="<?php echo __('Type');?>"
               sorting="true"
               name="type"
               filter="select"
               readonly="true"
               width="10%"
               required="true">
            <option id="file"><?php echo __('File'); ?></option>
            <option id="directory"><?php echo __('Directory'); ?></option>
            <option id="image"><?php echo __('Image'); ?></option>
            <option id="page"><?php echo __('Page'); ?></option>
            <option id="module"><?php echo __('Module'); ?></option>
        </field>
        <field type="text"
               caption="<?php echo __l('Path'); ?>"
               name="path"
               hide="true"
               readonly="true"
               required="true" />

        <field type="checkbox"
               clicable="true"
               caption="<?php echo __l('Is Menu Item?'); ?>"
               name="is_menu_item"
               width="20%"
               filter="select"
               isnull="true"
               sorting="true" />

        <field type="text"
               caption="<?php echo __l('Meta Title'); ?>"
               name="meta_title"
               hide="true"
               isnull="true" />

        <field type="text"
               caption="<?php echo __l('Meta Keywords'); ?>"
               name="meta_keywords"
               hide="true"
               isnull="true" />

        <field type="textarea"
               caption="<?php echo __l('Meta Description'); ?>"
               name="meta_description"
               hide="true"
               maxChars="180"
               isnull="true" />

    </fields>

    <filters>
        <filter field="id_site"><?php echo $idSite; ?></filter>
    </filters>

    <listeners>
        <listener event="<?php echo Store::EVENT_BEFORE_INSERT; ?>"
                  plugin="Sites"
                  method="onCreateNewCategory" />
        <listener event="<?php echo Store::EVENT_BEFORE_UPDATE; ?>"
                  plugin="Sites"
                  method="onUpdateContent" />

        <listener event="<?php echo Store::EVENT_REMOVE; ?>"
                  plugin="Sites"
                  method="onDeleteContent" />
        <listener event="<?php echo Store::EVENT_ON_LIST_ACTIONS; ?>"
                  plugin="Sites"
                  method="onPrepareContentActions" />
    </listeners>

    <actions>
        <action type="parent"
                caption="<?php echo __l('Parent'); ?>"
                relation="site_contents"
                relationType="parent" />

        <action type="child"
                caption="<?php echo __l('Child Contents'); ?>"
                relation="site_contents"
                relationType="child" />

        <action type="upload"
                mode="new"
                caption="<?php echo __l('Upload'); ?>"
                view="top"
                link="<?php
                        $idParent = $this->store->getParentValue();
                        if ($idParent) {
                            echo Core::getInstance()->getUrl('/site/%s/manager/%s/upload/', $idSite, $idParent);
                        } else {
                            echo Core::getInstance()->getUrl('/site/%s/manager/upload/', $idSite);
                        }
                      ?>" />

        <action type="insert"
                caption="<?php echo __l('Create Directory'); ?>" />

        <action type="list"
                caption="<?php echo __l('Contents'); ?>" />

        <action type="edit"
                caption="<?php echo __l('Edit Item'); ?>" />
        <action type="remove"
                caption="<?php echo __l('Remove Item'); ?>" />
    </actions>

</table>

Manually Overriding the Error When Adding a New Record

<?xml version="1.0" encoding="UTF-8" ?>
<table charset="UTF-8"
       name="invoices"
       primaryKey="id"
       defaultOrderField="invoices.id"
       defaultOrderDirection="DESC"
       rowsForPage="50">

    <fields>
        <field type="readonly"
               name="id_rand_invoice"
               caption="<?php echo __('Invoice No')?>"
               width="20%"
               sorting="yes"
               readonly="true"
               link="<?php echo Core::getInstance()->getUrl('/invoice/%id%/'); ?>"
               isnull="true"
               filter="text" />

        <field type="readonly"
               name="rand_account_number"
               caption="<?php echo __('Account No')?>"
               width="20%"
               sorting="yes"
               regexp="#^[A-Za-z0-9]+$#Umis"
               filter="text"
               readonly="true"
               link="<?php echo Core::getInstance()->getUrl('/invoice/%id%/'); ?>"/>

        <field type="text"
               name="id_tobaccostock_order"
               caption="<?php echo __('Order No')?>"
               width="20%"
               sorting="yes"
               regexp="#^[A-Za-z0-9]+$#Umis"
               filter="text"
               required="true"
               link="<?php echo Core::getInstance()->getUrl('/invoice/%id%/'); ?>"/>

        <field type="select"
               caption="<?php echo __('Status')?>"
               sorting="true"
               name="status"
               onlyList="true"
               width="20%">
            <option id="new"><?php echo __('New')?></option>
            <option id="created"><?php echo __('Created')?></option>
        </field>

        <field type="foreignKey"
               name="id_user"
               caption="<?php echo __('Creator'); ?>"
               width="20%"
               foreignTable="users"
               foreignKeyField="id"
               foreignValueField="login"
               onlyList="true" />

    </fields>

    <externalValues>
        <value field="id_user"><?php echo Core::getInstance()->user->getID(); ?></value>
        <value field="status">new</value>
    </externalValues>

    <listeners>
        <listener event="<?php echo Store::EVENT_BEFORE_INSERT; ?>"
                  plugin="Invoice"
                  method="onBeforeCreateInvoice" />
    </listeners>


    <actions>

        <action type="list"
                caption="<?php echo __('Invoices')?>" />

        <action type="edit"
                caption="<?php echo __('Edit')?>"
                button="<?php echo __('Preview'); ?>"
                title="<?php echo __('Invoice Options'); ?>"
                cancelCaption="<?php echo __('Create Invoice'); ?>"
                cancelUrl="javascript:void(0)"
                mode="new" />

        <action type="remove"
                caption="<?php echo __('Remove'); ?>" />

        <action type="create"
                caption="<?php echo __('Create'); ?>"
                view="top"
                link="<?php echo Core::getInstance()->getUrl('/invoices/create/'); ?>"
                mode="new" />

    </actions>

</table>
/**
 * @event Store::EVENT_BEFORE_INSERT
 */
public function onBeforeCreateInvoice(FestiEvent &$event)
{
    $idOrder = $event->target['values']['id_tobaccostock_order'];
    $search = [
        'id_tobaccostock_order' => $idOrder
    ];

    $invoiceValues = $this->object->get($search);
    if ($invoiceValues) {
        $event->target['isUpdated'] = true;
        $event->target['lastErrorMessage'] = __(
            'Invoice for Order No %s already exists.',
            $idOrder
        );
        return false;
    }
    return true;
} // end onBeforeCreateInvoice
<?xml version="1.0" encoding="UTF-8" ?>
<table name="site_contents"
       charset="UTF-8"
       primaryKey="id"
       defaultOrderField="id"
       defaultOrderDirection="ASC"
       rowsForPage="200">

    <relations>
        <link type="parent"
              field="id_parent"
              foreignTable="site_contents"
              url="<?php echo '/site/'.$idSite.'/manager/'; ?>"
              foreignField="id" />

        <link type="child"
              field="id"
              url="<?php echo '/site/'.$idSite.'/manager/'; ?>"
              foreignTable="site_contents"
              foreignField="id_parent"
              cascade="true"
              treeCaption="name" />
    </relations>

    <filters>
        <filter field="status"><?php echo ContentValuesObject::STATUS_ACTIVE; ?></filter>
    </filters>

    <externalValues>
        <value field="id_site"><?php echo $idSite; ?></value>
    </externalValues>

    <fields>
        <field type="readonly"
               caption="<?php echo __l('ID'); ?>"
               name="id"
               width="5%"
               required="true"
               sorting="true"/>

        <field type="text"
               clicable="true"
               caption="<?php echo __l('Page Title'); ?>"
               name="title"
               width="30%" />

        <field type="text"
               clicable="true"
               caption="<?php echo __l('Name'); ?>"
               name="name"
               width="30%"
               required="true" />

    </fields>

    <filters>
        <filter field="id_site"><?php echo $idSite; ?></filter>
    </filters>

    <listeners>

        <listener event="<?php echo Store::EVENT_ON_FETCH_LIST_CELL_VALUE_WITH_LINK; ?>"
                  plugin="Sites"
                  method="onPrepareListCellValueLink" />

    </listeners>

    <actions>
        <action type="parent"
                caption="<?php echo __l('Parent'); ?>"
                relation="site_contents"
                relationType="parent" />

        <action type="child"
                caption="<?php echo __l('Child Contents'); ?>"
                relation="site_contents"
                relationType="child" />

    </actions>

</table>
public function onPrepareListCellValueLink(FestiEvent &$event)
{
    $values = &$event->getTargetValueByKey('row');

    $allowedTypes = [
        ContentValuesObject::TYPE_IMAGE,
        ContentValuesObject::TYPE_PAGE
    ];

    if (!in_array($values['type'], $allowedTypes)) {
        return false;
    }

    $site = System::getCurrentSiteInfo();
    $search = [
        'site_contents.id' => $values['id'],
        'id_site'          => $site['id']
    ];

    $contentValuesObject = $this->plugin->siteContents->loadRow($search);

    $url = &$event->getTargetValueByKey('url');
    $url = $contentValuesObject->getFullUrl();

    $target = &$event->getTargetValueByKey('target');
    $target = '_blank';

    return true;
} // end onPrepareListCellValueLink

Example of Using DGS to Create a Form and Manually Process Data

/**
 * @urlRule ~^/companies/new/$~
 * @area admin
 */
public function onAjaxCreateForm(Response &$response)
{
    $store = $this->createStoreInstance("company_create");

    $method = [&$this, 'onCreateNewCompany'];
    $store->addEventListener(Store::EVENT_BEFORE_INSERT, $method);

    $action = $store->createActionInstance(Store::ACTION_INSERT);

    if (!$response->isFlush()) {
        $response->setAction(Response::ACTION_CONTENT);
    }

    $action->onStart($response);

    return true;
} // end onAjaxCreateForm

public function onCreateNewCompany(StoreActionEvent &$event)
{
    $event->setUpdated();

    $store = &$event->getActionInstance()->getStore();

    $actionName = $store->getAction();
    $action = &$store->getModel()->getActionByRef($actionName);

    $action['redirectUrl'] = $this->getUrl('/companies/');
} // end onCreateNewCompany

Example of a List Where Columns Obtained Not from the Database, but Filled from a Plugin Need to be Added

<?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->getTargetValueByKey('data');
    $info      = &$event->getTargetValueByKey('info');
    $indexes   = &$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 = '&mdash;';
        $host = '&mdash;';
    } else {
        $service = $services[$idService];
        $status = $service['status'];
        $host = $service['host'].":".$service['port'];
    }

    return true;
} // end _prepareServiceRow

Changing Standard Notifications When Adding (Deleting) a Record

After successfully adding (deleting) a record, we see the standard message STATUS_SUCCESS. To change it, you can use the following code:

public function onDisplayInvites(Response &$response)
{
    $store = $this->createStoreInstance("user_invites");

    $msg = __('Your invitation has been sent successfully');

    if ($this->isRemoveRequest()) {
        $msg = __('Your invitation has been deleted successfully');
    }

    $store->setOption(Store::OPTION_MESSAGE_SUCCESS, $msg);

    $store->onRequest($response);
} // end onDisplayInvites

To disable messages, you should set the option Store::OPTION_MESSAGE_SUCCESS to null.

$store->setOption(Store::OPTION_MESSAGE_SUCCESS, null);