Modulo custom per Drupal 11: form e salvataggio su DB
Breadcrumb
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.phpcustom_notes.info.yml
name: Custom Notes
type: module
description: Modulo boilerplate con routing, form, database e permessi.
package: Custom
core_version_requirement: ^11custom_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 crAssegnare 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.