ICT:Drupal Custom module design for automatic numbering: Difference between revisions
No edit summary |
m Mngr moved page ICT:Drupal Custum module design for automatic numbering to ICT:Drupal Custom module design for automatic numbering without leaving a redirect |
||
| (4 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
= Automatic numbering of Asset records with custom desing module = | = Automatic numbering of Asset records with custom desing module = | ||
== Environment preparation for custom modules = | == Environment preparation for custom modules == | ||
Custom modules live in | Custom modules live in | ||
| Line 33: | Line 33: | ||
== The module itself == | == The module itself == | ||
The package = a php | The package = a php module can then be written and step for step tested. After each step don't forget to clean the cash | ||
<pre> | <pre> | ||
drush cr | drush cr | ||
| Line 112: | Line 112: | ||
} | } | ||
</pre> | |||
= Technical Overview - Code analysis and comments = | |||
This document explains how the ''costasano_asset'' custom module works, why it exists, and how its internal logic is structured. It is intended as a clear, successor‑friendly reference for future maintainers. | |||
== Purpose of the Module == | |||
The module provides two core behaviors for the '''asset''' content type: | |||
# Automatically generate a unique, human‑readable identifier when an Asset node is created. | |||
# Hide certain structural fields after creation so they cannot be modified later. | |||
These behaviors ensure that Asset nodes receive stable identifiers and that the metadata used to construct those identifiers remains immutable. | |||
---- | |||
= 1. Auto‑Generation of Asset Identifiers = | |||
The module implements '''hook_entity_presave()''' to modify Asset nodes before they are saved for the first time. | |||
== When the Logic Runs == | |||
The hook executes only when all of the following are true: | |||
* The entity is a node. | |||
* The node bundle is ''asset''. | |||
* The node has the field ''field_as_counter''. | |||
* The node is new (i.e., being created, not edited). | |||
This ensures the identifier is generated exactly once. | |||
== Counter Generation == | |||
The module queries the database table ''node__field_as_counter'' to find the highest existing counter value: | |||
* If a value exists → increment it by 1. | |||
* If no value exists → start at 1. | |||
The counter is then stored in ''field_as_counter''. | |||
The counter is padded to 5 digits: | |||
<code> | |||
00001, 00002, 00003, … | |||
</code> | |||
== Building the Identifier == | |||
The identifier is constructed from three components: | |||
; Chapter code | |||
: The label of the referenced ''field_as_chapter'' entity. | |||
; Context code | |||
: Determined by the referenced entities: | |||
* If ''field_as_place'' exists → use its label. | |||
* If ''field_as_organisation'' exists → override the place label. | |||
; Sequence number | |||
: The padded counter. | |||
The final identifier has the form: | |||
<code> | |||
CHAPTER-CONTEXT-00042 | |||
</code> | |||
This identifier is then assigned as the node title. | |||
---- | |||
= 2. Locking Structural Fields After Creation = | |||
The module implements: | |||
<code> | |||
hook_form_node_asset_edit_form_alter() | |||
</code> | |||
This hook runs when the edit form for an Asset node is displayed. | |||
== Behavior == | |||
If the node is not new (i.e., it is being edited), the following fields are removed from the form: | |||
* ''field_as_chapter'' | |||
* ''field_as_place'' | |||
* ''field_as_organisation'' | |||
This prevents users from altering the metadata that was used to generate the identifier. | |||
---- | |||
= 3. Architectural Summary = | |||
Below is a simplified flow of how the module behaves: | |||
<pre> | |||
User creates Asset node | |||
↓ | |||
hook_entity_presave() | |||
• Determine next counter | |||
• Build identifier (chapter + context + padded counter) | |||
• Set title and counter field | |||
↓ | |||
Node is saved | |||
↓ | |||
User edits Asset node | |||
↓ | |||
hook_form_alter() | |||
• Hide structural fields | |||
</pre> | |||
This ensures stable identifiers and consistent metadata. | |||
---- | |||
= 4. Strengths of the Current Implementation = | |||
* Simple and easy to understand. | |||
* Logic runs only when needed. | |||
* No unnecessary services or complexity. | |||
* Successor‑friendly and predictable. | |||
* Works well in low‑traffic environments. | |||
---- | |||
= 5. Potential Improvements (Optional) = | |||
These are not required but may be useful for future refinement. | |||
== Replace Direct Database Query with EntityQuery == | |||
A more Drupal‑native approach would avoid querying field tables directly. | |||
== Clarify Context Priority == | |||
Currently, ''organisation'' overrides ''place'' if both are present. | |||
A comment or explicit rule may help future maintainers. | |||
== Concurrency Considerations == | |||
Simultaneous node creation could theoretically produce duplicate counters. | |||
If this becomes a concern, a locking mechanism can be added. | |||
== Service‑Based Refactoring == | |||
Moving the identifier logic into a service would improve testability and structure. | |||
---- | |||
= 6. Summary = | |||
The ''costasano_asset'' module provides a clean, reliable mechanism for generating stable identifiers for Asset nodes and ensuring that the metadata used to construct those identifiers remains unchanged after creation. Its design is intentionally simple, making it easy to maintain and extend. | |||
Future enhancements can focus on improving robustness, clarity, and architectural structure without altering the module’s core behavior. | |||
= Service-based Refactor of costasano_asset – Step-by-step Guide = | = Service-based Refactor of costasano_asset – Step-by-step Guide = | ||
| Line 451: | Line 610: | ||
and decide whether this architectural style matches your long-term goals for clarity, maintainability, and successor-friendliness. | and decide whether this architectural style matches your long-term goals for clarity, maintainability, and successor-friendliness. | ||
Latest revision as of 10:13, 20 March 2026
Automatic numbering of Asset records with custom desing module
Environment preparation for custom modules
Custom modules live in
/var/www/drupal/web/modules/custom$
Our custom module is called costasano_asset and as such a directory should be created for this module. Minimum 2 files are necessary for a custom module.
/var/www/drupal/web/modules/custom/costasano_asset costasano_asset.info.yml costasano_asset.module
A first file costasano_asset.info.yml defines the module inside the Drupal environment
name: Costasano Asset type: module description: Asset numbering logic for the Costasano Heritage Project core_version_requirement: ^11 package: Costasano
Once this files exists, the module can be enabled:
drush en costasano_asset
The module itself
The package = a php module can then be written and step for step tested. After each step don't forget to clean the cash
drush cr
The final code for the custom module costasano_asset.module is as follows:
/**
* Implements hook_entity_presave().
*/
function costasano_asset_entity_presave(EntityInterface $entity) {
if ($entity->getEntityTypeId() !== 'node') {
return;
}
if ($entity->bundle() !== 'asset') {
return;
}
if (!$entity->hasField('field_as_counter')) {
return;
}
if ($entity->isNew()) {
$connection = Database::getConnection();
$max = $connection->select('node__field_as_counter', 'c')
->fields('c', ['field_as_counter_value'])
->orderBy('field_as_counter_value', 'DESC')
->range(0, 1)
->execute()
->fetchField();
$counter = $max ? $max + 1 : 1;
$entity->set('field_as_counter', $counter);
$sequence = str_pad($counter, 5, '0', STR_PAD_LEFT);
$chapter = $entity->get('field_as_chapter')->entity;
$place = $entity->get('field_as_place')->entity;
$organisation = $entity->get('field_as_organisation')->entity;
$chapter_code = $chapter ? $chapter->label() : '';
$context_code = '';
if ($place) {
$context_code = $place->label();
}
if ($organisation) {
$context_code = $organisation->label();
}
$identifier = $chapter_code . '-' . $context_code . '-' . $sequence;
$entity->setTitle($identifier);
}
}
/**
* Hide structural fields after creation.
*/
function costasano_asset_form_node_asset_edit_form_alter(&$form, FormStateInterface $form_state) {
$node = $form_state->getFormObject()->getEntity();
if (!$node->isNew()) {
unset($form['field_as_chapter']);
unset($form['field_as_place']);
unset($form['field_as_organisation']);
}
}
Technical Overview - Code analysis and comments
This document explains how the costasano_asset custom module works, why it exists, and how its internal logic is structured. It is intended as a clear, successor‑friendly reference for future maintainers.
Purpose of the Module
The module provides two core behaviors for the asset content type:
- Automatically generate a unique, human‑readable identifier when an Asset node is created.
- Hide certain structural fields after creation so they cannot be modified later.
These behaviors ensure that Asset nodes receive stable identifiers and that the metadata used to construct those identifiers remains immutable.
1. Auto‑Generation of Asset Identifiers
The module implements hook_entity_presave() to modify Asset nodes before they are saved for the first time.
When the Logic Runs
The hook executes only when all of the following are true:
- The entity is a node.
- The node bundle is asset.
- The node has the field field_as_counter.
- The node is new (i.e., being created, not edited).
This ensures the identifier is generated exactly once.
Counter Generation
The module queries the database table node__field_as_counter to find the highest existing counter value:
- If a value exists → increment it by 1.
- If no value exists → start at 1.
The counter is then stored in field_as_counter.
The counter is padded to 5 digits:
00001, 00002, 00003, …
Building the Identifier
The identifier is constructed from three components:
- Chapter code
- The label of the referenced field_as_chapter entity.
- Context code
- Determined by the referenced entities:
- If field_as_place exists → use its label.
- If field_as_organisation exists → override the place label.
- Sequence number
- The padded counter.
The final identifier has the form:
CHAPTER-CONTEXT-00042
This identifier is then assigned as the node title.
2. Locking Structural Fields After Creation
The module implements:
hook_form_node_asset_edit_form_alter()
This hook runs when the edit form for an Asset node is displayed.
Behavior
If the node is not new (i.e., it is being edited), the following fields are removed from the form:
- field_as_chapter
- field_as_place
- field_as_organisation
This prevents users from altering the metadata that was used to generate the identifier.
3. Architectural Summary
Below is a simplified flow of how the module behaves:
User creates Asset node
↓
hook_entity_presave()
• Determine next counter
• Build identifier (chapter + context + padded counter)
• Set title and counter field
↓
Node is saved
↓
User edits Asset node
↓
hook_form_alter()
• Hide structural fields
This ensures stable identifiers and consistent metadata.
4. Strengths of the Current Implementation
- Simple and easy to understand.
- Logic runs only when needed.
- No unnecessary services or complexity.
- Successor‑friendly and predictable.
- Works well in low‑traffic environments.
5. Potential Improvements (Optional)
These are not required but may be useful for future refinement.
Replace Direct Database Query with EntityQuery
A more Drupal‑native approach would avoid querying field tables directly.
Clarify Context Priority
Currently, organisation overrides place if both are present. A comment or explicit rule may help future maintainers.
Concurrency Considerations
Simultaneous node creation could theoretically produce duplicate counters. If this becomes a concern, a locking mechanism can be added.
Service‑Based Refactoring
Moving the identifier logic into a service would improve testability and structure.
6. Summary
The costasano_asset module provides a clean, reliable mechanism for generating stable identifiers for Asset nodes and ensuring that the metadata used to construct those identifiers remains unchanged after creation. Its design is intentionally simple, making it easy to maintain and extend.
Future enhancements can focus on improving robustness, clarity, and architectural structure without altering the module’s core behavior.
Service-based Refactor of costasano_asset – Step-by-step Guide
This document explains, step by step, how to refactor the costasano_asset module to use a Drupal service for the identifier logic.
The goal is not to add features, but to:
- Move the core logic into a dedicated service class.
- Keep hooks thin and readable.
- Make the architecture clearer and more successor-friendly.
Overview of the Target Architecture
After the refactor, the module will look like this:
- costasano_asset.module
- Contains only small hooks that delegate to a service.
- costasano_asset.services.yml
- Registers the service with Drupal’s container.
- src/Service/AssetIdentifierGenerator.php
- Contains the actual logic for:
- Finding the next counter.
- Building the identifier.
- Applying it to a node.
- Contains the actual logic for:
Hooks become “event listeners”; the service becomes the “engine”.
Step 1 – Prepare the Folder Structure
In your module directory (modules/custom/costasano_asset or similar), ensure you have:
- costasano_asset.info.yml
- costasano_asset.module
- costasano_asset.services.yml (we will create this)
- src/Service/ (we will create this folder)
- AssetIdentifierGenerator.php (we will create this file)
If src or Service does not exist yet, create them:
modules/custom/costasano_asset/
costasano_asset.info.yml
costasano_asset.module
costasano_asset.services.yml
src/
Service/
AssetIdentifierGenerator.php
Step 2 – Create the Service Class
Create the file:
src/Service/AssetIdentifierGenerator.php
with the following content:
<?php
namespace Drupal\costasano_asset\Service;
use Drupal\node\Entity\Node;
use Drupal\Core\Entity\EntityInterface;
/**
* Service for generating and applying asset identifiers.
*/
class AssetIdentifierGenerator {
/**
* Get the next counter value for asset nodes.
*
* @return int
* The next counter value.
*/
public function getNextCounter(): int {
$query = \Drupal::entityQuery('node')
->condition('type', 'asset')
->sort('field_as_counter', 'DESC')
->range(0, 1);
$nids = $query->execute();
if (!empty($nids)) {
$latest_nid = reset($nids);
$latest_node = Node::load($latest_nid);
$max = (int) $latest_node->get('field_as_counter')->value;
}
else {
$max = 0;
}
return $max + 1;
}
/**
* Build the identifier string for a given node and counter.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The asset node entity.
* @param int $counter
* The counter value.
*
* @return string
* The identifier string.
*/
public function buildIdentifier(EntityInterface $entity, int $counter): string {
$sequence = str_pad($counter, 5, '0', STR_PAD_LEFT);
$chapter = $entity->get('field_as_chapter')->entity;
$place = $entity->get('field_as_place')->entity;
$organisation = $entity->get('field_as_organisation')->entity;
$chapter_code = $chapter ? $chapter->label() : '';
$context_code = '';
if ($place) {
$context_code = $place->label();
}
if ($organisation) {
$context_code = $organisation->label();
}
return $chapter_code . '-' . $context_code . '-' . $sequence;
}
/**
* Apply counter and identifier to an asset node.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The asset node entity.
*/
public function applyIdentifier(EntityInterface $entity): void {
// Only act on asset nodes.
if ($entity->getEntityTypeId() !== 'node' || $entity->bundle() !== 'asset') {
return;
}
// Only run on creation.
if (!$entity->isNew()) {
return;
}
// Ensure the counter field exists.
if (!$entity->hasField('field_as_counter')) {
return;
}
// Get next counter and set it.
$counter = $this->getNextCounter();
$entity->set('field_as_counter', $counter);
// Build identifier and set title.
$identifier = $this->buildIdentifier($entity, $counter);
$entity->setTitle($identifier);
}
}
What this class does
- getNextCounter()
- Uses EntityQuery to find the highest existing counter and returns the next value.
- buildIdentifier()
- Builds the identifier string from chapter, place/organisation, and the padded counter.
- applyIdentifier()
- Checks that the entity is an asset node, is new, has the counter field, then:
- Gets the next counter.
- Sets the field.
- Builds and sets the title.
This is the “engine” of your module.
Step 3 – Register the Service
Create (or edit) the file:
costasano_asset.services.yml
with the following content:
services:
costasano_asset.identifier_generator:
class: 'Drupal\costasano_asset\Service\AssetIdentifierGenerator'
This tells Drupal:
- There is a service named costasano_asset.identifier_generator.
- It is implemented by the class Drupal\costasano_asset\Service\AssetIdentifierGenerator.
Later, we will ask Drupal for this service and call its methods.
Step 4 – Update the Hooks to Use the Service
Now we simplify costasano_asset.module.
4.1 – Update hook_entity_presave()
Replace your existing hook_entity_presave() with:
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_entity_presave().
*/
function costasano_asset_entity_presave(EntityInterface $entity) {
// Delegate to the service.
\Drupal::service('costasano_asset.identifier_generator')
->applyIdentifier($entity);
}
Key idea:
- The hook no longer contains logic.
- It simply calls the service and passes the entity.
4.2 – Keep the form alter hook as is (for now)
You can keep your existing form alter hook unchanged:
/**
* Hide structural fields after creation.
*/
function costasano_asset_form_node_asset_edit_form_alter(&$form, FormStateInterface $form_state) {
$node = $form_state->getFormObject()->getEntity();
if (!$node->isNew()) {
unset($form['field_as_chapter']);
unset($form['field_as_place']);
unset($form['field_as_organisation']);
}
}
Later, if you wish, you could also move the “hide fields” logic into a service, but it is not necessary for understanding the concept.
Step 5 – Clear Caches and Test
After making these changes:
- Clear Drupal caches:
- Via UI: Configuration → Development → Performance → Clear all caches
- Or via Drush:
drush cr
- Create a new Asset node:
- Ensure:
- The counter is set.
- The title is generated as before.
- Edit the Asset node:
- Confirm:
- The structural fields are hidden as before.
If everything behaves the same, the refactor is functionally correct.
Step 6 – Understanding the Conceptual Shift
Before
- Logic lived inside hook_entity_presave().
- The hook:
- Checked conditions.
- Queried the database.
- Built the identifier.
- Set fields and title.
After
- Logic lives inside AssetIdentifierGenerator (a service).
- The hook:
- Simply calls the service: applyIdentifier($entity).
Why this can be better
- The service is:
- Easier to find (src/Service/AssetIdentifierGenerator.php).
- Easier to test.
- Easier to reuse (e.g., in migrations, batch processes, custom commands).
- The hook is:
- Thin and readable.
- Clearly just an entry point.
You now have a clear separation:
- Hooks = “When should this run?”
- Service = “What should actually happen?”
Step 7 – Possible Next Steps (Optional)
Once you are comfortable with this structure, you could:
- Add concurrency-safe logic (using Drupal’s lock service) inside the service.
- Make the identifier pattern configurable (via config).
- Add unit tests for AssetIdentifierGenerator.
- Move the “hide fields after creation” logic into a separate service if desired.
None of this is required now. The current refactor is enough to understand the service-based concept.
Summary
By following these steps, you have:
- Introduced a Drupal service (AssetIdentifierGenerator).
- Moved the identifier logic out of the hook and into a dedicated class.
- Kept the module’s behavior identical, while improving structure and clarity.
You can now study:
- The old procedural version.
- The new service-based version.
and decide whether this architectural style matches your long-term goals for clarity, maintainability, and successor-friendliness.