ux2dev / prim
Framework-agnostic PHP SDK for Prim.io (Antipodes) cloud ERP
Requires
- php: ^8.2
- ext-json: *
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.0
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
Suggests
- guzzlehttp/guzzle: Supplies PSR-18 client + PSR-17 factories out of the box
- illuminate/support: ^11.0|^12.0
This package is auto-updated.
Last update: 2026-08-12 04:00:24 UTC
README
Warning: This is a developer testing version of the library -- use at your own risk.
Framework-agnostic PHP SDK for the Prim.io (Antipodes) cloud ERP. Covers every RPC operation exposed on https://api.prim.io/apidoc/ as resource methods with named arguments or typed input DTOs, returning typed result DTOs. Works with plain PHP or Laravel.
Requirements
- PHP 8.2 or higher
- JSON extension
- A PSR-18 HTTP client and PSR-17 request/stream factories (Guzzle provides both and is installed transitively by Laravel)
Installation
composer require ux2dev/prim
Quick Start
Plain PHP
use GuzzleHttp\Client; use GuzzleHttp\Psr7\HttpFactory; use Ux2Dev\Prim\Config\PrimConfig; use Ux2Dev\Prim\Prim; $config = new PrimConfig( baseUrl: 'https://acme.prim.io/api', // your tenant host; trailing slash optional token: 'your-prim-api-token', ); $factory = new HttpFactory(); $prim = new Prim($config, new Client(), $factory, $factory); $response = $prim->items()->get(sku: 'ABC-001', limit: 50); foreach ($response as $item) { echo $item->sku . PHP_EOL; }
Laravel
use Ux2Dev\Prim\Dto\Input\SalesOrders\CreateSalesOrdersInput; use Ux2Dev\Prim\Dto\Input\SalesOrders\CreateSalesOrdersPartnerInput; use Ux2Dev\Prim\Dto\Input\SalesOrders\CreateSalesOrdersRowsInput; use Ux2Dev\Prim\Laravel\Facades\Prim; $result = Prim::salesOrders()->set(new CreateSalesOrdersInput( posCode: 'MS', partner: new CreateSalesOrdersPartnerInput(name: 'Retail', isCompany: true), rows: [ new CreateSalesOrdersRowsInput(sku: '10012', price: '55.00', quantity: '1'), ], )); $id = $result->id;
Configuration
PrimConfig
Every client takes a PrimConfig. It is a final readonly value object: all inputs are validated at construction, the token is private and redacted from var_dump(), and serialization is blocked.
use Ux2Dev\Prim\Config\PrimConfig; $config = new PrimConfig( baseUrl: 'https://acme.prim.io/api', // required; each tenant has its own host token: 'your-prim-api-token', // required; passed as ?token=… on every request timeout: 30, // optional; seconds, default 30 );
Prim.io has no sandbox vs. production split -- each customer's instance is its own host, so baseUrl is per-tenant. The token is a long-lived API key with no expiry.
Laravel configuration
Publish the config file:
php artisan vendor:publish --tag=prim-config
This creates config/prim.php:
return [ 'default' => env('PRIM_DEFAULT_TENANT', 'main'), 'tenants' => [ 'main' => [ 'base_url' => env('PRIM_BASE_URL'), 'token' => env('PRIM_TOKEN'), 'timeout' => (int) env('PRIM_TIMEOUT', 30), ], ], ];
Add the matching keys to .env:
PRIM_BASE_URL=https://acme.prim.io/api PRIM_TOKEN=your-prim-api-token
Multiple tenants
Add more entries under tenants and switch at runtime:
use Ux2Dev\Prim\Laravel\PrimManager; $response = app(PrimManager::class) ->tenant('other') ->items() ->get(sku: 'X');
tenant() returns an immutable clone; the default tenant stays untouched.
How the SDK is organised
Prim.io's API is uniform: every operation is a POST to {baseUrl}/RPC.common.Api.{Resource}.{action}?token={TOKEN} with a JSON envelope of the shape {"data": [...], "get_all"?, "offset"?, "limit"?}. The SDK maps this to:
| Layer | Location | Purpose |
|---|---|---|
| Config | Ux2Dev\Prim\Config\PrimConfig |
Base URL + token, validated |
| Transport | Ux2Dev\Prim\Http\PrimTransport |
PSR-18 dispatch, envelope parsing, error mapping |
| Input DTOs | Ux2Dev\Prim\Dto\Input\{Group}\{Action}Input |
One per mutation RPC operation; toArray() emits the canonical body |
| Result DTOs | Ux2Dev\Prim\Dto\Result\{Group}\{Action}Result |
One per RPC operation; fromArray() parses rows |
| Resources | Ux2Dev\Prim\Resources\{Group} |
Thin classes with one method per RPC action; method chooses named args or an input DTO depending on the operation |
| Root client | Ux2Dev\Prim\Prim |
Aggregator exposing every resource |
| Laravel | Ux2Dev\Prim\Laravel\* |
Service provider + multi-tenant manager + facade |
Resource methods come in three shapes:
- List operations (
$prim->items()->get(...)) take named argument filters and return aResultList<T>of typed result DTOs. - Single-record operations (
$prim->salesOrders()->getOneData(id: 42)) take scalar arguments and return the typed result DTO directly. - Mutations (
$prim->salesOrders()->set(...)) take a single input DTO and return the typed result DTO.
Generated from bin/endpoints.json (captured from https://api.prim.io/apidoc/) by bin/generate.php. Re-run the generator whenever the Prim.io catalogue changes -- it wipes and rewrites src/Dto/ and the generated src/Resources/ classes.
Resources
All 43 generated resources, each mapping to one Prim.io RPC group:
accounts deliveryMethods pointsOfSale supplierTypes
addresses documentRecord priceList sysLog
availability files prices tasks
bonusPoints financialDocuments promoCards taxInstances
brands incomeExpensesCategories quantitiesInBatches taxTemplates
categories integrationSystems salesOrders templates
clientsSuppliers items salesTypes unitsOfMeasurement
contacts listOfOperations services warehouses
currencies offer stockTransfer
customerTypes operations storeIn
order storeOut
parameters payment
payment paymentMethods
97 total RPC operations are covered.
Results
List endpoints return Ux2Dev\Prim\Http\ResultList<T>:
$response = $prim->items()->get(sku: 'ABC-001'); $response->items; // list<GetItemsResult> $response->totalCount; // total count across pages, if present $response->status; // 'OK' | null $response->code; // integer status code, if present $response->first(); // typed result DTO or null $response->all(); // list<GetItemsResult> count($response); // same as $response->count() foreach ($response as $item) { /* ... */ }
Single-record and mutation endpoints return the typed result DTO directly:
$order = $prim->salesOrders()->getOneData(id: 42); // GetASingleSalesOrderResult $result = $prim->salesOrders()->set(new CreateSalesOrdersInput(/* ... */)); // CreateSalesOrdersResult
Result DTO scalar fields are typed as mixed. Prim.io's documented example responses are inconsistent (the same field appears as int in one example and string in another), so the SDK deliberately does not enforce scalar types on inbound data. Nested objects and lists do use typed sub-DTOs.
Exceptions
All SDK exceptions extend Ux2Dev\Prim\Exception\PrimException:
| Exception | When it is thrown |
|---|---|
ConfigurationException |
Invalid PrimConfig input, unknown tenant |
TransportException |
PSR-18 client failure (network error, timeout) |
InvalidResponseException |
Empty body, malformed JSON, invalid envelope shape |
ApiException |
HTTP non-2xx or response status != "OK". Carries httpStatus, status, apiCode, and the full decoded body. |
Regenerating the DTOs
php bin/generate.php
This reads bin/endpoints.json and rewrites src/Dto/Input/, src/Dto/Result/, src/Resources/ (preserving Resource.php), and src/Prim.php. Hand edits to generated files will be lost on re-run.
Testing
composer install vendor/bin/pest XDEBUG_MODE=coverage vendor/bin/pest --coverage --min=100
The suite mocks a PSR-18 client to exercise every resource method end-to-end.
License
MIT