Implementing a Real-World Feature Across Drupal, Symfony, and Laravel
Feature: E-Commerce “Discount Code Validator”
Requirement:
- When a user applies a discount code during checkout:
- Validate the code
- Check expiration and usage limits
- Calculate discount
- Return an updated order total
- Must be extensible for future promotion types
This feature is small but touches services, events, plugins, and DI—perfect for comparing modular approaches.
1. Drupal: Module + Plugin + Service
1.1 Module Structure
discount_code/
├── discount_code.info.yml
├── discount_code.services.yml
└── src/
├── Plugin/
│ └── DiscountValidator/
│ └── BaseValidator.php
│ └── PercentageValidator.php
└── Service/
└── DiscountService.php
1.2 Base Plugin (Interface)
namespace Drupal\discount_code\Plugin\DiscountValidator;
use Drupal\commerce_order\Entity\OrderInterface;
abstract class BaseValidator {
abstract public function validate(OrderInterface $order, string $code): bool;
}
1.3 Custom Plugin
namespace Drupal\discount_code\Plugin\DiscountValidator;
use Drupal\commerce_order\Entity\OrderInterface;
class PercentageValidator extends BaseValidator {
public function validate(OrderInterface $order, string $code): bool {
// Example: 10% off if code = 'NEWYEAR10'
return $code === 'NEWYEAR10';
}
}
1.4 Service
namespace Drupal\discount_code\Service;
use Drupal\discount_code\Plugin\DiscountValidator\BaseValidator;
class DiscountService {
protected array $validators;
public function __construct(iterable $validators) {
$this->validators = $validators;
}
public function applyDiscount($order, $code) {
foreach ($this->validators as $validator) {
if ($validator->validate($order, $code)) {
// apply discount logic
return true;
}
}
return false;
}
}
Key Takeaways (Drupal)
- Plugin API allows adding new validators easily
- Service centralizes logic
- Runtime discovery + DI makes it flexible
- UI can expose configurable discount rules
2. Symfony: Service + Event
Symfony doesn’t have a plugin system, so we use services + events.
2.1 Validator Interface
namespace App\Service\Discount;
use App\Entity\Order;
interface DiscountValidatorInterface {
public function validate(Order $order, string $code): bool;
}
2.2 Concrete Validator
namespace App\Service\Discount;
use App\Entity\Order;
class PercentageValidator implements DiscountValidatorInterface {
public function validate(Order $order, string $code): bool {
return $code === 'NEWYEAR10';
}
}
2.3 Discount Service
class DiscountService {
public function __construct(private iterable $validators) {}
public function applyDiscount(Order $order, string $code): bool {
foreach ($this->validators as $validator) {
if ($validator->validate($order, $code)) {
return true;
}
}
return false;
}
}
2.4 Event Integration
namespace App\EventListener;
use App\Event\OrderApplyDiscountEvent;
class DiscountListener {
public function __construct(private DiscountService $service) {}
public function onDiscount(OrderApplyDiscountEvent $event) {
$this->service->applyDiscount($event->getOrder(), $event->getCode());
}
}
Key Takeaways (Symfony)
- Services and interfaces handle modularity
- Adding a new validator requires manual registration
- DI + events = decoupled architecture
3. Laravel: Package + Service Provider + Events
Laravel uses service providers and packages.
3.1 Validator Interface
namespace App\Services\Discount;
interface DiscountValidator {
public function validate($order, string $code): bool;
}
3.2 Concrete Validator
class PercentageValidator implements DiscountValidator {
public function validate($order, string $code): bool {
return $code === 'NEWYEAR10';
}
}
3.3 Service
class DiscountService {
public function __construct(private array $validators) {}
public function applyDiscount($order, $code) {
foreach ($this->validators as $validator) {
if ($validator->validate($order, $code)) {
return true;
}
}
return false;
}
}
3.4 Service Provider
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\Discount\DiscountService;
use App\Services\Discount\PercentageValidator;
class DiscountServiceProvider extends ServiceProvider {
public function register() {
$this->app->singleton(DiscountService::class, function ($app) {
return new DiscountService([
new PercentageValidator(),
]);
});
}
public function boot() {
// Optional: register events
}
}
Key Takeaways (Laravel)
- Service Provider bootstraps the service and validators
- Easy to extend with new validators
- DI + automatic resolution = minimal boilerplate
- Manual registration, unlike Drupal’s runtime discovery
4. Side-by-Side Summary
| Aspect | Drupal | Symfony | Laravel |
|---|---|---|---|
| Validator extensibility | Runtime via Plugin API | Compile-time service registration | Manual via ServiceProvider |
| DI | Yes | Yes | Yes |
| Runtime discoverable | ✅ Yes | ❌ No | ⚠ Only via provider |
| UI integration | ✅ Optional | ❌ | ❌ |
| Adding a new validator | Drop new plugin | Add new service + configure | Add validator + update provider |
| Boilerplate | Higher | Medium | Low |
5. Architectural Lessons
- Drupal: Best for pluggable, CMS-integrated, runtime discoverable features.
- Symfony: Best for clean architecture, explicit contracts, and decoupled services.
- Laravel: Best for developer speed, packages, and simple DI.
Conclusion:
This single feature shows why Drupal feels “heavier” but more flexible, Symfony is strict but robust, and Laravel is fast but relies on conventions.
Comments