Install and wire up
composer require darkaonline/ripcord
On Laravel 5.5+ the service provider and Xmlrpc facade are auto-discovered. If you've disabled discovery (or you're on an older skeleton), add them manually:
// config/app.php
'providers' => [
DarkaOnLine\Ripcord\RipcordServiceProvider::class,
],
'aliases' => [
'Xmlrpc' => DarkaOnLine\Ripcord\Facades\Xmlrpc::class,
],
Publish the package config and read it first — it's the shortest way to understand how the package instantiates clients:
php artisan vendor:publish --provider="DarkaOnLine\Ripcord\RipcordServiceProvider"
Where to look first
config/ripcord.php — the client prefix/class defaults the wrapper uses.vendor/darkaonline/ripcord/src/ — the Laravel layer is tiny (a provider, a facade, and a small registry class). Read it in five minutes.Ripcord_Client from the underlying library — this is where all the real behavior lives: call(), magic method proxies, transports, exceptions.First use case: talk to a legacy XML-RPC endpoint
Register a client and make a call from Tinker:
use DarkaOnLine\Ripcord\Facades\Xmlrpc;
Xmlrpc::addClient('erp', \Ripcord_Client::class, 'https://erp.example.com/xmlrpc');
$client = Xmlrpc::getClient('erp');
// Magic proxy style
$methods = $client->system->listMethods();
// Explicit style (easier to grep, easier to mock)
$methods = $client->call('system.listMethods', []);
// Or through the facade registry
$methods = Xmlrpc::call('erp', 'system.listMethods', []);
Start with system.listMethods / system.methodHelp against the target server — it tells you what the remote actually exposes before you write any domain code.
Register clients once, in a provider
Don't call addClient() from controllers. Register in a dedicated (optionally deferred) provider so the client registry is populated once per request and is trivial to fake in tests:
namespace App\Providers;
use DarkaOnLine\Ripcord\Facades\Xmlrpc;
use Illuminate\Support\ServiceProvider;
class XmlRpcServiceProvider extends ServiceProvider
{
public function boot(): void
{
Xmlrpc::addClient('erp', \Ripcord_Client::class, config('services.erp.xmlrpc_url'));
Xmlrpc::addClient('crm', \Ripcord_Client::class, config('services.crm.xmlrpc_url'));
}
}
Keep the URLs in config/services.php (env-driven), never inline in addClient() calls.
Wrap the client in a typed service
The facade returns a raw Ripcord_Client. Don't let that leak into your application layer — bind it to a small class with named methods:
namespace App\Services;
class ErpGateway
{
public function __construct(private \Ripcord_Client $client) {}
public function customer(int $id): array
{
return $this->client->call('customer.get', [$id]);
}
public function createInvoice(array $payload): int
{
return (int) $this->client->call('invoice.create', [$payload]);
}
}
// AppServiceProvider::register()
$this->app->bind(ErpGateway::class, fn () => new ErpGateway(
\DarkaOnLine\Ripcord\Facades\Xmlrpc::getClient('erp')
));
This is the single most valuable pattern with this package: it isolates the old Ripcord API (and any future replacement) behind something your app owns.
Batch independent lookups with async calls
XML-RPC is chatty and synchronous by default. If you need several unrelated results, use Ripcord's async call objects so requests pipeline instead of serializing:
$a = $client->callAsync('customer.get', [1]);
$b = $client->callAsync('customer.get', [2]);
$c = $client->callAsync('invoice.list', [1]);
$customerA = $a->send();
$customerB = $b->send();
$invoices = $c->send();
Reach for this when a page needs more than two or three round trips — it's usually the difference between a 300 ms and a 1.2 s response.
Server side: expose a Laravel service over XML-RPC
The package doesn't add much here; use Ripcord_Server directly inside a route or controller and let it write the response:
// routes/web.php (or api.php, but CSRF matters — see below)
Route::post('/xmlrpc', function () {
$server = new \Ripcord_Server(new \App\XmlRpc\ReportsService());
$server->service();
});
Put the exposed logic in a plain PHP class with public methods and no Laravel dependencies; Ripcord_Server introspects public methods. Treat the endpoint as untrusted input — validate argument types inside the service methods, because XML-RPC's type system is loose.
Error handling: convert faults into domain exceptions
Ripcord surfaces transport and protocol faults as Ripcord_Exception (HTTP errors, malformed responses, server faults). Wrap once, at the gateway boundary:
try {
return $client->call('invoice.create', [$payload]);
} catch (\Ripcord_Exception $e) {
report($e);
throw new \App\Exceptions\ErpUnavailable($e->getMessage(), previous: $e);
}
Callers then deal with one exception type instead of a library class.
Testing
Because the facade is backed by a normal container binding, you can mock it:
Xmlrpc::shouldReceive('getClient')->with('erp')->andReturn($mock);
That's brittle for anything beyond a smoke test. The durable approach is the gateway class above: bind a fake ErpGateway implementation (or a Ripcord_Client subclass whose call() returns fixtures) in the test container. Ripcord performs real network I/O, so never let tests hit a live endpoint.
call semantics, faults, transports, encoding) is Ripcord's, and Ripcord's core dates from the PHP 5 era. Read the source when in doubt — it's small.vendor/.addClient() expects a constructible class, not an instance. The third argument is the URL, and the wrapper does new $client($url). So any subclass you register must accept the same constructor signature — that's your extension point for logging, retries, or header injection.Ripcord_Client. Add request/response logging, retry logic, or auth headers by overriding call()/transport, then register your class with addClient(). This is cleaner than wrapping every call site.default_socket_timeout in php.ini) or via your Ripcord_Client subclass. A hung XML-RPC endpoint will otherwise hold a PHP worker until the default socket timeout.<array> from <struct>; PHP's empty array doesn't. If a remote rejects a payload, capture the raw request (log from your transport subclass) and check what was actually encoded.DateTime is auto-converted — send ISO 8601 strings in the format the remote documents. Base64 payloads bloat fast; watch post_max_size/memory_limit on both ends when moving files.VerifyCsrfToken (or validateCsrfTokens() in Laravel 11's bootstrap/app.php). Also consider a shared-secret middleware — XML-RPC has no built-in auth.Ripcord_Server::service() writes output directly. Don't expect Response objects, middleware-applied headers, or exception rendering to behave normally; wrap it deliberately.config:cache and env(). Resolve endpoint URLs in config files, not inside addClient() calls; otherwise you'll break under config:cache.aliases array; rely on package discovery or register the alias explicitly in bootstrap/app.php. If Xmlrpc:: throws "class not found", that's the cause.composer.json, and keep the gateway abstraction so you can replace the implementation with a hand-rolled Guzzle/Saloon XML-RPC client if the package stalls. For greenfield integrations, check whether the remote offers JSON-RPC or REST first — XML-RPC is usually the legacy option, not the best one.How can I help you explore Laravel packages today?