DEV Community

Dariusz Gafka
Dariusz Gafka

Posted on

Your Legacy PHP Codebase Isn't Hopeless

You ship a small bug fix. Suddenly, two other features break. Every deployment feels like gambling. The business depends on this app — it brings in revenue, customers use it daily — but nobody feels confident working on it.

You open a file that should be a simple list of functions and find a 2,000‑line monolith of nested loops and if‑statements. Comments like // Temporary fix from years before. Presentation, database queries, and business logic all mixed together in what one developer described as a “glorious spaghetti mashup.”

You're not alone. And your codebase isn't hopeless.


The Industry’s Dirty Secret

  • ~13% of Composer installs still run end‑of‑life PHP versions
  • ~27% are on EOL or security‑only versions
  • Over 50% of popular PHP packages support unsupported PHP versions

Legacy systems are the norm, not the exception.


The Trap: Rewrite or Suffer

  • Rewrites have ~23% success rate
  • Incremental modernization has ~53% success rate
  • Small changes fail only ~4% of the time

Rewrites discard years of encoded business knowledge.


The Third Path: Incremental Transformation

Instead of rewriting everything:

  1. Identify a painful area
  2. Extract logic into a message handler
  3. Test it in isolation
  4. Make it async if needed
  5. Repeat

This is exactly what Ecotone enables.


Example: The 800‑Line Controller

class OrderController
{
    public function placeOrder(Request $request)
    {
        // validation, persistence, payments, emails, analytics...
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 1: Extract an Event

class OrderWasPlaced
{
    public function __construct(
        public string $orderId,
        public string $customerId,
        public float $total
    ) {}
}
Enter fullscreen mode Exit fullscreen mode
class OrderNotificationHandler
{
    #[EventHandler]
    public function sendConfirmation(OrderWasPlaced $event): void {}
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Publish From Legacy Code

$this->eventBus->publish(new OrderWasPlaced(
    $order->id,
    $customer->id,
    $order->total
));
Enter fullscreen mode Exit fullscreen mode

Step 3: Test in Isolation

class OrderNotificationHandlerTest extends TestCase {}
Enter fullscreen mode Exit fullscreen mode

Step 4: Make It Async

#[Asynchronous('notifications')]
#[EventHandler]
public function sendConfirmation(OrderWasPlaced $event): void {}
Enter fullscreen mode Exit fullscreen mode

Step 5: Repeat

Analytics, loyalty points, reporting — each becomes isolated and testable.


Command Handlers

class PlaceOrder {}
class OrderHandler {}
Enter fullscreen mode Exit fullscreen mode
$this->commandBus->send(new PlaceOrder(...));
Enter fullscreen mode Exit fullscreen mode

Database Queries

interface OrderQueries
{
    #[DbalQuery(...)]
    public function getPendingOrders(string $customerId): array;
}
Enter fullscreen mode Exit fullscreen mode

Resilience & Retries

Retries, DLQs, and error handling are declarative and consistent.


Idempotency

#[Deduplicated('orderId')]
#[CommandHandler]
public function placeOrder(PlaceOrder $command): string {}
Enter fullscreen mode Exit fullscreen mode

Why This Works

  • System keeps running
  • Small PRs
  • Fast tests
  • Compounding improvements
  • Attracts senior engineers

Getting Started

composer require ecotone/ecotone
Enter fullscreen mode Exit fullscreen mode

Laravel and Symfony integrations available.


Conclusion

Legacy codebases are not failures — they are assets.
Incremental modernization gives them a future.

Top comments (0)