mcfedr/memcached-bundle
Symfony bundle that integrates the Memcached PHP extension, providing a preconfigured Memcached client service and simple bundle configuration. Drop it into your Symfony app to use Memcached for caching or sessions without boilerplate wiring.
1. Install the extension and the bundle
# The PHP extension is a hard requirement; the bundle is only a thin wrapper
pecl install memcached # needs libmemcached-dev on the host
composer require mcfedr/memcached-bundle
2. Register the bundle (the package predates Symfony 4 Flex recipes, so this is manual)
// app/AppKernel.php (Symfony 2.x/3.x)
$bundles = [
// ...
new Mcfedr\MemcachedBundle\McfedrMemcachedBundle(),
];
// config/bundles.php (Symfony 3.4+/4.x if you are retrofitting)
return [
// ...
Mcfedr\MemcachedBundle\McfedrMemcachedBundle::class => ['all' => true],
];
3. Point it at your servers
# app/config/config.yml
mcfedr_memcached:
servers:
- { host: 127.0.0.1, port: 11211 }
# add more entries for a pool; the extension handles consistent hashing
Because this is a tiny bundle, the Configuration class is short — open src/DependencyInjection/Configuration.php in vendor/mcfedr/memcached-bundle first to see the exact keys for your pinned version (options like persistent_id, options, and servers have shifted between releases). Do not trust blog posts for this; read the class.
4. Find the service and prove it works
bin/console debug:container | grep -i memcached
# then, in a controller or a console command:
$mc = $this->get('mcfedr_memcached'); // confirm the id from the step above
$mc->set('smoke_test', 'ok', 60);
var_dump($mc->get('smoke_test')); // string(2) "ok"
First real use case: replace a database read on a hot, low-churn lookup (settings table, category tree, rendered fragment) with a get/set pair wrapped in a small repository or service. That is the fastest place to see value and the easiest place to add a cache-busting key later.
Wrap it — don't inject \Memcached everywhere. Define one application-facing service that owns key naming, serialization, and TTL defaults. This is what makes the bundle's thinness an advantage rather than a liability.
namespace App\Cache;
class AppCache
{
private $mc;
private $prefix;
public function __construct(\Memcached $mc, string $prefix)
{
$this->mc = $mc;
$this->prefix = $prefix;
}
public function remember(string $key, int $ttl, callable $loader)
{
$k = $this->prefix . $key;
$value = $this->mc->get($k);
if ($this->mc->getResultCode() === \Memcached::RES_SUCCESS) {
return $value;
}
$value = $loader();
$this->mc->set($k, $value, $ttl);
return $value;
}
}
# services.yml
App\Cache\AppCache:
arguments:
$mc: '@mcfedr_memcached' # use the id confirmed via debug:container
$prefix: '%kernel.environment%.myapp.v1.'
Namespace by environment and schema version. Memcached has no concept of tenants or pools; all apps hitting a server share the keyspace. A prefix of env.app.version. lets you invalidate an entire generation by bumping v1 → v2 in config — the old keys expire on their own TTL.
Bulk reads for list pages. getMulti/setMulti are the single biggest win vs. a naive loop; the round-trip count is what kills you, not the payload.
$keys = array_map(fn($id) => "user.$id", $ids);
$found = $mc->getMulti($keys); // misses are simply absent
$missing = array_diff($keys, array_keys($found));
// hydrate only $missing from the DB, then setMulti them back
CAS for counter-ish data. The extension gives you cas() and getResultCode() === RES_DATA_EXISTS; use it for lightweight counters or "last writer wins" conflicts instead of reaching for Redis.
Bridge into Symfony's cache layer if the rest of the app already uses PSR-6. Wrap the client in Symfony\Component\Cache\Adapter\MemcachedAdapter::createConnection() — but note this bypasses the bundle entirely, which is often the right call once you are on Symfony 4+.
Warm and invalidate from the console. Put cache warming in a cache:warmup-adjacent command or a post-deploy step; also expose a scoped flush that walks only your prefixed keys (getAllKeys() is available and fine for a small keyspace, dangerous at scale).
symfony/cache's MemcachedAdapter instead and keep this only if you're on a legacy branch.libmemcached version mismatches, missing SASL, and OPT_BINARY_PROTOCOL oddities all surface as generic RES_* codes. When get() returns false, always check getResultCode() before assuming a miss (RES_NOTFOUND vs RES_CONNECTION_SOCKET_CREATE_FAILURE).time() + $ttl, a small TTL will silently become "expire in 1970" behavior — always pass durations, or branch on the threshold.sha1($key)) or store a lookup.version keys or short TTLs; don't try to bolt tag invalidation onto this.flush() is per-server, immediate, and nukes everyone's data — including other apps on the same memcached. Never wire it to a user-facing "clear cache" button without a prefix check.OPT_DISTRIBUTION (consistent hashing), OPT_LIBKETAMA_COMPATIBLE, OPT_PREFIX_KEY, and timeouts belong in the bundle config / a compiler pass so every request gets the same behavior. Per-call setOption on a persistent connection is a classic source of "works in dev, wrong server in prod."persistent_id can leak state across requests in PHP-FPM workers; if you see stale values after a deploy, unset the persistent id first.\Memcached type in your services.yml (\Memcached: '@mcfedr_memcached') so constructor injection works without remembering the string id.test, otherwise unit tests require a live daemon and fail on CI.How can I help you explore Laravel packages today?