ODXProxy
SDKs

PHP SDK

A zero-dependency, PHP 8.1+ client for ODXProxy with first-class multi-tenant support.

odxproxy/client is the official PHP client for ODXProxy. It's zero library dependencies (just ext-curl + ext-json), strictly typed, and built for multi-tenant apps — a global singleton for the common case plus a per-config "context switcher" for iterating over many Odoo instances.

Source: terrakernel/ODXProxyClient-PHP · Packagist odxproxy/client · PHP 8.1+ · PSR-4 namespace OdxProxy\.

Install

composer require odxproxy/client

Initialize once

Call Odx::init($config) a single time at startup (Laravel AppServiceProvider, or index.php), then call the static helpers anywhere. The config is a flat array carrying both the proxy and the Odoo-instance details:

use OdxProxy\Odx;

Odx::init([
    'gateway_url'     => 'https://your-proxy.example.com', // optional; default https://gateway.odxproxy.io
    'gateway_api_key' => '<proxy x-api-key>',              // the PROXY's key
    'url'             => 'https://erp.example.com',        // Odoo base URL
    'db'              => 'prod',                           // Odoo database
    'user_id'         => 2,                                // Odoo user id
    'api_key'         => '<odoo user api key>',            // the Odoo USER's key
]);

Two different secrets

gateway_api_key is the proxy's x-api-key; api_key is the Odoo user's API key. They sit side by side in the same array but are never the same value — don't conflate them. gateway_url is optional (defaults to https://gateway.odxproxy.io; a trailing slash is trimmed).

Multi-tenant: Odx::with()

Odx::with($config) returns a client bound to a specific config without touching the global singleton — ideal for a cron job iterating over many tenants/users, each with their own Odoo instance:

foreach ($tenants as $tenant) {
    $client = Odx::with($tenant->toOdxConfig()); // same flat-array shape as init()
    $partners = $client->searchRead('res.partner', [[]]);
    // ...process this tenant...
}

This mirrors the proxy's design — one gateway in front of many Odoo instances, with the target instance carried per request.

Fetch records

Helpers return the result directly (already unwrapped from the JSON-RPC envelope) and throw on any error — so there's no ->result to reach through. searchRead is the common one; pass the domain directly (the client handles the execute_kw nesting):

use OdxProxy\Odx;
use OdxProxy\Model\KeywordRequest;

$kw = (new KeywordRequest())
    ->setFields(['id', 'name', 'email'])
    ->setLimit(50)
    ->setContext(['tz' => 'UTC']);

$partners = Odx::searchRead(
    'res.partner',
    [['is_company', '=', true]], // domain (conditions only — no extra nesting)
    $kw
);

foreach ($partners as $p) {
    echo $p['name'];
}

KeywordRequest has fluent setters (setFields, setOrder, setLimit, setOffset, setContext) and is optional (null is fine). For every action except search_read the client strips fields/order/limit/offset automatically — they only apply to a combined search+read.

The 8 helpers + execute()

Each allowed action is a static method on Odx (also available on the instance from Odx::with() / Odx::client()):

ActionMethodReturns
search_countOdx::searchCount(model, domain, kw?)int
searchOdx::search(model, domain, kw?)int[] (IDs)
readOdx::read(model, ids, kw?)array records
search_readOdx::searchRead(model, domain, kw?)array records
createOdx::create(model, values, kw?)new ID
writeOdx::write(model, ids, values, kw?)bool
unlinkOdx::unlink(model, ids)bool
call_methodOdx::call(model, method, args, kw?)mixed
$id      = Odx::create('res.partner', ['name' => 'Acme Inc', 'is_company' => true]);
$ok      = Odx::write('res.partner', [$id], ['name' => 'Acme LLC']);
$deleted = Odx::unlink('res.partner', [$id]);

// call_method — method name first, then its args
$confirmed = Odx::call('sale.order', 'action_confirm', [[42]]);

fields_get has no named helper

The 9th action, fields_get, isn't wrapped as a named method. Reach it through the generic execute() on a client instance: Odx::client()->execute('fields_get', 'res.partner', [], $kw). Anything outside the 9 actions must go through call_method (i.e. Odx::call(...)).

Errors

Every failure throws OdxProxy\Exception\OdxException (a RuntimeException). It carries the JSON-RPC or HTTP code (also on ->statusCode), the message, and optional ->data. Proxy-level failures and Odoo logic errors (HTTP 200 with an error body) both surface here — branch on the code against the error catalog:

use OdxProxy\Exception\OdxException;

try {
    $partners = Odx::searchRead('res.partner', [[]], $kw);
} catch (OdxException $e) {
    switch ($e->getCode()) {
        case -32000: /* bad x-api-key — reauth */ break;
        case -32003: /* upstream Odoo timeout — retry */ break;
        default:     error_log($e->getCode() . ': ' . $e->getMessage());
    }
}

Like the Java client, there's a single OdxException (no per-code subclasses) — switch on $e->getCode(). A JSON-RPC error can arrive with HTTP 200 (an Odoo logic error such as access-denied); the client already inspects the envelope's error field for you and throws, so you never infer success from the status code.

Connection reuse: the client keeps a cURL handle alive across calls within a request for keep-alive. In a long-running worker you can release it explicitly with OdxProxy\Client\OdxProxyClient::close().


Looking for another language? See the SDK overview — every client mirrors this same wire protocol.

On this page