PaymentConstructorFields
The field catalog of the Payment Constructor cluster: it defines the reusable fields a payout is made of, and attaches them to individual constructors. This is the plugin that decides what gets calculated — the reports plugin then computes it.
Overview
The plugin owns two things:
- A catalog of field definitions (
/payment/constructors/fields/) — a workspace-scoped library where each field has a caption, a unique machineident, atype, adata_type, a status, and an optionalvalue(a source column, a formula, a SQL expression, or a constant). - The constructor-to-field attachments (
/payment/constructors/{id}/fields/) — which catalog fields a given constructor uses, in what order, whether each one shows in reports, and which ones the total includes.
You define a field once in the catalog and attach it to as many constructors as you like; the reports plugin materialises a report column for it when it is activated.
Requirements
The three plugins of the cluster work only together.
Each plugin class names the other two plugins in @pluginDependency.
| Requirement | Why the plugin needs it |
|---|---|
PaymentConstructors plugin |
@pluginDependency and getDepends(); the link table has a foreign key to payment_constructors |
PaymentConstructorReports plugin |
@pluginDependency and getDepends(); it validates each field and adds the report column |
Modules plugin |
The plugin is a module. The install SQL writes to modules. |
System plugin setting db_datetime_format |
The format of cdate |
Theme package (Festi\Theme) |
DisplayPlugin and StageBuilder |
Tables users_types, festi_* |
The install SQL gives the sections to the user type admin |
The repository has no init.php file.
Installation
The link table has a foreign key to payment_constructors.
Thus, install the cluster in this order:
PaymentConstructorsPaymentConstructorFields(this plugin)PaymentConstructorReports
Install the plugin with the Festi CLI:
./vendor/bin/festi-plugin --mode install --path ./src/dashboard/ --dump ./dumps/ \
--plugin PaymentConstructorFields \
--repository [email protected]:FestiPlugins/php_festi_plugin_payment_constructor_fields.git
This CLI example was not run for this page.
Set --path to the directory that contains the project config.php.
Set --dump to the dumps directory of the project.
The command adds the repository as a git submodule.
Then it copies install/install.<database type>.sql into the dumps directory as a new migration and applies it.
Commit the submodule and the new migration together.
On each other database of the project, apply the migration with ./vendor/bin/festi-migrate.
The install/ directory contains install.mysql.sql, install.pgsql.sql and install.mssql.sql.
The install SQL makes these objects:
- the
payment_constructor_fieldstable and the link tablepayment_constructors2payment_constructor_fields; - the plugin row in
festi_plugins; - the module
payment_constructor_fieldsinmodules; - four URL rules in the area
backend; - three sections, with the permission for the user type
admin.
| Section | Use |
|---|---|
payment_constructors_fields |
The catalog page |
payment_constructors_relating_fields |
The page with the fields of one constructor |
payment_constructors_sql_fields |
The permission to write a field of the type sql |
The schema is engine-clean and host-agnostic — nothing here is tied to a specific host beyond the Festi framework and the two cluster dependencies.
Configuration
The plugin has no module options.
Permissions on a multi-workspace host
The seed grants each section to the
user type with ident admin, which is the right default for a host with one
global role set. A host that keeps a separate role set per workspace
(users_types.id_workspace) has to repeat those grants against its own
per-workspace roles — otherwise the pages install correctly but no workspace user
can open them. On such a host ident is not unique either, so the follow-up
migration must qualify the role lookup with id_workspace.
Write those grants as a migration file of the project.
Apply the migration with ./vendor/bin/festi-migrate.
Workspace URLs
Both pages also answer under the workspace prefix
(/workspace/{id}/payment/constructors/...), and every link the plugin builds goes
through IPaymentConstructorFacade::getWorkSpaceUrl(), which decides between the two.
The store hands its tblDef links over as parameters. The prefix segment is configurable
on the payment_constructors module — see the
PaymentConstructors README.
Usage
- Open the list of payment constructors.
- Click Fields Configuration to open the catalog.
- Add a field. Select the type and the data type, and enter the value.
- Set the status of the field to
active. - Go back to the list and click Fields in the row of a constructor.
- Add the field to the constructor.
The form of an attached field has these controls:
| Control | Column | Function |
|---|---|---|
| Field | id_payment_constructor_field |
The catalog field. The list contains only active fields. |
| Show in Report | is_report |
The reports grid shows the column. |
| Is Total | is_total |
The total of the report includes the value. |
| Order Index | order_index |
The position of the field. |
The catalog form has the checkbox Is Decrypt (is_decrypt).
When it is set, the reports plugin decrypts the value with the ICryptProvider of the host.
Field types
The type of a catalog field decides where its value comes from:
- general — reads a physical
table.columnsource (the mapping is the field'svalue). The columns on offer are the payment-user source's filterable columns minus enum/foreign-key ones; a host narrows them further by passing an allowlist as the$printableColumnsof itsPaymentUserSourceConfig. The table must be one of the allowed tables of that configuration. The ident of the field is always the column name. - constant — a fixed value. An empty value is not permitted.
- formula — an expression referencing other fields by
%ident%, with numbers, brackets and the operators+,-,*,/. See Formula rules. - sql — a value produced by a SQL expression. See SQL fields are gated by their own section.
- custom — system-managed fields contributed in code. They appear read-only in a "Custom Fields" block and are not part of the editable catalog grid.
Formula rules
The reports plugin (FormulaField) does the checks when you save a formula field:
- Each
%ident%must be an active field. The lookup reads system fields only (id_workspaceNULL). - An ident with an upper-case letter is not permitted.
- The check removes each whitespace character from the formula first. Thus
%my field%is read as%myfield%. - An ident that a host
IAfterUserCalculationListenerowns is not permitted. - A number directly after
%ident%is not permitted.
When you attach a formula field to a constructor, each %ident% must be an active field of that constructor.
The calculation reads only idents made of lower-case Latin letters and underscores from a formula. Do not refer to an ident that contains a digit.
Data types and the shared-column rule
Each field also has a data_type — string, integer, decimal,
price, date, or datetime — which the reports plugin uses to shape the
physical report column.
The important consequence: an ident maps to one physical column, shared across
workspaces. The ident column has a unique index, and the report table has one
column for each ident. The save is rejected in these cases:
- A different field that you can see has the same ident.
- No field that you can see has the ident, but the report table has a column with that name.
- A system field has the same ident and a different data type.
Choose the ident and data type of a field as if they were permanent, because for that column they are.
Lifecycle: draft → active → deleted
A field is draft, active, or deleted, and only active fields can be attached
to a constructor. Activating a field is the moment it reaches into the reports
plugin — activation calls the reports facade to add the matching report column.
If existing report data is incompatible with the field's data type, the activation
is rolled back rather than corrupting the report table.
Attachments are guarded once a constructor has a report row. The plugin then removes the Add and Remove actions and makes the Field control read-only.
Workspace scoping
A catalog field is either system (id_workspace NULL — visible and usable in
every workspace) or workspace-owned (visible only in its workspace). Workspace
users see system fields plus their own but cannot edit system fields. In
single-workspace (officetool) mode everything is system-scoped, so scoping is
effectively a no-op.
Without an active workspace, the catalog grid has no workspace filter. The Field control of a constructor then offers system fields only.
SQL fields are gated by their own section
A field of type SQL carries a raw query, so it is not part of what a workspace
operator may author. The payment_constructors_sql_fields section
(PaymentConstructorFieldsSections::SQL_FIELDS) gates the authoring of one: without that
section the SQL option is missing from the type select of the add/edit form, an SQL row
loses its edit and remove actions, and a submitted type = sql is rejected on save. The
install scripts grant the section to admin only, so a workspace role never receives it
unless the host grants it explicitly.
The gate is on writing, not on reading: an SQL row stays listed and its Type column still
reads "SQL". That is what RestrictedSelectField is for — the framework's SelectField
drops a permission-gated option from its value list altogether, which also blanks the list
cell of every row already holding that value. The subclass keeps the full list for display
and filtering and narrows only what the form offers.
The Value cell of an SQL row shows the text "SQL expression". Only a user with the section sees the expression, as the tooltip of the cell.
The reports plugin (SqlField) checks the expression on save.
It rejects the words DELETE, ALTER, INSERT, DROP, UPDATE, CREATE and BACKUP in any letter case.
Then it runs the expression against the payment-user source table to make sure that it is valid.
The expression can contain tokens. The reports plugin replaces each token with a quoted value:
| Token | Value |
|---|---|
%id-payout% |
The id of the payout |
%month-year% |
The month of the payout, as m-Y |
%year-month% |
The month of the payout, as Y-m |
| Host tokens | The tokens of each ICollectSqlFieldTokensListener |
Host integration
The plugin does not add its own top-level menu — it hangs off the base
constructors grid. Through IPaymentConstructorsStoreInitListener it injects two
actions into that grid (each gated on its own permission): a top-bar Fields
Configuration link to the catalog, and a per-row Fields link to a
constructor's attachments. It also reacts to constructor duplication by copying the
source constructor's field attachments onto the new one.
It exposes IPaymentConstructorFieldsFacade for the reports plugin and the host:
| Method | Result |
|---|---|
getByID() |
One field by id |
getByIdent() |
One field by ident, from the system fields and the fields of the given workspace |
getByPaymentConstructorID() |
The fields of a constructor in the sequence of order_index, optionally of one status |
getByPaymentConstructorIDAndType() |
The fields of a constructor of one type |
updateByIdent() |
Changes the catalog row of an ident |
createRelating() |
Attaches a field to a constructor |
Ident generation — the form has no ident control. The store sets the ident on save:
- For a
generalfield, the ident is the column name of the value. The store writes it again on each save, so a change of the column changes the ident. - For each other type, a new field gets an ident that the store makes from the caption.
- A field of each other type that has an ident keeps it. A formula refers to the ident, and the report column has its name.
The ident is not cosmetic: it becomes a real column on the report table and an
unquoted alias in the calculation query (<expression> AS <ident>). That is why
every ident has to match:
^[a-z_][a-z0-9_]*$
Lower-case latin letters, digits and underscores. A digit cannot be the first character.
The built-in DefaultFieldIdentGenerator only lower-cases the caption and turns spaces and
hyphens into underscores. The store then normalizes the result:
- It changes each group of characters that are not
a-zor0-9to one underscore. - It removes the underscores at the two ends and the digits at the start.
- If nothing remains, the save is rejected.
- If the ident is in use, the store adds a number:
bonus,bonus_2,bonus_3.
Thus, a caption without Latin letters or digits cannot be saved with the default generator.
A host with other captions implements
IFieldIdentGeneratorListener and returns its own IFieldIdentGenerator:
class SchoolFieldIdentGenerator implements IFieldIdentGenerator
{
public function getIdentByCaption(string $caption): string
{
// transliterate the caption to Latin letters
}
}
This PHP example was not run for this page.
The generator only has to transliterate. The engine deliberately keeps the validation narrow rather than widening it per host, so an ident is always a safe bare SQL identifier.