*/ final readonly class AccessToken implements Arrayable { public function __construct( public string $accessToken, public ?string $refreshToken, public ?CarbonImmutable $expiresAt, public string $tokenType = 'Bearer', /** @var array */ public array $scopes = [], ) {} /** * @param array $payload */ public static function fromResponse(array $payload): self { $expiresIn = isset($payload['expires_in']) ? (int) $payload['expires_in'] : null; return new self( accessToken: (string) $payload['access_token'], refreshToken: isset($payload['refresh_token']) ? (string) $payload['refresh_token'] : null, // Shave a minute off so we refresh slightly before the server // would start rejecting the token mid-request. expiresAt: $expiresIn !== null ? CarbonImmutable::now()->addSeconds($expiresIn)->subMinute() : null, tokenType: (string) ($payload['token_type'] ?? 'Bearer'), scopes: array_values(array_filter(explode(' ', (string) ($payload['scope'] ?? '')))), ); } /** * @param array $state */ public static function fromArray(array $state): self { return new self( accessToken: (string) $state['access_token'], refreshToken: $state['refresh_token'] ?? null, expiresAt: isset($state['expires_at']) ? CarbonImmutable::parse($state['expires_at']) : null, tokenType: (string) ($state['token_type'] ?? 'Bearer'), scopes: $state['scopes'] ?? [], ); } public function hasExpired(): bool { return $this->expiresAt !== null && $this->expiresAt->isPast(); } public function isRefreshable(): bool { return $this->refreshToken !== null; } public function hasScope(string $scope): bool { return in_array($scope, $this->scopes, true); } /** * @return array */ public function toArray(): array { return [ 'access_token' => $this->accessToken, 'refresh_token' => $this->refreshToken, 'expires_at' => $this->expiresAt?->toIso8601String(), 'token_type' => $this->tokenType, 'scopes' => $this->scopes, ]; } }