Relationship tuples are where the rubber meets the road. They’re the actual permissions in your system - who can do what to which resource.
A tuple is simply: (user, relation, object)
For example: (user:anne, editor, document:roadmap)
means “Anne can edit the roadmap document.”
The examples in this guide assume you have the following setup:
<?php
use OpenFGA\Client;
use OpenFGA\Exceptions\{ClientError, ClientException};
use OpenFGA\Models\{ConditionParameter, ConditionParameters, RelationshipCondition, TupleKey, TupleKeys};
use function OpenFGA\{tuple, tuples, write, delete, result};
// Client initialization - see Getting Started for full details
$client = new Client(url: 'http://localhost:8080');
// Store and model identifiers from your configuration
$storeId = 'your-store-id';
$modelId = 'your-model-id';
Give someone access by writing a tuple:
// Give Anne editor access to a document
write(
client: $client,
store: $storeId,
model: $modelId,
tuples: tuple('user:anne', 'editor', 'document:roadmap')
);
Take away access by deleting a tuple:
// Remove Anne's editor access
delete(
client: $client,
store: $storeId,
model: $modelId,
tuples: tuple('user:anne', 'editor', 'document:roadmap')
);
Handle multiple permission changes in one transaction:
// Grant access to multiple users and revoke old permissions
$client->writeTuples(
store: $storeId,
model: $modelId,
writes: tuples(
tuple('user:bob', 'viewer', 'document:roadmap'),
tuple('user:charlie', 'editor', 'document:roadmap'),
tuple('team:marketing#member', 'viewer', 'folder:campaigns')
),
deletes: tuples(
tuple('user:anne', 'owner', 'document:old-spec')
)
)->unwrap();
Check what permissions exist by reading tuples:
// Find all permissions for a specific document
$response = $client->readTuples(
store: $storeId,
model: $modelId,
tupleKey: new TupleKey(object: 'document:roadmap')
)->unwrap();
foreach ($response->getTuples() as $tuple) {
echo "{$tuple->getUser()} has {$tuple->getRelation()} on {$tuple->getObject()}\n";
}
// Find all documents Anne can edit
$response = $client->readTuples(
store: $storeId,
model: $modelId,
tupleKey: new TupleKey(user: 'user:anne', relation: 'editor')
)->unwrap();
foreach ($response->getTuples() as $tuple) {
echo "Anne can edit: {$tuple->getObject()}\n";
}
// Paginate through all tuples
$continuationToken = null;
do {
$response = $client->readTuples(
store: $storeId,
model: $modelId,
pageSize: 100,
continuationToken: $continuationToken
)->unwrap();
foreach ($response->getTuples() as $tuple) {
// Process each tuple...
}
$continuationToken = $response->getContinuationToken();
} while ($continuationToken !== null);
Add conditions to make permissions context-dependent:
// Only allow access during business hours
$client->writeTuples(
store: $storeId,
model: $modelId,
writes: new TupleKeys([
new TupleKey(
user: 'user:contractor',
relation: 'viewer',
object: 'document:sensitive',
condition: new RelationshipCondition(
name: 'business_hours',
context: [
'timezone' => 'America/New_York'
]
)
)
])
)->unwrap();
Monitor permission changes over time for auditing:
// Get all permission changes for documents in the last hour
$startTime = (new DateTimeImmutable())->sub(new DateInterval('PT1H'));
$response = $client->listTupleChanges(
store: $storeId,
model: $modelId,
type: 'document',
startTime: $startTime
)->unwrap();
foreach ($response->getChanges() as $change) {
$tuple = $change->getTupleKey();
echo "{$change->getOperation()->value}: {$tuple->getUser()} {$tuple->getRelation()} {$tuple->getObject()}\n";
}
Grant permissions to groups instead of individual users:
// Add user to a group
write(
client: $client,
store: $storeId,
model: $modelId,
tuples: tuple('user:anne', 'member', 'team:engineering')
);
// Grant permission to the entire group
write(
client: $client,
store: $storeId,
model: $modelId,
tuples: tuple('team:engineering#member', 'editor', 'document:technical-specs')
);
Now Anne can edit the technical specs because she’s a member of the engineering team.
For checking permissions and querying relationships, see Queries.
When working with tuples, it’s important to handle errors properly using the SDK’s enum-based exception handling:
// Example: Writing tuples with robust error handling
function addUserToDocument(string $userId, string $documentId, string $role = 'viewer'): bool
{
global $client, $storeId, $modelId;
// Use result helper for cleaner error handling
return result(function() use ($client, $storeId, $modelId, $userId, $documentId, $role) {
return write(
client: $client,
store: $storeId,
model: $modelId,
tuples: tuple("user:{$userId}", $role, "document:{$documentId}")
);
})
->success(function() {
logger()->info('Access granted', [
'user' => $userId,
'document' => $documentId,
'role' => $role
]);
return true;
})
->failure(function(Throwable $error) use ($userId, $documentId, $role) {
// Enum-based error handling with match expression
if ($error instanceof ClientException) {
match($error->getError()) {
// Handle validation errors specifically
ClientError::Validation => logger()->warning(
'Validation error granting access',
['context' => $error->getContext()]
),
// Handle authorization model mismatches
ClientError::InvalidConfiguration => logger()->error(
'Model configuration error',
['message' => $error->getMessage()]
),
// Default case for other client errors
default => logger()->error(
'Failed to grant access',
['error_type' => $error->getError()->name]
)
};
} else {
// Handle unexpected errors
logger()->error('Unexpected error granting access', [
'error' => $error->getMessage(),
'user' => $userId,
'document' => $documentId
]);
}
return false;
})
->unwrap();
}
The error messages from tuple operations will automatically use the language configured in your client:
// Create a client with Spanish error messages
$client = new Client(
url: 'https://api.openfga.example',
language: 'es' // Spanish
);
try {
// Attempt to write an invalid tuple
write(
client: $client,
store: $storeId,
model: $modelId,
tuples: tuple('', 'viewer', 'document:report')
);
} catch (ClientException $e) {
// The error message will be in Spanish
echo $e->getMessage(); // "El identificador del usuario no puede estar vacío"
// But the error enum remains the same for consistent handling
if ($e->getError() === ClientError::Validation) {
// Handle validation error regardless of language
}
}