AxiamClient
in package
The AXIAM PHP SDK's public REST entry point (CONTRACT.md §1–§9, SC#1).
tenant is a REQUIRED, non-nullable-defaulted constructor parameter (D-13, §5) — there
is no overload or default that lets a caller omit it; AXIAM is multi-tenant and there is
no default tenant. login($email, $password) returns a typed LoginResult, never a
raw array/stdClass (D-09); a two-phase MFA flow is completed via self::verifyMfa().
checkAccess/can/batchCheck delegate to AuthzDispatcher — this class never
hand-rolls REST/gRPC transport selection (D-03). self::verifyLocallyOrFallback() is
the seam the Laravel/Symfony framework bridges (a later plan) call: local JwksVerifier
verification first, falling back to the reactive single-flight refresh path (D-02, D-06).
Composition, not reimplementation: this class wires together the already-built wave-2/3
pieces — Session (CookieJar + CSRF + single-flight refresh promise, D-06),
AuthMiddleware/RefreshMiddleware (the HandlerStack auth/refresh mechanism),
JwksVerifier (local EdDSA/JWKS verification), and AuthzDispatcher
(REST-default, gRPC-when-available authz, D-03) — it does not reimplement any of their
internal mechanisms.
Two Guzzle clients share ONE CookieJar (§4):
$plainHttpcarries ONLY AuthMiddleware (tenant/auth/CSRF header injection, no 401-triggered refresh-and-retry). This is the client handed to Session's own constructor for its internal/api/v1/auth/refreshPOST (per Session's own doc comment: the refresh call itself must never be able to recursively re-enter the single-flight guard), and is also used forlogin()/verifyMfa()/logout()— a failed login/logout attempt (401/403) must surface as its own clear error, not trigger an unrelated token-refresh attempt first.$authzHttpcarries BOTH AuthMiddleware and RefreshMiddleware — the full production stack — and is the client AuthzRestClient (and therefore AuthzDispatcher's REST path) sends every authz request through, so a 401 on an authz call transparently triggers the shared single-flight refresh-and-retry-once (D-06).
§6/D-12: the Guzzle verify option is ALWAYS true (strict TLS, system trust roots) unless
$customCa (a CA bundle FILE PATH) is supplied, in which case verify is set to that path —
the ONLY escape hatch. There is no code path in this class that sets verify to false or
any other TLS-bypass value.
§6.1 (mTLS): supplying $clientCert + $clientKey (both PEM strings) makes this client
present an X.509 client identity for mutual TLS on BOTH transports — the REST Guzzle clients
(via cert/ssl_key) and any gRPC channel (via
\Grpc\ChannelCredentials::createSsl(rootCerts, privateKey, certChain)). This is strictly
ADDITIVE to §6: presenting a client certificate NEVER relaxes server verification — verify
is untouched by this code path (contract rule §6.1.2). The private key is secret (§7): it is
held behind Sensitive, written only to a 0600 temp file consumed by cURL, and never
appears in any debug/log/exception output. Both PEM strings must be supplied together;
supplying exactly one is a construction-time InvalidArgumentException.
Table of Contents
Methods
- __construct() : mixed
- __destruct() : mixed
- §6.1: cleans up the `0600` temp files backing the client-certificate identity when this client is destroyed, so no PEM material (least of all the private key) outlives the object on disk. A no-op when mTLS was not configured.
- batchCheck() : array<int, bool>
- `batchCheck` — results preserve input order (CONTRACT.md §1).
- can() : bool
- `can` — the browser/UI-scenario alias for {@see self::checkAccess()} (CONTRACT.md §1 note). Argument order is `(action, resource)` — matching {@see self::checkAccess()} and every other AXIAM SDK's `can`/`Can` (D-09/SDK-Q09; this was previously reversed relative to the rest of the SDK family).
- checkAccess() : bool
- `checkAccess` — delegates to {@see AuthzDispatcher} (REST default, gRPC when available).
- debugClientCertOptions() : array{cert: string|null, ssl_key: string|null}
- Test-only seam (not part of the public API contract, mirroring {@see self::debugVerifyOption()}): exposes the §6.1 client-identity options (`cert` = certificate-chain file, `ssl_key` = private-key file) actually configured on this client's authz Guzzle transport, so tests can assert the mTLS wiring without performing a live TLS handshake. Both entries are `null` when mTLS was not configured. The values are FILE PATHS, never the PEM bytes — this seam never surfaces the private key itself.
- debugVerifyOption() : string|bool
- getUserInfo() : UserInfo
- `getUserInfo` — the gRPC-ONLY OIDC-style userinfo operation (CONTRACT.md §1.1, contract 1.3): returns the authenticated caller's identity claims from `axiam.v1.UserInfoService/GetUserInfo`, the low-latency counterpart of the server's REST `GET /oauth2/userinfo`. Delegates to {@see AuthzDispatcher} — this class never hand-rolls the gRPC transport (D-03).
- login() : LoginResult
- `POST /api/v1/auth/login` (CONTRACT.md §1). Returns a typed {@see LoginResult} — an MFA challenge (HTTP 202) is an expected outcome, not an exception: callers MUST check {@see LoginResult::$mfaRequired} before assuming a session was established (SC#1).
- logout() : void
- `POST /api/v1/auth/logout` (CONTRACT.md §1) and clears local session state: the shared cookie jar (§4) and the captured CSRF token (§3). The session id comes from the current access token's `jti` claim (unverified decode — an operational hint only, never an authorization decision, mirroring the Python/C# sibling SDKs).
- refresh() : void
- `POST /api/v1/auth/refresh` (CONTRACT.md §1), routed through {@see Session}'s single-flight guard (§9, D-06) — the SAME mechanism {@see RefreshMiddleware} triggers reactively on a `401`. A failure surfaces as {@see AuthError} with no retry (§9.3).
- verifyLocallyOrFallback() : array<string, mixed>|null
- Local-first JWT verification with a reactive-refresh fallback (D-02) — the seam the Laravel/Symfony framework bridges (a later plan) call for every incoming request.
- verifyMfa() : LoginResult
- `POST /api/v1/auth/mfa/verify` (CONTRACT.md §1) — completes the two-phase flow started by {@see self::login()} when {@see LoginResult::$mfaRequired} was `true`. `$challengeToken` is the `Sensitive`-wrapped value from that `LoginResult` (D-11: never a raw string on the public surface).
Methods
__construct()
public
__construct(string $baseUrl, string $tenant[, string|null $orgSlug = null ][, string|null $orgId = null ][, string|null $customCa = null ][, string|null $clientCert = null ][, string|null $clientKey = null ][, LoggerInterface|null $logger = null ][, bool|null $restOnly = null ][, int $cacheTtlSeconds = 300 ][, string|null $grpcTarget = null ][, callable|null $transportHandler = null ]) : mixed
Parameters
- $baseUrl : string
-
The AXIAM server's base URL (e.g.
https://api.axiam.example). - $tenant : string
-
The tenant slug — REQUIRED, no nullable default anywhere on this signature (D-13, §5). There is no default tenant; constructing this client without one is a compile-time (missing required argument) error, and an empty string is rejected at runtime as a backstop.
- $orgSlug : string|null = null
-
Organization slug — mutually exclusive with
$orgId. The real login/refresh handlers require an org identifier beyond CONTRACT.md §5's tenant-only minimum (mirrors the Python/C# sibling SDKs'org_slug/org_idconstructor options). - $orgId : string|null = null
-
Organization UUID — mutually exclusive with
$orgSlug. - $customCa : string|null = null
-
A CA bundle FILE PATH (PEM-encoded) — the ONLY TLS escape hatch (§6/D-12). Never pass a value here to disable TLS verification; there is no such option on this class.
- $clientCert : string|null = null
-
§6.1 (mTLS): the client's X.509 identity certificate CHAIN as a PEM STRING (not a path). When supplied together with
$clientKey, this client presents that certificate for mutual TLS on both the REST and gRPC transports. Purely additive — server verification is never relaxed (§6.1.2). Must be a PEM value; a non-PEM string is rejected at construction.null(default) leaves the default bearer-cookie behavior unchanged (§6.1.5). - $clientKey : string|null = null
-
§6.1/§7 (mTLS): the PEM STRING of the private key matching
$clientCert(PKCS#8 or PKCS#1). Secret material — it is held behind Sensitive and never logged, displayed, or exposed via a getter.$clientCertand$clientKeyare all-or-nothing: supplying exactly one throws InvalidArgumentException. - $logger : LoggerInterface|null = null
-
Injectable logger (D-15: diagnostic-only — status codes and operation names, NEVER a token/credential value). Defaults to a silent NullLogger.
- $restOnly : bool|null = null
-
Force REST-only authz transport.
null(default) resolves totruewhen$grpcTargetis not supplied (there would be nothing to connect the gRPC transport to) andfalseotherwise — an explicittrue/falsealways wins. REST authz ALWAYS works regardless of this setting (D-03). - $cacheTtlSeconds : int = 300
-
JwksVerifier's local JWKS TTL cache lifetime.
- $grpcTarget : string|null = null
-
gRPC target host:port (e.g.
api.axiam.example:9443), required only to actually use the gRPC authz transport. - $transportHandler : callable|null = null
-
Test-only seam (NOT part of the public API contract, trailing/optional so it never affects SC#1's "tenant is required" reflection check): a raw Guzzle handler (e.g.
GuzzleHttp\Handler\MockHandler) used as the base handler for both internalHandlerStacks instead of Guzzle's default cURL/stream handler. Mirrors the C# sibling SDK'sCreateForTestinginternal seam, adapted to Guzzle's own documentedHandlerStack::create($mockHandler)testing idiom (docs.guzzlephp.org/en/stable/testing.html) — never used by production code.
__destruct()
§6.1: cleans up the `0600` temp files backing the client-certificate identity when this client is destroyed, so no PEM material (least of all the private key) outlives the object on disk. A no-op when mTLS was not configured.
public
__destruct() : mixed
batchCheck()
`batchCheck` — results preserve input order (CONTRACT.md §1).
public
batchCheck(array<int, array{action: string, resourceId: string, scope?: string|null}> $checks) : array<int, bool>
Parameters
- $checks : array<int, array{action: string, resourceId: string, scope?: string|null}>
Return values
array<int, bool>can()
`can` — the browser/UI-scenario alias for {@see self::checkAccess()} (CONTRACT.md §1 note). Argument order is `(action, resource)` — matching {@see self::checkAccess()} and every other AXIAM SDK's `can`/`Can` (D-09/SDK-Q09; this was previously reversed relative to the rest of the SDK family).
public
can(string $action, string $resource) : bool
Parameters
- $action : string
- $resource : string
Return values
boolcheckAccess()
`checkAccess` — delegates to {@see AuthzDispatcher} (REST default, gRPC when available).
public
checkAccess(string $action, string $resourceId[, string|null $scope = null ][, string|null $subjectId = null ]) : bool
Parameters
- $action : string
- $resourceId : string
- $scope : string|null = null
- $subjectId : string|null = null
-
Additive, optional (CONTRACT.md §11.2.2 — declarative authorization helpers): when given, the check is evaluated for THIS subject (a UUID) rather than whichever identity this client's own session represents. This matters for a framework bridge sharing ONE
AxiamClientinstance (typically authenticated as a service account, or not authenticated at all) to authorize each inbound HTTP request's OWN end user: passingsubjectId: $endUserIdhere checks the end user's permissions, never the shared client's.null(the default) preserves this method's pre-§11 behavior exactly.
Return values
booldebugClientCertOptions()
Test-only seam (not part of the public API contract, mirroring {@see self::debugVerifyOption()}): exposes the §6.1 client-identity options (`cert` = certificate-chain file, `ssl_key` = private-key file) actually configured on this client's authz Guzzle transport, so tests can assert the mTLS wiring without performing a live TLS handshake. Both entries are `null` when mTLS was not configured. The values are FILE PATHS, never the PEM bytes — this seam never surfaces the private key itself.
public
debugClientCertOptions() : array{cert: string|null, ssl_key: string|null}
Return values
array{cert: string|null, ssl_key: string|null}debugVerifyOption()
public
debugVerifyOption() : string|bool
Return values
string|bool —The Guzzle verify option: true, or a CA bundle path (never false).
getUserInfo()
`getUserInfo` — the gRPC-ONLY OIDC-style userinfo operation (CONTRACT.md §1.1, contract 1.3): returns the authenticated caller's identity claims from `axiam.v1.UserInfoService/GetUserInfo`, the low-latency counterpart of the server's REST `GET /oauth2/userinfo`. Delegates to {@see AuthzDispatcher} — this class never hand-rolls the gRPC transport (D-03).
public
getUserInfo() : UserInfo
Identity is derived server-side from the current bearer token; the request is empty.
sub/tenantId/orgId are always populated on the returned UserInfo;
email is present only with the "email" token scope and preferredUsername only with
"profile" (the server gates them exactly as the REST endpoint does). Requires a prior
successful self::login() — calling it with no token raises AuthError
before any wire call (§1.1.3) — and, being gRPC-only (§1.1.6), requires the grpc PECL
extension plus a configured grpcTarget; there is NO REST fallback, so on a REST-only
runtime it raises NetworkError rather than degrading. A gRPC UNAUTHENTICATED
response drives the shared single-flight refresh (§9) and retries once (§1.1.4).
Return values
UserInfologin()
`POST /api/v1/auth/login` (CONTRACT.md §1). Returns a typed {@see LoginResult} — an MFA challenge (HTTP 202) is an expected outcome, not an exception: callers MUST check {@see LoginResult::$mfaRequired} before assuming a session was established (SC#1).
public
login(string $email, string $password) : LoginResult
Parameters
- $email : string
- $password : string
Return values
LoginResultlogout()
`POST /api/v1/auth/logout` (CONTRACT.md §1) and clears local session state: the shared cookie jar (§4) and the captured CSRF token (§3). The session id comes from the current access token's `jti` claim (unverified decode — an operational hint only, never an authorization decision, mirroring the Python/C# sibling SDKs).
public
logout() : void
refresh()
`POST /api/v1/auth/refresh` (CONTRACT.md §1), routed through {@see Session}'s single-flight guard (§9, D-06) — the SAME mechanism {@see RefreshMiddleware} triggers reactively on a `401`. A failure surfaces as {@see AuthError} with no retry (§9.3).
public
refresh() : void
verifyLocallyOrFallback()
Local-first JWT verification with a reactive-refresh fallback (D-02) — the seam the Laravel/Symfony framework bridges (a later plan) call for every incoming request.
public
verifyLocallyOrFallback(string $token, string $tenant) : array<string, mixed>|null
Tries JwksVerifier::verify() first (no network call on the happy path); if that
fails (expired/unknown-kid/invalid token), attempts the shared single-flight refresh
(§9, D-06) and re-verifies the FRESH access token. Returns null — never unverified
claims — on any failure; this method is fail-closed exactly like JwksVerifier
itself, since callers use the result for an authorization decision.
Parameters
- $token : string
- $tenant : string
Return values
array<string, mixed>|null —Verified claims, or null.
verifyMfa()
`POST /api/v1/auth/mfa/verify` (CONTRACT.md §1) — completes the two-phase flow started by {@see self::login()} when {@see LoginResult::$mfaRequired} was `true`. `$challengeToken` is the `Sensitive`-wrapped value from that `LoginResult` (D-11: never a raw string on the public surface).
public
verifyMfa(Sensitive $challengeToken, string $totpCode) : LoginResult
Parameters
- $challengeToken : Sensitive
- $totpCode : string