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/clientInitialize 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()):
| Action | Method | Returns |
|---|---|---|
search_count | Odx::searchCount(model, domain, kw?) | int |
search | Odx::search(model, domain, kw?) | int[] (IDs) |
read | Odx::read(model, ids, kw?) | array records |
search_read | Odx::searchRead(model, domain, kw?) | array records |
create | Odx::create(model, values, kw?) | new ID |
write | Odx::write(model, ids, values, kw?) | bool |
unlink | Odx::unlink(model, ids) | bool |
call_method | Odx::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.