Customizing models and templates with Yii Behaviors

Developer page

Introduction

TIP

There exists a shorter introduction to this topic in the developer documentation that is perhaps easier to read than the text below.

Yii Behaviors for models

Yii Behaviors (opens new window) are related to PHP traits (opens new window) and "mixins" (opens new window), and similar constructs (opens new window) known from other programming languages.

In mDIS we use Yii Behaviours to change the capabilities of domain models / linked tables. There are two main situations where Behaviors are used in mDIS model classes:

  • We want to use one particular feature in a lot of different but related model classes.
  • We want to provide a lot of optional features for a model class.

Yii Behaviors

For Databases

With regard to databases, Yii behaviors are similar to database triggers (opens new window). But behaviors allow to implement functionality that database triggers cannot offer.

Just like database triggers, behaviors can be bound to many different events, e.g. the BEFORE_INSERT-event, and the AFTER_INSERT-, BEFORE_UPDATE-, AFTER_UPDATE-, BEFORE_VALIDATE-events. However the events are processed by the Yii ActiveRecord Object (opens new window), not by the database.

Not only for databases

Actually, every class that inherits from the yii\Component class has the behaviors() method. Class yii\base\Component inherits from yii\BaseObject, and thus is very high (on level 2) in the class hierarchy of the Yii framework.

Thus, you can attach some new behaviour not only to ActiveRecords, but also to Controllers, Modules or Components. And you can create that new behavior by inheriting from the yii\base\Behavior class.

In mDIS, there are a few Yii-Behaviors already available to use. They required custom PHP programming. Examples see below.

For the implementation of mDIS Yii Behaviours see the *.php-Files in directoy backend/behaviors.

mDIS Custom Behaviors

There are 2 important types of behaviors:

mDIS makes use of Behaviors to create...

  • very specific combined keys (CombinedIdBehavior - Automatic)
  • IGSNs (IgsnBehavior - Automatic)
  • autoincremented values that are customizable (UniqueCombinationAutoIncrementBehavior - User-defined)
  • numeric constraints on some groupings inside a data table (ChildrenLimitBehavior - User-defined)
  • serialized JSON strings that can be converted back to Javascript Objects (JsonFieldBehaviour - User-defined)
  • numeric values that are carried over from a previous record to the next (bottom-depth n becomes top-depth of record n+1)

Besides, mDIS makes use of Behaviors to...

  • store JSON Arrays as Strings in database columns, and converting back, serializing multivalued entries. This happens in the Dashboard widgets.
  • manage Access Control for users in the cg Module. This is an internal task.

Automatic Behaviors

For Automatic Behaviors, (Attribute Behaviours) you don't have to do anything except assigning a column name, and little else. The system will attach a behavior to a form or to an item, when you give a column that certain name.

  • If you name a column combined_id, mDIS will attach a CombinedIdBehavior to your input form.
  • If you name a column igsn, mDIS will attach an IgsnBehavior to your input form. However, remember that you must set the correct default value for the IGSN type in the table designer of the mDIS Template Manager. Set "C" as default value for Core IGSN, for example. - Similar functionality exists for columns that are named igsn_ukbgs, see class IgsnUkBgsBehavior.
  • AnalystBehavior: If you name a column analyst, the name of the logged-in user will be inserted into this field after saving the record.

Actually, the correct name for "Automatic" behaviors is AttributeBehavior. The custom UniqueCombinationAutoIncrementBehavior is also an AttributeBehavior but it needs multi-column (=multi-attribute) input.

CombinedIdBehavior

CombinedIdBehavior creates a human-readable ID hierarchy like it was used before, in the Legacy DIS. Strings such as "5063_1_A_3_1" are easier to work with for humans than tuples such as [5063, 1, "A", 3, 1]. Every time a record is inserted or updated, the value of the assigned column (combined_id) is being updated. This behavior does not have to be assigned manually, it is invoked automatically as soon as there exists a column named combined_id in the mysql datatable.

The combined_id model definition should have these characteristics when assigned in the table designer of the Template manager:

// JSON fragment
"combined_id": {
            "name": "combined_id",
            "importSource": "",
            "type": "string",
            "size": 16,  // use appropriate length
            "required": false,
            "primaryKey": false,
            "autoInc": false,
            "label": "Combined Id",
            "description": "Convenience column to print on labels etc. Filled out automatically",
            "validator": "",
            "validatorMessage": "",
            "unit": "",
            "selectListName": "",
            "calculate": "",
            "defaultValue": ""
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

IgsnBehavior

IgsnBehavior assigns a unique IGSN (opens new window) value to a newly inserted record. IgsnBehavior uses the algorithm described in Wiki Article "How to generate igsns (int geo sample nr) in legacy dis" (opens new window). Depending on the table type (Core, Section, ...), a different letter (= "Object Tag" in IGSN jargon) has to be used. The different Object Tags are described in an older mDIS Wiki Article (opens new window), or in the PHP source code of the behavior (File backend/behaviors/IgsnBehavior.php (opens new window)).
The behavior requires an object tag (one character) in a parameter objectTag. This behavior does not have to be assigned manually: If a table has a column igsn (type string, size 15) and the defaultValue attribute for that column is a single letter (H, C, S, X, B, W, U, T, Y, Z, or F), then the behavior will be automatically applied, and this letter will be used as an appropriate object tag by the IGSN algorithm. You should set the default value in the table designer of the mDIS Template manager.

Table column igsn should have the following characteristics when assigned in the table designer of the Template manager:

//JSON fragment from backend/dis_templates/models/CoreCore.json
"igsn": {
    "name": "igsn",
    "importSource": "IGSN",
    "type": "string",
    "size": 15,
    "required": false,
    "primaryKey": false,
    "autoInc": false,
    "label": "IGSN",
    "description": "IGSN",
    "validator": "",
    "validatorMessage": "",
    "unit": "",
    "selectListName": "",
    "calculate": "",
    "defaultValue": "C" // C for core. Can be H, C, S, X, B, W, U, T, Y, Z, or F
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Form Field igsn could be designed like this:

//JSON fragment from backend/dis_templates/forms/core.json
{
  "name": "igsn",
  "label": "IGSN",
  "description": "IGSN (autom. calculated)",
  "validators": [
    {
      "type": "string",
      "min": null,
      "max": 15
    }
  ],
  "formInput": {
    "type": "text",
    "disabled": false,
    "calculate": "",
    "jsCalculate": ""
  },
  "group": "Subgroup",
  "order": 3
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Actual values of some fields (such as Label descriptions) are a matter of the form designer's personal taste and preferences.

Parameterizable Behaviors

UniqueCombinationAutoIncrementBehavior

Creates versatile autoincremented values that can be customized (e.g. they can have gaps, can be nonunique, can be given a start value > 1).

UniqueCombinationAutoIncrementBehavior fills a "fake" auto-increment column. It is usually populated with a unique value, but nonunique values may sometimes occur. It might also be necessary that an autoincrement column starts at an unusual value, say 40, because the project requires it. A normal mySQL autoincrement column does not (easily) support such customizations.
Thus, a UniqueCombinationAutoIncrementBehavior column is not a "real", production-quality auto-increment column. It is usually used to create unique values for records with the same parent record, like the column section in table core_section. You have to provide the parameter fieldToFill, which specifies the behavior of the column to be filled by the calculated value. The second parameter searchFields is an array of the columns in the record, that build the group in which a unique value should be created.
Here is the example of that behavior used for the table core_section (from Model file backend/models/CoreSection.php (opens new window)):

     [
          'class' => app\behaviors\UniqueCombinationAutoIncrementBehavior::class,
          'searchFields' => ['core_id'],
          'fieldToFill' => 'section'
      ],
1
2
3
4
5

When a new record is inserted, the behavior looks up the maximum value of all records with the same value in the column core_id as the new record, increments that value and writes it into the column 'section'.

Note

For details you have to look at the source code in file backend/behaviors/UniqueCombinationAutoIncrementBehavior.php.

ChildrenLimitBehavior

ChildrenLimitBehavior generally constrains a column value based on the number of records in the same table, or in a different table. Use this when you know in advance: "The maximum number of child items allowed for this record is n". In this respect, ChildrenLimitBehavior is a multi-row constraint, similar to a database assertion.

More concretely, we can use ChildrenLimitBehavior to set a max-threshold for the section count of a single core. e.g. "The number of sections in this corebox is 3".

This code snippet shows how to use ChildrenLimitBehavior to set such a max-threshold on the count of core sections for a core, depending on the value of column last_section in the core table.

    [
        'class' => app\behaviors\ChildrenLimitBehavior::class,
        // 'parentRefColumn' and 'limit' are the two _parameters_ of this behavior:
        'parentRefColumn' => 'core_id',
        'limit' => function ($model) {
            return $model->core->last_section;
        }
    ]
1
2
3
4
5
6
7
8

At data entry time, mDIS looks up the value specified in the column last_section in the (parent) core. When a new record is inserted, and more sections exist than the value of last section (and with the same value of column core_id) specifies, then that new record will be rejected by mDIS, and the record cannot be stored.

  • The required parameter parentRefColumn defines which column is used to group by, and then to determine the current rowcount of that group.
  • The required parameter limit is a PHP function that returns the maximum count-value for the records. The count-value is known in advance.

JsonFieldBehavior

JsonFieldBehavior automatically converts the specified attributes from JSON string to array and back.

This behavior is needed (... perhaps to store serialized data for some Widgets on the Dashboard?)

TBC

 /*
 * To use JsonFieldBehavior, insert the following code to your ActiveRecord class:
 */
  public function behaviors()
  {
     return [
         [
             'class'  => \app\behaviors\JsonFieldBehavior::class,
             'fields' => ['foo_data', 'bar_data'],
         ],
     ];
  }
1
2
3
4
5
6
7
8
9
10
11
12

More

There are even more behaviors in backend/behaviors/template:

  • DefaultFromParentBehavior
  • DefaultFromSiblingBehavior
  • SiblingsLimitBehavior
  • TemplateManagerBehaviorInterface

TBC

Tutorial: Using a behavior

This tutorial is designed to teach beginner PHP programmers how to customize PHP files generated by the Template Manager. This is needed when the Templates-Manager GUI for attaching behaviors to tables/models is not sufficient or not flexible enough.

Using ChildrenLimitBehavior

You can easily attach a behavior to the model of a table:

  • A PHP file for customizing the desired behavior is needed. Fortunately, a good starting point, a basic PHP file with a only very few lines of code, was already generated by the Templates-Manager.

  • Locate that PHP file in the directory backend/models/. For example, for the data table core_section that PHP file name is backend/models/CoreSection.php (opens new window).
    Do not modify the files in the subdirectory backend/models/base; these files are generated from the template manager and could be overridden.

  • Open the file with a text editor.

  • First, it is indispensable that we add this line to the top of the file backend/models/CoreSection.php:

    use app\behaviors\ChildrenLimitBehavior;
1

This imports the behaviour, or just "switches on" the ability to use it.

  • Locate the following snippet inside the file. If there is no such public function behaviors(){} in that file, simply add it to the file. Inject the code on the second-to-last line of the file, before the closing } on the last line. (The } should remain occupying its own line, because it closes the class { ... } definition block stated at the top of the file). If there is no function behaviors(), inject this code snippet:
    public function behaviors()
    {
        return array_merge(parent::behaviors(), [
          // Enter the behavior(s) here
        ]);
    }
1
2
3
4
5
6

If there is already a function behaviors(), add a new item to the end of the [] array in the return array_merge([...]) statement:

  • Each behavior is a block written in square brackets. If there are more behaviors in the method, they are separated by commas. The lines in the square brackets are also separated by commas.
  • The first line defines the class of the behavior, i.e.
      [
        // This single line is not enough,
        'class' => app\behaviors\ChildrenLimitBehavior::class,
        // ...since ChildrenLimitBehavior requires parameters, see below
      ],
1
2
3
4
5
  • Depending on the behavior, you can or must provide parameters. These parameters are written in additional lines after the class. Each line consists of of the name of the parameter and its value. See the example below, how to add the parameters.
  • Save the changes and try it out: Insert a new record into the table the ChildrenLimitBehavior has been assigned to. You can add as many items in the dependent table as specified in the record, but not more.

ChildrenLimitBehavior - Complete code example

The complete function/method might look like this:

    // in file: backend/models/CoreSection.php
    // add this line at the _top_ of the file, above the line `use Yii;`
    use app\behaviors\ChildrenLimitBehavior;

    // add this at the _bottom_ of the file before the closing curly bracket of the class.
    public function behaviors()
    {
        return array_merge(parent::behaviors(), [
            [
                'class' => ChildrenLimitBehavior::class,
                'parentRefColumn' => 'core_id',
                'limit' => function ($model) {
                    return $model->core->last_section;
                }
            ]
        ]);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Here ChildrenLimitBehavior was assigned to the CoreSection class.

The behavior is defined in a different file. Our function behaviors() inside the file backend/models/CoreSection.php just registers it with mDIS tables/forms, in a parameterized manner.

Advanced

Summary and some Hints

  • 3 Types. As mentioned above, there are Automatic Behaviors / Attribute Behaviours (IgsnBehavior, CombinedIdBehavior) and User-Assigned Behaviors (ChildrenLimitBehavior, UniqueCombinationAutoIncrementBehavior, JsonFieldBehavior,...).

  • New Behaviours. If you want to register a completely new type of Custom Behavior (Automatic or User-defined) with mDIS, you must create a new class in directory backend/behaviors, e.g. backend/behaviors/WaitForCorescannerBehavior.php (fictitious example).

  • Reuse. Assume it is a new Automatic behaviour you have invented, and if you think you will need to use it again, in many of your model classes. Then you might consider inserting an appropriate block of code in file backend/models/base/Base.php, method behaviors(). The additional code should be similar to what is already there.

  • Client-Side analogue. Yii Behaviors are a server-side feature. On the client-side you can also implement your own "behaviors", in JavaScript, to calculate properties on the fly, perform checks, etc. See VueJS / DisFormTutorial. For a bigger picture, you should learn more about VueJS. It has a feature similar to Yii Behaviors, called "Instance Lifecycle Hooks". See the diagram in the corresponding Vue Documentation (opens new window). (mDIS does not use them that much).

  • As yet, there is no central place in the Template manager or in the file system to manage behaviors and to store their PHP code.

  • GUI limitations. In the mDIS Template manager, there is a GUI tool to parameterize column-wise behaviours, but its capabilities are limited. Editing and postprocessing source code is required.

  • Advanced, unused features. Behaviors can also be used to govern the caching behavior of controllers, and other Yii Objects. This topic (opens new window) is not discussed here, because it is not used in mDIS.

Template Behaviors

New feature

There exists a GUI to customize and parameterize behaviors.

We implemented this in Winter 2019/2020, documentation and examples coming soon.

TBC