Usage¶
Instantiation¶
use Daycry\JWT\JWT;
$jwt = JWT::for(); // resolves config('JWT') for you
// or, with an explicit config (tests, multi-tenant)
use Daycry\JWT\Config\JWT as JWTConfig;
$config = new JWTConfig();
$config->signer = '...';
$config->issuer = 'https://api.my-app.com';
// ...
$jwt = new JWT($config);
JWT is immutable: withSplitData(), withParamData(), withLeeway(), withExpiresAt() all return new instances.
$base = JWT::for();
$splitter = $base->withSplitData(); // new instance
$base->isSplitData(); // false (unchanged)
$splitter->isSplitData(); // true
encode()¶
| Parameter | Type | Description |
|---|---|---|
$data |
mixed |
Payload. Scalar, array, or object. |
$uid |
int\|string\|null |
Override for the uid claim. Defaults to Config\JWT::$uid. Accepts a string or an integer ID (e.g. a DB primary key) — lcobucci preserves the JSON type, so an integer uid round-trips as an integer. Pass 0 if you actually want zero — only null and '' skip the claim. |
The standard claims (
iss,aud,jti) come fromConfig\JWT::$issuer,$audienceand$identifier. Each one is required: anullor empty-string value throwsDaycry\JWT\Exceptions\JWTConfigurationException(runphp spark jwt:publishand fill them in).
Standard claims added automatically¶
| Claim | Source |
|---|---|
iss |
Config\JWT::$issuer |
aud |
Config\JWT::$audience |
jti |
Config\JWT::$identifier |
iat |
Current timestamp |
nbf |
iat + Config\JWT::$canOnlyBeUsedAfter |
exp |
iat + Config\JWT::$expiresAt |
cty (header) |
'json' — only for compact-mode array payloads |
Scalar payload¶
Array payload — compact (default)¶
$token = $jwt->encode(['user_id' => 1, 'role' => 'admin']);
$payload = $jwt->getPayload($token); // ['user_id' => 1, 'role' => 'admin']
The array is JSON-encoded into the single data claim (or the name set via withParamData()). The token carries header cty=json so getPayload() can auto-decode it.
Array payload — split mode¶
$jwt = JWT::for()->withSplitData();
$token = $jwt->encode(['user_id' => 1, 'role' => 'admin']);
$decoded = $jwt->decode($token);
echo $decoded->claims()->get('role'); // "admin"
In split mode each entry becomes its own top-level claim. Avoid keys that collide with registered claims (iss, aud, exp, etc.) — Builder::withClaim() throws RegisteredClaimGiven for those.
decode()¶
Parses and validates the token. Always throws on failure:
Daycry\JWT\Exceptions\InvalidTokenException— malformed input.Lcobucci\JWT\Validation\RequiredConstraintsViolated— at least one constraint failed.Daycry\JWT\Exceptions\JWTConfigurationException—Config\JWT::$validateClaimsdoes not containSignedWithwhile$validate = true. The library refuses to silently skip signature verification. To decode without any validation, setConfig\JWT::$validate = false.
When Config\JWT::$validate = false, decode() skips validation and logs a warning via log_message() (parallel to extractClaimsUnsafe()) — it is intended for tests/debug only, never production.
use Daycry\JWT\Exceptions\InvalidTokenException;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
try {
$token = $jwt->decode($input);
echo $token->claims()->get('uid');
} catch (RequiredConstraintsViolated $e) {
// signature, issuer, audience, exp, jti…
} catch (InvalidTokenException $e) {
// not a JWT at all
}
Reading claims and headers¶
$decoded = $jwt->decode($token);
$decoded->claims()->get('uid'); // custom uid
$decoded->claims()->get('iss'); // issuer
$decoded->claims()->get('exp'); // DateTimeImmutable
$decoded->claims()->all(); // associative array
$decoded->headers()->get('alg'); // signing algorithm
$decoded->headers()->get('cty'); // 'json' if compact-mode array payload
tryDecode()¶
Same as decode() but returns null instead of throwing. Use this in middleware where you want to short-circuit on bad tokens without try/catch.
$decoded = $jwt->tryDecode($input);
if ($decoded === null) {
return $this->response->setStatusCode(401);
}
getPayload()¶
Validate and return the original payload value:
- Scalar → returned as is.
- Compact-mode array (header
cty=json) → alreadyjson_decoded back into an array. - Split-mode → returns the value of
paramData(oftennullsince data is spread across many claims).
$jwt->getPayload($jwt->encode('hello')); // "hello"
$jwt->getPayload($jwt->encode(['x' => 1])); // ['x' => 1]
Throws the same exceptions as decode().
withSplitData() / withParamData() / withLeeway() / withExpiresAt()¶
Immutable configuration:
$jwt = JWT::for()
->withSplitData() // each array key becomes its own claim
->withParamData('payload') // change the default 'data' claim name
->withLeeway(30) // ±30 seconds of clock skew tolerance
->withExpiresAt('+5 minutes'); // override the configured expiresAt modifier
echo $jwt->getParamData(); // 'payload'
$jwt->isSplitData(); // true
Each call returns a fresh instance — the original is untouched.
withLeeway(?int $seconds)¶
Accepts an integer number of seconds, or null to reset back to "no leeway". A negative int throws InvalidArgumentException.
withExpiresAt(string $modifier)¶
Per-instance override of the expiresAt modifier — mint short-lived access tokens without mutating the shared Config\JWT::$expiresAt. The modifier is any string accepted by DateTimeImmutable::modify() (an invalid modifier throws InvalidArgumentException at encode time). An empty string throws InvalidArgumentException immediately.
$access = JWT::for()->withExpiresAt('+5 minutes')->encode($data); // short-lived
$refresh = JWT::for()->encode($data); // uses Config::$expiresAt
withIssuer / withAudience / withIdentifier (3.2.0)¶
Per-instance overrides of the iss / aud / jti claims, applied to both encode and validate so the two stay symmetric. Each rejects an empty string. withAudience() is variadic — pass several audiences to write a multi-valued aud (validation requires the token to be permitted for the first one).
$jwt = JWT::for()
->withIssuer('https://api.my-app.com')
->withAudience('https://app-a.com', 'https://app-b.com')
->withIdentifier(bin2hex(random_bytes(16))); // unique jti per token
withClaims / withHeader / withHeaders (3.2.0)¶
Add custom top-level claims or JOSE headers. Reserved claim names (uid and the registered claims) and the internal cty header are rejected to surface collisions early.
$jwt = JWT::for()
->withClaims(['scope' => 'admin', 'tenant' => 'acme'])
->withHeader('x-trace-id', $traceId);
withKeyId (3.2.0)¶
Stamp the active key id as a kid header for key rotation. See the Advanced guide for the verifying-key map.
Reading claims¶
| Method | Returns | Validates? |
|---|---|---|
getPayload(string $token) |
mixed |
✅ Returns the original payload (auto-decodes compact JSON). |
getClaims(string $token) |
array |
✅ All claims — the safe counterpart of extractClaimsUnsafe(). |
getClaim(string $token, string $name) |
mixed |
✅ A single claim (null when absent). |
extractClaimsUnsafe(string $token) |
?array |
❌ Parse only — logs a warning unless allowUnsafeExtraction = true. |
$claims = $jwt->getClaims($token); // ['data' => ..., 'uid' => ..., 'iss' => ...]
$scope = $jwt->getClaim($token, 'scope'); // validated single value
Validation constraints¶
Controlled by Config\JWT::$validateClaims. Allowed values:
| Name | Library class | Notes |
|---|---|---|
SignedWith |
Lcobucci\JWT\Validation\Constraint\SignedWith |
Signature verification |
IssuedBy |
Lcobucci\JWT\Validation\Constraint\IssuedBy |
iss |
IdentifiedBy |
Lcobucci\JWT\Validation\Constraint\IdentifiedBy |
jti |
PermittedFor |
Lcobucci\JWT\Validation\Constraint\PermittedFor |
aud |
LooseValidAt (default) |
Lcobucci\JWT\Validation\Constraint\LooseValidAt |
iat/nbf/exp with leeway |
StrictValidAt |
Lcobucci\JWT\Validation\Constraint\StrictValidAt |
iat/nbf/exp requiring all three |
ValidAt (alias) |
→ LooseValidAt |
Provided for v2.x config compatibility |