The MemcachedServer Class

One of the most unique and powerful—yet heavily under-documented—features of the php-memcached extension is the MemcachedServer class.

Rather than acting as a client connecting to a cache, this class allows you to write a PHP script that acts as a Memcached server daemon. It opens a network socket, listens for the Memcached Binary Protocol, and maps incoming network commands directly to PHP closures (callbacks).

Prerequisites

To use this feature, you must compile php-memcached from source and explicitly pass the protocol flag:

./configure --enable-memcached-protocol

Why Build a Memcached Server in PHP?

While PHP isn't designed to replace the C-based Memcached daemon for raw speed, this capability is incredibly powerful for specific architectural patterns:

  1. Smart Proxies & Routing: Act as an intermediary proxy. A client sends a Memcached command to your PHP script, which inspects the key, decides dynamically where it belongs, and routes the request to different databases or backend services.
  2. Exposing Different Datastores: Wrap a MySQL, Redis, or API backend in the Memcached protocol. Any legacy application that knows how to speak Memcached can magically talk to your new microservice without altering its legacy code.
  3. Advanced Mocking: Create a localized, mock cache server for unit/integration testing without needing to spin up actual Memcached Docker containers.

How It Works

The implementation uses libmemcached's internal server protocol parsers combined with an asynchronous libevent loop.

You instantiate the server, register closures for the operations you care about (ON_GET, ON_SET, ON_DELETE), and call run(). The run() method blocks the script, putting it into a continuous listening loop.

Complete Example: A Custom Array-Backed Server

Here is a complete, working example of a custom Memcached server that stores its data in a local PHP array.

<?php

// 1. Define our Backend Storage (A simple array in this case)
class ArrayStorage {
    private $data = [];

    public function set($key, $value, $expiration) {
        $this->data[$key] = [
            'value'   => $value,
            // Simple expiry math
            'expires' => $expiration > 0 ? time() + $expiration : 0 
        ];
    }

    public function get($key) {
        if (isset($this->data[$key])) {
            $item = $this->data[$key];
            if ($item['expires'] !== 0 && $item['expires'] < time()) {
                unset($this->data[$key]); // Evict expired
                return null;
            }
            return $item['value'];
        }
        return null;
    }
}

$storage = new ArrayStorage();
$server = new MemcachedServer();

// 2. Register the Connection Callback
$server->on(Memcached::ON_CONNECT, function ($remote_addr) {
    echo "[Server] Connection established from {$remote_addr}\n";
    return Memcached::RESPONSE_SUCCESS;
});

// 3. Register the GET Callback
$server->on(Memcached::ON_GET, function ($client_id, $key, &$value, &$flags, &$cas) use ($storage) {
    echo "[Server] GET requested for key: {$key}\n";

    $fetchedValue = $storage->get($key);

    if ($fetchedValue !== null) {
        $value = $fetchedValue;
        return Memcached::RESPONSE_SUCCESS;
    }

    // Mandatory response if the key is a cache miss
    return Memcached::RESPONSE_KEY_ENOENT;
});

// 4. Register the SET Callback
$server->on(Memcached::ON_SET, function ($client_id, $key, $value, $flags, $expiration, $cas, &$result_cas) use ($storage) {
    echo "[Server] SET requested for key: {$key}\n";

    $storage->set($key, $value, $expiration);
    return Memcached::RESPONSE_SUCCESS;
});

// 5. Start the Event Loop
$port = 3434;
echo "Starting PHP Memcached Server on 127.0.0.1:{$port}...\n";
$server->run("127.0.0.1:{$port}");

To test this, run the script in a terminal. In a separate terminal or PHP process, use a standard Memcached client to interact with it:

<?php
$client = new Memcached();
// Connect to your custom PHP server
$client->addServer('127.0.0.1', 3434);

// Because the custom server uses the Memcached Protocol handler from libmemcached,
// you MUST communicate using the Binary Protocol.
$client->setOption(Memcached::OPT_BINARY_PROTOCOL, true);

$client->set('my_custom_key', 'Hello PHP Server!');
echo $client->get('my_custom_key');

Supported Callbacks and Signatures

The following events can be mapped via $server->on(). Every callback must return a valid Memcached::RESPONSE_* constant.

  • Memcached::ON_CONNECT (function($remote_addr))
  • Memcached::ON_GET (function($client_id, $key, &$value, &$flags, &$cas))
  • Memcached::ON_SET (function($client_id, $key, $value, $flags, $expiration, $cas, &$result_cas))
  • Memcached::ON_ADD (function($client_id, $key, $value, $flags, $expiration, &$cas))
  • Memcached::ON_REPLACE (function($client_id, $key, $value, $flags, $expiration, $cas, &$result_cas))
  • Memcached::ON_DELETE (function($client_id, $key, $cas))
  • Memcached::ON_INCREMENT (function($client_id, $key, $delta, $initial, $expiration, &$result, &$result_cas))
  • Memcached::ON_DECREMENT (function($client_id, $key, $delta, $initial, $expiration, &$result, &$result_cas))
  • Memcached::ON_APPEND (function($client_id, $key, $value, $cas, &$result_cas))
  • Memcached::ON_PREPEND (function($client_id, $key, $value, $cas, &$result_cas))
  • Memcached::ON_FLUSH (function($client_id, $when))
  • Memcached::ON_STAT (function($client_id, $key, array &$values))
  • Memcached::ON_VERSION (function($client_id, &$value))
  • Memcached::ON_NOOP (function($client_id))
  • Memcached::ON_QUIT (function($client_id))

Common Pitfalls

  • Binary Protocol Only: Clients attempting to connect to your MemcachedServer must have Memcached::OPT_BINARY_PROTOCOL enabled. The underlying libmemcached protocol handler only parses binary requests.
  • Blocking Operations: Because the run() loop operates in a single PHP thread, if your callback executes a slow database query or sleep(), it will block all other clients attempting to communicate with the server until it finishes.
  • References: Notice the & in the callback signatures (e.g., &$value, &$cas). You must assign your return data to these variables by reference so the underlying C library can read them and construct the binary network response.