Modulo custom per Drupal 11: form e salvataggio su DB

· Esperto · Moduli

Un modulo di riferimento che copre i pattern fondamentali dello sviluppo Drupal 11: permessi dichiarati in YAML, due rotte con accesso differenziato, form con redirect e controller che legge i dati per utente corrente.


Un modulo che copre routing, form, salvataggio su database, permessi custom e lettura dei dati in un controller. Il caso d'uso è volutamente semplice: note private per utenti autenticati. L'obiettivo non è la funzionalità in sé, ma avere un riferimento concreto e corretto da cui partire per qualsiasi modulo custom.

Struttura

web/modules/custom/custom_notes/
├── custom_notes.info.yml
├── custom_notes.install
├── custom_notes.permissions.yml
├── custom_notes.routing.yml
└── src/
    ├── Controller/
    │   └── NotesController.php
    └── Form/
        └── NoteForm.php

custom_notes.info.yml

name: Custom Notes
type: module
description: Modulo boilerplate con routing, form, database e permessi.
package: Custom
core_version_requirement: ^11

custom_notes.permissions.yml

view own notes:   title: 'Visualizza le proprie note'   description: 'Permette di accedere alla lista delle note personali.'  create notes:   title: 'Crea note'   description: 'Permette di salvare nuove note tramite il form.'

I permessi vengono resi disponibili in /admin/people/permissions automaticamente all'abilitazione del modulo. Nessun codice PHP richiesto per dichiararli.

custom_notes.install

<?php
 /**
 * Implements hook_schema().
 */
function custom_notes_schema(): array {
  $schema['custom_notes'] = [
    'description' => 'Salva le note degli utenti.',
    'fields' => [
      'id' => [
        'type' => 'serial',
        'unsigned' => TRUE,
        'not null' => TRUE,
      ],
      'uid' => [
        'description' => 'UID dell\'utente autore.',
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
        'default' => 0,
      ],
      'title' => [
        'type' => 'varchar',
        'length' => 255,
        'not null' => TRUE,
        'default' => '',
      ],
      'body' => [
        'type' => 'text',
        'size' => 'medium',
        'not null' => TRUE,
      ],
      'created' => [
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ],
    ],
    'primary key' => ['id'],
    'indexes' => [
      'uid' => ['uid'],
    ],
  ];
   return $schema;
}

L'indice su uid è necessario: ogni query filtrerà per utente corrente e senza indice la tabella viene scansionata per intero.

custom_notes.routing.yml

custom_notes.list:   path: '/notes'   defaults:     _controller: '\Drupal\custom_notes\Controller\NotesController::list'     _title: 'Le mie note'   requirements:     _permission: 'view own notes'  custom_notes.add:   path: '/notes/add'   defaults:     _form: '\Drupal\custom_notes\Form\NoteForm'     _title: 'Aggiungi nota'   requirements:     _permission: 'create notes'

Le due rotte usano permessi distinti: un utente può avere view own notes senza create notes, utile per scenari dove la creazione viene disabilitata per certi ruoli.

src/Controller/NotesController.php

<?php
 namespace Drupal\custom_notes\Controller;
 use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Symfony\Component\DependencyInjection\ContainerInterface;
 class NotesController extends ControllerBase {
   public function __construct(
    protected readonly Connection $database,
  ) {}
   public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('database'),
    );
  }
   public function list(): array {
    $uid = $this->currentUser()->id();
     $results = $this->database->select('custom_notes', 'n')
      ->fields('n', ['id', 'title', 'body', 'created'])
      ->condition('n.uid', $uid)
      ->orderBy('n.created', 'DESC')
      ->execute()
      ->fetchAll();
     $rows = [];
    foreach ($results as $record) {
      $rows[] = [
        $record->title,
        $record->body,
        date('d/m/Y H:i', $record->created),
      ];
    }
     return [
      '#type' => 'table',
      '#header' => [
        $this->t('Titolo'),
        $this->t('Nota'),
        $this->t('Data'),
      ],
      '#rows' => $rows,
      '#empty' => $this->t('Nessuna nota salvata.'),
    ];
  }
 }

$this->currentUser() è disponibile senza injection esplicita perché ControllerBase lo espone già tramite il container interno. Il servizio database invece va iniettato: non è tra quelli che ControllerBase carica automaticamente.

src/Form/NoteForm.php

<?php
 namespace Drupal\custom_notes\Form;
 use Drupal\Core\Database\Connection;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
 class NoteForm extends FormBase {
   public function __construct(
    protected readonly Connection $database,
    protected readonly AccountProxyInterface $currentUser,
  ) {}
   public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('database'),
      $container->get('current_user'),
    );
  }
   public function getFormId(): string {
    return 'custom_notes_note_form';
  }
   public function buildForm(array $form, FormStateInterface $form_state): array {
    $form['title'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Titolo'),
      '#required' => TRUE,
      '#maxlength' => 255,
    ];
     $form['body'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Nota'),
      '#required' => TRUE,
      '#rows' => 6,
    ];
     $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Salva nota'),
    ];
     return $form;
  }
   public function submitForm(array &$form, FormStateInterface $form_state): void {
    $this->database->insert('custom_notes')
      ->fields([
        'uid' => $this->currentUser->id(),
        'title' => $form_state->getValue('title'),
        'body' => $form_state->getValue('body'),
        'created' => \Drupal::time()->getRequestTime(),
      ])
      ->execute();
     $this->messenger()->addStatus(
      $this->t('Nota "@title" salvata.', [
        '@title' => $form_state->getValue('title'),
      ])
    );
     $form_state->setRedirect('custom_notes.list');
  }
 }

Nel form current_user viene iniettato esplicitamente come AccountProxyInterface perché serve il suo id() nel submit. messenger() non richiede injection: FormBase include MessengerTrait. Il redirect dopo il submit usa il nome della rotta, non un path hardcoded.

Abilitazione

drush en custom_notes -y
drush cr

Assegnare i permessi da /admin/people/permissions o via Drush:

drush role:perm:add authenticated 'view own notes,create notes'

Il form è su /notes/add, la lista su /notes.