typhoon/reflection
Static, fast alternative to PHP’s native Reflection. Reflects code without running or autoloading it, uses lazy loading + caching, and stays compatible with native reflection. Supports Psalm/PHPStan phpDoc types, template resolution, and avoids memory leaks (safe with zend.enable_gc=0).
Installation:
composer require typhoon/reflection
Add to composer.json under require-dev if only needed for testing.
First Use Case: Reflect a class without instantiating it:
use Typhoon\Reflection\ReflectionClass;
$reflection = ReflectionClass::fromName('App\Models\User');
$properties = $reflection->getProperties(); // Static analysis only
Key Entry Points:
ReflectionClass::fromName() – Primary static class reflection.ReflectionMethod::fromName() – Method reflection.ReflectionProperty::fromName() – Property reflection.ReflectionParameter::fromName() – Parameter reflection.Where to Look First:
@var, @param, etc.$method = ReflectionMethod::fromName('App\Services\OrderService', 'calculateTotal');
$returnType = $method->getReturnType(); // e.g., `float`
$params = $method->getParameters();
$property = ReflectionProperty::fromName('App\Models\User', 'email');
$type = $property->getType(); // e.g., `string`
$rules = $property->getDocComment() // Parse `@Assert\Email` annotations
?->match('/@Assert\\\\Email/') ?? false;
$class = ReflectionClass::fromName('App\Services\PaymentProcessor');
$constructor = $class->getConstructor();
$params = $constructor->getParameters();
foreach ($params as $param) {
$service = $param->getType()?->getName(); // e.g., 'App\Services\Gateway'
// Resolve $service from container
}
Collection<int, User>).$collection = ReflectionClass::fromName('Illuminate\Support\Collection');
$templateArgs = $collection->getTemplateArguments(); // ['int', 'App\Models\User']
$mockClass = ReflectionClass::fromName('App\DTOs\UserDTO');
$properties = $mockClass->getProperties();
$testData = collect($properties)->mapWithKeys(fn ($prop) => [
$prop->getName() => match ($prop->getType()?->getName()) {
'int' => 123,
'string' => 'test@example.com',
default => null,
}
]);
Service Provider Bootstrapping:
public function boot(): void
{
$this->app->resolving(function ($service, $app) {
$reflection = ReflectionClass::fromName(get_class($service));
if ($reflection->hasTag('lazy')) {
// Custom lazy-loading logic
}
});
}
Artisan Commands:
$commands = collect(Artisan::all())
->map(fn ($name) => ReflectionClass::fromName($name))
->filter(fn ($reflection) => $reflection->hasTag('hidden') === false);
Event Listeners:
$listeners = ReflectionClass::fromName('App\Listeners\LogEvent')
->getMethods()
->filter(fn ($method) => $method->hasTag('listen'));
Cache Reflection Results:
$cache = new \Symfony\Component\Cache\Simple\FilesystemCache();
$reflection = ReflectionClass::fromName('App\Models\Post', cache: $cache);
Batch Processing:
$classes = ['App\Models\User', 'App\Models\Post'];
$reflections = array_map(
fn ($class) => ReflectionClass::fromName($class),
$classes
);
Custom DocBlock Parsing:
Use getDocComment() + regex or a parser like phpDocumentor/reflection-docblock.
$docComment = $method->getDocComment();
$annotations = (new \phpDocumentor\Reflection\DocBlockFactory())->createFromString($docComment);
Native Reflection Fallback:
try {
$reflection = ReflectionClass::fromName('App\Models\User');
} catch (\Typhoon\Reflection\Exception\ReflectionException $e) {
$reflection = new \ReflectionClass('App\Models\User'); // Fallback
}
DocBlock Parsing Limitations:
@var/@param types are supported (e.g., complex generics).getDocComment() + custom parsing or fall back to native reflection.// Unsupported: `@var Collection<int, User>`
$type = $property->getType(); // May return `Collection` without template args
Template Argument Resolution:
Collection<T>) may not resolve template args (T) if not explicitly defined.getTemplateArguments() and handle null cases.
$args = $collection->getTemplateArguments(); // [null, null] if unresolved
Memory Leaks:
Symfony\Cache) must be managed.$cache->delete('typhoon_reflection_*');
Case Sensitivity:
ReflectionClass::fromName('app/models/user') fails if the actual class is App\Models\User.$normalized = str_replace('\\', DIRECTORY_SEPARATOR, $className);
$reflection = ReflectionClass::fromName($normalized);
Autoloading:
ClassNotFoundException.Enable Verbose Errors:
ReflectionClass::setDebug(true); // Logs missing classes/annotations
Inspect DocBlocks:
getDocComment() to debug annotation parsing:
dd($method->getDocComment());
Compare with Native Reflection:
$native = new \ReflectionClass('App\Models\User');
$typhoon = ReflectionClass::fromName('App\Models\User');
dd(
$native->getProperties(),
$typhoon->getProperties()
);
Handle Exceptions:
try {
$reflection = ReflectionClass::fromName('NonExistentClass');
} catch (\Typhoon\Reflection\Exception\ClassNotFoundException $e) {
// Fallback logic
}
Cache Directories:
Symfony\Cache, ensure the cache directory is writable:
$cache = new \Symfony\Component\Cache\FilesystemCache(
storage_path('framework/cache/typhoon_reflection')
);
Psalm/PHPStan Integration:
psalm-plugin-phpdoc or phpstan/extension-installer to align static analysis tools with Typhoon’s type support.How can I help you explore Laravel packages today?