experteam/api-redis-bundle
Symfony 5.1+ bundle providing Redis services for APIs: a Predis client wrapper for reading and writing, plus a transport to persist entities and dispatch message queues, configured via YAML with per-entity prefixes and serialization groups.
This is a Symfony bundle (5.1+), not a Laravel package. Before anything else, confirm you're actually on Symfony — there is no service provider, no config/api_redis.php, and nothing auto-discovers in a Laravel app. If you were evaluating it for a Laravel codebase, stop here and reach for predis/predis or illuminate/redis directly.
Assuming Symfony:
Install
composer require experteam/api-redis-bundle
Get the config file into place. Two options in the README; prefer the manual copy for a first run so you can read what you're agreeing to:
cp vendor/experteam/api-redis-bundle/experteam_api_redis.yaml.example \
config/packages/experteam_api_redis.yaml
The vendor_copy.php + composer.json script route is only worth wiring up once you've confirmed the file is something you actually want re-seeded on every composer install (it uses --not-overwrite, so it won't clobber your edits — but a fresh clone with no committed config will silently gain one).
Look here first. The example YAML in the package root is the documentation. There is no docs site, no API reference, and the README ends mid-sentence. Read the example file top to bottom before writing any code — it defines the connection block, serialize_groups, elk_logger, and the entities map that drives everything else. Then open vendor/experteam/api-redis-bundle/Resources/config/services.xml (or wherever the bundle registers services) to get the real service IDs; you'll need those to inject anything, and they aren't in the README.
Minimal working config — register one entity and turn on only what you need:
experteam_api_redis:
serialize_groups:
save: read
message: read
elk_logger:
save: false
message: false
entities:
App\Entity\Product:
prefix: product
save: true
message: false
First use case. The bundle gives you two things: a Predis wrapper for raw read/write, and a "transport" that serializes an entity into Redis and (optionally) dispatches it as a message. Start with save: true, message: false on a single low-risk entity. Inject the client/transport service, save a record, then verify from the outside:
redis-cli --scan --pattern 'product*'
redis-cli GET product:1
If a key with your prefix and a JSON blob shows up, the wiring works and you can start layering on message: true.
Config is the API. Almost every behavior is driven by the entities map rather than by code. Per entity you get prefix, save, message, and message_class. Treat that YAML as the source of truth and keep it in version control alongside the entities it describes.
Save-then-dispatch is the core workflow. The Redis Transport exists to persist an entity and push a queue message in one call. The intended shape is:
entities:
App\Entity\Order:
prefix: order
save: true
message: true
message_class: App\Message\OrderCreated
With message: true you must supply message_class — the README explicitly marks it #required only if mess... (truncated, but the intent is clear). The transport serializes the entity and hands the payload to your message class, which is what the consumer side deserializes.
Per-entity prefixes are your key namespace. prefix: order produces keys like order:<id>. Choose prefixes deliberately:
Reuse Symfony serializer groups aggressively. serialize_groups.save and .message map to groups defined on your entity with @Groups/#[Groups]. Define a narrow read group on each persisted entity and point both settings at it. Do not point message at a group that exposes internal fields — that payload goes onto a queue and outlives the request.
Keep save and message groups separate once they diverge. The same read group for both is fine initially, but the moment a consumer needs a field the Redis snapshot shouldn't hold (or vice versa), split into redis and queue groups rather than compromising.
ELK logging is an observability toggle, not a feature you want everywhere. elk_logger.save / .message emit to whatever ELK sink is configured. Turn both on in staging to validate the pipeline, then disable per environment — an unreachable log endpoint should not be on the hot path of a write.
Integration tip — wrap the transport, don't scatter it. Inject the transport service into a thin repository/gateway class per aggregate so your domain code doesn't take a hard dependency on the bundle:
final class OrderCache
{
public function __construct(private $transport) {}
public function persist(Order $order): void
{
$this->transport->save($order); // whatever the actual method is
}
}
That wrapper is also your escape hatch when the package goes unmaintained — you swap one class instead of 40 call sites.
Pair it with a real queue consumer. The message flag dispatches onto a queue; it does not consume. You still need a worker (Symfony Messenger or your own) on the other end, plus a decision about what happens when Redis is up but the consumer is down and messages pile up.
This is a zero-star, zero-dependent, README-maturity package. That's the single biggest risk and it should shape everything else. Concretely: pin an exact version in composer.json ("experteam/api-redis-bundle": "1.0.*" — whatever the real tag is, not a caret range), commit composer.lock, and be prepared to fork. If this touches production writes, budget for the fork on day one rather than pretending you'll do it later.
No Laravel support, at all. Worth restating because the framing invites the mistake. Symfony bundle registration is via config/bundles.php; nothing here works in a Laravel service container.
message_class is a runtime failure, not a config-time one. Omit it while message: true and you'll likely get an error at the moment of dispatch, not at container compile. Grep the config for message: true and confirm every one has a matching class before deploying.
Serializer groups that don't exist fail quietly. If serialize_groups.save: read but your entity has no read group, you'll persist an empty or over-broad payload depending on the serializer's behavior — no exception. Verify by inspecting the actual key contents in Redis, not by trusting that the write returned.
prefix collisions are silent and destructive. No validation. Write a quick test that loads the container config and asserts prefix uniqueness across all configured entities; it's ten lines and saves an incident.
Nothing in the README mentions TTL, eviction, or maxmemory-policy. That means keys written here live until Redis evicts them. On a shared instance with allkeys-lru, your entity snapshots are candidates for eviction alongside session data. Check the instance's policy explicitly, and consider setting TTLs yourself through the Predis client if the bundle doesn't expose it.
Debugging recipe, in order:
redis-cli MONITOR while exercising the code path — confirms whether the command is even issued.redis-cli --scan --pattern 'yourprefix*' — confirms the key shape and prefix.redis-cli GET <key> — confirms the serialized payload and groups.The vendor_copy.php script has three flags that matter: --not-overwrite (won't touch an existing file — good), --ignore-no-source (won't fail if the package's example moves — hides breakage), and the -s/-d path pair (hardcoded to vendor/experteam/..., so it breaks the day you fork to a different vendor name). If you fork, you own this script.
Extension points are limited but real. This is a small bundle — the practical lever is decoration, not subclassing. Symfony lets you decorate the registered client/transport service to add retries, metrics, or a circuit breaker without touching vendor code. Do that rather than overriding service definitions, and keep the decorator in your own src/ where it's testable.
Test the failure modes you actually care about before adopting: what happens when Redis is down mid-request, what happens when it's up but slow, and whether a failed save aborts the business operation or logs and continues. Decide that policy deliberately — the bundle won't decide it for you, and the answer determines whether this is a cache or a system of record.
How can I help you explore Laravel packages today?