SOAP – Simple Object Access Protocol was born at the turn of the millennium when XML-RPC turned out to be not quite enough for the web’s growing need for integration and collaboration. 1
Horde has always been about integrations and APIs. It’s a framework driving collaboration software after all. Before “API-first” became a buzzword, Horde applications exposed their capabilities through XML-RPC, SOAP, JSON-RPC, and half a dozen other protocols… We did Distributed Architectures even back in Horde 4, with a master instance orchestrating stateless workers over RPC. Applications like Turba Addressbook exposed their addressbook data over CardDAV, ActiveSync, JSON-RPC, XML-RPC, and SOAP simultaneously. The Horde_Rpc library was the machinery behind all of that. It has been part of the framework almost from the beginning.
That library is now getting a fundamental rethink.
Your Lipstick on My Face, All Over my Face
The original Horde_Rpc was tightly coupled to the Horde base application’s rpc.php entry point and the Horde_Registry inter-application API. It took whatever an application registered in the registry and exposed it over JSON-RPC 1.1, XML-RPC, SOAP, a PHPGroupware dialect of XML-RPC and some other formats. It also served as the entry point for ActiveSync, WebDAV/CalDAV/CardDAV and SyncML synchronization. Yes I know, SyncML is a distant memory now. It was the ActiveSync of the Nokia era. 2
That was a lot of responsibility for a single library. And it came with a lot of assumptions: A running Horde instance, a configured registry, the whole framework stack beneath it. If you wanted to use Horde’s RPC capabilities outside of Horde itself, tough luck my friend. You will have the whole framework for the rest of your life. Say yes! Really!
This tight coupling was a product of its time. Monolithic frameworks were the norm and a library’s value was measured by how well it integrated with its parent framework rather than how well it stood on its own. But the PHP ecosystem has changed. Libraries are gravitating from framework glue towards individual value propositions and potential for reuse. The modernization of the language and the adoption of PHP-FIG standards are accelerating this trend. So we have PSR-4 autoloading, PSR-11 containers, we have PSR-15 request handlers in the application skeleton and a PSR-14 event bus. We do have PSR-3 logging but we don’t use it here, not directly. I will explain this a bit later. Monolithic frameworks are decomposing into reusable parts. Horde is no exception. If anything, we’re a little late.
Just Like a Bird You Leave Your Robin’s Nest
The new Horde\Rpc library is built on PSR standards from the ground up. PSR-7 for HTTP messages. PSR-15 for server request handlers and middleware. PSR-14 for event dispatching. PSR-17 and PSR-18 for HTTP factories and clients. The question of whether Horde’s internal API could adopt PSR-7 is no longer hypothetical. This tight coupling to Horde_Registry has been replaced with a generic concept of a pluggable API provider.
The architecture is straightforward. Two interfaces define the contract:
interface ApiProviderInterface
{
public function hasMethod(string $method): bool;
public function getMethodDescriptor(string $method): ?MethodDescriptor;
public function listMethods(): array;
}
interface MethodInvokerInterface
{
public function invoke(string $method, array $params): Result;
}
Whatever implements these interfaces can expose APIs through the RPC layer. A Horde registry, a simple callable map, a PSR-11 container lookup, a domain-specific service class. It is all up to you. The dispatch layer does not care where the methods come from.
JSON-RPC, SOAP, MCP: Each protocol adapter implements both RequestHandlerInterface and MiddlewareInterface. This dual-interface pattern means the same handler object works for direct routing and in a middleware stack:
// Stack all three protocols in a PSR-15 middleware pipeline
$app->pipe($soapHandler); // text/xml -> SOAP
$app->pipe($mcpServer->getHandler()); // /mcp + JSON -> MCP
$app->pipe($jsonRpc->getHandler()); // /rpc/jsonrpc + JSON -> JSON-RPC
Each handler claims its protocol based on path and content type or passes the request through to the next one. No interference, no routing conflicts.
Begin to Take Away the Hurt
The old Horde_Rpc baked logging and error handling into its own internals. The new library replaces all built-in logging with an event emitter compatible with PSR-14’s EventDispatcherInterface. Events like RequestReceived, RequestDispatched, ErrorOccurred, and BatchProcessing fire as they happen in a request’s lifecycle. Wire up whatever listeners you want. Structured logging, metrics collection, audit trails. Don’t let the library make all the wrong assumptions about severity and what matters to you.
The JSON-RPC adapter now implements both JSON-RPC 1.1 and 2.0 properly, including batch requests and notifications. The protocol handling lives in a clean Codec class that understands the transport format differences between versions. Errors follow the standard JSON-RPC error code scheme, and any non-JsonRpcThrowable exception from a provider gets sanitized to a generic “Internal error” before it reaches the client. No information leaks, no sudden drop of stack traces where valid JSON is expected.
The new JSON-RPC implementation also serves as the foundation for a new MCP adapter. The Model Context Protocol3 is built on JSON-RPC 2.0 and MCP lets AI agents such as Claude, Cline and others discover and invoke tools programmatically. The MCP adapter translates between the tools/list and tools/call protocol methods and the shared dispatch layer. Same provider, different protocol. SP/DP – Should we start a band? No.
Will Take Away Your Perfume Eventually
The library ships with a demonstrational MathApiProvider to play with. It exposes four simple math operations that serve as documentation-by-code for the provider pattern:
$math = MathApiProvider::create();
// Same provider powers all three protocols
$jsonRpc = new JsonRpcHandler(
provider: $math, invoker: $math,
responseFactory: new ResponseFactory(),
streamFactory: new StreamFactory(),
eventDispatcher: $eventDispatcher,
);
$mcp = new McpServer(
new ServerInfo('horde-math', '1.0.0', 'Math tools for LLMs'),
$math, $math,
new ResponseFactory(), new StreamFactory(),
);
$soap = new SoapHandler(
provider: $math, invoker: $math,
responseFactory: new ResponseFactory(),
streamFactory: new StreamFactory(),
);
That math.add method? It can be consumed as a JSON-RPC remote call, a SOAP request or an MCP tool for AI agents. Write your provider once, serve it everywhere. The MathApiProvider is intentionally simple, but the pattern scales to complex domain APIs.
For existing Horde installations the HordeRegistryApiProvider bridges the old world to the new. It translates the dot-notation method names that JSON-RPC uses (calendar.list) to the slash-notation that Horde_Registry expects (calendar/list). The migration path is incremental. Existing applications keep working while new providers adopt the modern interface. In a future version we will probably also offer an upgrade path from the Horde_Registry_API way of defining such APIs to something which runs on Stringable value objects until it hits the protocol layer. But that’s for another song.
Like All the REST You Flew Away
We are only beginning to explore this new concept and have not yet built it into horde/base. The current release is 3.0.0-beta2, and the API surface may still change as we gather feedback.
The team is also working towards rebasing ActiveSync from Horde_Controller_Request to PSR-7 ServerRequestInterface, which would be a natural fit for the new middleware-based architecture. This has been coming for a long time. In 2022, PHP 8.1 compatibility testing already flagged RPC interface gaps and unclear ActiveSync status. This work addresses both. With DAV it is a bit more tricky. The SabreDAV authors deliberately chose not to implement PSR-7 but their own slightly different HTTP request/response/handler pattern. Bridging that gap will require some creative adapter work.
The old protocols… XML-RPC, the PHPGroupware dialect, SyncML, they still live in the lib/ directory as legacy PSR-0 code. They are not going away immediately, but they are no longer the focus. SyncML is like latin, highly interesting but you rarely meet a native speaker. PHPGroupware is deader than disco and its spinoff eGroupware doesn’t provide this specific interface anymore. XML-RPC is still relevant in some niches but we rely on PHP’s xmlrpc extension 4 which isn’t maintained anymore. We would need to rebase it on our own Horde XML primitives. That’s a lot of work I don’t see as my most pressing concern right now and I am willing to support anybody else who tries to do that.
The future is pluggable providers, PSR middleware stacks, and protocol adapters that earn their keep individually rather than depending on the full framework.
Nothing will ever wash away the memory of twenty years of Horde integrations. But the memory is getting a fresh interface.
Links:
Related posts:
- Distributed Applications with Horde 4 – the original RPC architecture
- Can Horde’s Internal API Use PSR-7 Messages? – the thought experiment that led here
- Horde/Skeleton: Modernized – PSR-15 handlers in the application layer
- Horde Development Review October 2020 – JSON-RPC default, XML-RPC deprecation
- Horde on PHP 8.1 and Composer Update – RPC testing gaps identified
Footnotes:
-
SOAP emerged around 1999–2000 as XML-based remote messaging standardized at W3C. XML-RPC predates it slightly and influenced early SOAP designs. See W3C SOAP 1.1 note (https://www.w3.org/TR/2000/NOTE-SOAP-20000508/) and historical background: https://www.xml.com/pub/a/ws/2001/04/04/soap.html ↩
-
SyncML (later OMA Data Sync) was widely implemented in early 2000s mobile devices, especially Nokia and Symbian phones, before being eclipsed by ActiveSync and DAV-based protocols. See https://en.wikipedia.org/wiki/Syncml ↩
-
MCP is young and rapidly evolving. We want to provide our data resources and tools to the younger generation of tools in the language that they speak—even though it might be out of fashion tomorrow. MCP background and specification: https://modelcontextprotocol.io and https://en.wikipedia.org/wiki/Model_Context_Protocol ↩
-
The PHP xmlrpc extension was removed from core in PHP 8.0, moved to PECL, and is effectively unmaintained due to its dependency on abandoned libraries. See https://php.watch/versions/8.0/xmlrpc and https://pecl.php.net/package/xmlrpc ↩
Leave a Reply