One Feature, Three Approaches: Drupal Module vs Symfony vs Laravel
Feature Description
We will implement the same simple feature in all three systems:
User Registration Logger
When a new user registers, the system logs:
- user ID
- registration time
Why this feature?
Because it:
- touches core lifecycle events
- requires extensibility
- is implemented without modifying core code
- exists in real-world projects
Perfect example to compare modularity.
1. Drupal: Module + Event Subscriber
1.1 Module structure
user_register_logger/
├── user_register_logger.info.yml
├── user_register_logger.services.yml
└── src/
└── EventSubscriber/
└── UserRegisterSubscriber.php
1.2 Module definition
user_register_logger.info.yml
name: User Register Logger
type: module
description: Logs user registrations
core_version_requirement: ^10 || ^11
package: Custom
dependencies:
- user
Drupal module is:
- discoverable
- enable/disable via UI
- dependency-aware
1.3 Service registration
user_register_logger.services.yml
services:
user_register_logger.subscriber:
class: Drupal\user_register_logger\EventSubscriber\UserRegisterSubscriber
arguments: ['@logger.channel.user']
tags:
- { name: event_subscriber }
1.4 Event Subscriber
namespace Drupal\user_register_logger\EventSubscriber;
use Drupal\user\Event\UserEvents;
use Drupal\user\Event\UserCreateEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class UserRegisterSubscriber implements EventSubscriberInterface {
public function __construct(
private LoggerInterface $logger
) {}
public static function getSubscribedEvents(): array {
return [
UserEvents::USER_CREATE => 'onUserCreate',
];
}
public function onUserCreate(UserCreateEvent $event): void {
$user = $event->getUser();
$this->logger->info('New user registered: {uid} ({mail})', [
'uid' => $user->id(),
'mail' => $user->getEmail(),
]);
}
}
✅ What this shows
- Feature is a self-contained module
- No core modification
- Uses Symfony EventDispatcher
- Integrated with Drupal logging & permissions
Drupal modularity = feature-level plugins
2. Symfony: Event Listener + Service
Symfony does not have “modules” — only application code + services.
2.1 Event Listener
namespace App\EventListener;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Psr\Log\LoggerInterface;
class UserRegisterListener {
public function __construct(
private LoggerInterface $logger
) {}
public function onLogin(InteractiveLoginEvent $event): void {
$user = $event->getAuthenticationToken()->getUser();
$this->logger->info('User registered/logged in', [
'user' => $user->getUserIdentifier(),
]);
}
}
2.2 Service configuration
services:
App\EventListener\UserRegisterListener:
tags:
- { name: kernel.event_listener, event: security.interactive_login }
✅ What this shows
- Feature is code-level, not system-level
- No runtime enable/disable
- No UI awareness
- Modularity achieved via:
- DI
- Events
- Contracts
Symfony modularity = architectural modularity
3. Laravel: Package + Service Provider
Laravel prefers packages for feature isolation.
3.1 Event Listener
namespace App\Listeners;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Log;
class LogUserRegistration {
public function handle(Registered $event): void {
$user = $event->user;
Log::info('New user registered', [
'id' => $user->id,
'email' => $user->email,
]);
}
}
3.2 Event registration
EventServiceProvider
protected $listen = [
Registered::class => [
LogUserRegistration::class,
],
];
3.3 Package-style modularity (conceptually)
In real projects this logic usually lives in a Composer package with:
- ServiceProvider
- Config
- Events
- Commands
class UserRegisterLoggerServiceProvider extends ServiceProvider {
public function boot() {
//
}
}
✅ What this shows
- Feature modularity via packages
- Clear lifecycle hooks
- Strong developer experience
- Less strict boundaries than Drupal
Laravel modularity = feature packages
4. Direct Comparison
| Aspect | Drupal | Symfony | Laravel |
|---|---|---|---|
| Feature isolation | Module | Service | Package |
| Runtime enable/disable | ✅ Yes | ❌ No | ⚠ Composer |
| Entry point | .info.yml | services.yaml | ServiceProvider |
| Events | Symfony-based | Native | Native |
| DI strictness | High | Very high | Medium |
| CMS integration | Full | None | None |
5. Architectural Insight
The same feature shows three philosophies:
Drupal
“Features must be pluggable by non-developers.”
Symfony
“Architecture must be clean and explicit.”
Laravel
“Developer productivity comes first.”
Drupal sits on top of Symfony, but adds:
- UI
- configuration
- permissions
- content modeling
- plugin discovery
Final Takeaway
If you can write this feature cleanly in all three systems, you truly understand modern PHP.
Symfony teaches structure
Laravel teaches flow
Drupal teaches extensibility
Comments