From 59b291fec45c9fb65d76c2f8831a77f70daf95b8 Mon Sep 17 00:00:00 2001 From: Filippo Tessarotto Date: Thu, 5 Dec 2024 08:13:26 +0100 Subject: [PATCH] Add migration API for `readonly` classes --- docs/configuration.md | 8 +-- docs/extending-the-library.md | 6 +- docs/issuing-tokens.md | 4 +- src/Configuration.php | 100 +++++++++++++++++++++++++++++++--- src/JwtFacade.php | 2 +- src/Token/Builder.php | 6 ++ tests/ConfigurationTest.php | 77 +++++++++++++++++++++++--- tests/Token/BuilderTest.php | 6 +- 8 files changed, 179 insertions(+), 30 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 68ea2966..72942496 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -136,7 +136,7 @@ $configuration = Configuration::forSymmetricSigner( InMemory::base64Encoded('mBC5v1sOKVvbdEitdSBenu59nfNfhwkedkJVNabosTw=') ); -$configuration->setBuilderFactory( +$configuration = $configuration->withBuilderFactory( static function (ClaimsFormatter $formatter): Builder { // This assumes `MyCustomBuilder` is an existing class return new MyCustomBuilder(new JoseEncoder(), $formatter); @@ -165,7 +165,7 @@ $configuration = Configuration::forSymmetricSigner( ); // This assumes `MyParser` is an existing class -$configuration->setParser(new MyParser()); +$configuration = $configuration->withParser(new MyParser()); ``` ### Validator @@ -189,7 +189,7 @@ $configuration = Configuration::forSymmetricSigner( ); // This assumes `MyValidator` is an existing class -$configuration->setValidator(new MyValidator()); +$configuration = $configuration->withValidator(new MyValidator()); ``` ### Validation constraints @@ -216,7 +216,7 @@ $configuration = Configuration::forSymmetricSigner( InMemory::base64Encoded('mBC5v1sOKVvbdEitdSBenu59nfNfhwkedkJVNabosTw=') ); -$configuration->setValidationConstraints( +$configuration = $configuration->withValidationConstraints( new SignedWith($configuration->signer(), $configuration->signingKey()), new StrictValidAt(SystemClock::fromUTC()), new IssuedBy('https://api.my-awesome-company.com') diff --git a/docs/extending-the-library.md b/docs/extending-the-library.md index 52c63d5a..1820d77d 100644 --- a/docs/extending-the-library.md +++ b/docs/extending-the-library.md @@ -32,7 +32,7 @@ use Lcobucci\JWT\Configuration; $config = $container->get(Configuration::class); assert($config instanceof Configuration); -$config->setBuilderFactory( +$configuration = $configuration->withBuilderFactory( static function (ClaimsFormatter $formatter): Builder { return new MyCustomTokenBuilder($formatter); } @@ -99,7 +99,7 @@ use Lcobucci\JWT\Configuration; $config = $container->get(Configuration::class); assert($config instanceof Configuration); -$config->setParser(new MyCustomTokenParser()); +$configuration = $configuration->withParser(new MyCustomTokenParser()); ``` ## Signer @@ -157,7 +157,7 @@ use Lcobucci\JWT\Configuration; $config = $container->get(Configuration::class); assert($config instanceof Configuration); -$config->setValidator(new MyCustomTokenValidator()); +$configuration = $configuration->withValidator(new MyCustomTokenValidator()); ``` ## Validation constraints diff --git a/docs/issuing-tokens.md b/docs/issuing-tokens.md index e6700ce4..e13f9b3a 100644 --- a/docs/issuing-tokens.md +++ b/docs/issuing-tokens.md @@ -14,7 +14,7 @@ use Lcobucci\JWT\Token\Builder; require 'vendor/autoload.php'; -$tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); +$tokenBuilder = Builder::new(new JoseEncoder(), ChainedFormatter::default()); $algorithm = new Sha256(); $signingKey = InMemory::plainText(random_bytes(32)); @@ -58,7 +58,7 @@ use Lcobucci\JWT\Token\Builder; require 'vendor/autoload.php'; -$tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); +$tokenBuilder = Builder::new(new JoseEncoder(), ChainedFormatter::default()); $algorithm = new Sha256(); $signingKey = InMemory::plainText(random_bytes(32)); diff --git a/src/Configuration.php b/src/Configuration.php index 488ea3e2..ea05e3b9 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -24,21 +24,29 @@ final class Configuration private Closure $builderFactory; /** @var Constraint[] */ - private array $validationConstraints = []; + private array $validationConstraints; + /** @param Closure(ClaimsFormatter $claimFormatter): Builder|null $builderFactory */ private function __construct( private readonly Signer $signer, private readonly Key $signingKey, private readonly Key $verificationKey, - Encoder $encoder, - Decoder $decoder, + private readonly Encoder $encoder, + private readonly Decoder $decoder, + ?Parser $parser, + ?Validator $validator, + ?Closure $builderFactory, + Constraint ...$validationConstraints, ) { - $this->parser = new Token\Parser($decoder); - $this->validator = new Validation\Validator(); + $this->parser = $parser ?? new Token\Parser($decoder); + $this->validator = $validator ?? new Validation\Validator(); - $this->builderFactory = static function (ClaimsFormatter $claimFormatter) use ($encoder): Builder { - return new Token\Builder($encoder, $claimFormatter); - }; + $this->builderFactory = $builderFactory + ?? static function (ClaimsFormatter $claimFormatter) use ($encoder): Builder { + return Token\Builder::new($encoder, $claimFormatter); + }; + + $this->validationConstraints = $validationConstraints; } public static function forAsymmetricSigner( @@ -54,6 +62,9 @@ public static function forAsymmetricSigner( $verificationKey, $encoder, $decoder, + null, + null, + null, ); } @@ -69,15 +80,38 @@ public static function forSymmetricSigner( $key, $encoder, $decoder, + null, + null, + null, ); } - /** @param callable(ClaimsFormatter): Builder $builderFactory */ + /** + * @deprecated Deprecated since v5.5, please use {@see self::withBuilderFactory()} instead + * + * @param callable(ClaimsFormatter): Builder $builderFactory + */ public function setBuilderFactory(callable $builderFactory): void { $this->builderFactory = $builderFactory(...); } + /** @param callable(ClaimsFormatter): Builder $builderFactory */ + public function withBuilderFactory(callable $builderFactory): self + { + return new self( + $this->signer, + $this->signingKey, + $this->verificationKey, + $this->encoder, + $this->decoder, + $this->parser, + $this->validator, + $builderFactory(...), + ...$this->validationConstraints, + ); + } + public function builder(?ClaimsFormatter $claimFormatter = null): Builder { return ($this->builderFactory)($claimFormatter ?? ChainedFormatter::default()); @@ -88,11 +122,27 @@ public function parser(): Parser return $this->parser; } + /** @deprecated Deprecated since v5.5, please use {@see self::withParser()} instead */ public function setParser(Parser $parser): void { $this->parser = $parser; } + public function withParser(Parser $parser): self + { + return new self( + $this->signer, + $this->signingKey, + $this->verificationKey, + $this->encoder, + $this->decoder, + $parser, + $this->validator, + $this->builderFactory, + ...$this->validationConstraints, + ); + } + public function signer(): Signer { return $this->signer; @@ -113,19 +163,51 @@ public function validator(): Validator return $this->validator; } + /** @deprecated Deprecated since v5.5, please use {@see self::withValidator()} instead */ public function setValidator(Validator $validator): void { $this->validator = $validator; } + public function withValidator(Validator $validator): self + { + return new self( + $this->signer, + $this->signingKey, + $this->verificationKey, + $this->encoder, + $this->decoder, + $this->parser, + $validator, + $this->builderFactory, + ...$this->validationConstraints, + ); + } + /** @return Constraint[] */ public function validationConstraints(): array { return $this->validationConstraints; } + /** @deprecated Deprecated since v5.5, please use {@see self::withValidationConstraints()} instead */ public function setValidationConstraints(Constraint ...$validationConstraints): void { $this->validationConstraints = $validationConstraints; } + + public function withValidationConstraints(Constraint ...$validationConstraints): self + { + return new self( + $this->signer, + $this->signingKey, + $this->verificationKey, + $this->encoder, + $this->decoder, + $this->parser, + $this->validator, + $this->builderFactory, + ...$validationConstraints, + ); + } } diff --git a/src/JwtFacade.php b/src/JwtFacade.php index 90464757..41d0f7c1 100644 --- a/src/JwtFacade.php +++ b/src/JwtFacade.php @@ -38,7 +38,7 @@ public function issue( Key $signingKey, Closure $customiseBuilder, ): UnencryptedToken { - $builder = new Token\Builder(new JoseEncoder(), ChainedFormatter::withUnixTimestampDates()); + $builder = Token\Builder::new(new JoseEncoder(), ChainedFormatter::withUnixTimestampDates()); $now = $this->clock->now(); $builder = $builder diff --git a/src/Token/Builder.php b/src/Token/Builder.php index 355766b7..3ba82b59 100644 --- a/src/Token/Builder.php +++ b/src/Token/Builder.php @@ -25,10 +25,16 @@ final class Builder implements BuilderInterface /** @var array */ private array $claims = []; + /** @deprecated Deprecated since v5.5, please use {@see self::new()} instead */ public function __construct(private readonly Encoder $encoder, private readonly ClaimsFormatter $claimFormatter) { } + public static function new(Encoder $encoder, ClaimsFormatter $claimFormatter): self + { + return new self($encoder, $claimFormatter); + } + /** * @inheritDoc * @pure diff --git a/tests/ConfigurationTest.php b/tests/ConfigurationTest.php index 99a91556..807933db 100644 --- a/tests/ConfigurationTest.php +++ b/tests/ConfigurationTest.php @@ -81,8 +81,8 @@ public function builderShouldCreateABuilderWithDefaultEncoderAndClaimFactory(): $builder = $config->builder(); self::assertInstanceOf(BuilderImpl::class, $builder); - self::assertNotEquals(new BuilderImpl($this->encoder, ChainedFormatter::default()), $builder); - self::assertEquals(new BuilderImpl(new JoseEncoder(), ChainedFormatter::default()), $builder); + self::assertNotEquals(BuilderImpl::new($this->encoder, ChainedFormatter::default()), $builder); + self::assertEquals(BuilderImpl::new(new JoseEncoder(), ChainedFormatter::default()), $builder); } #[PHPUnit\Test] @@ -96,11 +96,11 @@ public function builderShouldCreateABuilderWithCustomizedEncoderAndClaimFactory( $builder = $config->builder(); self::assertInstanceOf(BuilderImpl::class, $builder); - self::assertEquals(new BuilderImpl($this->encoder, ChainedFormatter::default()), $builder); + self::assertEquals(BuilderImpl::new($this->encoder, ChainedFormatter::default()), $builder); } #[PHPUnit\Test] - public function builderShouldUseBuilderFactoryWhenThatIsConfigured(): void + public function builderShouldUseBuilderFactoryWhenThatIsConfiguredWithDeprecatedSet(): void { $builder = $this->createMock(Builder::class); @@ -108,6 +108,7 @@ public function builderShouldUseBuilderFactoryWhenThatIsConfigured(): void new KeyDumpSigner(), InMemory::plainText('private'), ); + /** @phpstan-ignore method.deprecated */ $config->setBuilderFactory( static function () use ($builder): Builder { return $builder; @@ -116,6 +117,24 @@ static function () use ($builder): Builder { self::assertSame($builder, $config->builder()); } + #[PHPUnit\Test] + public function builderShouldUseBuilderFactoryWhenThatIsConfigured(): void + { + $builder = $this->createMock(Builder::class); + + $config = Configuration::forSymmetricSigner( + new KeyDumpSigner(), + InMemory::plainText('private'), + ); + $newConfig = $config->withBuilderFactory( + static function () use ($builder): Builder { + return $builder; + }, + ); + self::assertNotSame($builder, $config->builder()); + self::assertSame($builder, $newConfig->builder()); + } + #[PHPUnit\Test] public function parserShouldReturnAParserWithDefaultDecoder(): void { @@ -142,17 +161,31 @@ public function parserShouldReturnAParserWithCustomizedDecoder(): void } #[PHPUnit\Test] - public function parserShouldNotCreateAnInstanceIfItWasConfigured(): void + public function parserShouldNotCreateAnInstanceIfItWasConfiguredWithDeprecatedSet(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); + /** @phpstan-ignore method.deprecated */ $config->setParser($this->parser); self::assertSame($this->parser, $config->parser()); } + #[PHPUnit\Test] + public function parserShouldNotCreateAnInstanceIfItWasConfigured(): void + { + $config = Configuration::forSymmetricSigner( + new KeyDumpSigner(), + InMemory::plainText('private'), + ); + $newConfig = $config->withParser($this->parser); + + self::assertNotSame($this->parser, $config->parser()); + self::assertSame($this->parser, $newConfig->parser()); + } + #[PHPUnit\Test] public function validatorShouldReturnTheDefaultWhenItWasNotConfigured(): void { @@ -166,17 +199,31 @@ public function validatorShouldReturnTheDefaultWhenItWasNotConfigured(): void } #[PHPUnit\Test] - public function validatorShouldReturnTheConfiguredValidator(): void + public function validatorShouldReturnTheConfiguredValidatorWithDeprecatedSet(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); + /** @phpstan-ignore method.deprecated */ $config->setValidator($this->validator); self::assertSame($this->validator, $config->validator()); } + #[PHPUnit\Test] + public function validatorShouldReturnTheConfiguredValidator(): void + { + $config = Configuration::forSymmetricSigner( + new KeyDumpSigner(), + InMemory::plainText('private'), + ); + $newConfig = $config->withValidator($this->validator); + + self::assertNotSame($this->validator, $config->validator()); + self::assertSame($this->validator, $newConfig->validator()); + } + #[PHPUnit\Test] public function validationConstraintsShouldReturnAnEmptyArrayWhenItWasNotConfigured(): void { @@ -189,17 +236,31 @@ public function validationConstraintsShouldReturnAnEmptyArrayWhenItWasNotConfigu } #[PHPUnit\Test] - public function validationConstraintsShouldReturnTheConfiguredValidator(): void + public function validationConstraintsShouldReturnTheConfiguredValidatorWithDeprecatedSet(): void { $config = Configuration::forSymmetricSigner( new KeyDumpSigner(), InMemory::plainText('private'), ); + /** @phpstan-ignore method.deprecated */ $config->setValidationConstraints($this->validationConstraints); self::assertSame([$this->validationConstraints], $config->validationConstraints()); } + #[PHPUnit\Test] + public function validationConstraintsShouldReturnTheConfiguredValidator(): void + { + $config = Configuration::forSymmetricSigner( + new KeyDumpSigner(), + InMemory::plainText('private'), + ); + $newConfig = $config->withValidationConstraints($this->validationConstraints); + + self::assertNotSame([$this->validationConstraints], $config->validationConstraints()); + self::assertSame([$this->validationConstraints], $newConfig->validationConstraints()); + } + #[PHPUnit\Test] public function customClaimFormatterCanBeUsed(): void { @@ -209,6 +270,6 @@ public function customClaimFormatterCanBeUsed(): void InMemory::plainText('private'), ); - self::assertEquals(new BuilderImpl(new JoseEncoder(), $formatter), $config->builder($formatter)); + self::assertEquals(BuilderImpl::new(new JoseEncoder(), $formatter), $config->builder($formatter)); } } diff --git a/tests/Token/BuilderTest.php b/tests/Token/BuilderTest.php index 9857a7e1..aa41c91e 100644 --- a/tests/Token/BuilderTest.php +++ b/tests/Token/BuilderTest.php @@ -42,7 +42,7 @@ public function initializeDependencies(): void #[PHPUnit\Test] public function withClaimShouldRaiseExceptionWhenTryingToConfigureARegisteredClaim(): void { - $builder = new Builder($this->encoder, new MicrosecondBasedDateConversion()); + $builder = Builder::new($this->encoder, new MicrosecondBasedDateConversion()); $this->expectException(RegisteredClaimGiven::class); $this->expectExceptionMessage( @@ -76,7 +76,7 @@ public function getTokenShouldReturnACompletelyConfigureToken(): void ->with('1.2') ->willReturn('3'); - $builder = new Builder($this->encoder, new MicrosecondBasedDateConversion()); + $builder = Builder::new($this->encoder, new MicrosecondBasedDateConversion()); $token = $builder->identifiedBy('123456') ->issuedBy('https://issuer.com') ->issuedAt($issuedAt) @@ -108,7 +108,7 @@ public function getTokenShouldReturnACompletelyConfigureToken(): void public function immutability(): void { $map = new SplObjectStorage(); - $builder = new Builder($this->encoder, new MicrosecondBasedDateConversion()); + $builder = Builder::new($this->encoder, new MicrosecondBasedDateConversion()); $map[$builder] = true; $builder = $builder->identifiedBy('123456'); $map[$builder] = true;