Vibe coding Enterprise PHP Applications

Vibe coding Enterprise PHP Applications

Everyone's talking about vibe coding — describing what you want in natural language and letting AI generate the code. Solo developers are shipping features in hours and Startups are prototyping before lunch.

But here's what nobody mentions at the demo stage: most vibe-coded applications are architectural disasters waiting to happen.

The AI produces code that runs. It does the thing. But under the surface:

  • Business logic tangles with infrastructure
  • There's no clear path to scaling
  • Error handling assumes sunny days forever
  • Testing is an afterthought — if it exists at all
  • One change breaks three unrelated features
AI learned from the internet, and most internet code is tutorial-grade: optimized for "make it work" rather than "make it last."

So vibe coders face an uncomfortable tradeoff: ship fast and pray, or slow down and learn architecture patterns that take years to master. But what if there's a third option? What if the framework itself guided you toward good design — whether you understood it or not?

Enterprise Patterns Without the Enterprise Learning Curve

Enterprise patterns like: CQRS, Event Sourcing, Sagas, Workflows Message-Driven architecture — exist because they solve real problems at scale:

  • Event Sourcing captures every change as immutable fact, enabling audits, replays, and debugging
  • Sagas and Workflows coordinate complex processes across multiple services without distributed transactions
  • Message-driven architecture decouples components so failures don't cascade

The traditional path to using these patterns:

  1. Read the books
  2. Try implementing them yourself
  3. Fail, learn, try again

That's 2-3 years and several failed attempts to reach competency in those areas.

Ecotone compresses this to a Composer install:

composer require ecotone/ecotone

The patterns are baked in. You don't implement CQRS — you use it. You don't build event sourcing infrastructure — you add an attribute. You don't build messaging integrations — you state what, and Ecotone handles how.

Architecture That Guides, Not Just Enables

Ecotone is a PHP framework built on a radical idea: enterprise patterns should be the default, not the exception.

Most frameworks give you tools and say "good luck." They don't guide you toward architecture that's scalable and fault-tolerant by design. Ecotone does — these qualities aren't features, they're the foundation.

Let's look at command handlers:

#[CommandHandler]
public function placeOrder(PlaceOrder $command): void
{
    // This method does ONE thing
}

This is one of Ecotone's key building blocks — the default way to expose business capabilities. But there's more than meets the eye. When you send a command like this, Ecotone under the hood:

  • It connects to the Messaging System — meaning it can now be wrapped with powerful features like database transactions, message deduplication, and retries, using a simple config switch or an extra attribute.
  • Every message automatically includes metadata: Message ID, Causation ID, and Correlation ID. This makes tracking and connecting to monitoring systems trivial.
  • Since execution now flows through messaging, switching to asynchronous is as simple as adding a single `#[Asynchronous]` attribute.

In Ecotone, there is no other way to expose business capabilities than through Messaging. All applications, modules, and classes communicate through Messages — and this matters enormously for vibe coding. The AI isn't fighting against bad habits in the training data. It's guided by a framework that makes the right path the easy path.

Vibe your way to Enterprise-Grade

Let's now take a look on example of Order Processing, and some key differences, based on what AI can generate for us.

Typical vibe-coded version (fragile):

class OrderController
{
    public function placeOrder(Request $request)
    {
        $order = new Order($request->get('products'));
        $this->orderRepository->save($order);
        
        // Everything jammed together
        $this->mailer->send($order->customerEmail(), 'Order confirmed!');
        $this->inventory->decrease($order->products());
        
        return new Response('Order placed!');
    }
}

Problems:

  • If the Invetory API is slow, the customer waits
  • If email fails, the whole order fails
  • Testing requires mocking everything
  • Changing one thing risks breaking others

Ecotone version (enterprise-grade):

class OrderService
{
    #[CommandHandler]
    public function placeOrder(PlaceOrder $command): void
    {
        $order = new Order($command->orderId, $command->products);
        $this->orderRepository->save($order);
        
        $this->eventBus->publish(new OrderWasPlaced(
            $order->id(),
            $order->customerEmail(),
            $order->products()
        ));
    }
}

class NotificationService
{
    #[Asynchronous('notifications')]
    #[EventHandler]
    public function onOrderPlaced(OrderWasPlaced $event): void
    {
        $this->mailer->send($event->customerEmail, 'Order confirmed!');
    }
}

class InventoryService
{
    #[Asynchronous('inventory')]
    #[EventHandler]
    public function onOrderPlaced(OrderWasPlaced $event): void
    {
        foreach ($event->products as $product) {
            $this->inventory->decrease($product->id, $product->quantity);
        }
    }
}

What the Ecotone version gives you:

  • Immediate response — customer doesn't wait for email servers
  • Failure isolation — Inventory outage doesn't break orders
  • Independent scaling — high notification volume? Add notification workers
  • Focused testing — test OrderService without touching NotificationService
  • Self-documenting — the attributes tell you what's async, what handles what

And here's the key insight: the AI can generate both versions equally easily. But only one of them will survive contact with production.

Ensuring your Application works

Vibe coding without tests is like driving blindfolded.
The AI generates code. Does it work? You run it manually. It breaks.
You describe the error. The AI "fixes" it — often introducing new bugs.
You're now in a hallucination feedback loop.

Tests break this cycle.

When you have focused, fast tests, the AI gets immediate, precise feedback. Not "something's wrong somewhere" but "this specific assertion failed because this specific behavior changed."

However most testing approaches are terrible for vibe coding:

  • Integration tests require setting up databases, queues, external services — massive friction
  • Async testing means running actual workers, waiting, checking results — slow and flaky
  • Tightly coupled code means testing one thing requires setting up ten unrelated things

Ecotone solves all of this:

Testing Isolation

Notice what's happening:

public function test_order_placement_sends_notification(): void
{
    $messagingSystem = EcotoneLite::bootstrapFlowTesting([
        OrderService::class,
    ]);
    
    $messagingSystem->sendCommand(new PlaceOrder(
        orderId: '123',
        products: ['widget']
    ));
    
    $recordedEvents = $messagingSystem->getRecordedEvents();
    
    $this->assertCount(1, $recordedEvents);
    $this->assertInstanceOf(OrderWasPlaced::class, $recordedEvents[0]);
}
  1. Choose exactly which classes to testOrderService or more depending on scenario
  2. No meaningless side effects — you're not setting up state for things you're not testing thanks to isolation
  3. Testing Input and Outputs - We sending Command, expecting Events, logic is encapsulated inside. Those kind of tests survive changes and refactors.

This is surgical testing. The AI can generate a test, run it, see exactly what failed, and fix precisely that thing.

Testing Async Flows Synchronously

Ecotone lets you test asynchronous code and switch the components easily

public function test_order_placement_sends_notification(): void
{
    $mailer = new StubMailer();
    $messagingSystem = EcotoneLite::bootstrapFlowTesting(
      [
          OrderService::class,
          NotificationHandler::class,
      ], 
      [
        new NotificationHandler(),
        Mailer::class => $mailer
      ]
      enableAsynchronousProcessing: true
    );
    
    $messagingSystem->sendCommand(new PlaceOrder(
        orderId: '123',
        products: ['widget']
    ));

    $this->assertCount(0, $mailer->countMails());

    $messagingSystem->run('async');
    
    $this->assertCount(1, $mailer->countMails());
}

Notice what's happening:

  1. No queue workers — async handlers execute synchronously in the test
  2. Easily replace actors — We simply switched for this particular test case Mailer implementation
  3. Synchronous testing— We are not in need to run Workers in other processes, we simply run the Worker from within the test

This is crucial for reliable testing that gives the output directly to the AI tool we use, as we don't run any external workers. Whatever fails - fails within this process, giving immediate feedback to the AI Model.

Vibe coding loves this. When the AI generates code and immediately runs synchronous tests that complete in milliseconds, it can iterate rapidly toward correct behavior without spiraling into hallucinated "fixes."

The Token Efficiency Advantage

Here's something that individual vibe coders care about deeply: AI tokens cost money and time. Every line of boilerplate the AI generates is:

  • Tokens spent on infrastructure code instead of business logic
  • More surface area for bugs
  • More code to understand when things break
  • Longer feedback loops

Without Ecotone — generating a message queue setup:

// AI generates 50-100 lines of:
// - Queue connection configuration
// - Message serialization
// - Worker process management
// - Retry logic
// - Error logging
// ... and probably gets several things wrong

With Ecotone — same functionality:

#[Asynchronous('orders')]
#[CommandHandler]
public function placeOrder(PlaceOrder $command): void
{
    // Just business logic
}

One attribute - the infrastructure is handled. Ecotone will provide Worker Process to consume Messages, will set up Message Channel in the Broker, do deserialization and serialization, wiring if needed. This means that it's indeed single attribute, that handles all the complexity for you.

This means:

  • Fewer tokens burned on boilerplate
  • Less code to generate means fewer opportunities for hallucination
  • Faster feedback loops because tests are simpler
  • No "guide the AI" dance toward enterprise patterns — they're the default

When you're paying per token and waiting for responses, this efficiency compounds dramatically over a development session.

All the infrastructure set up for you

Let's take a look on one more example, using Ecotone's Event Sourcing.

#[EventSourcingAggregate]
class Wallet
{
    #[Identifier]
    private string $walletId;
    private int $balance = 0;

    #[CommandHandler]
    public static function create(CreateWallet $command): array
    {
        return [new WalletWasCreated($command->walletId)];
    }

    #[CommandHandler]
    public function deposit(DepositMoney $command): array
    {
        return [new MoneyWasDeposited($this->walletId, $command->amount)];
    }

    #[CommandHandler]
    public function withdraw(WithdrawMoney $command): array
    {
        if ($command->amount > $this->balance) {
            throw new InsufficientFunds();
        }
        
        return [new MoneyWasWithdrawn($this->walletId, $command->amount)];
    }

    #[EventSourcingHandler]
    public function applyCreated(WalletWasCreated $event): void
    {
        $this->walletId = $event->walletId;
    }

    #[EventSourcingHandler]
    public function applyDeposit(MoneyWasDeposited $event): void
    {
        $this->balance += $event->amount;
    }

    #[EventSourcingHandler]
    public function applyWithdraw(MoneyWasWithdrawn $event): void
    {
        $this->balance -= $event->amount;
    }
}

That's a complete audit trail of every financial transaction. And it takes zero infrastructure work, as Ecotone will bind this Aggregate to Event Stream, set up Event Streams in Database and serialize and deserialize Events for us.
Meaning we can fully focus on the flow we vibe code, and the rest will be done for us.

And I just have to show, how easy it's for AI Models to generate tests for this:

public function test_wallet_tracks_balance(): void
{
    $ecotone = EcotoneLite::bootstrapFlowTesting([Wallet::class]);
    
    $ecotone->sendCommand(new CreateWallet('wallet-1'));
    $ecotone->sendCommand(new DepositMoney('wallet-1', 100));
    $ecotone->sendCommand(new DepositMoney('wallet-1', 50));
    $ecotone->sendCommand(new WithdrawMoney('wallet-1', 30));
    
    $events = $ecotone->getRecordedEvents();
    
    $this->assertCount(4, $events);
    $this->assertInstanceOf(WalletWasCreated::class, $events[0]);
    $this->assertInstanceOf(MoneyWasDeposited::class, $events[1]);
    $this->assertInstanceOf(MoneyWasDeposited::class, $events[2]);
    $this->assertInstanceOf(MoneyWasWithdrawn::class, $events[3]);
}

Why Ecotone + AI Works

Several factors make Ecotone unusually suited for vibe coding:

1. Five Years of Stable API

Ecotone has maintained same API for user-land features since it was open sourced — meaning for five years. Ecotone due to it's design based on declarative configuration have allowed userland applications to grow decoupled from the framework and vice versa.

AI models learned from historical code. Frameworks that break APIs between versions create a problem: the AI learned the old way, but the old way does not work anymore.

2. Enterprise Examples as Training Data

Most PHP examples online demonstrate basic CRUD. The AI learned from those, so it generates more of the same.

Ecotone's documentation, blog posts, examples has always demonstrated enterprise patterns. When AI generates Ecotone code, it reaches for CQRS and event-driven patterns because that's what Ecotone examples look like.

The training data is inherently higher quality.

3. Constraints as Guardrails

Ecotone's design philosophy guides toward good architecture through constraints:

  • Command is handled exactly one Handler — single-purpose by design
  • Events are published, not returned — proper decoupling
  • Async is declared, not implemented — consistent patterns

The AI can't easily generate antipatterns because the framework makes them awkward.

Summary

Previously, there was a hard line between "startup code" and "enterprise code." Startups moved fast with fragile architectures. Enterprises moved slow with robust architectures. You picked your tradeoff.

Ecotone blurs this line. Enterprise patterns become a Composer install. And vibe coding lets anyone — regardless of architectural expertise — generate code that uses those patterns correctly.

A solo developer with an AI assistant can now generate applications more robust and scalable than what many teams craft by hand over months.

Now you can build systems like the big companies do. Without their resources. Without even understanding how — at first.

The architecture is in the framework. The tests prove it works. The AI does the typing.

That's not cheating. That's evening the odds.