Skip to main content
Home
Drupal life hacks

Main navigation

  • Drupal
  • React
  • WP
  • Contact
  • About
User account menu
  • Log in

Breadcrumb

  1. Home

How to Build Reusable, Enterprise-Grade PHP Systems in 2026

By admin, 31 December, 2025
How to Build Reusable, Enterprise-Grade PHP Systems in 2026

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

AspectDrupal ModuleSymfony AppLaravel 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

LayerResponsibility
DomainBusiness rules
ServicesUse cases
InfrastructureDrupal adapters
ControllersHTTP 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.

Tags

  • Drupal
  • Symfony
  • Laravel
  • PHP architecture
  • Drupal Module Development
  • Symfony Components
  • Laravel Framework
  • Clean Architecture
  • Domain-Driven Design
  • Dependency Injection
  • Reusable Code
  • Enterprise PHP
  • CMS vs Framework
  • software architecture
  • Backend Development

Comments

About text formats

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Powered by Drupal