Download the PHP package revenexx/app-sdk without Composer
On this page you can find all versions of the php package revenexx/app-sdk. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download revenexx/app-sdk
More information about revenexx/app-sdk
Files in revenexx/app-sdk
Package app-sdk
Short Description revenexx App SDK for PHP — build revenexx Apps: a typed data client (mock, remote and runtime adapters), a router with one-line CRUD routes, App settings, the acting person and App events.
License MIT
Homepage https://packagist.org/packages/revenexx/app-sdk
Informations about the package app-sdk
revenexx App SDK for PHP
Build revenexx Apps in PHP. The SDK gives an App function everything it needs to answer requests on the revenexx platform:
- A typed data client for the App's own records, with the same code running against in-memory fixtures in a test and against the platform's data plane in production.
- A router with one-line CRUD routes, safe error answers and stable error codes.
- The App's settings as the merchant configured them, per market.
- Who is acting behind a request.
- The App's own events, fired by name.
It is the PHP counterpart of the JavaScript SDK
@revenexx/app-sdk. The two
behave the same way, call for call.
Requirements
- PHP 8.2 or newer, with
ext-curlandext-json - The revenexx CLI to generate the typed client (Node.js 18+)
Install
How an App is put together
An App declares its data in two files, and the CLI turns them into a typed client:
| File | Declares |
|---|---|
schema.json |
the App's entities and their columns |
manifest.json |
what the App may do with them (read, create, update, delete), its events and its settings |
This writes src/Db.php. It holds one typed repository and one row class per entity,
plus Db::entities(), which lists the entities for the SDK. Regenerate it whenever
schema.json or manifest.json changes; never edit it by hand.
Quick start: a function
The runtime loads your entrypoint once per process and calls the function it returns for every request.
Answer through $c->json(), not $context->res->json(). The runtime's own
json() encodes every list as an object ({"0": …}). The router writes JSON itself
for that reason.
The data client
Client reaches the App's records. The generated Db wraps the same calls with
typed repositories ($db->markets->list(...)); Client takes an Entity and works
with plain arrays.
A guard on a number gives an atomic decrement. Read, compute, write only if nobody
wrote in between, and retry when the answer is null:
Two concurrent reservations cannot both spend the same amount. The second one's
condition no longer holds, so it gets null, reads again and decides again.
Only the operations the manifest grants are available. Any other call fails with a
403 and the code not_permitted, naming the method.
Queries
Every read takes the same shape: where, select, order, limit, offset.
where entry |
Matches |
|---|---|
['status' => 'published'] |
equal to the value |
['locale' => ['en', 'de']] |
any of the values |
['total' => ['op' => 'gte', 'value' => 100]] |
the comparison: eq neq gt gte lt lte like ilike in is |
['deleted_at' => null] |
is null |
All entries must hold. An order without a direction sorts ascending ('code' is
'code.asc').
Adapters
| Adapter | Use it for |
|---|---|
mock |
unit tests: in-memory rows, no network, no setup |
remote |
trying your code against a development tenant |
runtime |
production: configured from the request the function serves |
The mock fills in column defaults and generated keys. It also refuses a filter on a column the entity does not have, as the platform does. It does not imitate tenant isolation, unique constraints or foreign keys, so try your App against a development tenant once before you ship it.
The router
RouteContext gives a handler the following:
$c->params |
path parameters |
$c->query |
the query string, decoded |
$c->body |
the parsed JSON body |
$c->tenant |
the tenant the request is for |
$c->header($name) |
any request header |
$c->actor |
who is acting |
$c->settings() |
the App's settings for this request |
$c->json($data, $status) |
the answer |
$c->log($message) |
the function log |
Routing rules:
- Literal path segments match before parameters, so
/markets/defaultswins over/markets/{id}. - An unknown path answers
404; a known path with the wrong method answers405. GET /answers the App's name and its routes, as a health check.
One-line CRUD
Crud::mount mounts the five standard routes for an entity:
| Route | Answer |
|---|---|
GET /markets |
{ items, page: { limit, offset, total, returned, hasMore }, filter } |
POST /markets |
the created row, 201 |
GET /markets/{id} |
the row, or 404 |
PUT /markets/{id} |
the updated row |
DELETE /markets/{id} |
{ deleted: true, id } |
Options:
onlymounts a subset, for example['list', 'get'].parentnests the routes under a parent:['param' => 'marketId', 'column' => 'market_id', 'entity' => $client->entity('markets')]for/markets/{marketId}/locales. Rows are then scoped to the parent in the path; nothing in a body or query string reaches another parent's rows. Withentitygiven, an unknown parent answers404.queryParamsnames extra query keys your App reads itself.
List filters. A caller filters the list by exact value: ?currency=EUR. limit,
offset and order page and sort it. Anything the list cannot apply is refused with
a 400, never silently ignored:
| Refusal | code |
|---|---|
a key that is not a column and not declared in queryParams |
unknown_filter |
a value carrying an operator, e.g. ?sku=in.(a,b) |
unsupported_filter_operator |
| a filter on a JSON column that is not a whole JSON document | invalid_value |
The trash in one call
An entity with trash: true in schema.json gets the routes a trash needs from
Trash::mount, the same as mountTrash in the JavaScript SDK:
DELETE /organizations/{id}on a live row puts it into the trash with its live children, as one group (trash_batch), recordingdeleted_by/deleted_by_idfrom the acting person.guardreasons refuse it (409 trash_blocked).- The same
DELETEon a trashed row deletes it for good: the group's children first, then the row.blockersreasons refuse it (409 delete_blocked);canDeleteForGoodcan require its own permission (403 not_permitted). POST /organizations/{id}/restorebrings back the row and the children of its group (409 parent_in_trashwhen itsparentis in the trash).$trash->purge($caller)is the scheduled job. It readstrash_retention_daysthrough the gateway (GET /v1/audit/settings, 90 when unset;REVENEXX_AUDIT_SETTINGS_URLoverrides the URL) and deletes for good what is older, through the sameblockers. A blocked row stays, with its reason in the result and inpurge_blocked_reason('purgeReasonColumn' => falsefor none). If the retention cannot be read, the run is skipped.
A check that throws, and a cross-app count the gateway does not answer, block.
Errors
Throw an HttpError to refuse a request. The router answers it with its status, and
the caller sees only what is safe to show:
Branch on code, never on the message. A code is stable across versions; a
message may be reworded. message and type repeat error and code under the
names the revenexx platform client reads.
Anything else a handler throws answers 500 {"error": "internal error", "code": "internal_error"}. Its message goes to the function log and never reaches the
caller.
The data client raises HttpErrors itself:
| Situation | Status | code |
|---|---|---|
| an operation the manifest does not grant | 403 | not_permitted |
| an update with nothing to update | 400 | empty_update |
updateIf without a condition |
400 | empty_condition |
| an update whose row is gone | 404 | not_found |
| a duplicate value | 409 | unique_violation |
| a referenced record that does not exist | 400 | foreign_key_violation |
| a record still referenced by others, on delete | 409 | foreign_key_violation |
| a required value missing / a check failed | 400 | not_null_violation / check_violation |
| a value in the wrong format | 400 | invalid_value |
| the data plane unavailable | 500 | data_error |
No table, column or constraint name ever appears in an answer. The full diagnosis is
on $e->serverDetail and in the function log.
Reading another App's refusal
When your App calls another App and is refused, Refusal::of() reads what it said:
Settings
$c->settings() returns the App's settings for the tenant and market of the request,
resolved by the platform: a market value, else the tenant's value, else the default
from your settings declaration.
- Reads are cached per tenant, App and market for 60 seconds.
- A settings outage never fails a request: the reader answers with the last values it
resolved, or with your
defaults. - Settings declared sensitive are withheld;
$c->settingsSnapshot()lists their keys undermasked.
Who is acting
$c->actor reads the identity the platform forwarded with the request:
user, email |
the person, when one is known |
contact, organization |
the customer contact and organization the call is for |
permissions, holds('orders.approve') |
what that person may do |
trust |
authenticated, asserted or none |
plane |
contact, operator or system |
Check trust before enforcing a per-person rule. A contact named by a storefront
server is asserted, not proven: authenticated() answers true only for a person
the platform verified itself. Today the platform verifies no person itself, so treat
every contact as asserted.
The App's own events
An App declares the events it fires itself in its manifest, for facts with no row behind them:
With the generated client, the same call is $db->events()->emit(…). A Client
built by hand needs Db::EMIT_FN and Db::declaredEvents() passed to its
constructor (see the quick start). An event the manifest does not declare is refused
before anything is sent. On the
mock adapter nothing is published; $client->events()->emitted() lists what the App
fired, so a test can assert it.
Testing an App
Build the same routes on the mock adapter and run requests through $app->handle(),
with a small stand-in for the runtime's context:
Keep the route setup in one function that both main.php and your tests call, so the
tests run the routes production runs. For Settings, pass a transport callable to
answer without a network:
Not included
- Tracing. The JavaScript SDK traces requests with OpenTelemetry; the PHP SDK does not.
Versioning
The SDK follows semantic versioning. The public classes and
methods, the answers of the routes it mounts, and the status and code of every
refusal are bound: changing any of them needs a new major version. A message may be
reworded in a minor version. See CHANGELOG.md.
Working on the SDK itself
A few tests check the SDK against cases it shares with the JavaScript SDK. Those cases are not part of this package, so these tests are skipped here.
This repository is published from the SDK's main repository and is replaced on every release, so pull requests opened here cannot be merged. Report problems to revenexx.
License
MIT — see LICENSE.
All versions of app-sdk with dependencies
ext-curl Version *
ext-json Version *