ahgCustomFieldsPlugin — Technical Manual¶
Version: 1.0.0 Category: Metadata Management Load Order: 45 Dependencies: ahgCorePlugin
Architecture¶
The plugin implements an Entity-Attribute-Value (EAV) pattern for extensible metadata. Field definitions (attributes) are stored in custom_field_definition, and field values are stored in custom_field_value with typed columns.
Layer Diagram¶
┌──────────────────────────────────────────────────┐
│ Admin UI (/admin/customFields) │
│ customFieldAdmin module — CRUD for definitions │
├──────────────────────────────────────────────────┤
│ Entity UI (edit/view pages) │
│ customField module — save/get values (AJAX) │
│ Display panel — _custom_fields_panel.php │
├──────────────────────────────────────────────────┤
│ Services │
│ CustomFieldService — business logic │
│ CustomFieldRenderService — HTML generation │
├──────────────────────────────────────────────────┤
│ Repositories │
│ FieldDefinitionRepository — definition CRUD │
│ FieldValueRepository — value CRUD │
├──────────────────────────────────────────────────┤
│ Database │
│ custom_field_definition + custom_field_value │
└──────────────────────────────────────────────────┘
Database Schema¶
custom_field_definition¶
Stores field definitions (the EAV "attribute" catalog).
| Column | Type | Description |
|---|---|---|
| id | BIGINT UNSIGNED PK | Auto-increment |
| field_key | VARCHAR(100) | Machine name, unique per entity_type |
| field_label | VARCHAR(255) | Display name |
| field_type | VARCHAR(30) | text, textarea, date, number, boolean, dropdown, url |
| entity_type | VARCHAR(50) | informationobject, actor, accession, repository, donor, function |
| field_group | VARCHAR(100) NULL | UI section grouping label |
| dropdown_taxonomy | VARCHAR(100) NULL | ahg_dropdown taxonomy key (when type=dropdown) |
| is_required | TINYINT(1) | Require value on save |
| is_searchable | TINYINT(1) | Flag for search index integration |
| is_visible_public | TINYINT(1) | Show on public view page |
| is_visible_edit | TINYINT(1) | Show on edit form |
| is_repeatable | TINYINT(1) | Allow multiple values |
| default_value | VARCHAR(500) NULL | Pre-filled default |
| help_text | VARCHAR(500) NULL | Form guidance text |
| validation_rule | VARCHAR(255) NULL | e.g. max:255, regex:/^[A-Z]/ |
| sort_order | INT | Display ordering |
| is_active | TINYINT(1) | Active/inactive (soft delete) |
| created_at | DATETIME | Auto |
| updated_at | DATETIME | Auto |
Indexes: uk_field_entity (field_key, entity_type), idx_entity_type, idx_active_entity, idx_group
custom_field_value¶
Stores field values (the EAV "value" rows). Uses typed columns to preserve data types.
| Column | Type | Description |
|---|---|---|
| id | BIGINT UNSIGNED PK | Auto-increment |
| field_definition_id | BIGINT UNSIGNED FK | References custom_field_definition(id) ON DELETE CASCADE |
| object_id | INT | FK to the entity (information_object.id, actor.id, etc.) |
| value_text | TEXT NULL | For text, textarea, url types |
| value_number | DECIMAL(15,4) NULL | For number type |
| value_date | DATE NULL | For date type |
| value_boolean | TINYINT(1) NULL | For boolean type |
| value_dropdown | VARCHAR(100) NULL | For dropdown type (ahg_dropdown code) |
| sequence | INT | Ordering for repeatable fields (0 = first) |
| created_at | DATETIME | Auto |
| updated_at | DATETIME | Auto |
Indexes: idx_field_object (field_definition_id, object_id), idx_object, idx_dropdown
Plugin Configuration¶
File: config/ahgCustomFieldsPluginConfiguration.class.php¶
- Registers modules:
customFieldAdmin,customField - Connects to
routing.load_configurationto register routes via RouteLoader - Connects to
response.filter_contentto inject CSS/JS assets - Registers PSR-4 autoloader for
AhgCustomFieldsPlugin\namespace underlib/
Routes (registered in PluginConfiguration)¶
| Route Name | URL | Module | Action |
|---|---|---|---|
| custom_field_admin_index | /admin/customFields | customFieldAdmin | index |
| custom_field_admin_edit | /admin/customFields/edit | customFieldAdmin | edit |
| custom_field_admin_save | /admin/customFields/save | customFieldAdmin | save |
| custom_field_admin_delete | /admin/customFields/delete | customFieldAdmin | delete |
| custom_field_admin_reorder | /admin/customFields/reorder | customFieldAdmin | reorder |
| custom_field_admin_export | /admin/customFields/export | customFieldAdmin | export |
| custom_field_admin_import | /admin/customFields/import | customFieldAdmin | import |
| custom_field_save_values | /customFields/save | customField | saveValues |
| custom_field_get_values | /customFields/get/:entityType/:objectId | customField | getValues |
Services¶
CustomFieldService¶
Namespace: AhgCustomFieldsPlugin\Service\CustomFieldService
Path: lib/Service/CustomFieldService.php
| Method | Returns | Description |
|---|---|---|
getDefinitionsForEntity($entityType) |
array | Active definitions sorted by sort_order |
getDefinitionsByGroup($entityType) |
array | Definitions grouped by field_group |
getDefinition($id) |
object|null | Single definition by ID |
getValuesForObject($objectId, $entityType) |
array | Values keyed by field_key |
getRawValuesForObject($objectId, $entityType) |
array | Raw value rows with joined definition data |
saveValues($objectId, $entityType, $fieldValues) |
void | Upsert values from form submission |
deleteValuesForObject($objectId) |
void | Cleanup on entity delete |
createDefinition($data) |
int | Create definition, returns ID |
updateDefinition($id, $data) |
void | Update definition |
deleteDefinition($id) |
void | Soft-delete (is_active=0) |
hardDeleteDefinition($id) |
bool | Hard-delete if no values exist |
reorderDefinitions($orderedIds) |
void | Update sort_order by ID array |
validateValue($definition, $value) |
bool|string | Validate; returns true or error message |
exportDefinitions($entityType) |
array | Export schema as array |
importDefinitions($definitions) |
int | Import schema, returns count imported |
getEntityTypes() |
array | Available entity types |
getFieldTypes() |
array | Available field types |
getDropdownTaxonomies() |
array | Taxonomies from ahg_dropdown |
getDropdownOptions($taxonomy) |
array | Options for a taxonomy |
generateFieldKey($label) |
string | Generate machine name from label |
CustomFieldRenderService¶
Namespace: AhgCustomFieldsPlugin\Service\CustomFieldRenderService
Path: lib/Service/CustomFieldRenderService.php
| Method | Returns | Description |
|---|---|---|
renderViewFields($entityType, $objectId, $publicOnly) |
string | HTML for view display (grouped, read-only) |
renderEditFields($entityType, $objectId) |
string | HTML form inputs (grouped, editable) |
getFieldInput($def, $value) |
string | Single form input by field type |
getRepeatableInput($def, $values) |
string | Repeatable field with add/remove buttons |
Repositories¶
FieldDefinitionRepository¶
Path: lib/Repository/FieldDefinitionRepository.php
All operations via Illuminate\Database\Capsule\Manager as DB on custom_field_definition.
| Method | Description |
|---|---|
getByEntityType($entityType) |
Active definitions for entity type |
getByEntityTypeGrouped($entityType) |
Grouped by field_group |
getAll() |
All definitions (admin listing) |
getAllGroupedByEntity() |
Grouped by entity_type |
find($id) |
Find by ID |
findByKey($fieldKey, $entityType) |
Find by key + entity type |
create($data) |
Insert, returns ID |
update($id, $data) |
Update |
deactivate($id) |
Set is_active=0 |
delete($id) |
Hard delete |
isKeyUnique($key, $entityType, $excludeId) |
Check uniqueness |
reorder($orderedIds) |
Update sort_order |
getFieldGroups() |
Distinct field groups |
getUsedEntityTypes() |
Distinct entity types in use |
exportByEntityType($entityType) |
Export for entity type |
countValues($defId) |
Count values for a definition |
FieldValueRepository¶
Path: lib/Repository/FieldValueRepository.php
| Method | Description |
|---|---|
getByObject($objectId) |
All values for an object |
getByObjectAndEntity($objectId, $entityType) |
Values joined with definitions |
getByDefinitionAndObject($defId, $objectId) |
Values for specific field + object |
upsertValue($defId, $objectId, $valueData, $seq) |
Insert or update a value |
deleteByDefinitionAndObject($defId, $objectId) |
Delete values for field + object |
deleteByObject($objectId) |
Delete all values for object |
deleteByDefinition($defId) |
Delete all values for definition |
countByDefinition($defId) |
Count values |
countObjectsByDefinition($defId) |
Count distinct objects |
Modules¶
customFieldAdmin (Admin CRUD)¶
Extends AhgController. All actions require admin role ($this->checkAdmin()).
| Action | Method | Description |
|---|---|---|
| index | GET | List definitions grouped by entity type |
| edit | GET | Create/edit form |
| save | POST/AJAX | Create or update definition |
| delete | POST/AJAX | Soft or hard delete |
| reorder | POST/AJAX | Drag-drop reorder |
| export | GET | Download JSON |
| import | POST/AJAX | Import JSON |
customField (Entity Value CRUD)¶
| Action | Method | Description |
|---|---|---|
| saveValues | POST/AJAX | Save field values for an entity |
| getValues | GET/AJAX | Get field values for an entity |
Display Panel Integration¶
The plugin registers display panels via extension.json:
"display_panels": [
{ "id": "custom_fields_io", "contexts": ["informationobject"], ... },
{ "id": "custom_fields_actor", "contexts": ["actor"], ... },
{ "id": "custom_fields_accession", "contexts": ["accession"], ... },
{ "id": "custom_fields_repository", "contexts": ["repository"], ... }
]
The panel template (templates/display/_custom_fields_panel.php) renders custom field values in a card with grouped <dl> elements.
Template Integration¶
Including edit fields in a plugin template¶
<?php include_partial('customField/editFields', [
'entityType' => 'informationobject',
'objectId' => $resource->id,
]); ?>
Including view fields in a plugin template¶
<?php include_partial('customField/viewFields', [
'entityType' => 'informationobject',
'objectId' => $resource->id,
'publicOnly' => true,
]); ?>
JavaScript API¶
The edit partial exposes a global function for saving values programmatically:
// Save custom fields via AJAX
window.cfSaveFields().then(function(data) {
if (data.success) { /* saved */ }
});
Validation Rules¶
| Rule Syntax | Example | Description |
|---|---|---|
max:N |
max:255 |
Maximum string length |
regex:/pattern/ |
regex:/^[A-Z]{2}\d+$/ |
Regular expression match |
Built-in type validation is also applied:
- number: Must be numeric
- date: Must match YYYY-MM-DD format
- url: Must pass PHP FILTER_VALIDATE_URL
Reporting Views¶
Path: atom-framework/database/views/reporting_views.sql
v_report_descriptions¶
Joins: information_object, information_object_i18n, term_i18n (level, status), repository/actor_i18n (repository name), event/event_i18n (creation date), status/term_i18n (publication status).
Excludes root node (id=1). Uses MIN(event.id) subquery to avoid duplicate rows from multiple events.
v_report_authorities¶
Joins: actor, actor_i18n, term_i18n (entity type, description status).
Excludes root node (id=1).
v_report_accessions¶
Joins: accession, accession_i18n, term_i18n (acquisition type, processing status, processing priority).
File Structure¶
ahgCustomFieldsPlugin/
├── config/
│ ├── ahgCustomFieldsPluginConfiguration.class.php
│ └── routing.yml
├── database/
│ └── install.sql
├── extension.json
├── lib/
│ ├── Repository/
│ │ ├── FieldDefinitionRepository.php
│ │ └── FieldValueRepository.php
│ └── Service/
│ ├── CustomFieldService.php
│ └── CustomFieldRenderService.php
├── modules/
│ ├── customFieldAdmin/
│ │ ├── actions/actions.class.php
│ │ └── templates/
│ │ ├── indexSuccess.php
│ │ ├── editSuccess.php
│ │ └── _fieldForm.php
│ └── customField/
│ ├── actions/actions.class.php
│ └── templates/
│ ├── _editFields.php
│ └── _viewFields.php
├── templates/
│ └── display/
│ └── _custom_fields_panel.php
└── web/
├── css/custom-fields.css
└── js/custom-fields.js
Seed Data¶
Restriction Codes (in ahgCorePlugin)¶
Added to ahgCorePlugin/database/install.sql under taxonomy restriction_code with 9 base values. See Feature Overview for the full list.
The Archive and Heritage Group (Pty) Ltd