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:

use function OpenFGA\{delete, tuple, tuples, write};

// 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:

use function OpenFGA\{tuple, tuples, writes};

// 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";
        }
    }
}


Advanced patterns

Conditional tuples

Use conditions to make permissions context-dependent:

use function OpenFGA\{tuple, tuples};

// Conditional tuples - permissions with conditions
// Note: The condition 'business_hours' must be defined in your authorization model
$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";
}
// When checking permissions with conditions, provide context
$checkResult = $client->check(
    store: $storeId,
    model: $modelId,
    tuple: tuple('user:contractor', 'viewer', 'document:confidential-report'),
    context: (object) [
        'time_of_day' => new DateTimeImmutable('14:30'),
        'timezone' => 'America/New_York',
    ],
);

if ($checkResult->succeeded() && $checkResult->unwrap()->getAllowed()) {
    echo "✓ Access granted (within business hours)\n";
}

Auditing changes

Monitor permission changes over time for auditing:

// Simple example to demonstrate listing changes
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:

use function OpenFGA\{allowed,tuple, tuples, write};

// Working with groups using write() helper
$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
// Let's verify this:
$canEdit = allowed(
    client: $client,
    store: $storeId,
    model: $modelId,
    tuple: tuple('user:anne', 'editor', 'document:technical-specs'),
);

if ($canEdit) {
    echo "✓ Confirmed: Anne can edit technical-specs through team membership\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.

use OpenFGA\Exceptions\{ClientError, ClientException};
use Throwable;

use function OpenFGA\{result, tuple, write};

// 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:

<?php

declare(strict_types=1);

use OpenFGA\{Client, Language};
use OpenFGA\Exceptions\{ClientError, ClientException};

use function OpenFGA\{tuple, tuples, write};

// Create a client with Spanish error messages
$client = new Client(
    url: $_ENV['FGA_API_URL'] ?? 'http://localhost:8080',
    language: Language::Spanish,
);

// Store configuration
$storeId = $_ENV['FGA_STORE_ID'];
$modelId = $_ENV['FGA_MODEL_ID'];

// 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";
    }
}

// Switch to French for another example
$frenchClient = new Client(
    url: $_ENV['FGA_API_URL'] ?? 'http://localhost:8080',
    language: Language::French,
);

$frenchResult = $frenchClient->writeTuples(
    store: $storeId,
    model: $modelId,
    writes: tuples(
        tuple('user:bob', 'invalid_relation', 'document:test'),
    ),
);

if ($frenchResult->failed()) {
    $error = $frenchResult->err();

    if ($error instanceof ClientException) {
        // Error message in French
        echo "Error (French): {$error->getMessage()}\n";

        // Same enum-based handling works regardless of language
        if (ClientError::Validation === $error->kind()) {
            echo "✓ Invalid relation 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** ⚠️