bytes-commerce/orgamax-php
Composer 安装命令:
composer require bytes-commerce/orgamax-php
包简介
Modern, framework-agnostic PHP 8.3+ client for the OrgaMax Cloud OpenAPI — issue bearer tokens, manage articles, customers, invoices, orders, offers, expenses, files, suppliers, todos, tags, settings and bookkeeping charts of accounts.
README 文档
README
Modern, framework-agnostic PHP 8.3+ client for the OrgaMax Cloud OpenAPI.
Vendor: Bytes Commerce (bytes-commerce.de) License: MIT Coverage: every operation published by the OrgaMax OpenAPI spec.
Why this library?
You hand the client your apiKey, apiSecret, and ownershipId once
in a factory call. From that point on, every resource method
($client->articles()->list(...),
$client->invoices()->get(...), …) reuses the cached bearer token, the
shared HTTP client, and the shared PSR-17 factories. No per-call
plumbing.
Three factory styles are available so you can pick the one that fits your code:
| Style | When to reach for it |
|---|---|
OrgaMaxClientBuilder::create()->...->build() |
Application code: fluent, descriptive, easy to extend |
OrgaMaxClient::create(apiKey:, apiSecret:, ownershipId:) |
Tiny one-liners, scripts, throwaway demos |
OrgaMaxClient::fromConfig(OrgaMaxConfig::default(...)) |
DI containers (Symfony, Laravel): immutable config, shareable |
Requirements
- PHP
^8.3 - A working PSR-18 HTTP client (Guzzle 7, Symfony HttpClient PSR-18
adapter,
nyholm/psr7+php-http/discovery, …) - A PSR-17 factory pair (
nyholm/psr7,guzzle/psr7, Laminas, ...) or rely onphp-http/discovery - Optional: a PSR-16 cache for the bearer token
(
symfony/cache,predis/predis, ...). Otherwise an in-memory cache is used
Install
composer require bytes-commerce/orgamax-php
Quick start
1. Fluent builder (recommended)
use BytesCommerce\Orgamax\OrgaMaxClient; $client = OrgaMaxClient::builder() ->withApiKey(getenv('ORGAMAX_API_KEY')) ->withApiSecret(getenv('ORGAMAX_API_SECRET')) ->withOwnershipId(getenv('ORGAMAX_OWNERSHIP_ID')) ->build(); $articles = $client->articles()->list(limit: 50); $invoice = $client->invoices()->get(id: '507f1f77bcf86cd799439011');
2. Named-arg factory
use BytesCommerce\Orgamax\OrgaMaxClient; $client = OrgaMaxClient::create( apiKey: getenv('ORGAMAX_API_KEY'), apiSecret: getenv('ORGAMAX_API_SECRET'), ownershipId: getenv('ORGAMAX_OWNERSHIP_ID'), );
3. Immutable config (DI container)
use BytesCommerce\Orgamax\OrgaMaxClient; use BytesCommerce\Orgamax\OrgaMaxConfig; $config = OrgaMaxConfig::default( apiKey: getenv('ORGAMAX_API_KEY'), apiSecret: getenv('ORGAMAX_API_SECRET'), ownershipId: getenv('ORGAMAX_OWNERSHIP_ID'), ); $client = OrgaMaxClient::fromConfig($config);
OrgaMaxConfig is final readonly and its with*() methods return a
new instance. Share it across services, freeze it at boot, and you are
guaranteed it cannot be mutated downstream.
Auth flow
OrgaMax Cloud uses a two-step handshake that matches the
/auth/token operation:
- HTTP Basic auth with
apiKey+apiSecret→ callPOST /openapi/auth/tokenwith body{ "ownershipId": "..." }. - The response carries
{ "token": "...", "expires_in": ... }. That bearer token is attached asAuthorization: Bearer ...to every subsequent call.
The bundled CachedTokenProvider stores the bearer token under
orgamax.token.{ownershipId} in your PSR-16 cache, so you only pay the
handshake once per TTL window (default 24 h).
StaticTokenProvider lets you inject a pre-minted token: handy for
test suites or for tokens issued by a sidecar process.
use BytesCommerce\Orgamax\OrgaMaxClient; $client = OrgaMaxClient::builder() ->withApiKey(getenv('ORGAMAX_API_KEY')) ->withApiSecret(getenv('ORGAMAX_API_SECRET')) ->withOwnershipId(getenv('ORGAMAX_OWNERSHIP_ID')) ->withStaticToken($mintFromVault) ->build();
Configuration reference
| Field | Required | Default | What to override it with |
|---|---|---|---|
apiKey |
yes | none | OrgaMax Cloud API key |
apiSecret |
yes | none | OrgaMax Cloud API secret |
ownershipId |
yes | none | The ownership / tenant ID |
baseUrl |
no | https://api.orgamax.de/openapi |
Staging / on-prem gateway |
httpClient |
no | php-http/discovery PSR-18 lookup |
Guzzle 7, Symfony HttpClient, … |
requestFactory |
no | php-http/discovery PSR-17 lookup |
nyholm/psr7, guzzle/psr7, … |
streamFactory |
no | php-http/discovery PSR-17 lookup |
nyholm/psr7, guzzle/psr7, … |
tokenCache |
no | InMemoryTokenCache (process-local) |
Redis, filesystem, memcached |
tokenProvider |
no | CachedTokenProvider::withDefaultTtl() |
StaticTokenProvider, custom impl |
Customisation examples
use BytesCommerce\Orgamax\Auth\StaticTokenProvider; use BytesCommerce\Orgamax\OrgaMaxClient; use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\HttpClient\Psr18Client; // Bring your own HTTP client (Symfony's PSR-18 bridge): $http = Psr18Client::create(['timeout' => 5]); // Bring your own PSR-16 cache so tokens survive across requests: $cache = new RedisAdapter(new \Redis(), 'orgamax', 86400); $client = OrgaMaxClient::builder() ->withApiKey('...') ->withApiSecret('...') ->withOwnershipId('...') ->withHttpClient($http) ->withTokenCache($cache) ->build();
Override the token provider entirely (e.g. for static tokens, vault lookups, sidecar minting):
$client = OrgaMaxClient::builder() ->withApiKey('...') ->withApiSecret('...') ->withOwnershipId('...') ->withTokenProvider(new StaticTokenProvider($vault->get('orgamax-jwt'))) ->build();
Resource map
Every endpoint in the OpenAPI spec has a strongly-typed accessor:
| Resource | Accessor on $client |
|---|---|
| Article | $client->articles() |
Authenticator (raw /auth/token) |
$client->authenticator() |
| Bookkeeping (chart of accounts) | $client->bookkeepings() |
| Customer | $client->customers() |
| DeliveryCondition | $client->deliveryConditions() |
| DeliveryNote | $client->deliveryNotes() |
| Expense | $client->expenses() |
| File | $client->files() |
| Invoice | $client->invoices() |
| Offer | $client->offers() |
| Order | $client->orders() |
| PayCondition | $client->payConditions() |
| Setting | $client->settings() |
| Supplier | $client->suppliers() |
| Tag | $client->tags() |
| Todo | $client->todos() |
| User | $client->users() |
Examples
A few common write paths to get you started. For runnable, copy-paste
scripts see examples/:
| File | What it shows |
|---|---|
01-quickstart.php |
three factory styles for building the client |
02-customers.php |
customer CRUD and upsert() |
03-orders-invoices.php |
order → invoice flow (createInvoice, send, lock) |
04-articles-suppliers.php |
article / supplier writes and per-article settings |
05-error-handling.php |
catching every exception type |
06-pagination.php |
walking every page with $client->paginate() |
07-retry.php |
retrying transient failures with RetryHandler |
08-custom-cache.php |
swapping in a Redis / APCu PSR-16 token cache |
Create a customer
POST /customer/ accepts the full customer record and returns the new
record (with the assigned id) in data.
$response = $client->customers()->create([ 'customer' => [ 'name' => 'ACME GmbH', 'customerNumber' => 'C-1001', 'email' => 'buchhaltung@acme.example', 'addresses' => [[ 'street' => 'Musterstr. 12', 'zip' => '12345', 'city' => 'Berlin', 'countryCode' => 'DE', ]], ], ]); $newId = $response->first()['id'] ?? null;
Update a customer
PUT /customer/{id} overwrites the customer record. Pass the fields you
want to keep; the API treats the payload as the new state.
$client->customers()->update('c-1001', [ 'customer' => [ 'email' => 'neu@acme.example', ], ]);
Upsert a customer (create-or-update)
Customer::upsert() saves you the branching: pass an id and it
PUTs, omit it (or pass '') and it POSTs.
$client->customers()->upsert( payload: ['customer' => ['name' => 'ACME GmbH']], id: $existingId, // or null to create );
The same convenience exists on Article and Supplier.
Create an order
Orders are the primary sales document in OrgaMax and they are the only way to drive the invoice lifecycle.
$order = $client->orders()->create([ 'order' => [ 'customerId' => 'c-1001', 'orderDate' => '2026-07-06', 'positions' => [[ 'articleId' => 'a-42', 'quantity' => 2, 'price' => 199.00, ]], ], ]); $orderId = $order->first()['id'];
Create an invoice from an order
Invoices are never created directly via POST /invoice/. The
OrgaMax OpenAPI does not expose that endpoint. Instead, you draft an
invoice by posting to the order that owns it.
$invoice = $client->orders()->createInvoice( orderId: $orderId, payload: [ 'invoiceDate' => '2026-07-06', 'dueDate' => '2026-07-20', ], ); $invoiceId = $invoice->first()['id']; $client->invoices()->send($invoiceId, [ 'method' => 'email', 'to' => 'buchhaltung@acme.example', ]);
After the invoice exists you can lock it, download the document, record a payment, and look it up the usual way:
$client->invoices()->lock($invoiceId); $pdf = $client->invoices()->downloadDocument($invoiceId); $client->invoices()->addPayment($invoiceId, [ 'amount' => 199.00, 'paidAt' => '2026-07-15', 'paymentType' => 'bankTransfer', ]); $full = $client->invoices()->get($invoiceId);
A note on Users
The OrgaMax OpenAPI exposes Users as a read-only resource. Only
GET /user is published, so User::list() is the only available call
on $client->users(). User accounts are provisioned in the OrgaMax
admin UI; this client cannot create, update, or delete them.
Error handling
Every API call can throw one of the following. All live under
BytesCommerce\Orgamax\Exception\:
| Exception | When it fires |
|---|---|
AuthenticationException |
/auth/token returned non-2xx or no token field |
RateLimitException |
429 Too Many Requests (includes Retry-After hint) |
OrgaMaxException |
any other non-2xx response (4xx / 5xx) with parsed error body |
TransportException |
PSR-18 client threw (network down, DNS, TLS, …) |
use BytesCommerce\Orgamax\Exception\AuthenticationException; use BytesCommerce\Orgamax\Exception\RateLimitException; use BytesCommerce\Orgamax\Exception\TransportException; try { $invoice = $client->invoices()->get(id: '507f1f77bcf86cd799439011'); } catch (AuthenticationException $e) { // re-mint a token, or surface the credential issue } catch (RateLimitException $e) { sleep($e->retryAfterSeconds); // retry } catch (TransportException $e) { // bubble up to your retry layer }
Testing
The library ships with PSR-7 / PSR-18 / ClientExceptionInterface test
doubles under tests/Fixtures/TestDouble/:
FakeHttpClient: queue responses, assert on sent requestsFakeResponse/FakeResponseWithBody: PSR-7 responses with bodiesFakeResponseFactory: PSR-17 factoryFakeStream: PSR-7 streamFakeClientException: PSR-18 transport failure
Wire them in via the builder or constructor:
use BytesCommerce\Orgamax\OrgaMaxClient; use BytesCommerce\Orgamax\Tests\Fixtures\TestDouble\FakeHttpClient; use BytesCommerce\Orgamax\Tests\Fixtures\TestDouble\FakeResponseWithBody; use Nyholm\Psr7\Factory\Psr17Factory; $http = new FakeHttpClient(); $http->enqueue(new FakeResponseWithBody(200, json_encode([ 'token' => 'fake-jwt', 'expires_in' => 86400, ], JSON_THROW_ON_ERROR))); $client = OrgaMaxClient::builder() ->withApiKey('k') ->withApiSecret('s') ->withOwnershipId('o') ->withHttpClient($http) ->withRequestFactory(new Psr17Factory()) ->withStreamFactory(new Psr17Factory()) ->build();
Contributing
composer install composer test # PHPUnit 11 composer phpstan # PHPStan MAX (phpstan-strict + ergebnis/phpstan-rules) composer ecs # ECS: code style composer rector:dry # Rector: refactor suggestions (review the diff before applying)
The CI workflow (.github/workflows/ci.yml) runs all four checks on PHP
8.3 and 8.4 against the lowest and highest dependency resolutions.
PRs are welcome.
License
MIT, see LICENSE.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-06