rossel/rossel-kafka
rossel/rossel-kafka provides Kafka integration for Laravel and PHP applications, helping you configure producers and consumers, publish messages to topics, and process Kafka events using familiar Laravel conventions. Useful for event streaming and service-to-service messaging.
0. Know what you're actually adopting: this is a Symfony bundle, not a Laravel package.
The 0.7.0 release makes it explicit — the composer type is now symfony-bundle, the extension class was removed in favour of AbstractBundle, config lives in Symfony's config/packages/ world, handlers are registered via the rossel_kafka.consumer DI tag, and the worker is a Symfony console command (rossel:kafka:listen). There is no service provider, no facade, no vendor:publish tag, and no artisan command. Treat the previous "it's a small Laravel package" framing as wrong and budget for glue code: you will bind the bundle's services into Laravel's container yourself and wrap the listener in an artisan command. Read UPGRADE-0.7.md first — it is now the most important file in the repo.
1. Native requirements first
composer require rossel/rossel-kafka:0.7.0
You still need ext-rdkafka and librdkafka. A missing extension surfaces as Class "RdKafka\..." not found at first send(), not at install time — and 0.7.0 made both the connection and the producer lazy, so it surfaces even later than before. In Docker:
RUN pecl install rdkafka && docker-php-ext-enable rdkafka
2. Port the config into Laravel yourself
There is no publish command. Create config/rossel_kafka.php mirroring the bundle's structure and feed it from .env. Two things changed in 0.7.0 worth designing around:
broker and producer sections are now required. Previously a missing section blew up with "Undefined array key" while building the container; now it's an explicit requirement. Validate both at boot rather than discovering it in production.PLAIN when sasl_mechanism is unset. Before, librdkafka defaulted to GSSAPI and failed loudly. This is a silent behaviour change — if your cluster uses SCRAM-SHA-512, set it explicitly or you'll get a confusing auth failure that looks like a credential problem.3. Decide what to do about the listener
rossel:kafka:listen is a Symfony console command; it won't appear in php artisan list. Either register the bundle's console command on Laravel's kernel (check whether the class is a plain Command you can $this->commands([...]), or whether it depends on Symfony's console event dispatcher) or write a thin artisan command that resolves the consumer service from the container and calls it in a loop. Prove this before anything else — it's the single largest unknown in the integration.
4. First use case: one message out, one message back
Resolve MessageFactory from the container, build a message with MessageType, and send() it. Then run your listener wrapper against the same topic and log the body. Only after that round trip works should you touch application code.
Where to look first
UPGRADE-0.7.md — mandatory; the breaking-changes list is longer than the release-note summary.README.md — check which version it documents; a 0.7.0 bundle with a 0.6.x README is likely.broker, producer, consumer sections, producer.send_log_messages) — now that the extension class is gone, the config object itself is the authoritative schema.src/MessageFactory, MessageInterface, MessageType — these are the API surface you'll actually touch.tests/ — the consumer-tag wiring and message construction are exactly the things unit tests document best.Put a wrapper between your app and the bundle — non-negotiable now
This is a 0.x Symfony bundle with one listed release, being driven from the wrong framework. Both facts argue for a boundary you own:
interface MessagePublisher
{
public function publish(string $topic, array $payload, ?string $key = null): void;
}
The implementation resolves MessageFactory, builds the message, and calls send(). Everything else in your codebase depends only on the interface. When 0.8.0 removes another type — and 0.7.0 removed four — you change one class.
Publish from domain events, keyed by aggregate ID
Keep Kafka out of controllers and models. Fire a Laravel event, publish from a listener, and always set the message key to the aggregate ID — the key determines the partition, and partition assignment is what gives per-aggregate ordering. No key means round-robin and no ordering guarantee.
Emulate the consumer tag with a config map
The bundle routes messages to handlers via the rossel_kafka.consumer DI tag. In Laravel, replace that with an explicit topic → handler-class map in config, resolved through the container. It keeps the listener wrapper trivial and makes each handler unit-testable without Kafka.
Assume at-least-once and design for it
0.7.0 disables auto-commit: offsets are committed only after processing. The upside is you no longer lose a message if the listener stops. The downside is stated plainly in the release notes — a message in flight when the listener stops is read again on restart. Handlers must be idempotent: upsert by message ID / aggregate version, never blind insert.
Handle poison messages yourself — the listener won't stop for you
This is the biggest semantic change in 0.7.0. A failing message is now logged (EXEC_ERROR), acknowledged, and skipped. You lose the old "crash and page someone" behaviour and gain silent message loss unless you act:
dead_letters table (or republish to <topic>.dlq), then rethrow or swallow deliberately.EXEC_ERROR rows, not on the log line — 0.7.0 emits exactly one log message per consumed message (EXEC_SUCCESS or EXEC_ERROR), which is a decent hook but a poor alerting source once volume grows.Outbox for anything you can't lose
Still the right pattern: write to an outbox_messages table inside the same DB transaction as your business write, then drain and publish from a scheduled job. The verbatim-body change in 0.7.0 makes this cleaner — you can store the exact bytes you'll put on the wire and replay them unchanged.
Serialization discipline
The bundle no longer JSON-decodes and re-encodes consumed bodies — they arrive verbatim. That's a good change (no more silent reshaping of your payload), but it means the envelope contract is entirely yours. Pick JSON with type and version fields, enforce it in the publisher wrapper, and parse defensively in handlers.
Supervise the consumer
Long-running process, not per-request. Supervisor alongside queue:work, numprocs=1 per partition you intend to read, stopwaitsecs long enough to finish the current message, and restart as part of every deploy — same discipline as queue:restart.
Testing
Bind a fake MessagePublisher in tests and assert topic, key, and payload. Don't stand up Kafka in CI — the failure modes you'd catch there (extension missing, cert path wrong) are environment problems, not code problems.
The upgrade instructions are Symfony-shaped and half of them don't apply to you. "Remove EnqueueBundle from config/bundles.php" and "Flex registers the bundle" are irrelevant to a Laravel app — but the underlying change is not: enqueue/enqueue-bundle and enqueue/fs are gone and the default transport is no longer pre-configured. If you were relying on that default transport for anything, you now own the configuration.
Types removed in 0.7.0 — grep for all four. MessageHeadersInterface, RdKafkaMessageWrapperInterface, UnsupportedTopicException, and the RosselKafkaExtension/Configuration/services.yaml trio. getRdKafkaMessage() still exists but moved onto MessageInterface. Any catch (UnsupportedTopicException) or header-interface type hint in your glue code is now dead code that will either fatal or silently never match.
MessageType::from() now throws \ValueError instead of BadRequestException. This is the nastiest break to spot: an existing catch (BadRequestException) around message-type resolution compiles fine and catches nothing. Audit every catch block that touches message construction.
MessageFactory no longer takes a logger. If you were constructing it in a service provider with a logger argument — which, as a Laravel developer, you almost certainly were — that call now fatals.
Lazy connection means config errors move to first use. SSL certificates and the CA download resolve on first use, and the rdkafka producer is only created on the first send(). A consume-only app never opens one. Nice for boot time, terrible for smoke tests: a deploy that boots cleanly proves nothing. Your post-deploy check must actually send and consume one message. Same for certificate paths — they must exist in the runtime image, not just the build image, which is the classic "works locally, dies in prod" Kafka failure.
producer.send_log_messages defaults to true. One EXEC_* line per consumed message at default verbosity. On a busy topic that's your entire log bill. Turn it off once you have real metrics, not before.
Config caching bakes in SASL credentials. Normal Laravel behaviour, but it means credential rotation requires a config:cache re-run, and until then the consumer keeps using the old ones — with the new PLAIN fallback quietly making that failure harder to diagnose.
Pin the version exactly. 0.7.0 removed four types and swapped an exception class on a minor bump. That's the pattern you should expect:
"rossel/rossel-kafka": "0.7.0"
Read the diff and the upgrade guide before every bump.
No extension class means no extension point. RosselKafkaExtension is gone, and with no serializer/middleware hook advertised, the only stable boundary is the wrapper interface above. If a future release does expose one, prefer it over subclassing internals.
Instrument it yourself. One log line per message is all you get out of the box. Publish failures and consumer lag are the two numbers you'll want at 3am, and you will have to produce both.
How can I help you explore Laravel packages today?