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;

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

$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:

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

Check multiple permissions at once

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 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…
$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.

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

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 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…
$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";
        }
    }
}

Expand relationships

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


Advanced patterns

Contextual tuples

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";
}

Consistency levels

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

Advanced error handling

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


Common Query Patterns

Permission gates for routes

$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',
        };
    }
}

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

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

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

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