Customizing models and templates with Yii Behaviors
Related Pages
Introduction
Tips
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 are related to PHP traits and "mixins", and similar constructs known from other programming languages.
In mDIS we use Yii Behaviors 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. But behaviors allow us 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, and BEFORE_VALIDATE events. However, the events are processed by the Yii ActiveRecord Object, 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 behavior 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 require custom PHP programming. Examples are seen below.
For the implementation of mDIS Yii Behaviors, see the *.php
files in the directory backend/behaviors
.
mDIS Custom Behaviors
There are 2 important types of behaviors:
- "AttributeBehaviors" Behaviors - also known as "Automatic" in our Documentation: see below. They are parameterizable.
- User-Assigned (Parameterizable) Behaviors, for example, see below, ChildrenLimitBehavior - Complete code example.
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 (
JsonFieldBehavior
- 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 Behaviors), 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 aCombinedIdBehavior
to your input form. - If you name a column
igsn
, mDIS will attach anIgsnBehavior
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 the default value for Core IGSN, for example. - Similar functionality exists for columns that are namedigsn_ukbgs
, see classIgsnUkBgsBehavior
. AnalystBehavior
: If you name a columnanalyst
, 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 data table.
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": ""
}
IgsnBehavior
IgsnBehavior assigns a unique IGSN value to a newly inserted record. IgsnBehavior
uses the algorithm described in the Wiki Article "How to generate igsns (int geo sample nr) in legacy dis". 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, or in the PHP source code of the behavior (File backend/behaviors/IgsnBehavior.php
).
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
},
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
}
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
):
[
'class' => app\behaviors\UniqueCombinationAutoIncrementBehavior::class,
'searchFields' => ['core_id'],
'fieldToFill' => 'section'
],
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;
}
]
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 row count 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 into your ActiveRecord class:
*/
public function behaviors()
{
return [
[
'class' => \app\behaviors\JsonFieldBehavior::class,
'fields' => ['foo_data', 'bar_data'],
],
];
}
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 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 tablecore_section
, that PHP file name isbackend/models/CoreSection.php
.
Do not modify the files in the subdirectorybackend/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;
This imports the behavior, 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 theclass { ... }
definition block stated at the top of the file). If there is no functionbehaviors()
, inject this code snippet:
public function behaviors()
{
return array_merge(parent::behaviors(), [
// Enter the behavior(s) here
]);
}
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
],
- Depending on the behavior, you can or must provide parameters. These parameters are written in additional lines after the class. Each line consists of the name of the parameter and its value. See the example below for 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;
}
]
]);
}
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 Behaviors (
IgsnBehavior
,CombinedIdBehavior
) and User-Assigned Behaviors (ChildrenLimitBehavior
,UniqueCombinationAutoIncrementBehavior
,JsonFieldBehavior
,...).New Behaviors. 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 the directory
backend/behaviors
, e.g.backend/behaviors/WaitForCorescannerBehavior.php
(fictitious example).Reuse. Assume it is a new Automatic behavior 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 the file
backend/models/base/Base.php
, methodbehaviors()
. 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. (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 behaviors, but its capabilities are limited. Editing and post-processing 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 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 are coming soon.
TBC