lekoala / odoo-php-client
Generic PHP client for the Odoo 19+ External JSON-2 API
Requires
- php: >=8.3
- composer/ca-bundle: ^1.5
- psr/event-dispatcher: ^1.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^3.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.5
Suggests
- ext-curl: Required by the zero-configuration CurlTransport fallback
- guzzlehttp/guzzle: Use Guzzle as the PSR-18 transport
- nyholm/psr7: PSR-7 and PSR-17 implementation commonly used with Symfony HttpClient
- symfony/http-client: Use Symfony HttpClient through its PSR-18 adapter
Provides
None
Conflicts
None
Replaces
None
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
contextcan be set once withConfig::setDefaultContext(); explicit per-call context wins key by key. Client::fieldsOf()cachesfields_getper instance; callclearFieldsCache()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\StripeandAddon\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
Clientfor 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.