PHP API

All classes live under the Padosoft\Iam\Client\ namespace.

Facades\Iam

Iam::can(Authenticatable|string|null $user, string $ability, array $context = []): bool
Iam::denies(Authenticatable|string|null $user, string $ability, array $context = []): bool
Iam::check(Authenticatable|string|null $user, string $ability, array $context = []): IamDecision

Backed by IamClient. Reserved $context keys (organization, application, resource, aal, explain)
are mapped onto the query; everything else is sent as ABAC facts.

IamClient

The class behind the facade.

public function __construct(Decider $decider, array $config = [])

public function can(Authenticatable|string|null $user, string $ability, array $context = []): bool
public function denies(Authenticatable|string|null $user, string $ability, array $context = []): bool
public function check(Authenticatable|string|null $user, string $ability, array $context = []): IamDecision
public function request(string $subjectId, string $ability, array $context = []): DecisionRequest
public function resolveSubjectId(Authenticatable|string|null $user): string
Method Returns Notes
can bool check(...)->granted()
denies bool !can(...)
check IamDecision deny('no-subject') when no subject resolves (fail-closed)
request DecisionRequest builds the DTO; pulls reserved keys, applies config defaults
resolveSubjectId string getAuthIdentifier() (scalar) / the string / '' for null

Contracts\Decider

interface Decider {
    public function decide(DecisionRequest $request): IamDecision;
}

Deciders\LocalDecider

public function __construct(AuthorizationEngine $engine)
public function decide(DecisionRequest $request): IamDecision

Calls the in-process PDP. Any throwable → IamDecision::deny('engine: ' . $e::class).

Deciders\HttpDecider

public function __construct(ClientInterface $http, string $baseUrl, ?string $token)
public function decide(DecisionRequest $request): IamDecision

POST {baseUrl}/decisions/check with Accept: application/json and (if $token) Authorization: Bearer.
http_errors => false. Non-2xx → deny("http {status}"); non-array body → deny('invalid body'); throwable
deny('transport: ' . $e::class). Unwraps the { "data": ... } envelope before parsing.

Deciders\CachingDecider

public function __construct(Decider $inner, CacheRepository $cache, int $ttl, bool $enabled = true)
public function decide(DecisionRequest $request): IamDecision

Bypasses (delegates to $inner) when !$enabled, $ttl <= 0, or $request->explain. Otherwise reads/writes
'iam:dec:' . $request->cacheKey(), storing IamDecision::toArray() for $ttl seconds.

DecisionRequest (final readonly)

public function __construct(
    string  $permission,
    string  $subjectId,
    string  $subjectType = 'user',
    ?string $organization = null,
    ?string $application = null,
    ?string $resource = null,
    array   $context = [],          // ABAC facts
    string  $currentAal = 'aal1',
    bool    $explain = false,
)

public function cacheKey(): string   // sha256 over subjectType, subjectId, permission,
                                     //   organization, application, resource, context, currentAal
public function toArray(): array     // { subject:{type,id}, permission, organization, application,
                                     //   resource, context, current_aal, explain }

IamDecision (final readonly)

public function __construct(
    bool    $allowed,
    string  $decisionId = '',
    int     $policyVersion = 0,
    bool    $requiresStepUp = false,
    ?string $requiredAal = null,
    array   $explanation = [],       // list<string>
)

public static function deny(string $reason): self          // fail-closed denial
public static function fromArray(array $data): self         // parse the PDP response (defensive)
public function granted(): bool                             // allowed && !requiresStepUp  ← gate on this
public function toArray(): array

fromArray() reads allowed, decision_id, policy_version, requires_step_up, required_aal,
explanation, each with a safe fallback (allowed/requires_step_up default to false).

Gate\IamGateAdapter

public function __construct(IamClient $client, string $intercept = 'namespaced')
public function register(Gate $gate): void
public function decide(Authenticatable $user, string $ability, array $arguments = []): ?bool

register() hooks Gate::before. decide() returns null for abilities it doesn’t own (so local
Gates/policies decide), otherwise IamClient::check(...)->granted(). Owns: with intercept = 'all', every
ability; with 'namespaced', abilities containing :. A non-empty string first argument becomes the
resource.

Middleware

// Http\Middleware\IamAuthenticate — alias iam.auth
public function handle(Request $request, Closure $next): Response   // 401 when $request->user() is null

// Http\Middleware\IamCan — alias iam.can
public function handle(
    Request $request, Closure $next, string $permission, ?string $resourceParam = null
): Response   // 401 without a user; 403 when !granted; binds $resourceParam (route value or model key) as resource

IamClientServiceProvider

Extends spatie/laravel-package-tools. Binds Decider, IamClient, IamGateAdapter as singletons;
registers the iam.can / iam.auth aliases only if not already registered; registers the Gate adapter
when gate.enabled is true. Selects HttpDecider vs LocalDecider from mode, and wraps in
CachingDecider when cache.enabled.

See also