docs/source/development/frontend/models_design.rst
2026-01-21 08:43:15 +01:00

267 lines
9.7 KiB
ReStructuredText
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-------------------
Designing the model
-------------------
.. contents:: Index
Creating models for OPNsense is divided into two separate blocks:
#. A PHP class describing the actions on our data (also acts as a
wrapper to our data),
#. The definition of the data and the rules it should apply to.
Every model's class should be derived from OPNsense\\Base\\BaseModel, a very
simple model without any (additional) logic is defined with:
.. code-block:: php
<?php
namespace myVendorName\myModule;
 
use OPNsense\Base\BaseModel;
 
class myModel extends BaseModel
{
}
This class should be placed inside the model directory of our project, in this
case the full path for our class file would be
- /usr/local/opnsense/mvc/app/models/**myVendorName/myModule/myModel.php**
When you design a model, the next thing to do is to figure out what data is
relevant for your application or module and think of the rules it should comply
to (for example, if you need an email address you might want to validate the
input). Designing the actual model is as simple as creating an xml file and
putting in your structure, the name of our XML file should be the same as the
base name of our model suffixed by .xml.
Using the same model, we would create the following file:
- /usr/local/opnsense/mvc/app/models/**myVendorName/myModule/myModel.xml**
And start describing our (information) model, like this:
.. code-block:: xml
<model>
<mount>//myManufacturer/myModule</mount>
<description>A description of this model (metadata)</description>
<version>1.0.0</version>
<items>
<exampleNumber type="TextField">
<Mask>/^([0-9]){0,1}$/</Mask>
<Default>5</Default>
<ValidationMessage>you should input a number from 0 to 9</ValidationMessage>
</exampleNumber>
<contacts>
<entity type="ArrayField">
<email type="EmailField">
<ValidationMessage>you should input a valid email address!</ValidationMessage>
</email>
<name type="TextField"/>
</entity>
<someText type="TextField"/>
</contacts>
</items>
</model>
Now let's explain what's happing here one tag at a time.
#. the <model> tag is used for identification of the file. (this is a
model file)
#. Next in line is the <mount> tag, this tells the system where this
information lives in the configuration file, in this case
ROOT\_tag/myManufacturer/myModule
#. If desired, there is some space reserved to explain the usage of the
model, the <description> tag
#. A version tag is added, which is used to support migrations. These are described in more details in the :doc:`migration <models_migrations>` topic
#. Last item on the top of our list is the <items> tag, this is where
the magic begins.
The content of a items tag describes the full tree based structure which holds
our data, in theory this could be as large as you want it to be, but keep in
mind that the content for your model should be logical and understandable. Every
node in the tree could have a type, which defines its behavior, nodes without a
type are just containers.
From top to bottom we find the following nodes in our tree:
- exampleNumber, defined as a TextField
- Mask, validation can be performed by a regex expression, this sets
the expression
- Default, this field is default filled with a number: 5
- ValidationMessage, when validation fails, this message is returned
- contacts, this is a container
- entity, is defined as a recurring item, which holds the next items
- email, defined as an EmailField
- when validation fails, the **ValidationMessage** is returned
- name, defined as a TextField without any validations
- someText, not part of the entity tag and also defined as text without
validation
The fieldtypes are easily extendable in the base system and all common ones live in
their own namespace at *OPNsense\\Base\\FieldTypes* deriving from *BaseField*.
.. Note::
When designing application specific fieldtypes, you can point to a field
type within the application namespace using a full or partial path.
For example using *Vendor\\Component\\FieldTypes\\MyFieldType* to point to a specific non
common field type or *.\\MyFieldType* when linked from the application model itself (which assumes a namespace FieldTypes
exists)
volatile fields
.........................................
In some cases it can be practical to define fields which act as standard fields, but will not be persisted
in the configuration. Examples of this are fields used to generate other type of content, such as a certificate
or fields that reflect data stored elsewhere.
To make a field volatile, just add the tag :code:`volatile="true"` in the xml clause, for example:
::
<serial type="IntegerField" volatile="true"/>
------------------------------------
Special model types
------------------------------------
In memory models
.........................................
In same cases it might be practical to use all of the standard model tools, but prevent data from being persisted.
For this purpose the memory model may be used. Examples of such applications are diagnostic tools, which do require
user input, but is only relevant for that particular call.
To use these models, use the following mountpoint: :code:`<mount>:memory:</mount>`
Legacy wrappers
.........................................
While migrating legacy components, sometimes the distance between the current situation (using raw xml access) and the desired
one (being a fully validated model) is hard to overcome.
It's not always clear which type of data is being used, and when moving data inside a new model and changing it's access
path, a proper validation is mandatory.
When data lives inside it's own easy to distinct "container", a standard model may be overlaid. An example of such a
case is the static route component. Which underneath looks like this (without payload):
.. code-block:: xml
<staticroutes>
<route/>
<route/>
</staticroutes>
The other case is when a collection of items does not live inside a unique container, for example the following
payload:
.. code-block:: xml
<cert/>
<cert/>
<cert/>
Legacy modules would iterate over :code:`$config['cert']` in this case. Since :code:`cert` does not have an upper container
the model is able to control in full (as it's the root of the :code:`config.xml`), we can not overlay a standard model
to specify the fields used and their constraints.
This is where our legacy wrapper comes into play. In order to use this feature, you have to use the following mountpoint format:
:code:`/tag+` << start with an exact path [:code:`/`] and end with a plus [:code:`+`]
In the "cert" example our mountpoint would like like : :code:`<mount>/cert+</mount>`
.. Note::
All used fields still need to be specified, fields left out of the model, will be removed from the configuration
in the same way a regular model would act.
.. Note::
As legacy wrappers can not be versioned, migrations do not apply. In the long run
it's always better to use full models, but these constructions offer an option for a "softer landing".
When target fields contain structures, it is possible to apply an inline conversion using custom (app specific) field
types. This pattern is likely best explained with an example.
Below an example implementation which unpacks multiple :code:`<priv>` tags per user object and converts them to a comma
separated lists.
The first block :code:`actionPostLoadingEvent()` just collects the available options, which is part of the :code:`BaseListField`
type. The :code:`setValue()` is where the magic happens, when the input is of type :code:`SimpleXMLElement`, we know
this is being set by the loader, if in that case the underlying config contains multiple values, we merge them into
a single list.
.. code-block:: php
class PrivField extends BaseListField
{
protected $internalIsContainer = false;
private static $priv_list = null;
protected function actionPostLoadingEvent()
{
if (self::$priv_list === null) {
self::$priv_list = [];
foreach ((new \OPNsense\Core\ACL())->getPrivList() as $aclKey => $priv) {
self::$priv_list[$aclKey] = $priv['name'];
}
}
$this->internalOptionList = self::$priv_list;
}
/**
* {@inheritdoc}
*/
public function setValue($value)
{
if (is_a($value, 'SimpleXMLElement') && $value->count() > 1) {
return parent::setValue(implode(',', (array)$value));
} else {
return parent::setValue($value);
}
}
}
------------------------------------
Cached data
------------------------------------
In some cases access to model data requires too much handling for the time the user is willing to wait, in these
cases it is possible to serve cached data to the user of the model.
For this purpose, each model node in the config contains a :code:`persisted_at` field with the most recent update time,
in which case a static function :code:`getCachedData()` can be used to fetch the data.
This quickly checks if an object was changed (via an xpath expression) returning the data of the model in an array structure.
.. Note::
Although the cache will contain programmaticly generated rows using :code:`getStaticChildren()` in :code:`ArrayField`,
it can't automatically figure out if this content has changed.
In cases you need to ensure these records are updated as well, you do need to call :code:`flushCacheData()` on the
static model. (e.g. :code:`\OPNsense\Firewall\Alias::flushCacheData()`)