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