Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
```
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
<?php

namespace GoodPhp\Reflection\NativePHPDoc\Definition\Fallback;

use GoodPhp\Reflection\NativePHPDoc\Definition\DefinitionProvider;
use GoodPhp\Reflection\NativePHPDoc\Definition\TypeDefinition;
namespace GoodPhp\Reflection\NativePHPDoc\Definition;

class FallbackDefinitionProvider implements DefinitionProvider
{
Expand Down
25 changes: 25 additions & 0 deletions src/NativePHPDoc/Definition/LazyDefinitionProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace GoodPhp\Reflection\NativePHPDoc\Definition;

class LazyDefinitionProvider implements DefinitionProvider
{
private readonly DefinitionProvider $provider;

/**
* @param callable(): DefinitionProvider $resolve
*/
public function __construct(
private readonly mixed $resolve,
) {}

public function forType(string $type): ?TypeDefinition
{
return $this->provider()->forType($type);
}

private function provider(): DefinitionProvider
{
return $this->provider ??= ($this->resolve)();
}
}
157 changes: 110 additions & 47 deletions src/ReflectorBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,30 +34,47 @@

class ReflectorBuilder
{
/** @var list<DefinitionProvider> */
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(
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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<DefinitionProvider>
*/
private function fallbackDefinitionProviders(): array
{
return $this->definitionProvider ??= new FallbackDefinitionProvider([
return [
...$this->addedFallbackDefinitionProviders,
new BuiltInSpecialsDefinitionProvider(),
new BuiltInCoreDefinitionProvider(),
$this->innerDefinitionProvider(),
]);
];
}
}
37 changes: 37 additions & 0 deletions tests/Unit/Reflection/ReflectorBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Tests\Unit\Reflection;

use GoodPhp\Reflection\NativePHPDoc\Definition\DefinitionProvider;
use GoodPhp\Reflection\NativePHPDoc\Definition\TypeDefinition;
use GoodPhp\Reflection\Reflection\SpecialTypeReflection;
use GoodPhp\Reflection\ReflectorBuilder;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use RuntimeException;

#[CoversClass(ReflectorBuilder::class)]
class ReflectorBuilderTest extends TestCase
{
public function testBuildsWithAddedReflectionProvider(): void
{
$reflector = (new ReflectorBuilder())
->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');
}
}