Tuples - evansims/openfga-php GitHub Wiki

Relationship tuples are where the rubber meets the road. They're the actual permissions in your system - they define who can do what to which resource.


Prerequisites

The examples in this guide assume you have the following setup:

<?php

declare(strict_types=1);

use OpenFGA\Client;

$client = new Client(
    url: $_ENV['FGA_API_URL'] ?? 'http://localhost:8080',
);

$storeId = $_ENV['FGA_STORE_ID'];
$modelId = $_ENV['FGA_MODEL_ID'];


Granting Permissions

Use the write helper to give someone access:

// Granting permissions using the write() helper
write(
    tuples: tuple('user:anne', 'viewer', 'document:planning-doc'),
    client: $client,
    store: $storeId,
    model: $modelId,
);

echo "✓ Anne can now view the planning document\n";

// Grant multiple permissions at once
write(
    tuples: tuples(
        tuple('user:anne', 'viewer', 'document:planning-doc'),
        tuple('user:bob', 'editor', 'document:planning-doc'),
    ),
    client: $client,
    store: $storeId,
    model: $modelId,
);


Removing Permissions

Use the delete helper to take away access:

// Removing permissions using the delete() helper
delete(
    tuples: tuple('user:anne', 'viewer', 'document:planning-doc'),
    client: $client,
    store: $storeId,
    model: $modelId,
);

echo "✓ Anne's view access has been revoked\n";

// Remove multiple permissions
delete(
    tuples: tuples(
        tuple('user:anne', 'viewer', 'document:planning-doc'),
        tuple('user:bob', 'editor', 'document:planning-doc'),
    ),
    client: $client,
    store: $storeId,
    model: $modelId,
);


Bulk Operations

Use the writes helper to handle multiple permission changes in one transaction:

// Bulk operations using the writes() helper
writes(
    $client,
    writes: tuples(
        // Add anne as viewer
        tuple('user:anne', 'viewer', 'document:roadmap'),
        // Add bob as editor
        tuple('user:bob', 'editor', 'document:roadmap'),
        // Make alice the owner
        tuple('user:alice', 'owner', 'document:roadmap'),
    ),
    store: $storeId,
    model: $modelId,
);

echo "✓ Bulk write completed successfully\n";


Reading Existing Permissions

Use the read helper to check what permissions exist:

use function OpenFGA\{read};

// Reading all tuples for a specific object
$tuples = read(
    client: $client,
    store: $storeId,
);

echo "Permissions for planning-doc:\n";

foreach ($tuples as $tuple) {
    if ('document:planning-doc' === $tuple->getObject()) {
        echo "- {$tuple->getUser()} has {$tuple->getRelation()} access\n";
    }
}

Alternatively, use the Client's readTuples method for more control:

// Reading permissions by user
$userResult = $client->readTuples(
    store: $storeId,
);

echo "\nAnne's permissions:\n";

if ($userResult->succeeded()) {
    $response = $userResult->unwrap();

    foreach ($response->getTuples() as $tuple) {
        if ('user:anne' === $tuple->getKey()->getUser()) {
            echo "- {$tuple->getKey()->getRelation()} on {$tuple->getKey()->getObject()}\n";
        }
    }
}
// Find all documents a user can edit
$editableResult = $client->readTuples(
    store: $storeId,
);

echo "\nDocuments Bob can edit:\n";

if ($editableResult->succeeded()) {
    $response = $editableResult->unwrap();

    foreach ($response->getTuples() as $tuple) {
        if ('user:bob' === $tuple->getKey()->getUser()
            && 'editor' === $tuple->getKey()->getRelation()
            && str_starts_with($tuple->getKey()->getObject(), 'document:')) {
            echo "- {$tuple->getKey()->getObject()}\n";
        }
    }
}


Advanced Patterns

Conditional Tuples

Use conditions to make permissions context-dependent:

$result = $client->writeTuples(
    store: $storeId,
    model: $modelId,
    writes: tuples(
        tuple(
            'user:contractor',
            'viewer',
            'document:confidential-report',
            new Condition(
                name: 'business_hours',
                expression: '', // Expression is defined in the model
                context: [
                    'timezone' => 'America/New_York',
                    'start_hour' => 9,
                    'end_hour' => 17,
                ],
            ),
        ),
    ),
);

if ($result->succeeded()) {
    echo "✓ Contractor can view confidential report during business hours\n";
}

Auditing Changes

Monitor permission changes over time for auditing:

echo "Recent permission changes:\n";

$result = $client->listTupleChanges(
    store: $storeId,
    pageSize: 2,
);

if ($result->succeeded()) {
    $response = $result->unwrap();
    $changes = $response->getChanges();

    if (0 < count($changes)) {
        foreach ($changes as $change) {
            $tuple = $change->getTupleKey();
            $timestamp = $change->getTimestamp()->format('Y-m-d H:i:s');
            $operation = $change->getOperation()->value;

            echo "[{$timestamp}] {$operation}: {$tuple->getUser()} {$tuple->getRelation()} {$tuple->getObject()}\n";
        }
    } else {
        echo "No changes found in the store.\n";
    }
}

Working with Groups

Use the write helper to grant permissions to groups instead of individual users:

$result = write(
    tuples: tuples(
        // Add user to a group
        tuple('user:anne', 'member', 'team:engineering'),
        // Grant permission to the entire group
        tuple('team:engineering#member', 'editor', 'document:technical-specs'),
    ),
    client: $client,
    store: $storeId,
    model: $modelId,
);

echo "✓ Anne added to engineering team\n";
echo "✓ Engineering team granted editor access to technical specs\n";

Now Anne can edit the technical specs because she's a member of the engineering team.

For checking permissions and querying relationships, see Queries.


Error Handling with Tuples

The SDK has a powerful enum-based exception handling system that allows you to handle errors in a type-safe way.

// Example: Writing tuples with robust error handling
function addUserToDocument(Client $client, string $storeId, string $modelId, string $userId, string $documentId, string $role = 'viewer'): bool
{
    return result(fn () => write(
        tuples: tuple("user:{$userId}", $role, "document:{$documentId}"),
        client: $client,
        store: $storeId,
        model: $modelId,
    ))
        ->success(function () use ($userId, $documentId, $role): void {
            echo "✓ Access granted: {$userId} as {$role} on {$documentId}\n";
        })
        ->then(fn () => true)
        ->failure(function (Throwable $error): void {
            if ($error instanceof ClientException) {
                match ($error->kind()) {
                    ClientError::Validation => print ('⚠️  Validation error granting access: ' . json_encode($error->context()) . PHP_EOL),
                    ClientError::Configuration => print ("❌ Model configuration error: {$error->getMessage()}\n"),
                    default => print ("❌ Failed to grant access: {$error->kind()->name}\n"),
                };
            } else {
                print "❌ Unexpected error: {$error->getMessage()}\n";
            }
        })
        ->unwrap();
}

// Usage example
$success = addUserToDocument($client, $storeId, $modelId, 'anne', 'budget-2024', 'editor');

if ($success) {
    echo "Permission successfully granted!\n";
}

Supporting Multiple Languages

The error messages from tuple operations will automatically use the language configured in your client:

// Attempt to write an invalid tuple
try {
    // This will throw a validation exception because of invalid identifier format
    $tupleKey = tuple('user: anne', 'viewer', 'document:report');
} catch (ClientException $e) {
    // The error message will be in Spanish
    echo "Error (Spanish): {$e->getMessage()}\n";

    // But the error enum remains the same for consistent handling
    if (ClientError::Validation === $e->kind()) {
        echo "Validation error detected\n";
    }
}


What's Next?

After writing tuples to grant permissions, you'll want to verify those permissions are working correctly. The Queries guide covers how to check permissions, list user access, and discover relationships using the tuples you've created.

⚠️ **GitHub.com Fallback** ⚠️