Jump to content

ICT:Drupal Custom module design for automatic numbering: Difference between revisions

From Costa Sano MediaWiki
No edit summary
 
(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 mpodule can then be written and step for step tested. After each step don't forget to clean the cash
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.
</pre>
= costasano_asset Module – 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.

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:

  1. Automatically generate a unique, human‑readable identifier when an Asset node is created.
  2. 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.

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:

  1. Clear Drupal caches:
  • Via UI: Configuration → Development → Performance → Clear all caches
  • Or via Drush: drush cr
  1. Create a new Asset node:
  • Ensure:
    • The counter is set.
    • The title is generated as before.
  1. 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.