ux2dev/prim

Framework-agnostic PHP SDK for Prim.io (Antipodes) cloud ERP

Maintainers

Package info

github.com/ux2dev/prim

pkg:composer/ux2dev/prim

Transparency log

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-08-12 03:52 UTC

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 a ResultList<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