Dezvoltare module Drupal: ghid practic pentru proiecte enterprise
Articol
Cea mai costisitoare greșeală pe care o vedem în proiectele Drupal enterprise este un modul custom scris pentru o problemă pe care contrib-ul a rezolvat-o deja. Înainte să scrii o linie de cod, caută pe drupal.org. Chiar și când modulul contrib existent nu se potrivește 100%, configurarea sau extinderea lui printr-un adaptor subțire este aproape întotdeauna mai ieftină decât întreținerea unui modul paralel.
Acest articol este playbook-ul pe care îl aplicăm pe fiecare angajament Drupal la Softescu: când are sens un modul custom, cum să-l structurezi în Drupal 10/11 modern, cum să-l conectezi corect cu services, configurare, teste, și capcanele de performanță de evitat.
Când un modul custom este alegerea corectă — și când nu
Un modul custom este răspunsul corect când:
- Logica de business este cu adevărat specifică domeniului și nu aparține într-o soluție generică — calcul de primă de asigurare, raportare regulatorie, fluxuri industrie-specifice.
- Un modul contrib existent face alegeri arhitecturale fundamentale care intră în conflict cu cerințele proiectului.
- Ai nevoie de o integrare cu un sistem intern care nu va fi niciodată public.
Anti-pattern: un modul custom pentru "validare număr de telefon" când telephone plus field_validation acoperă deja problema. Am preluat proiecte în care trei generații de dezvoltatori au scris fiecare un modul custom nou pentru același caz de utilizare — niciunul testat. Caută înainte să scrii.
Structura unui modul în Drupal modern (10/11)
Un modul Drupal modern este stratificat, declarat în info.yml și conectat prin service container. Structura minimă:
my_module/
my_module.info.yml # metadate modul
my_module.module # hooks (subțire)
my_module.services.yml # definiții servicii
my_module.routing.yml # rute
my_module.permissions.yml # permisiuni
src/
Controller/ # route handlers
Service/ # logică de business
Plugin/ # plugins (Block, Field, ...)
Form/ # formulare
EventSubscriber/ # event listeners
config/
install/ # default-uri la instalare
schema/ # schema configurației
tests/
src/
Unit/ # PHPUnit
Kernel/ # Kernel tests
Functional/ # browser tests
Fișierul info.yml declară dependențele explicit:
name: 'My Module'
type: module
description: 'Domain-specific business logic for ...'
package: 'Custom'
core_version_requirement: ^10 || ^11
dependencies:
- drupal:node
- drupal:user
- paragraphs:paragraphs
Anti-pattern: îngrămădirea a tot ce există în fișierul .module. În 2026, fișierul .module este în principal un container pentru declarațiile de hook-uri. Logica de business aparține în services sub src/Service/.
Hooks, services, dependency injection — modul modern
Drupal 10/11 este un framework bazat pe Symfony. Hook-urile rămân punctul de contract între core și module, dar ar trebui să fie cât mai subțiri posibil și să delege imediat la un serviciu:
// my_module.module
function my_module_node_presave(NodeInterface $node) {
\Drupal::service('my_module.node_normalizer')->normalize($node);
}
Serviciul în sine este o clasă simplă cu dependențe injectate prin constructor:
# my_module.services.yml
services:
my_module.node_normalizer:
class: Drupal\my_module\Service\NodeNormalizer
arguments:
- '@entity_type.manager'
- '@logger.channel.my_module'
namespace Drupal\my_module\Service;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Psr\Log\LoggerInterface;
final class NodeNormalizer {
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly LoggerInterface $logger,
) {}
public function normalize(NodeInterface $node): void { ... }
}
Anti-pattern: apelarea \Drupal::service(...) în mijlocul logicii de business. Este pattern-ul service-locator — face testarea imposibilă și ascunde dependențele. Constructor injection este singura formă defensibilă în 2026.
Logger channels (logger.channel.my_module) trebuie declarate explicit ca services pentru ca drush watchdog:show --type=my_module să funcționeze.
Configuration management — config/install vs config/schema
Configuration Management din Drupal este una dintre cele mai subestimate puncte forte ale framework-ului — și cea mai comună sursă de probleme de migrare între medii. Două directoare contează:
config/install/ conține configurația default care este copiată în configurația activă o singură dată, la drush en my_module. Aceste fișiere nu mai sunt citite niciodată — sunt default-uri de instalare, nu o sursă continuă de adevăr.
config/schema/ conține schema (*.schema.yml) care descrie structura configurației. Fără schemă, Drupal nu poate serializa, valida sau exporta configurația prin Configuration API. Am moștenit proiecte unde configurația custom a rulat ani fără schemă și a cedat tăcut prima dată când cineva a rulat drush config:export.
# config/schema/my_module.schema.yml
my_module.settings:
type: config_object
label: 'My Module settings'
mapping:
api_endpoint:
type: string
label: 'API endpoint URL'
timeout:
type: integer
label: 'Timeout in seconds'
Anti-pattern: editarea config/install/ și așteptarea ca schimbarea să ajungă pe site-urile existente. Nu va ajunge — configurația existentă trebuie migrată prin hook_update_N.
Testare — PHPUnit, Kernel tests, Functional tests
Drupal are trei niveluri de teste:
- Unit tests (
tests/src/Unit/) — rapide, fără bootstrap Drupal. Pentru calcul de valori pure, validare, conversie de format. Mock pe toate serviciile Drupal. - Kernel tests (
tests/src/Kernel/) — rapide la nivel de secunde, pornesc kernel-ul Drupal fără stack-ul web. Pentru logică de servicii, operații pe entități, reacții de hook. - Functional tests (
tests/src/Functional/) — lente, stack HTTP complet. Doar pentru comportament de endpoint și fluxuri de browser.
Regulă: fiecare clasă nouă de serviciu primește un Kernel test care acoperă cele trei căi importante (happy path, edge case, error case). Rezervă Functional tests pentru flow-uri de submit și granițe de permisiuni.
namespace Drupal\Tests\my_module\Kernel;
use Drupal\KernelTests\KernelTestBase;
final class NodeNormalizerTest extends KernelTestBase {
protected static $modules = ['node', 'user', 'my_module'];
public function testNormalizeStripsTrailingWhitespace(): void {
$node = Node::create(['type' => 'page', 'title' => 'Test ']);
$this->container->get('my_module.node_normalizer')->normalize($node);
$this->assertSame('Test', $node->getTitle());
}
}
Anti-pattern: testarea doar a happy path-ului. Edge cases sunt exact ce cedează în producție la 3 dimineața.
Performanță și capcane comune
Trei probleme de performanță recurente în module custom pe care le vedem la clienții enterprise:
- Query-uri N+1 în
hook_entity_load— fiecare nod încărcat declanșează un query suplimentar. Un listing de 50 noduri devine 51+ query-uri. Soluție: batch-load cuentity_load_multipleși un cache per-request. - Rebuild la container la fiecare request — un
services.ymlprost configurat (de ex.factory:pe un singleton cu state) forțează Drupal să reconstruiască container-ul la fiecare request. Simptom: TTFB ~500 ms în loc de ~50 ms. Soluție:drush cache:rebuildși revizuirea compilării container-ului. - Invalidare cache lipsă — blocuri și controllere custom care randează date din entități fără să seteze
cacheTagsnu sunt invalidate la actualizări de conținut. Simptom: conținut învechit în cache-ul anonim. Soluție: fiecare render array primește#cache.tags,#cache.contextsși#cache.max-ageexplicite.
Anti-pattern: dezactivarea sistemului de cache ca să ocolești aceste probleme. Asta doar mută datoria de performanță, nu o rezolvă.
Când să-l urci ca modul contrib
Dacă modulul tău custom rezolvă aceeași problemă în mai multe proiecte, este candidat pentru upstreaming la drupal.org. Am adus în comunitatea contrib mai multe module Softescu de-a lungul anilor. Procesul merită pentru că forțează ce orice cod bun are deja: configurare explicită în loc de presupuneri hardcodate, teste pentru orice cale ne-trivială, hook-uri documentate pentru extindere.
Verifică înainte să faci upstreaming:
- Există deja un modul comparabil pe drupal.org? Dacă da, co-maintainership este de obicei mai bun decât un fork concurent.
- Configurația este suficient de abstractă încât să-i poți documenta schema?
- Toate namespace-urile de clase, ID-urile de servicii și cheile de configurare sunt suficient de generice încât să nu scape numele unui client?
Cum abordăm asta la Softescu
Pe parcursul ultimului deceniu, am construit module Drupal pentru platforme enterprise în asigurări, administrație publică și educație. Poziția noastră: modulele custom sunt o investiție costisitoare — scrie-le doar când logica de business este cu adevărat specifică domeniului, testează-le, și planifică upstreaming-ul când pattern-ul este reutilizabil. Articolul nostru despre enterprise Drupal support acoperă partea de mentenanță pe termen lung din aceeași ecuație.
Dacă echipa ta are nevoie de ajutor cu dezvoltarea de module Drupal — review-uri arhitecturale, dezvoltatori dedicați, sau un angajament Drupal enterprise complet — contactează-ne.