diff --git a/docs/src/content/docs/internals/adding-and-overriding-definitions.mdx b/docs/src/content/docs/internals/adding-and-overriding-definitions.mdx index e46d21d..809953a 100644 --- a/docs/src/content/docs/internals/adding-and-overriding-definitions.mdx +++ b/docs/src/content/docs/internals/adding-and-overriding-definitions.mdx @@ -83,6 +83,6 @@ you can effectively override the definition of any type (including PHP internal use GoodPhp\Reflection\ReflectorBuilder; $reflector = new ReflectorBuilder() - ->withAddedDefinitionProvider(new YourCustomDefinitionProvider(), prepend: true) + ->withAddedFallbackDefinitionProvider(new YourCustomDefinitionProvider()) ->build(); ``` diff --git a/src/NativePHPDoc/Definition/Fallback/FallbackDefinitionProvider.php b/src/NativePHPDoc/Definition/FallbackDefinitionProvider.php similarity index 79% rename from src/NativePHPDoc/Definition/Fallback/FallbackDefinitionProvider.php rename to src/NativePHPDoc/Definition/FallbackDefinitionProvider.php index 56053ca..3828bbe 100644 --- a/src/NativePHPDoc/Definition/Fallback/FallbackDefinitionProvider.php +++ b/src/NativePHPDoc/Definition/FallbackDefinitionProvider.php @@ -1,9 +1,6 @@ provider()->forType($type); + } + + private function provider(): DefinitionProvider + { + return $this->provider ??= ($this->resolve)(); + } +} diff --git a/src/ReflectorBuilder.php b/src/ReflectorBuilder.php index 0b9a804..19076e9 100644 --- a/src/ReflectorBuilder.php +++ b/src/ReflectorBuilder.php @@ -11,7 +11,8 @@ use GoodPhp\Reflection\NativePHPDoc\Definition\Cache\FileModificationCacheDefinitionProvider; use GoodPhp\Reflection\NativePHPDoc\Definition\Cache\StaticCacheDefinitionProvider; use GoodPhp\Reflection\NativePHPDoc\Definition\DefinitionProvider; -use GoodPhp\Reflection\NativePHPDoc\Definition\Fallback\FallbackDefinitionProvider; +use GoodPhp\Reflection\NativePHPDoc\Definition\FallbackDefinitionProvider; +use GoodPhp\Reflection\NativePHPDoc\Definition\LazyDefinitionProvider; use GoodPhp\Reflection\NativePHPDoc\Definition\NativePHPDoc\File\FileContextParser; use GoodPhp\Reflection\NativePHPDoc\Definition\NativePHPDoc\Native\NativeTypeMapper; use GoodPhp\Reflection\NativePHPDoc\Definition\NativePHPDoc\NativePHPDocDefinitionProvider; @@ -33,30 +34,47 @@ class ReflectorBuilder { + /** @var list */ + private array $addedFallbackDefinitionProviders = []; + private ?DefinitionProvider $innerDefinitionProvider = null; + /** @var callable(DefinitionProvider): DefinitionProvider|null */ + private mixed $wrapInnerDefinitionProvider = null; + private ?DefinitionProvider $definitionProvider = null; + /** @var callable(DefinitionProvider): DefinitionProvider|null */ + private mixed $wrapDefinitionProvider = null; + private (CacheItemPoolInterface&NamespacedPoolInterface)|null $inMemoryCache = null; + /** + * Use file cache to cache definitions of the inner provider. Can (and should) be used together with {@see withMemoryCache()} + */ public function withFileCache(?string $path = null, ?DateInterval $ttl = null): self { $path ??= sys_get_temp_dir() . '/good-php-reflection'; - return $this->withInnerDefinitionProvider(new FileModificationCacheDefinitionProvider( - $this->innerDefinitionProvider(), - new VerifiedCache( - new Psr16Cache( - new PhpFilesAdapter( - namespace: (string) InstalledVersions::getReference('good-php/reflection'), - defaultLifetime: (int) $ttl?->format('s'), - directory: $path, + return $this->wrapInnerDefinitionProvider( + fn (DefinitionProvider $provider) => new FileModificationCacheDefinitionProvider( + $provider, + new VerifiedCache( + new Psr16Cache( + new PhpFilesAdapter( + namespace: (string) InstalledVersions::getReference('good-php/reflection'), + defaultLifetime: (int) $ttl?->format('s'), + directory: $path, + ) ) ) ) - )); + ); } + /** + * Use memory cache to cache all definitions. Can (and should) be used together with {@see withFileCache()} + */ public function withMemoryCache(int $maxItems = 100, ?DateInterval $ttl = null): self { $this->inMemoryCache = new ArrayAdapter( @@ -65,24 +83,20 @@ public function withMemoryCache(int $maxItems = 100, ?DateInterval $ttl = null): maxItems: $maxItems, ); - return $this->withDefinitionProvider(new StaticCacheDefinitionProvider( - $this->definitionProvider(), - new Psr16Cache($this->inMemoryCache->withSubNamespace('definitions')) - )); + return $this->wrapDefinitionProvider( + fn (DefinitionProvider $provider) => new StaticCacheDefinitionProvider( + $provider, + new Psr16Cache($this->inMemoryCache->withSubNamespace('definitions')) + ) + ); } - public function build(): Reflector + public function withAddedFallbackDefinitionProvider(DefinitionProvider $provider): self { - $reflector = new DefinitionProviderReflector($this->definitionProvider()); - - if ($this->inMemoryCache) { - $reflector = new StaticCacheReflector( - $reflector, - new Psr16Cache($this->inMemoryCache->withSubNamespace('reflections')) - ); - } + $builder = clone $this; + $builder->addedFallbackDefinitionProviders[] = $provider; - return $reflector; + return $builder; } public function withInnerDefinitionProvider(DefinitionProvider $provider): self @@ -93,6 +107,17 @@ public function withInnerDefinitionProvider(DefinitionProvider $provider): self return $builder; } + /** + * @param callable(DefinitionProvider): DefinitionProvider $wrap + */ + public function wrapInnerDefinitionProvider(callable $wrap): self + { + $builder = clone $this; + $builder->wrapInnerDefinitionProvider = $wrap; + + return $builder; + } + public function withDefinitionProvider(DefinitionProvider $provider): self { $builder = clone $this; @@ -101,37 +126,75 @@ public function withDefinitionProvider(DefinitionProvider $provider): self return $builder; } - public function innerDefinitionProvider(): DefinitionProvider + /** + * @param callable(DefinitionProvider): DefinitionProvider $wrap + */ + public function wrapDefinitionProvider(callable $wrap): self { - if ($this->innerDefinitionProvider) { - return $this->innerDefinitionProvider; + $builder = clone $this; + $builder->wrapDefinitionProvider = $wrap; + + return $builder; + } + + public function build(): Reflector + { + $reflector = new DefinitionProviderReflector($this->definitionProvider()); + + if ($this->inMemoryCache) { + $reflector = new StaticCacheReflector( + $reflector, + new Psr16Cache($this->inMemoryCache->withSubNamespace('reflections')) + ); } - $config = new ParserConfig(usedAttributes: []); - $typeAliasResolver = new TypeAliasResolver(); - $constExprParser = new ConstExprParser($config); - $typeParser = new TypeParser($config, $constExprParser); - $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); - $lexer = new Lexer($config); - $phpDocStringParser = new PhpDocStringParser($lexer, $phpDocParser); - - return $this->innerDefinitionProvider = new NativePHPDocDefinitionProvider( - $phpDocStringParser, - new FileContextParser( - (new ParserFactory())->createForNewestSupportedVersion(), - ), - $typeAliasResolver, - new NativeTypeMapper(), - new PhpDocTypeMapper($typeAliasResolver), - ); + return $reflector; + } + + private function innerDefinitionProvider(): DefinitionProvider + { + // Resolving some of these dependencies is quite expensive. So if we can + // avoid doing so with fully cached definitions, then we should do so :) + $provider = $this->innerDefinitionProvider ?? new LazyDefinitionProvider(static function () { + $config = new ParserConfig(usedAttributes: []); + $typeAliasResolver = new TypeAliasResolver(); + $constExprParser = new ConstExprParser($config); + $typeParser = new TypeParser($config, $constExprParser); + $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); + $lexer = new Lexer($config); + $phpDocStringParser = new PhpDocStringParser($lexer, $phpDocParser); + + return new NativePHPDocDefinitionProvider( + $phpDocStringParser, + new FileContextParser( + (new ParserFactory())->createForNewestSupportedVersion(), + ), + $typeAliasResolver, + new NativeTypeMapper(), + new PhpDocTypeMapper($typeAliasResolver), + ); + }); + + return $this->wrapInnerDefinitionProvider ? ($this->wrapInnerDefinitionProvider)($provider) : $provider; + } + + private function definitionProvider(): DefinitionProvider + { + $provider = $this->definitionProvider ?? new FallbackDefinitionProvider($this->fallbackDefinitionProviders()); + + return $this->wrapDefinitionProvider ? ($this->wrapDefinitionProvider)($provider) : $provider; } - public function definitionProvider(): DefinitionProvider + /** + * @return list + */ + private function fallbackDefinitionProviders(): array { - return $this->definitionProvider ??= new FallbackDefinitionProvider([ + return [ + ...$this->addedFallbackDefinitionProviders, new BuiltInSpecialsDefinitionProvider(), new BuiltInCoreDefinitionProvider(), $this->innerDefinitionProvider(), - ]); + ]; } } diff --git a/tests/Unit/Reflection/ReflectorBuilderTest.php b/tests/Unit/Reflection/ReflectorBuilderTest.php new file mode 100644 index 0000000..94e5728 --- /dev/null +++ b/tests/Unit/Reflection/ReflectorBuilderTest.php @@ -0,0 +1,37 @@ +withMemoryCache() + ->withAddedFallbackDefinitionProvider(new class () implements DefinitionProvider { + public function forType(string $type): ?TypeDefinition + { + if ($type === 'test') { + throw new RuntimeException('My provider'); + } + + return null; + } + }) + ->build(); + + self::assertInstanceOf(SpecialTypeReflection::class, $reflector->forType('string')); + + $this->expectExceptionObject(new RuntimeException('My provider')); + $reflector->forType('test'); + } +}