Queries - evansims/openfga-php GitHub Wiki
Queries are how your application enforces access control. With an authorization model and relationship tuples in place, it's time to put them to use.
Before diving into the examples, make sure you have the necessary 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'];
Queries let you ask OpenFGA about permissions. There are four types:
-
Check permissions
"Can Alice edit this document?" -
List accessible objects
"What documents can Alice edit?" -
Find users with permission
"Who can edit this document?" -
Expand relationships
"How does Alice have edit access?"
This is the query your application will make most often. Use the allowed
helper to check permissions and return a boolean value:
use function OpenFGA\{allowed, tuple, tuples};
// Using the allowed() helper for simple checks
$canView = allowed(
client: $client,
store: $storeId,
model: $modelId,
user: 'user:anne',
relation: 'viewer',
object: 'document:budget',
);
if ($canView) {
echo "Anne CAN view the budget document\n";
} else {
echo "Anne CANNOT view the budget document\n";
}
The
allowed
helper wraps the Clientcheck
method, and is intended for situations where graceful degradation is preferred over exception handling. It will silently ignore errors and returnfalse
if the request fails.
Use the Client check
method if you need more control over the operation…
// Direct check method for more control
$result = $client->check(
store: $storeId,
model: $modelId,
tupleKey: tuple('user:anne', 'viewer', 'document:roadmap'),
);
if ($result->succeeded()) {
$response = $result->unwrap();
if ($response->getAllowed()) {
echo "Anne CAN view the roadmap\n";
} else {
echo "Anne CANNOT view the roadmap\n";
}
} else {
// Handle failure
echo "Error checking permission\n";
}
Use the checks
helper to check multiple permissions at once:
use OpenFGA\Models\Collections\BatchCheckItems;
use function OpenFGA\{check, checks};
// Check multiple permissions using the checks() helper
$permissions = checks(
$client,
$storeId,
$modelId,
check(
correlation: 'alice_budget_viewer',
user: 'user:alice',
relation: 'viewer',
object: 'document:budget',
),
check(
correlation: 'alice_budget_editor',
user: 'user:alice',
relation: 'editor',
object: 'document:budget',
),
check(
correlation: 'bob_report_editor',
user: 'user:bob',
relation: 'editor',
object: 'document:report',
),
);
// Process results
foreach ($permissions as $key => $allowed) {
echo "{$key}: allowed = " . ($allowed ? 'true' : 'false') . "\n";
}
The
checks
helper wraps the ClientbatchCheck
method, and is intended for situations where graceful degradation is preferred over exception handling. It will silently ignore errors.
Use the Client batchCheck
method directly if you need more control over the operation…
$result = $client->batchCheck(
store: $storeId,
model: $modelId,
checks: new BatchCheckItems([
check(
user: 'user:bob',
relation: 'viewer',
object: 'document:strategy',
),
check(
user: 'user:bob',
relation: 'editor',
object: 'document:strategy',
),
check(
user: 'user:bob',
relation: 'owner',
object: 'document:strategy',
),
check(
user: 'user:bob',
relation: 'viewer',
object: 'document:roadmap',
),
]),
);
if ($result->succeeded()) {
$response = $result->unwrap();
$results = $response->getResult();
foreach ($results as $correlationId => $checkResult) {
if (null !== $checkResult->getError()) {
echo "Check {$correlationId} failed: {$checkResult->getError()->getMessage()}\n";
} else {
$allowed = $checkResult->getAllowed();
echo "Check {$correlationId}: " . ($allowed ? 'allowed' : 'denied') . "\n";
}
}
}
Use the objects
helper to retrieve a list of objects a user can access.
use function OpenFGA\{objects, tuple, tuples};
// List accessible objects using the helper
$documents = objects(
'document',
'viewer',
'user:anne',
$client,
$storeId,
$modelId,
);
echo "Documents Anne can view:\n";
foreach ($documents as $documentId) {
echo "- {$documentId}\n";
}
The
objects
helper wraps the ClientstreamedListObjects
method and is intended for situations where graceful degradation is preferred over exception handling. It will silently ignore errors.
Use the Client streamedListObjects
or listObjects
methods directly if you need more control over the operation…
$result = $client->streamedListObjects(
store: $storeId,
model: $modelId,
user: 'user:bob',
relation: 'owner',
type: 'document',
);
if ($result->succeeded()) {
$generator = $result->unwrap();
$objects = [];
foreach ($generator as $streamedResponse) {
$objects[] = $streamedResponse->getObject();
}
echo 'Bob owns ' . count($objects) . " documents:\n";
foreach ($objects as $object) {
echo "- {$object}\n";
}
}
Use the users
helper to retrieve a list of users who have a specific permission on an object.
use OpenFGA\Models\Collections\UserTypeFilters;
use OpenFGA\Models\UserTypeFilter;
use function OpenFGA\{filter, users};
// Find users with permission using the helper
$viewers = users(
object: 'document:budget',
relation: 'viewer',
filters: filter('user'),
client: $client,
store: $storeId,
model: $modelId,
);
echo "Users who can view document:budget:\n";
foreach ($viewers as $user) {
echo "- {$user}\n";
}
The
users
helper wraps the ClientlistUsers
method and is intended for situations where graceful degradation is preferred over exception handling. It will silently ignore errors.
Use the Client listUsers
method directly if you need more control over the operation…
$result = $client->listUsers(
store: $storeId,
model: $modelId,
object: 'document:budget-2024',
relation: 'editor',
userFilters: new UserTypeFilters([
new UserTypeFilter('user'),
new UserTypeFilter('team', 'member'),
]),
);
if ($result->succeeded()) {
$response = $result->unwrap();
$users = $response->getUsers();
echo "Users and teams who can edit budget-2024:\n";
foreach ($users as $user) {
$identifier = $user->getObject();
if ($user->getRelation()) {
echo "- {$identifier} (relation: {$user->getRelation()})\n";
} else {
echo "- {$identifier}\n";
}
}
}
When permissions aren't working as expected, use the Client expand
method to discover why. It returns the complete relationship tree, and is useful for debugging complex permission structures or understanding why a user has (or doesn't have) access.
use function OpenFGA\tuple;
// Expand relationships using the client
$result = $client->expand(
store: $storeId,
model: $modelId,
tupleKey: tuple('', 'viewer', 'document:planning-doc'),
);
if ($result->succeeded()) {
$tree = $result->unwrap()->getTree();
} else {
echo 'Error expanding: ' . $result->err()->getMessage();
$tree = null;
}
Test "what-if" scenarios without permanently saving relationships. Perfect for previewing permission changes.
use function OpenFGA\{tuple, tuples};
// Test "what-if" scenarios without permanently saving relationships
// What if anne was in the engineering team?
$wouldHaveAccess = $client->check(
store: $storeId,
model: $modelId,
tupleKey: tuple('user:anne', 'viewer', 'document:report'),
contextualTuples: tuples(
tuple('user:anne', 'member', 'team:engineering'),
),
);
if ($wouldHaveAccess->succeeded() && $wouldHaveAccess->unwrap()->getAllowed()) {
echo "Anne would have viewer access through team membership\n";
} else {
echo "Anne would not have viewer access through team membership\n";
}
For read-after-write scenarios, you might need stronger consistency:
use OpenFGA\Models\Enums\Consistency;
use function OpenFGA\tuple;
// Use MINIMIZE_LATENCY for fast reads (default)
$result = $client->check(
store: $storeId,
model: $modelId,
tupleKey: tuple('user:anne', 'viewer', 'document:report'),
consistency: Consistency::MINIMIZE_LATENCY,
);
// Use HIGHER_CONSISTENCY for critical operations
$criticalCheck = $client->check(
store: $storeId,
model: $modelId,
tupleKey: tuple('user:admin', 'owner', 'document:configuration'),
consistency: Consistency::HIGHER_CONSISTENCY,
);
Use enum-based exceptions for more precise error handling with i18n support:
use OpenFGA\Exceptions\{AuthenticationException, ClientException, NetworkException};
use OpenFGA\Exceptions\{ClientError, NetworkError};
use OpenFGA\Models\TupleKey;
use RuntimeException;
// Advanced error handling service
final class PermissionService
{
public function __construct(
private Client $client,
private string $storeId,
private string $modelId,
) {
}
public function checkAccess(string $user, string $relation, string $object): bool
{
$result = $this->client->check(
store: $this->storeId,
model: $this->modelId,
tupleKey: new TupleKey($user, $relation, $object),
);
// Handle specific error cases
if ($result->failed()) {
$error = $result->err();
if ($error instanceof NetworkException) {
// Handle network-specific errors
match ($error->kind()) {
NetworkError::Timeout => $this->logTimeout($user, $object),
NetworkError::Server => $this->alertOpsTeam(),
NetworkError::Request => $this->logGenericNetworkError($error),
default => $this->logGenericNetworkError($error),
};
// Fallback to cached permissions or safe default
return $this->getCachedPermission($user, $relation, $object) ?? false;
}
if ($error instanceof AuthenticationException) {
// Authentication errors should be fatal
throw new RuntimeException('Service authentication failed');
}
if ($error instanceof ClientException) {
// Handle client errors
match ($error->kind()) {
ClientError::Validation => $this->logInvalidRequest($error),
ClientError::Configuration => $this->reconfigureStore(),
default => $this->logClientError($error),
};
return false;
}
// Unknown error type - log and deny access
$this->logUnknownError($error);
return false;
}
return $result->unwrap()->getAllowed();
}
private function alertOpsTeam(): void
{
// Send alert to operations team
}
private function getCachedPermission(string $user, string $relation, string $object): ?bool
{
// Check cache for recent permission result
return null;
}
private function logClientError(Throwable $error): void
{
error_log('Client error: ' . $error->getMessage());
}
private function logGenericNetworkError(Throwable $error): void
{
error_log('Network error: ' . $error->getMessage());
}
private function logInvalidRequest(Throwable $error): void
{
error_log('Invalid request: ' . $error->getMessage());
}
private function logTimeout(string $user, string $object): void
{
error_log("Permission check timeout for {$user} on {$object}");
}
private function logUnknownError(Throwable $error): void
{
error_log('Unknown error: ' . $error::class . ' - ' . $error->getMessage());
}
private function reconfigureStore(): void
{
// Attempt to reconfigure with valid store
}
}
$service = new PermissionService($client, $storeId, $modelId);
$canAccess = $service->checkAccess('user:alice', 'viewer', 'document:roadmap');
// Permission-aware middleware
final class FgaAuthMiddleware
{
public function __construct(
private Client $client,
private string $storeId,
private string $modelId,
) {
}
public function handle($request, $next)
{
$user = $request->user();
$resource = $request->route('resource');
$action = $this->mapHttpMethodToRelation($request->method());
$allowed = $this->client->check(
store: $this->storeId,
model: $this->modelId,
tupleKey: new TupleKey("user:{$user->id}", $action, "document:{$resource}"),
)->unwrap()->getAllowed();
if (! $allowed) {
throw new RuntimeException('Access denied');
}
return $next($request);
}
private function mapHttpMethodToRelation(string $method): string
{
return match ($method) {
'GET' => 'viewer',
'PUT', 'PATCH' => 'editor',
'DELETE' => 'owner',
default => 'viewer',
};
}
}
function getEditableDocuments(Client $client, string $storeId, string $modelId, string $userId): array
{
// Get all documents the user can edit
$result = $client->streamedListObjects(
store: $storeId,
model: $modelId,
user: "user:{$userId}",
relation: 'editor',
type: 'document',
);
if ($result->failed()) {
return [];
}
$generator = $result->unwrap();
$documentIds = [];
foreach ($generator as $streamedResponse) {
$documentIds[] = $streamedResponse->getObject();
}
// Convert to your document IDs
return array_map(
fn ($objectId) => str_replace('document:', '', $objectId),
$documentIds,
);
}
function debugUserAccess(Client $client, string $storeId, string $modelId, string $user, string $object): void
{
echo "Debugging access for {$user} on {$object}\n";
// Check all possible relations
$relations = ['viewer', 'editor', 'owner'];
foreach ($relations as $relation) {
$result = $client->check(
store: $storeId,
model: $modelId,
tupleKey: new TupleKey($user, $relation, $object),
);
if ($result->succeeded()) {
$allowed = $result->unwrap()->getAllowed() ? '✓' : '✗';
echo "{$relation}: {$allowed}\n";
if ($allowed) {
// Expand to see why they have access
$tree = $client->expand(
store: $storeId,
tupleKey: new TupleKey($user, $relation, $object),
model: $modelId,
)->unwrap()->getTree();
echo ' Access path: ';
// Analyze tree structure to show access path
analyzeAccessPath($tree, $user);
echo "\n";
}
}
}
}
function analyzeAccessPath($tree, $targetUser, $path = []): bool
{
if (null === $tree) {
return false;
}
// Check leaf nodes for direct user assignment
if (null !== $tree->getLeaf()) {
$users = $tree->getLeaf()->getUsers();
if (null !== $users) {
foreach ($users as $user) {
if ($user->getUser() === $targetUser) {
echo implode(' → ', array_merge($path, [$targetUser]));
return true;
}
}
}
}
// Check union nodes (OR relationships)
if (null !== $tree->getUnion()) {
$nodes = $tree->getUnion()->getNodes();
foreach ($nodes as $index => $node) {
$nodePath = $path;
if (1 < count($nodes)) {
$nodePath[] = "branch_{$index}";
}
if (analyzeAccessPath($node, $targetUser, $nodePath)) {
return true;
}
}
}
return false;
}