Download the PHP package xpiston/openfga without Composer
On this page you can find all versions of the php package xpiston/openfga. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download xpiston/openfga
More information about xpiston/openfga
Files in xpiston/openfga
Package openfga
Short Description A PHP SDK for OpenFGA: checks, relationship tuples, authorization models and a real DSL parser, for Laravel, Symfony or no framework at all.
License MIT
Homepage https://github.com/xPiston/openfga
Informations about the package openfga
OpenFGA is a CNCF authorization engine built on Google's Zanzibar paper: you describe your permissions as a graph of relationships, write facts into it, and ask it questions. Can Anne view this document? โ answered in milliseconds, whether the answer comes from one tuple or from six hops through a folder hierarchy and two group memberships.
This package speaks all of it: the twenty-odd endpoints, the three authentication modes, the streaming queries, and โ the part that does not exist anywhere else in PHP โ the DSL.
Everything below was checked against a real OpenFGA server
OpenFGA is open source and starts in one command, so unlike a hosted platform there is no excuse for an SDK that has never spoken to it. This one has a live test suite that runs in CI against OpenFGA v1.21.0 on every push, and the DSL parser is additionally held to OpenFGA's own 29 reference fixtures.
Where the documentation and the server disagree, this README says what the server does.
๐ณ๏ธ The gap this fills
There is no official OpenFGA SDK for PHP. Measured rather than asserted:
Official SDKs in the openfga org |
go, js, python, java, dotnet โ all pushed within the last week |
| Official PHP SDK | none |
evansims/openfga-php |
archived on GitHub, marked abandoned on Packagist, last release July 2025, 6 open issues |
evansims/openfga-laravel |
archived, one release ever, ~3 downloads a month |
So: no first-party option, and the community one was put down. This package starts from PHP 8.2 rather than 8.3, so it installs where the archived one would not.
โจ What's inside
- ๐ A real DSL parser โ
.fgafiles to JSON and back, written against OpenFGA's published ANTLR grammar and checked against their own reference fixtures. No other PHP package does this; the reference transformer exists once, in TypeScript, as a separate tool - โก The five questions โ
check,batchCheck,listObjects,listUsers,expandโ with a paragraph on which to use, because picking wrong is what makes an integration slow - ๐ Streaming โ
streamListObjectsreads a result set larger than memory, and is not capped by the server's list limit - ๐งฉ Conditions (ABAC) โ time-limited grants, IP ranges, anything CEL can express, with the context passed per check
- ๐ฎ Contextual tuples โ "could Anne see it if we added her to this group?", answered without writing anything
- ๐ All three auth modes โ none, pre-shared key, OAuth2 client credentials with early refresh and jitter
- ๐งฐ Laravel: a Gate hook, route middleware, an Eloquent trait.
Symfony: a Security voter, so
#[IsGranted('viewer', subject: 'document')]just works. Both abstain for anything that did not opt in - ๐ง No retries, no magic, no failing open โ and a paragraph on each
- ๐งช 355 tests plus 30 against a live server, PHPStan at max
๐ Quick start
You also need any PSR-18 client. If you have none:
Start a server โ this is the whole setup, there is no account to create:
Then, for the rest of the application's life:
That model: argument is the most important one in this README. See
the model id.
๐ The DSL
Nobody writes an authorization model in JSON. They write it in OpenFGA's DSL, in a file next to the code it governs:
The HTTP API accepts only JSON, and every official SDK leaves that gap โ the transformer lives once, in TypeScript, in a separate package. This closes it:
What "checked" means here
Three claims, each with a test suite behind it:
| DSL โ JSON matches OpenFGA's reference transformer | 29/29 of their own fixtures โ including the real GitHub, Google Drive, Slack and banking models, and a torture case with eight nested mixed-operator relations |
| The round trip holds, both ways | 29/29 for JSON โ DSL โ JSON, and 29/29 for DSL โ DSL โ JSON |
| A real server agrees | every model above deployed to OpenFGA v1.21.0, read back, and compared as a model |
| Invalid DSL is refused | the 50 syntax-error cases in their validation corpus |
Using OpenFGA's fixtures rather than mine is the whole point: a transformer that agrees with examples its own author wrote has proved only that the author is consistent.
Syntax here, semantics there
This parser does syntax. The server does semantics, and that division is deliberate rather than lazy. Three of OpenFGA's own transformer fixtures parse perfectly here and are then refused by the server:
Those are model-validity and CEL-compilation rules. Re-implementing them in PHP
would be a second opinion that is sometimes wrong, so a condition's expression
is captured verbatim โ braces and quotes respected, so OpenFGA's own
x=="{}" comes out right โ and handed to the server to compile. There is
a live test for the harder case the
fixtures do not cover: x == "{", where a brace counter without string
awareness also breaks.
It tells you where
The grammar has rules that surprise people, and each gets a message rather than a token dump:
- Operators do not mix.
a and b or cis a syntax error, not an implicit grouping โ there is no precedence in this language, and inventing one would accept models the server rejects and silently change the meaning of others. but nottakes one right-hand side.a but not b but not chas to bea but not (b or c).- A direct assignment may only lead, at any depth.
- Containers do not nest.
list<list>is refused.
โ The five questions
Which one you ask is the decision that decides whether your application is fast or slow.
listUsers takes one kind of user per call. The wire field is an array, and
the OpenAPI definition declares no constraint on it โ but the server answers two
elements with "invalid ListUsersRequest.UserFilters: value must contain exactly
1 item(s)". So the signature takes a string: a list would have invited a 400 on
the second element. Measured on a live server, which is the only reason this
README does not say otherwise.
listObjects is the one that surprises people in production. It is bounded
by --listObjects-max-results (1000 by default) and --listObjects-deadline
(3s), and it does not tell you it truncated. For a page of objects you
already have, batchCheck is almost always right instead. For "show me
everything", use streamListObjects, which has no result cap.
expand deserves one note, because reading it wrong is easy. A relation
defined as [user] or owner expands into the directly assigned users and a
computed reference still to be followed โ so users() alone is a half-truth,
and usersets() holds the other half:
๐ Tuples
A write is transactional: all of it or none of it. That is a feature โ "add
Anne, remove Bob" is one change โ and it is why a list over the server's
--max-tuples-per-write is refused here rather than split. Splitting would
turn one atomic change into several that can half-fail, which is worse than an
error.
Three shapes of user, and they are different things rather than variations:
The third makes a relation public. Granting [user:*] grants the
unauthenticated, and it looks like a glob. The second is what makes the model
worth using: the grant follows the group's membership with no further tuples.
user:* is a perfectly good user and never a valid object โ the server
refuses document:* outright โ which is why this library has separate User
and TypedObject types rather than one Identifier that cannot say so.
Reading is not checking
read returns tuples that were written. It does no graph traversal, so a
document with no viewer tuples does not mean nobody can view it. There is
a live test asserting exactly that
asymmetry, because conflating the two is a plausible and expensive mistake.
Mirroring with the change log
Store the token, come back later, apply what happened. OpenFGA hands its final token back unchanged with no new entries, which is an infinite loop for a reader that only stops on a null token โ this one stops.
โฑ๏ธ Conditions and contextual tuples
A condition turns a grant into a rule. The tuple carries what was agreed; the check carries what is true now:
Same tuple, two answers. And a check that omits the context the expression needs is an error, not a denial โ OpenFGA says so rather than quietly answering no, and this library passes that through.
Contextual tuples answer a hypothetical without writing one:
๐ Pin the model id
Models are immutable and append-only: writing one adds a version and returns its id, and nothing is ever edited or deleted. That is what makes pinning safe, and what makes not pinning quietly dangerous.
Without an id, OpenFGA answers every check against whichever model version is newest. So somebody deploying a model change alters the answers a running application gets โ mid-request, with no deployment of its own and no way to notice.
Get the id at deploy time, put it in configuration, change it on purpose.
๐ซ Four things this SDK refuses to do
It never retries. Not a 429, not a 500. A check sits on the request path of whatever it authorises, so a hidden retry turns one slow response into three. More to the point, a write is transactional: a retried write that actually succeeded the first time comes back as a duplicate error, so a retrying client turns a success into a failure.
It never treats a denial as a failure. {"allowed": false} is a successful
call with a negative answer. The one thing this client must not do is conflate
that with a transport error, because that is how an application fails open โ so
check() returns a bool, exceptions mean the question was not answered, and a
missing allowed field reads as a denial rather than as approval.
It never takes over your authorisation. Laravel's Gate hook and Symfony's
voter both abstain unless the object implements FgaObject. Installing this
package changes the behaviour of exactly nothing until a model asks it to. (An
earlier version checked the object before the ability, which made
Gate::allows('viewAny', $unsavedModel) throw in applications that worked
before โ there is a test named after it.)
It never puts credentials in a URL. Header only.
Each carries OpenFGA's own code and message, because they are specific and
worth logging:
Note that Unauthenticated means this SDK was refused, which is a different
thing from a user being denied. Conflating them produces an application that
opens up when its credentials expire.
๐งฐ Laravel
The provider is auto-discovered.
Opt a model in, and the Gate answers:
Route middleware:
401 when nobody is authenticated, 403 on a denial โ and it refuses to start if the route parameter it was told to read does not exist, because a middleware that allowed a request it could not evaluate would be the worst possible bug in this package.
Or just use the client:
php artisan vendor:publish --tag=openfga-config for the rest โ including
openfga.gate = false to keep the Gate entirely yours.
๐งฐ Symfony
The voter is registered automatically, so the attribute is the relation name:
ROLE_* and PUBLIC_ACCESS are left to the voters that own them: only
lower-case, relation-shaped attributes are answered, and only for subjects
implementing FgaObject.
โ What's verified, and how
| The DSL parser | 29/29 reference fixtures, three directions, plus 50 invalid-DSL cases, plus every model deployed to a live server and read back |
| Every query โ check, batch, list, stream, expand | asserted byte-for-byte against a fake transport and run against OpenFGA v1.21.0 |
| Conditions | evaluated by the server's real CEL engine: the same tuple allowed at 09:30 and denied at 11:00 |
| Transactional writes | proved by making one half-invalid and asserting nothing was written |
| Nanosecond timestamps | the server sends nine fractional digits; the parser was fixed after being fed a value a live server had just produced |
| Both bridges | Laravel in an application booted by Testbench; Symfony compiling a real container, in five configurations |
| The OAuth2 exchange | shape taken from the official SDKs and asserted against a fake issuer. Not run against a real identity provider โ the one thing here that is conformant-by-reading rather than measured |
๐งฑ How it's built
Decisions visible in the code
UserandTypedObjectare separate types because OpenFGA treats them separately:user:*is a valid user anddocument:*is refused as an object. OneIdentifierclass cannot express that, so it finds out in production.Timestampparses OpenFGA's formats and nothing else. Go emits nine fractional digits and PHP'sumeans six, socreateFromFormatreturns false on a real response โ that bug would have silently nulled every tuple timestamp.new DateTimeImmutable($value)was not the fix: it accepts"tomorrow", and an impossible date like2026-02-30rolls over into a plausible wrong one, so the parsed value is written back out and compared.- An empty request body is
{}, never[].json_encode([])produces a JSON array, and the server answers one withsyntax error (line 1:1): unexpected token [โ which reads like a problem with a relation expression and is not. An unfilteredread()is the call that sends one. ApiError::from()usesnew static. Withnew self, every subclass factory returned the parent and everycatch (ValidationError)in every application would have been dead code. Found by catching a real 400 and noticing the catch block did not fire.- A relation's types live with the relation. The wire format splits a
relation's shape from the types it accepts, putting the second under
metadata.relations.<name>.directly_related_user_typesโ so reading a model means holding two trees open.Relationis the join. - Empty shapes are exact. A model with no types omits
type_definitionsentirely; a type with no relations emits"relations": {}and"metadata": null; a model with no conditions omitsconditions. Nothing would reject the wrong answer, and it would still differ from every model OpenFGA has produced. - Protobuf zero values mean absent. Read a model back and the gateway
renders
condition: "",module: "",source_info: null,conditions: {}. A reader that tookcondition: ""for a condition named""writes DSL the server then refuses.
โ Tests
The default suite needs nothing but PHP 8.2 and Composer, and runs in under half a second. Four suites:
unit โ each piece held still, with every request asserted in full against
a fake transport, plus the things a real server cannot be made to do on demand:
a 500, a body that is not JSON, a stream that stops halfway.
conformance โ this parser against OpenFGA's own, in three directions,
plus their 50 syntax-error cases.
laravel โ the bridge inside a real Laravel application, booted by
Testbench.
symfony โ the bundle compiling a real Symfony container in five
configurations. It earned its place immediately: it caught a service definition
using __construct as a factory, which Symfony does not allow and which no
unit test would have noticed.
The live suite is separate on purpose โ it needs a server, so a red run there means "OpenFGA is not running" as often as "this is broken", and that is not what a merge gate is for. It runs in CI against a real server on every push. Each test creates its own store and deletes it afterwards.
Some tests are named after specific mistakes rather than specific features: the
one proving 'yesterday' is not a timestamp, the one proving an empty body is
{}, the one proving each status maps to its own exception class, and the one
proving an unhandled Gate ability never touches the object.
โ ๏ธ Not included
Deliberately:
- Modular models (
module,extend type,fga.mod). The grammar is supported up to the point of recognising them, and they are refused with a message that says so rather than mis-parsed. Compose them with OpenFGA's CLI and parse the result. - A CEL parser. Condition expressions are captured verbatim and compiled by the server, which reports on them precisely. A PHP re-implementation could only add a way to be wrong.
- Semantic model validation. Same reason: the server has the rules and states them clearly. See syntax here, semantics there.
- The AuthZEN endpoints (
/access/v1/evaluationand friends). New in OpenFGA 1.21 and a different specification; they deserve their own release. - Private-key JWT client assertions. The client-secret flow is what deployments use; the assertion flow can follow if somebody needs it.
- Retries, backoff, caching. Above.
๐ What this was built from
- OpenFGA documentation ยท API reference ยท the OpenAPI definition
- The DSL grammar โ
OpenFGAParser.g4andOpenFGALexer.g4, and thetests/datacorpus vendored under tests/Conformance/fixtures (Apache-2.0, ยฉ OpenFGA authors) - Configuration language ยท Conditions ยท Relationship queries
- Google's Zanzibar paper, for why any of this is shaped the way it is
License
MIT. See LICENSE.
All versions of openfga with dependencies
ext-json Version *
psr/http-client Version ^1.0
psr/http-factory Version ^1.0
psr/http-message Version ^1.1 || ^2.0