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.


Prerequisites

Before diving into the examples, make sure you have the necessary setup:

<?php

declare(strict_types=1);

use OpenFGA\Client;

// Initialize the OpenFGA client
$client = new Client(
    url: $_ENV['FGA_API_URL'] ?? 'http://localhost:8080',
);

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


Queries

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?"

Check permissions

This is the query your application will make most often. Use the allowed helper to check permissions and return a boolean value:

// 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 Client check method, and is intended for situations where graceful degradation is preferred over exception handling. It will silently ignore errors and return false if the request fails.

Use the Client check method if you need more control over the operation…
$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";
}

// Check with additional context
$checkResult = $client->check(

Check multiple permissions at once

Use the checks helper to check multiple permissions at once:

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

The checks helper wraps the Client batchCheck 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…
    echo "{$key}: allowed = " . ($allowed ? 'true' : 'false') . "\n";
}

// Batch check using client directly
$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";
        }
    }

List accessible objects

Use the objects helper to retrieve a list of objects a user can access.

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

Find users with permission

Use the users helper to retrieve a list of users who have a specific permission on an object.

$modelId = $_ENV['FGA_MODEL_ID'];

// Find users with permission using the helper
$viewers = users(
    'document:budget',
    'viewer',
    filter('user'),
    $client,
    $storeId,
    $modelId,
);

echo "Users who can view document:budget:\n";

foreach ($viewers as $user) {
    echo "- {$user}\n";
}

// Find users using the client directly
$result = $client->listUsers(
    store: $storeId,
    model: $modelId,
    object: 'document:budget-2024',
    relation: 'editor',
    userFilters: new UserTypeFilters([

The users helper wraps the Client listUsers 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…
        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";
        }
    }
}

Expand relationships

When permissions aren't working as expected, use the expand helper to discovery 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.

$modelId = $_ENV['FGA_MODEL_ID'];

// 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->getError()->getMessage();
    $tree = null;

The expand helper wraps the Client expand method and is intended for situations where graceful degradation is preferred over exception handling. It will silently ignore errors.

Use the Client expand method directly if you need more control over the operation…
}

// Expand another relationship
$result2 = $client->expand(
    store: $storeId,
    model: $modelId,
    tupleKey: tuple('', 'editor', 'folder:project-files'),
);

if ($result2->succeeded()) {
    $response = $result2->unwrap();
    $tree = $response->getTree();

    // Process the tree structure
    echo '
Expanded permission tree:
';
    printTree($tree->getRoot());


Advanced patterns

Contextual tuples

Test "what-if" scenarios without permanently saving relationships. Perfect for previewing permission changes.

// 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 can view the report
';
} else {
    echo 'anne cannot view the report
';
}

Consistency levels

For read-after-write scenarios, you might need stronger consistency:

<?php

declare(strict_types=1);

use OpenFGA\Client;
use OpenFGA\Models\Enums\Consistency;

use function OpenFGA\tuple;

// Initialize the OpenFGA client
$client = new Client(
    url: $_ENV['FGA_API_URL'] ?? 'http://localhost:8080',
);

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

// 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,
);

echo "Write completed\n";

if ($criticalCheck->succeeded()) {
    $allowed = $criticalCheck->unwrap()->getAllowed();
    echo $allowed
        ? 'Admin has owner permission (with high consistency)'
        : 'Admin does not have owner permission';
    echo "\n";
}

Advanced error handling

Use enum-based exceptions for more precise error handling with i18n support:

$modelId = $_ENV['FGA_MODEL_ID'];

// 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());


Common Query Patterns

Permission gates for routes

    }
}

// Usage
$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);
    }

Efficient data filtering

    {
        return match ($method) {
            'GET' => 'viewer',
            'PUT', 'PATCH' => 'editor',
            'DELETE' => 'owner',
            default => 'viewer',
        };
    }
}

// Efficient data filtering
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',
    );

Debugging permission issues

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

// Debugging permission issues
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;
                }
            }
        }
    }
⚠️ **GitHub.com Fallback** ⚠️