Vortos
Messaging

InMemory Driver

A fully functional in-memory transport for local development and testing — no Kafka required.

InMemory Driver

The InMemory driver is a complete implementation of the messaging interfaces (ProducerInterface, ConsumerInterface) that stores messages in PHP memory instead of Kafka. It is designed for local development and testing environments where running a Kafka broker is unnecessary or undesirable.

How It Works

InMemoryBroker is a singleton service that holds an in-memory queue keyed by transport name. InMemoryProducer enqueues messages into it. InMemoryConsumer dequeues and processes them.

InMemoryProducer::produce('user.events', $event)
    └── serializes event
    └── InMemoryBroker::enqueue('user.events', $receivedMessage)

InMemoryBroker (shared singleton)
    └── ['user.events'] => [ReceivedMessage, ReceivedMessage, ...]

InMemoryConsumer::consume('user.events', $handler)
    └── InMemoryBroker::dequeue('user.events')
    └── $handler($receivedMessage)

// for assertions in tests:
InMemoryBroker::all('user.events') → all messages without consuming
InMemoryBroker::count('user.events') → message count
InMemoryBroker::reset() → clear all queues

Enabling InMemory Driver

The driver is configured via the env-based config override system. Create config/test/messaging.php:

<?php
use Vortos\Messaging\DependencyInjection\VortosMessagingConfig;
use Vortos\Messaging\Driver\InMemory\Runtime\InMemoryProducer;
use Vortos\Messaging\Driver\InMemory\Runtime\InMemoryConsumer;

return static function(VortosMessagingConfig $config): void {
    $config->driver()
        ->producer(InMemoryProducer::class)
        ->consumer(InMemoryConsumer::class);
};

Then set APP_ENV=test in your .env. When the container compiles, MessagingExtension reads config/test/messaging.php and binds ProducerInterface → InMemoryProducer and ConsumerInterface → InMemoryConsumer instead of their Kafka counterparts.

No Other Changes Required

Your application code stays identical. Everything that injects EventBusInterface, ProducerInterface, or ConsumerInterface automatically uses the InMemory implementation when APP_ENV=test. No mocking, no conditionals.

Using InMemoryBroker Directly

In tests or diagnostic controllers, inject InMemoryBroker to inspect or assert on messages:

use Vortos\Messaging\Driver\InMemory\Runtime\InMemoryBroker;

final class TestController
{
    public function __construct(
        private EventBusInterface $eventBus,
        private InMemoryBroker $broker,
    ) {}

    public function __invoke(): JsonResponse
    {
        $this->eventBus->dispatch(new UserCreatedEvent(
            id: new UuidV7(),
            name: 'test',
            email: 'test@example.com',
        ));

        return new JsonResponse([
            'messages_in_broker' => $this->broker->count('user.events'),
            'payload'            => $this->broker->all('user.events')[0]->payload ?? null,
        ]);
    }
}

Differences from Kafka Driver

FeatureKafkaInMemory
PersistenceDurable, survives restartsLost on process exit
Delivery guaranteeAt-least-onceIn-process only
Offset trackingFull Kafka consumer groupNot applicable
Multiple consumersPartition-distributedSingle process
Outbox supportFull (via vortos_outbox)Bypassed — direct enqueue
SASL/SSLConfigurableNot applicable

Outbox Behavior

With outbox(true) on your producer and APP_ENV=test, the outbox write still goes to the database — only the Kafka produce step is replaced by InMemory enqueue. To bypass the outbox entirely in tests, set outbox(false) on your producer definition.

Resetting Between Tests

Call InMemoryBroker::reset() in your test tearDown() to clear all queued messages:

protected function tearDown(): void
{
    $this->getContainer()->get(InMemoryBroker::class)->reset();
}

Without resetting, messages from one test bleed into the next.

Environment Config Files

The full config loading order when APP_ENV=test:

config/messaging.php          ← base config, loaded first
config/test/messaging.php     ← test overrides, loaded second, wins on conflict

Only the keys you set in the override file change — everything else from the base config remains. This means you can override just the driver and keep all your transport/producer/consumer definitions identical.

On this page