Search by

lekoala / odoo-php-client

lekoala

Generic PHP client for the Odoo 19+ External JSON-2 API

Package info

github.com/lekoala/odoo-php-client

pkg:composer/lekoala/odoo-php-client

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0 2026-09-16 16:30 UTC

This package is auto-updated.

Last update: 2026-09-16 16:39:05 UTC


README

A PHP 8.3+ client for Odoo 19's External JSON-2 API, with generic model calls and optional typed services for partners, invoices, subscriptions, products, payments, API keys, portal PDFs, Stripe and Peppol.

Installation

composer require lekoala/odoo-php-client

Until the first release is published, install the package from its Git repository or a Composer path repository.

Quick start

use LeKoala\OdooClient\Config;
use LeKoala\OdooClient\Odoo;

$odoo = new Odoo(new Config(
    url: 'https://mycompany.odoo.com',
    db: 'mycompany',
    apiKey: $_ENV['ODOO_API_KEY'],
));

$partners = $odoo->getClient()->searchRead(
    'res.partner',
    [['is_company', '=', true]],
    ['id', 'name', 'email'],
    limit: 20,
    order: 'name asc',
);

Every JSON-2 method remains available through named parameters:

$odoo->getClient()->call('sale.order', 'action_confirm', [
    'ids' => [123],
]);

Single and batch creation have distinct return contracts:

$id = $odoo->getClient()->create('res.partner', ['name' => 'Alice']);

$ids = $odoo->getClient()->createMany('res.partner', [
    ['name' => 'Alice'],
    ['name' => 'Bob'],
]);

Pagination and iteration

Typed service lists no longer apply a hidden result cap. Use QueryOptions for an explicit page, or iterate() to process large result sets in bounded requests:

use LeKoala\OdooClient\QueryOptions;

$page = $odoo->customers->list([], new QueryOptions(
    limit: 50,
    offset: 100,
    order: 'name asc',
    fields: ['id', 'name', 'email'],
    context: ['lang' => 'fr_BE'],
));

foreach ($odoo->customers->iterate(
    options: new QueryOptions(batchSize: 200, order: 'name asc'),
) as $customer) {
    // The effective order is "name asc, id asc" for stable page boundaries.
}

limit: null (the default) means no overall cap. Iterators remain bounded by batchSize; array-returning list() methods load all matching rows, so prefer iterate() for large tables. Passing an integer as the second argument remains supported as shorthand for an explicit limit.

Low-level searchRead() sends only the options you actually provide: an empty $fields, a zero limit/offset or a null order is omitted from the request body instead of being sent as []/0/null.

See the complete documentation, input DTOs, models, and examples.

AI agents: the generic integration skill shipped with this package is .agents/skills/odoo-php-client/SKILL.md.

Design guarantees

  • JSON-2 named arguments are sent without RPC-style positional inference through call().
  • Deprecated execute() compatibility is isolated at the public client boundary; adapters and services use named JSON-2 parameters only.
  • Read-only calls may be retried after transient gateway/network failures; writes and actions are not retried automatically. Retry count, backoff, timeouts and redirects are configurable on Config (setMaxRetries(), setRetryBackoff(), setConnectTimeout(), setTimeout(), setMaxRedirects()).
  • A default context can be set once with Config::setDefaultContext(); explicit per-call context wins key by key.
  • Client::fieldsOf() caches fields_get per instance; call clearFieldsCache() after module upgrades.
  • Events are dispatched after the remote mutation has committed: a listener exception is logged, never reported as an Odoo failure.
  • Stripe and Peppol live in Addon\Stripe and Addon\Peppol (Odoo::$stripe, Odoo::$peppol); the previous service methods remain as deprecated proxies.
  • HTTP 401, HTTP 403, network failures and Odoo validation errors use distinct exception types.
  • HTTP calls use an injectable transport; cURL is only the zero-configuration fallback.
  • A custom adapter can be injected into Client for tests or alternative transports.
  • Each JSON-2 call is a separate Odoo transaction. Multi-call business workflows are not atomic.

HTTP transport

The public HTTP boundary is transport-agnostic. With ext-curl installed, no additional configuration is needed. To reuse Guzzle, Symfony HttpClient, or another PSR-18 client, inject Psr18Transport through the configuration.

Guzzle:

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;
use LeKoala\OdooClient\Http\Psr18Transport;
use LeKoala\OdooClient\Config;

$guzzle = new Client([
    'connect_timeout' => 5,
    'timeout' => 30,
]);
$factory = new HttpFactory();

$config = (new Config($url, $db, $apiKey))->setHttpTransport(
    new Psr18Transport($guzzle, $factory, $factory)
);

Symfony HttpClient:

use LeKoala\OdooClient\Http\Psr18Transport;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Component\HttpClient\HttpClient as SymfonyHttpClient;
use Symfony\Component\HttpClient\Psr18Client;

$psr18 = new Psr18Client(SymfonyHttpClient::create([
    'timeout' => 30,
]));
$factory = new Psr17Factory();

$config->setHttpTransport(new Psr18Transport($psr18, $factory, $factory));

Redirects are handled consistently by this package. Sensitive JSON-2 headers are removed when a redirect changes origin, and portal PDF requests never receive the API bearer token.

Development

composer test
composer sa
composer validate --strict

License

MIT. See LICENSE.