PHP code example of axiam / axiam-sdk

1. Go to this page and download the library: Download axiam/axiam-sdk library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

axiam / axiam-sdk example snippets




use Axiam\Sdk\AxiamClient;
use Axiam\Sdk\Core\AuthError;

// `tenant` is a REQUIRED constructor argument — AXIAM is multi-tenant and there is no
// default tenant. There is no overload that lets you omit it. `login()`/`refresh()` also
// e',
    orgSlug: 'acme',
);

try {
    $result = $client->login('[email protected]', 'correct horse battery staple');
} catch (AuthError $e) {
    // typed exception hierarchy (AuthError/AuthzError/NetworkError), never a bare status code
    exit(1);
}

if ($result->mfaRequired) {
    $result = $client->verifyMfa($result->challengeToken, $totpCode);
}

// Same $client instance — the authenticated session's cookies/CSRF are shared automatically.
// `can(action, resource)` — same argument order as every other AXIAM SDK (CONTRACT.md §1).
$allowed = $client->can('read', 'documents');

if ($result->organizationLevel) {
    // This principal can act on another tenant by sending a different X-Tenant-ID on
    // the next request — no re-login, because it already is a principal of every
    // tenant in the organization. Offer the tenant selector.
}

// config/axiam.php (Laravel) — or the AXIAM_EXPECTED_* environment variables.
return [
    'base_url' => env('AXIAM_BASE_URL'),
    'tenant'   => env('AXIAM_TENANT'),

    // CONDITIONAL (§10.1 rules 5 and 6). Omit either to skip that check entirely.
    'expected_issuer'   => env('AXIAM_EXPECTED_ISSUER'),
    'expected_audience' => env('AXIAM_EXPECTED_AUDIENCE'),
];

use Axiam\Sdk\Attributes\RequireAccess;

final class DocumentController
{
    // Resolves the resource UUID from the {id} route parameter, checks 'read' for
    // the REQUEST'S authenticated user (never the shared AxiamClient's own session),
    // and returns 401/400/403/503 automatically on failure.
    #[RequireAccess(action: 'read', resourceParam: 'id')]
    public function show(string $id) { /* ... */ }
}

use Axiam\Sdk\AxiamClient;
use Axiam\Sdk\Core\AuthError;

$client = new AxiamClient(
    baseUrl: 'https://api.axiam.example',
    tenant: 'acme',
    oidcClientId: 'my-app',
    oidcClientSecret: getenv('AXIAM_OIDC_CLIENT_SECRET') ?: null, // omit for a public client
    oidcTenantId: '11111111-1111-1111-1111-111111111111', // UUID for the /oauth2/* query param (§12.3 rule 4)
);

$configuration = $client->oidcDiscover();
$request = $client->oidcBegin($configuration, 'https://app.example/callback', scope: 'openid profile');
// Persist $request->state / $request->nonce / $request->codeVerifier YOURSELF — see below.
// ...redirect the browser to $request->url...

// On the callback, having checked the IdP's `state` matches:
try {
    $tokens = $client->oidcExchange(
        code: $callbackCode,
        codeVerifier: $request->codeVerifier,
        redirectUri: 'https://app.example/callback',
        nonce: $request->nonce,
    );
} catch (AuthError $e) {
    // $e->getReason() is one of the §12.4 codes (invalid_alg, unknown_kid,
    // invalid_signature, invalid_issuer, invalid_audience, token_expired,
    // nonce_mismatch) when this was an ID-token validation failure, or an
    // Axiam\Sdk\Core\OAuthProtocolError (an AuthError sub-type — existing
    // catch(AuthError) blocks keep working) carrying ->error/->errorDescription.
}
echo $tokens->idClaims['sub']; // the validated ID-token subject

$tokens = $client->deviceLogin(
    onUserCode: function (DeviceAuthorization $a): void {
        // Called BEFORE the first poll. Display it however the device can — screen,
        // QR code, e-ink panel. The SDK never prints it for you.
        printf("visit %s and enter %s\n", $a->verificationUri, $a->userCode);
    },
    scope: 'openid profile',
);

$exchanged = $client->tokenExchange(
    subjectToken: $userToken,
    subjectTokenType: OidcClient::ACCESS_TOKEN_TYPE, // 

$exchanged = $client->tokenExchange(
    subjectToken: $partnerToken,
    subjectTokenType: OidcClient::JWT_TOKEN_TYPE, // 

// A PAT is a client-credentials token carrying `uma_protection` — never a user token,
// and never this client's own session (§20.2 rule 1).
$pat = $client->loginClientCredentials(scope: OidcClient::UMA_PROTECTION_SCOPE)->accessToken;

$resource = $client->umaRegisterResource($pat, 'invoice-7', 'document', ['view']);

// The returned id IS the AXIAM resource id — no translation step.
$ticket = $client->umaRequestTicket($pat, [
    new RequestedPermission($resource->id, ['view']),
]);

header($client->umaChallengeHeader('invoices', $issuer, $ticket));

$challenge = $client->umaParseChallenge($response->getHeaderLine('WWW-Authenticate'));
$rpt = $client->umaExchangeTicket($challenge->ticket, $usersAccessToken);

$challenger = new UmaChallenger('invoices', $client->oidcDiscover()->issuer, $pat, $client);
$enforcer = new AccessEnforcer($client, $logger, $challenger);

// A denied #[RequireAccess] now answers 403 with
//   WWW-Authenticate: UMA realm="invoices", as_uri="…", ticket="…"

$url = $client->logoutUrl($storedIdToken);

// …and at your registered backchannel_logout_uri:
$verified = $client->verifyLogoutToken($logoutToken);
if ($verified->sid !== null) {
    endSession($verified->sid);   // that session ONLY
}

use Axiam\Sdk\Core\Sensitive;
use Axiam\Sdk\Webhook\AxiamWebhooks;
use Axiam\Sdk\Webhook\WebhookVerificationException;

// Read the RAW body BEFORE any framework parses it as JSON.
$rawBody = file_get_contents('php://input');

try {
    $event = AxiamWebhooks::verify(
        new Sensitive($webhookSecret),
        $_SERVER['HTTP_X_AXIAM_SIGNATURE'] ?? '',
        $rawBody,
    );
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    return;
}

// $event->eventType, $event->deliveryId, $event->timestamp, $event->body

use Axiam\Sdk\Core\Sensitive;
use Axiam\Sdk\Reactor\AmqpLibReactorTransport;
use Axiam\Sdk\Reactor\ReactorAnswer;
use Axiam\Sdk\Reactor\ReactorConfig;
use Axiam\Sdk\Reactor\ReactorEvent;
use Axiam\Sdk\Reactor\ReactorEvents;
use Axiam\Sdk\Reactor\ReactorServer;

$config = new ReactorConfig(
    tenantId: $tenantId,
    // §8.1 + §22.12: the tenant AMQP subkey from the management API, wrapped.
    signingKey: new Sensitive($subkey),
    // The queue name is derived from it — but the SERVER declared it.
    reactorId: $reactorId,
);

$server = new ReactorServer(
    config: $config,
    // §8b: amqps:// only, optional CA bundle, no verification-skip switch anywhere.
    transport: AmqpLibReactorTransport::connect('amqps://broker.example:5671', $caPath),
    handler: function (ReactorEvent $event): ReactorAnswer {
        switch ($event->event) {
            case ReactorEvents::TOKEN_PRE_ISSUE:
                // `ext.` is the COMPLETE allow-list for this event.
                return ReactorAnswer::mutate(['ext.department' => 'eng']);
            case ReactorEvents::LOGIN_POST_AUTH:
                return fraudulent($event)
                    ? ReactorAnswer::deny('embargoed region')
                    : ReactorAnswer::allow(); // or ReactorAnswer::allowWithStepUp()
        }

        return ReactorAnswer::allow();
    },
);

$server->reactorServe(); // blocks; call $server->stop() from a signal handler

use Axiam\Sdk\Attributes\OnReactorEvent;
use Axiam\Sdk\Reactor\ReactorHandlers;

final class ClaimsReactor
{
    #[OnReactorEvent(ReactorEvents::TOKEN_PRE_ISSUE)]
    public function enrich(ReactorEvent $event): ReactorAnswer
    {
        return ReactorAnswer::mutate(['ext.department' => 'eng']);
    }

    #[OnReactorEvent(ReactorEvents::LOGIN_POST_AUTH)]
    public function screen(ReactorEvent $event): ReactorAnswer
    {
        return fraudulent($event) ? ReactorAnswer::deny('embargoed region') : ReactorAnswer::allow();
    }
}

$handlers = ReactorHandlers::of(new ClaimsReactor());
$server = new ReactorServer(config: $config, transport: $transport, handler: $handlers->handler());

if ($client->opaqueAvailable()) {
    $result = $client->loginOpaque('alice', $password);
} else {
    $result = $client->login('alice', $password);
}

$enrolment = $client->opaqueEnrollment($newPassword);
$body['opaque'] = $enrolment->toWire();

// Enrolment — ient->webauthnRegisterStart();
$credential = $client->webauthnRegisterFinish(
    $challenge->stateToken,
    "Alice's laptop",
    $platformResponseJson,          // verbatim
);

// Sign-in with no username at all — the authenticator picks the account.
$signIn = $client->webauthnDiscoverableStart();
$result = $client->webauthnDiscoverableFinish($signIn->stateToken, $assertionJson);

// Your start endpoint
$challenge = $client->webauthnRegisterStart();
echo json_encode([
    // §24.6a rule 1: the wire JSON, unparsed and unreassembled.
    'requestJson' => $challenge->requestJson(),
    'stateToken' => $challenge->stateToken->reveal(),
]);

$outcome = WebauthnFailure::classify($domExceptionName);
echo $outcome->message();

$result = $client->login('[email protected]', $password);

if ($result->mfaSetupRequired) {
    // The third outcome. The tenant ollment = $client->mfaSetupEnroll($result->setupToken);
    renderQr($enrollment->totpUri->reveal());
    $client->mfaSetupConfirm($result->setupToken, $code);   // completes the LOGIN
}

$client->requestPasswordReset(new PasswordResetRequest('[email protected]'));
// returns void, whether or not that address has an account

$context = $client->passwordResetContext($token);
if ($context->opaque !== null) {
    // This tenant runs §23. Build a registration record from these parameters;
    // a plaintext password would be refused, and refused late (§25.4 rule 1).
}
$client->confirmPasswordReset(new PasswordResetConfirmation($token, $newPassword, $tenantId));

// No session — a sign-up screen. Returns whatever happened; that is the point.
$client->resendVerification('[email protected]', $tenantId);

// Signed in — a profile page. Says what happened, and names no address.
try {
    $client->resendOwnVerification();
} catch (AuthzError) {
    // 409: already verified, or an account state that must not be sent a live token.
} catch (NetworkError) {
    // 429: the daily resend limit.
}

$config = $client->oidcDiscover();
if ($config->pushed_authorization_request_endpoint === null) {
    // §26 is optional; fall back to the plain oidcBegin redirect.
}

$begun = $client->oidcBegin($config, $redirectUri, 'openid profile');
$pushed = $client->oidcPar($begun, $redirectUri, $config, 'openid profile');

header('Location: ' . $pushed->url);   // exactly ?client_id=…&request_uri=…

// §27.2 — namespace handles, not 146 flat methods.
$page = $client->users()->listItems(new PageRequest(0, 50));

// Or reach the same handles behind one accessor.
$management = $client->management();
$same = $management->users()->listItems(new PageRequest(0, 50));

// §27.4 rule 4 — `total` is the SERVER's count, not count($page). Confusing the two is
// how a script silently processes the first fifty of four hundred users.
printf("%d of %d\n", count($page), $page->total);

// Every page. The walk stops on the first EMPTY page, never on a short one.
$users = $management->users();
foreach (ManagementTransport::walk(
    static fn (PageRequest $p): Page => $users->listItems($p),
) as $user) { /* ... */ }

// §27.4 rule 5 — name what you mean to change. What you leave unset is OMITTED from the
// request, not sent as null; on a sparse update those say opposite things.
$management->users()->update($id, new Models\UpdateUserRequest(status: Models\UserStatus::Locked));

// §27.4 rule 3 — `{org_id}`/`{tenant_id}` come from the client; ->inOrg()/->forTenant()
// override them for one handle and return a COPY, leaving the original pointing where it did.
$management->caCertificates()->inOrg($otherOrgId)->listItems();

$page = $client->users()->listItems(new PageRequest(0, 50, 'ada'));
printf("%d of %d matches\n", count($page), $page->total);   // total counts MATCHES, not rows

// And the whole filtered set: the term rides on the page request, so the walk carries it.
foreach (ManagementTransport::walk(
    static fn (PageRequest $p): Page => $users->listItems($p),
    new PageRequest(0, 50, 'ada'),
) as $match) { /* ... */ }

foreach ($client->tenants()->listItems() as $tenant) {
    $label = match ($tenant->kind) {
        Models\TenantKind::Organization => 'organization scope',
        Models\TenantKind::Standard, null => 'tenant',
        Models\TenantKind::Unknown => 'a kind this SDK predates — upgrade to name it',
    };
}

$manifest = ManagementManifest::builder()
    ->permission('docs.read', 'documents:read', 'Read documents')
    ->role('contractor', 'contractor', 'External', grants: [
        'docs.read'  => 'allow',
        'docs.write' => 'deny',   // AXIAM's RBAC is DENY-OVERRIDE, not most-specific-wins
    ])
    ->group('externals', 'externals', 'Contractors', roleKeys: ['contractor'])
    ->build();

$plan = $client->management()->manifest()->plan($manifest);   // writes NOTHING
if (!$plan->isConverged()) {
    $report = $client->management()->manifest()->apply($manifest);
}

#[ManagedPermission(key: 'docs.read', action: 'documents:read')]
#[ManagedRole(key: 'contractor', name: 'contractor', grants: ['docs.read' => 'allow'])]
final class AcmeTenant {}

$manifest = ManifestAttributeReader::read(AcmeTenant::class);

use Axiam\Sdk\AxiamClient;

$client = new AxiamClient(
    baseUrl: 'https://api.axiam.example',
    tenant:  'acme',
    clientCert: file_get_contents('/secure/device.crt.pem'),
    clientKey:  file_get_contents('/secure/device.key.pem'),
);
bash
composer grpc-gen    #