How to Build Reusable, Enterprise-Grade PHP Systems in 2026
Drupal, Symfony, and Laravel all live in the same PHP ecosystem — yet developing a Drupal module feels very different from building a Symfony or Laravel application.
This article explains:
- how Drupal module development really compares to Symfony and Laravel
- why “Drupal is built on Symfony” is only partly true
- how to structure Drupal modules using Symfony-style architecture
- how to share business logic between Drupal, Symfony, and Laravel projects
This is not a beginner guide. It’s written for developers who care about architecture, maintainability, and long-term reuse.
1. CMS vs Framework: the Fundamental Difference
The most important distinction is this:
Drupal is a platform. Symfony and Laravel are frameworks.
Drupal
- Provides content types, fields, permissions, workflows, multilingual support
- Enforces its own lifecycle, APIs, and architectural constraints
- Your code runs inside Drupal
Symfony / Laravel
- Provide tools, not solutions
- You control the application lifecycle
- Architecture is your responsibility
This difference impacts everything: structure, testing, reuse, and scalability.
2. Architecture Comparison
| Aspect | Drupal Module | Symfony App | Laravel App |
|---|---|---|---|
| Control over lifecycle | ❌ Limited | ✅ Full | ⚠️ Partial |
| Business logic isolation | ❌ Difficult | ✅ Natural | ⚠️ Mixed |
| Reusability | ❌ Low | ✅ High | ⚠️ Medium |
| CMS features | ✅ Built-in | ❌ None | ❌ None |
Drupal is excellent when content is the product.
Symfony excels when business logic is the product.
3. The Myth: “Drupal Is Just Symfony”
Yes, Drupal uses Symfony components:
- HttpKernel
- Routing
- Dependency Injection container
But:
- Controllers are not pure HTTP controllers
- Dependency Injection is constrained by Drupal’s container
- Entity API replaces Doctrine
- Hooks and plugins bypass object-oriented boundaries
In practice:
A Drupal module is not a Symfony application.
4. Typical Drupal Anti-Patterns
Business logic inside hooks
function my_module_entity_presave(EntityInterface $entity) {
if ($entity->bundle() === 'product') {
$price = $entity->get('price')->value * 1.2;
$entity->set('price_with_tax', $price);
}
}
Problems:
- Impossible to test
- Impossible to reuse
- Tightly coupled to Drupal APIs
5. Symfony-Style Thinking Applied to Drupal
The solution is separation of concerns.
Rule of thumb
Drupal should handle delivery and integration
Symfony-style code should handle business logic
6. Target Architecture
packages/
product-core/ ← reusable PHP / Symfony-style library
src/
Domain/
Service/
DTO/
web/
modules/custom/
product_ui/ ← Drupal module
src/
Controller/
Infrastructure/
Responsibilities
| Layer | Responsibility |
|---|---|
| Domain | Business rules |
| Services | Use cases |
| Infrastructure | Drupal adapters |
| Controllers | HTTP glue |
7. Symfony-Style Core Package
composer.json
{
"name": "my-company/product-core",
"type": "library",
"autoload": {
"psr-4": {
"MyCompany\\ProductCore\\": "src/"
}
},
"require": {
"php": "^8.1"
}
}
Domain Model (Pure PHP)
namespace MyCompany\ProductCore\Domain;
final class Product {
public function __construct(
private int $id,
private float $price
) {}
public function price(): float {
return $this->price;
}
}
No Drupal. No Symfony. No database.
Business Service
namespace MyCompany\ProductCore\Service;
final class PriceCalculator {
public function calculate(float $price): float {
return round($price * 1.2, 2);
}
}
Application Service
final class ProductService {
public function __construct(
private PriceCalculator $calculator
) {}
public function priceWithTax(Product $product): float {
return $this->calculator->calculate($product->price());
}
}
This code can run:
- in Symfony
- in Laravel
- in CLI
- in PHPUnit
8. Adapting the Core Package in Drupal
Install the package
composer require my-company/product-core
Drupal now becomes a consumer of your business logic.
Infrastructure Adapter (Node → Domain)
use Drupal\node\Entity\Node;
use MyCompany\ProductCore\Domain\Product;
final class DrupalProductRepository {
public function fromNode(Node $node): Product {
return new Product(
id: (int) $node->id(),
price: (float) $node->get('price')->value
);
}
}
Drupal APIs stop here.
Facade Service (Glue Layer)
final class DrupalProductFacade {
public function __construct(
private ProductService $coreService,
private DrupalProductRepository $repository
) {}
public function priceWithTax(Node $node): float {
$product = $this->repository->fromNode($node);
return $this->coreService->priceWithTax($product);
}
}
services.yml
services:
product_ui.repository:
class: Drupal\product_ui\Infrastructure\DrupalProductRepository
product_ui.core_calculator:
class: MyCompany\ProductCore\Service\PriceCalculator
product_ui.core_service:
class: MyCompany\ProductCore\Service\ProductService
arguments:
- '@product_ui.core_calculator'
product_ui.facade:
class: Drupal\product_ui\DrupalProductFacade
arguments:
- '@product_ui.core_service'
- '@product_ui.repository'
Drupal simply wires dependencies.
9. Controller = Thin Adapter
final class ProductController extends ControllerBase {
public function __construct(
private DrupalProductFacade $facade
) {}
public function view(Node $node): array {
return [
'#markup' => $this->facade->priceWithTax($node),
];
}
}
No calculations. No rules. Only orchestration.
10. Comparison with Laravel
Laravel encourages rapid development:
Route::get('/product/{product}', fn(Product $p) => $p->price * 1.2);
This is great for MVPs, but:
- business logic leaks into controllers
- reuse becomes difficult
- architecture degrades over time
Symfony-style separation avoids this problem — and works equally well inside Drupal.
11. When This Approach Makes Sense
✅ Enterprise Drupal projects
✅ Long-living platforms
✅ Multiple applications sharing logic
✅ Gradual migration from Drupal to Symfony API
❌ Small sites
❌ One-off landing pages
❌ Simple CRUD forms
12. Key Takeaway
Drupal should be your delivery layer, not your business layer.
By extracting logic into Symfony-style packages:
- you gain testability
- you gain reuse
- you future-proof your system
Drupal becomes what it does best:
content management and UI orchestration.
Final Thought
If your Drupal module cannot be reused outside Drupal,
you are building Drupal logic, not business logic.
And business logic should never belong to a CMS.
Comments