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.
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'];
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,
);
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,
);
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";
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";
}
}
}
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";
}
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";
}
}
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.
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";
}
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";
}
}
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.