This document is intended to provide guidance to anyone writing code against COmanage Registry PE (v5.0.0 or later), including plugin developers. It describes various facilities developed for COmanage specifically, to help make it easier to write DRY code.
Before continuing, be sure to be familiar with the following:
Registry PE is targeting PHP 8+. In particular:
Registry PE generally adopts PSR-12, with some variations. Where discrepancies exist between this section and the rest of this document, this section controls and the conflict should be assumed to be a legacy artifact that has not been updated.
In general, CakePHP Coding Conventions should also be followed, except where they conflict with guidance in this document.
For developers already familiar with the v4 and earlier coding style, this section highlights the changes:
int
and integer
were used interchangeable.XXX PHP_CodeSniffer
Before writing any code, start by proposing changes to the existing data model (which may include defining new tables that will map to the new models added to the application). Build a proposed functional design around the data model changes. For code that will be contributed to the project, review these designs with the development group before beginning any meaningful coding, since code that does not align with the project's direction will not be accepted.
Schema management is handled by Doctrine DBAL, using a Registry specific JSON schema file processed by DatabaseCommand
. The format of the schema file is fairly self documenting, but note the following:
columnLibrary
provides default definitions for commonly used attributes. These defaults will be used when the table defines a column name with the same name as the library definition. All library values are inherited by default, it is then only necessary to explicitly define the ones that should be changed.timestamps
is set to false in the table definition.changelog
is set to false in the table definition.sourced
to true will insert the necessary foreign key and index.mvea
to a list of parent tables will insert the necessary foreign keys and indexes.Note that since JSON files inexplicably can't have comments, the key comment
is reserved in all contexts except the list of column definitions to be used for comments.
"Implicit logic" must be documented in the form of Application Rules.
Application Rules are typically enforced using Cake's Application Rules, which are applied to a table using the table's buildRules()
function. By convention, COmanage rules are named ruleSomethingOrOther()
, and are defined in the table they apply to. Rules common to multiple tables should be implemented in RulesTrait
. Global rules that apply to all tables are implemented in the RuleBuilderEventListener
.
Application Rules must be labeled in a comment adjacent to the code that enforces them using the form AR-Model-#
, that is the string "AR-", the camel cased singular model name, a dash, and the number of the rule for that model (for example: AR-ApiUser-3
). Global rules are referred to as General Model Rules, and are labeled AR-GMR-#
.
Wherever possible, log entries (to the rules
level) should be generated when an Application Rule is applied.
In general, Application Rules are not configurable.
For migration of Registry tables from v4 to v5, appropriate support for migrating existing data must be added to TransmogrificationCommand
.
Each table must be added to $tables
in the same order as schema.json
(ie: to correctly sequence the population of primary keys). By default, fields are mapped 1-1 unless configured via fieldMap
. (The displayField
is used when Transmogrification is running.)
In some cases, a custom mapping function is required to calculate the target table value. In some of these cases, results from an earlier table should be cached so later mapping actions can quickly find earlier values. This is accomplished with the cache
entry.
If a table was not previously Changelog enabled but is Changelog enabled in v5, the key addChangelog
must be set to true
.
Boolean fields must currently be explicitly identified in the booleans
entry.
XXX
XXX
Registry builds various utilities on top of the Cake framework.
Unlike in v4 and earlier, API transactions are handled entirely by dedicated controllers. The standard Registry model level API is implemented by ApiV2Controller
, other APIs are implemented in plugins. As a result, controller specific logic (such as overriding beforeFilter
) will not automatically apply to APIs. Model specific logic that needs to apply to both the UI and API should be defined in the model (table) through the use of Traits or other similar techniques, and then referenced generically from the calling controllers.
XXX
In general, the previous documentation on Changelog Behavior applies, though not all features are implemented yet.
delete
requests and converts them to updates, beforeDelete
and afterDelete
callbacks should not be used.ChangelogBehaviorTrait
.XXX
delete
requests and converts them to updates, beforeDelete
and afterDelete
callbacks should not be used.ChangelogBehaviorTrait
, which implements afterSave
. As such, Tables should not implement their own afterSave
, but should implement localAfterSave
(with the same function signature as afterSave
). localAfterSave
will not be called when archived records are written to the database.XXX
When defining functions, use parameter type, return types, and default values. When the ID of the current Model is a parameter, it should be first in the list.
public myFunction(int $id, string $label, bool $colorful=false): string { ... } |
When calling functions, use parameter names wherever possible.
$s = $this->myFunction(id: $entity->id, label: __d('information', 'my.label'), colorful: true); |
Avoid the use of configuration arrays (though Cake still makes heavy use of these).
public badExample($id, $options=[]); |
When localizing text strings, use the table name and/or field name as is whenever possible.
Normally, a relation can simply be defined using something like
$this->belongsTo('Types') |
which implies the current table has a columns type_id
. However, sometimes it is necessary or desirable to use a different foreign key name, such as default_type_id
. This can be accomplished with something like
$this->belongsTo('Types')->setForeignKey('default_type_id'); |
Registry's foreign key checks further require a property to be set so that ruleValidateCO
can properly validate foreign keys at run time. This can be accomplished by setting a property with the name of the foreign key without the _id
:
$this->belongsTo('Type')->setForeignKey('default_type_id')->setProperty('default_type'); |
Many models can be ordered, eg Provisioning Targets. In order to leverage common utility code:
ordr
, spelled with out the "e". (This is because order
is a reserved keyword in MySQL.)ordr
if none is provided when a new entity is saved.ordr
value. So, for example, when a new Provisioning Target is added, max(ordr)
is determined for all Provisioning Targets within the same CO, while for Enrollment Flow Steps max(ordr)
is determined for all Enrollment Flow Steps within the same Enrollment Flow.All timestamps are stored in the database in UTC (AR-GMR-4).
To automatically convert to UTC on save, the table should load TimezoneBehavior
. FieldHelper::control()
will convert from UTC on rendering, as will the Standard index.php
when columns.inc
sets the field type to datetime
.
There is no timezone conversion for the REST API.
See also: Registry Timezones
Common code used to be placed in AppModel
, which led to a large and complicated pile of code. In general, common functionality is now implemented using traits.
XXX
Models should record History at appropriate points to facilitate administrator review of actions affecting a Person record. HistoryTrait
offers utility functions to simplify recording history. It is also possible to use HistoryRecordsTable
directly, though it may be more complicated to do so.
The Primary Link of a table is the foreign key to the most significant parent object, typically co_id
or person_id
. The Primary Link is used to automatically determine permissions, generate links, and other similar purposes.
After adding or editing an entity, different models may have different user experiences for where to go next. The selection of a redirect target can be controlled by setting a Redirect Goal. Currently supported Redirect Goals are
index
: Redirect to the index for the model, filtered by the Primary LinkprimaryLink
: Redirect to the Primary Link entityself
: Re-render the same formPrimary Links can be declared to Plugin models (for example, the a Plugin defines secondary models to a Primary Plugin model) using the notation Plugin.foreign_key_id
. Note that this is the physical plugin name and not the Entry Point Model. The foreign key ID will be inflected to get the Model name. For example, $this->setPrimaryLink('CoreAssigner.format_assigner_id');
will declare the primary link to be to CoreAssigner.FormatAssigners::id
.
Cake supports two main ways for referencing another model (table) from within a model (table) or controller. The first is via the model relation:
// eg, in GroupMembersTable.php: $person = $this->People->get($entity->person_id); |
The second is via the TableLocator
class. The TableLocator is directly available in a controller, or can be accessed using the LocatorAwareTrait
it a Model.
// In a controller $People = $this->getTableLocator()->get('People'); // In a model class MyTable extends Table { use \Cake\ORM\Locator\LocatorAwareTrait; public function doSomething() { $People = $this->getTableLocator()->get("People"); } } |
In general, either approach is acceptable. The first approach is usually simpler and more compact, however when a long chain of relations is required to get to the desired table, the second approach may be preferable. When there is no directly relation, the second approach is required.
In general, model code should obtain the current CO via parameters passed to the functions it implements, either directly (when there is no other parameter that implies a CO) or indirectly (when another parameter, such as $personId
, can be used to calculate the CO). The function findCoForRecord
(implemented in PrimaryLinkTrait
) can be helpful.
In rare cases, it may be necessary to determine the CO by other means, for example in order to adjust validation rules based on the current CO. Models can setAcceptsCoId
on their table (again via PrimaryLinkTrait
), and AppController
will then provide the CO to the table as part of setCO
. Note this currently works only for the primary table of the request and its immediate relations.
In general, avoid using joins unless required for performance reasons. Joins make the code harder to read, require special annotations, may interact poorly with ChangelogBehavior, and require special handling when the database configuration has quoteIdentifiers
enabled (which is required for MySQL).
XXX
It is possible to append a custom string (such as "Primary Link") to a field in the index view on a per-record basis using the append
directive. The value is a function implemented on the entity. For example,
$indexColumns = [ 'type_id' => [ 'type' => 'fk', 'append' => 'primaryLabel' ] ] |
when rendering the field type for the index of names, $name→primaryLink(): string
will be called and the returned string will be appended with a comma to the string for the current value of the type foreign key. The result will be something like "Official, Primary Name
".
XXX
By default, the display field used for a model is whatever is set using Cake's setDisplayField
. However, tables can implement model-specific display logic by implementing generateDisplayField($entity): string
.