PHP SDK

PHP client for InferaDB with Laravel and Symfony integration.

Coming soon. The PHP SDK is under active development. The API surface shown here is based on the Rust SDK and may change before release.

Typed client for InferaDB’s authorization APIs. Requires PHP 8.2+. Works with Laravel, Symfony, and any PSR-18 HTTP client.

Installation

composer require inferadb/inferadb-php

Authentication

Three authentication methods:

Method Use Case Security
Client Credentials (Ed25519 JWT) Service-to-service High
Bearer Token User sessions, OAuth Medium
API Key Testing, simple integrations Basic
use InferaDB\InferaDB;
use InferaDB\Auth\Ed25519PrivateKey;
use InferaDB\Auth\ClientCredentials;

$client = InferaDB::builder()
    ->url('https://engine.inferadb.com')
    ->credentials(new ClientCredentials(
        clientId: 'my-client',
        privateKey: Ed25519PrivateKey::fromPemFile('client.pem'),
        certificateId: 'cert-id',
    ))
    ->build();

Bearer Token

$client = InferaDB::builder()
    ->url('https://engine.inferadb.com')
    ->bearerToken(getenv('INFERADB_TOKEN'))
    ->build();

API Key

$client = InferaDB::builder()
    ->url('https://engine.inferadb.com')
    ->apiKey(getenv('INFERADB_API_KEY'))
    ->build();

Permission Checks

$vault = $client->organization('my-org')->vault('production');

// Simple check
$allowed = $vault->check('user:alice', 'can_edit', 'document:readme');

With ABAC Context

$allowed = $vault->check('user:alice', 'can_view', 'document:readme', [
    'context' => ['ip_address' => '10.0.0.1'],
]);

Require — Throws on Deny

use InferaDB\Exceptions\AccessDeniedException;

// Throws AccessDeniedException if permission is denied
$vault->require('user:alice', 'can_edit', 'document:readme');

With Consistency Token

$allowed = $vault->check('user:alice', 'can_view', 'document:readme', [
    'atLeastAsFresh' => $revisionToken,
]);

Batch Check

use InferaDB\CheckRequest;

$results = $vault->checkBatch([
    new CheckRequest('user:alice', 'can_edit', 'document:readme'),
    new CheckRequest('user:bob', 'can_view', 'document:readme'),
]);

if ($results->allAllowed()) {
    // all checks passed
}

Relationships

Write

use InferaDB\Relationship;

// Returns a revision token
$token = $vault->relationships()->write(
    new Relationship('document:readme', 'editor', 'user:alice')
);

Batch Write

$vault->relationships()->writeBatch([
    new Relationship('document:readme', 'editor', 'user:alice'),
    new Relationship('document:readme', 'viewer', 'user:bob'),
]);

List

$rels = $vault->relationships()
    ->list()
    ->resource('document:readme')
    ->collect();

Delete

$vault->relationships()
    ->deleteWhere()
    ->resource('document:readme')
    ->relation('viewer')
    ->subject('user:bob')
    ->execute();

Lookups

// What resources can Alice view?
$resources = $vault->resources()
    ->accessibleBy('user:alice')
    ->withPermission('can_view')
    ->resourceType('document')
    ->collect();

// Who can edit this document?
$subjects = $vault->subjects()
    ->withPermission('can_edit')
    ->onResource('document:readme')
    ->collect();

Testing

MockClient (Fastest)

use InferaDB\Testing\MockClient;

$client = MockClient::builder()
    ->onCheck('user:alice', 'can_edit', 'document:readme')->allow()
    ->onCheck('user:bob', 'can_edit', 'document:readme')->deny()
    ->onCheckAnySubject('can_view', 'document:readme')->allow()
    ->defaultDeny()
    ->verifyOnDestruct(true) // asserts all expectations were invoked on destruct
    ->build();

InMemoryClient (Full Policy Evaluation)

use InferaDB\Testing\InMemoryClient;

$client = InMemoryClient::withSchemaAndData(
    schema: <<<'IPL'
    type document {
        relation viewer
        relation editor
        relation can_view = viewer | editor
    }
    IPL,
    data: [
        new Relationship('document:readme', 'editor', 'user:alice'),
        new Relationship('document:readme', 'viewer', 'user:bob'),
    ],
);

TestVault (Real Instance)

use InferaDB\Testing\TestVault;

$vault = TestVault::create($org, schema: $schemaIpl);
// vault auto-cleans up on __destruct
// call $vault->preserve() to keep data for debugging

PHPUnit Integration

use PHPUnit\Framework\TestCase;
use InferaDB\Testing\InMemoryClient;

class DocumentAuthorizationTest extends TestCase
{
    private InMemoryClient $client;

    protected function setUp(): void
    {
        $this->client = InMemoryClient::withSchemaAndData(
            schema: '...',
            data: [new Relationship('document:readme', 'editor', 'user:alice')],
        );
    }

    public function testAliceCanEdit(): void
    {
        $vault = $this->client->organization('test')->vault('test');
        $this->assertTrue($vault->check('user:alice', 'can_edit', 'document:readme'));
    }
}

Error Handling

use InferaDB\Exceptions\AccessDeniedException;
use InferaDB\Exceptions\InferaDBException;

try {
    $vault->require('user:alice', 'can_edit', 'document:readme');
} catch (AccessDeniedException $e) {
    // permission denied
} catch (InferaDBException $e) {
    if ($e->isRetriable()) {
        // retry after $e->getRetryAfter() seconds
    }
    error_log(sprintf('Authorization error: kind=%s, requestId=%s',
        $e->getKind()->value, $e->getRequestId()));
}

ErrorKind enum: Unauthorized, Forbidden, NotFound, RateLimited, SchemaViolation, Unavailable, Timeout, InvalidArgument.

Framework Integrations

Laravel Middleware

// app/Http/Middleware/InferaDBAuthorize.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use InferaDB\VaultClient;
use InferaDB\Exceptions\AccessDeniedException;

class InferaDBAuthorize
{
    public function __construct(private VaultClient $vault) {}

    public function handle(Request $request, Closure $next, string $permission): mixed
    {
        $userId = $request->user()->id;
        $documentId = $request->route('document');

        try {
            $this->vault->require(
                "user:{$userId}",
                $permission,
                "document:{$documentId}",
            );
        } catch (AccessDeniedException) {
            abort(403);
        }

        return $next($request);
    }
}
Route::get('/documents/{document}', [DocumentController::class, 'show'])
    ->middleware('inferadb:can_view');

Route::put('/documents/{document}', [DocumentController::class, 'update'])
    ->middleware('inferadb:can_edit');

Laravel Gate

// app/Providers/AuthServiceProvider.php
use Illuminate\Support\Facades\Gate;
use InferaDB\VaultClient;

Gate::define('view-document', function ($user, $document) {
    return app(VaultClient::class)->check(
        "user:{$user->id}",
        'can_view',
        "document:{$document->id}",
    );
});

Symfony Voter

// src/Security/InferaDBVoter.php
namespace App\Security;

use InferaDB\VaultClient;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class InferaDBVoter extends Voter
{
    public function __construct(private VaultClient $vault) {}

    protected function supports(string $attribute, mixed $subject): bool
    {
        return str_starts_with($attribute, 'INFERADB_');
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token,
    ): bool {
        $user = $token->getUser();
        $permission = strtolower(str_replace('INFERADB_', '', $attribute));

        return $this->vault->check(
            "user:{$user->getUserIdentifier()}",
            $permission,
            "document:{$subject->getId()}",
        );
    }
}