adaiasmagdiel / loxodontu-php
PHP client for Loxodontu, an open-source Backend-as-a-Service (BaaS).
Requires
- php: ^8.2
- ext-curl: *
- ext-json: *
Requires (Dev)
- pestphp/pest: ^2.34
README
PHP client for Loxodontu, an open-source Backend-as-a-Service. Framework-agnostic, no HTTP client dependency (uses ext-curl directly).
Install
composer require adaiasmagdiel/loxodontu-php
Two clients, matching Loxodontu's two token types
Client— app-facing. REST passthrough, edge function invocation, and end-user auth for a single project. Authenticated with a project API key.Admin— platform-facing. Manage your account, projects, tables, keys, RLS policies, cron jobs, and functions. Authenticated with your platform login.
App client
use AdaiasMagdiel\Loxodontu\Client; $loxo = new Client( 'https://your-app.example.com/api/v1', 'my-project', // project id / slug 'PROJECT_API_KEY', ); // A chain ends with an explicit ->get() (PHP has no thenable to auto-execute on await). $response = $loxo->from('todos') ->select() ->eq('done', false) ->order('created_at', ascending: false) ->limit(20) ->get(); $todos = $response->data; $loxo->from('todos')->insert(['title' => 'Write docs'])->get(); $loxo->from('todos')->update(['id' => 1, 'done' => true])->get(); // single row, id in the body $loxo->from('todos')->update(['done' => true])->eq('done', false)->get(); // bulk, by filter $loxo->from('todos')->delete()->eq('id', 1)->get(); // single row, by filter $loxo->from('todos')->delete()->lt('views', 10)->get(); // bulk, by filter $loxo->from('todos')->delete([2, 3, 4])->get(); // bulk, by id list // End users (your app's own users, separate from your platform account) $loxo->auth->register('user@example.com', 'password123'); $loxo->auth->login('user@example.com', 'password123'); // token stored & sent automatically $loxo->auth->logout(); // Edge functions $response = $loxo->functions->invoke('daily-cleanup', body: ['source' => 'client']);
Filters mirror the REST passthrough API 1:1: eq, neq, gt, gte, lt, lte, like
(* wildcard), in. They work the same way on select(), update(), and delete(). For
update()/delete(), in order of precedence: a list (of row arrays, or ids for delete) is
always a bulk write; otherwise any chained filter (eq, gt, ...) scopes a filtered
update/delete over every matching row; otherwise, for update() only, an id key in the body
targets that single row.
Admin client
use AdaiasMagdiel\Loxodontu\Admin; $admin = new Admin('https://your-app.example.com/api/v1'); $admin->auth->login('me@example.com', 'password123'); $projects = $admin->projects->list()->unwrap(); $project = $admin->projects->create(['name' => 'New project'])->unwrap(); $project1 = $admin->projects->for($project['id']); $project1->tables->create([ 'name' => 'todos', 'columns' => [ ['name' => 'title', 'type' => 'text'], ['name' => 'done', 'type' => 'boolean', 'default_value' => false], ], ]); $project1->keys->create(['name' => 'frontend', 'permissions' => ['select', 'insert']]); $project1->tables->rlsPolicies($tableId)->create([ 'name' => 'owner can read', 'operation' => 'SELECT', 'conditions' => ['user_id' => '$auth.id'], ]); $project1->sql('SELECT COUNT(*) FROM todos');
Responses
Every request resolves to the same envelope — nothing throws on an API error by default,
matching the "check error" pattern of most BaaS clients:
final class LoxodontuResponse { public readonly mixed $data; public readonly ?array $error; // ['message' => string, 'status' => int] public readonly ?int $count; // from X-Total-Count on paginated list endpoints public readonly int $status; }
If you'd rather throw, call ->unwrap() — it returns data, or throws a LoxodontuError:
use AdaiasMagdiel\Loxodontu\LoxodontuError; try { $todos = $loxo->from('todos')->select()->get()->unwrap(); } catch (LoxodontuError $e) { // $e->getMessage(), $e->status() }
Session storage
Client's end-user token and Admin's platform token are held via a TokenStorage
implementation, passed as options.storage:
InMemoryStorage(default) — lives only for the current process; fine for a script that logs in and uses the token within the same run.SessionStorage— persists the token in PHP's$_SESSIONacross requests, for a traditional web app.- Your own implementation of the
TokenStorageinterface (getItem/setItem/removeItem) — a cookie, cache, or database row.
use AdaiasMagdiel\Loxodontu\SessionStorage; $loxo = new Client($url, $projectId, $apiKey, ['storage' => new SessionStorage()]);
Custom transport
Requests are sent via ext-curl by default (CurlTransport). Pass your own implementation of the
Transport interface via options.transport to swap it out — useful for testing, or for routing
through a different HTTP stack.
License
AGPL-3.0-only, matching Loxodontu itself.