Files
michaelschiemer/src/Framework/Reflection/CachedReflectionProvider.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

496 lines
16 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Reflection;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\Statistics;
use App\Framework\Reflection\Collections\AttributeCollection;
use App\Framework\Reflection\Collections\MethodCollection;
use App\Framework\Reflection\Collections\ParameterCollection;
use App\Framework\Reflection\Collections\PropertyCollection;
use App\Framework\Reflection\Factory\ReflectionCacheFactory;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use ReflectionType;
final readonly class CachedReflectionProvider implements ReflectionProvider
{
private ReflectionCache $cache;
/**
* @param ReflectionCache|null $cache The reflection cache to use, or null to create one
* @param ReflectionCacheFactory|null $factory The factory to use for creating a cache if none is provided
* @param string|null $environment The environment to use for cache configuration ('production' or 'development')
*/
public function __construct(
?ReflectionCache $cache = null,
?ReflectionCacheFactory $factory = null,
?string $environment = null
) {
if ($cache === null) {
$factory ??= new ReflectionCacheFactory();
$env = $environment ?? 'production';
// Use optimized cache in production, regular cache in development
$this->cache = $env === 'development'
? $factory->createForDevelopment()
: $factory->createOptimized();
} else {
$this->cache = $cache;
}
}
// ClassReflector implementation
public function getClass(ClassName $className): WrappedReflectionClass
{
return $this->cache->classCache->getClass($className);
}
public function getNativeClass(ClassName $className): ReflectionClass
{
return $this->cache->classCache->getNativeClass($className);
}
public function getProperties(ClassName $className): PropertyCollection
{
return $this->cache->classCache->getProperties($className);
}
public function getMethods(ClassName $className, ?int $filter = null): MethodCollection
{
return $this->cache->classCache->getMethods($className, $filter);
}
public function getAttributes(ClassName $className, ?string $attributeClass = null): AttributeCollection
{
return $this->cache->classCache->getAttributes($className, $attributeClass);
}
public function hasAttribute(ClassName $className, string $attributeClass): bool
{
return $this->cache->classCache->hasAttribute($className, $attributeClass);
}
public function isInstantiable(ClassName $className): bool
{
return $this->cache->classCache->isInstantiable($className);
}
public function implementsInterface(ClassName $className, string $interfaceName): bool
{
return $this->cache->classCache->implementsInterface($className, $interfaceName);
}
// MethodReflector implementation
public function getMethod(ClassName $className, string $methodName): WrappedReflectionMethod
{
return $this->cache->methodCache->getMethod($className, $methodName);
}
public function getNativeMethod(ClassName $className, string $methodName): ReflectionMethod
{
return $this->cache->methodCache->getNativeMethod($className, $methodName);
}
public function getMethodParameters(ClassName $className, string $methodName): ParameterCollection
{
return $this->cache->parameterCache->getParameters($className, $methodName);
}
public function getMethodAttributes(ClassName $className, string $methodName, ?string $attributeClass = null): AttributeCollection
{
return $this->cache->methodCache->getMethodAttributes($className, $methodName, $attributeClass);
}
public function getParameterInfo(ClassName $className, string $methodName): array
{
return $this->cache->parameterCache->getParameterInfo($className, $methodName);
}
// PropertyReflector implementation
public function getProperty(ClassName $className, string $propertyName): ReflectionProperty
{
return $this->getNativeClass($className)->getProperty($propertyName);
}
public function hasProperty(ClassName $className, string $propertyName): bool
{
return $this->getNativeClass($className)->hasProperty($propertyName);
}
public function getPropertiesWithAttribute(ClassName $className, string $attributeClass): PropertyCollection
{
$properties = $this->getProperties($className);
$result = [];
foreach ($properties as $property) {
if (count($property->getAttributes($attributeClass)) > 0) {
$result[] = $property;
}
}
return new PropertyCollection(...$result);
}
public function getPropertyAttributes(ClassName $className, string $propertyName, ?string $attributeClass = null): array
{
$property = $this->getProperty($className, $propertyName);
return $property->getAttributes($attributeClass);
}
public function getPropertyType(ClassName $className, string $propertyName): ?ReflectionType
{
$property = $this->getProperty($className, $propertyName);
return $property->getType();
}
public function getPropertyDefaultValue(ClassName $className, string $propertyName): mixed
{
$property = $this->getProperty($className, $propertyName);
if (! $property->hasDefaultValue()) {
return null;
}
return $property->getDefaultValue();
}
// ParameterReflector implementation
public function getMethodParameter(ClassName $className, string $methodName, string $parameterName): ReflectionParameter
{
$method = $this->getNativeMethod($className, $methodName);
foreach ($method->getParameters() as $parameter) {
if ($parameter->getName() === $parameterName) {
return $parameter;
}
}
throw new \ReflectionException("Parameter {$parameterName} does not exist in method {$methodName}");
}
public function hasMethodParameter(ClassName $className, string $methodName, string $parameterName): bool
{
$method = $this->getNativeMethod($className, $methodName);
foreach ($method->getParameters() as $parameter) {
if ($parameter->getName() === $parameterName) {
return true;
}
}
return false;
}
public function getParameterType(ClassName $className, string $methodName, string $parameterName): ?ReflectionType
{
$parameter = $this->getMethodParameter($className, $methodName, $parameterName);
return $parameter->getType();
}
public function getParameterDefaultValue(ClassName $className, string $methodName, string $parameterName): mixed
{
$parameter = $this->getMethodParameter($className, $methodName, $parameterName);
if (! $parameter->isDefaultValueAvailable()) {
return null;
}
return $parameter->getDefaultValue();
}
public function isParameterOptional(ClassName $className, string $methodName, string $parameterName): bool
{
$parameter = $this->getMethodParameter($className, $methodName, $parameterName);
return $parameter->isOptional();
}
public function isParameterVariadic(ClassName $className, string $methodName, string $parameterName): bool
{
$parameter = $this->getMethodParameter($className, $methodName, $parameterName);
return $parameter->isVariadic();
}
public function getParameterAttributes(
ClassName $className,
string $methodName,
string $parameterName,
?string $attributeClass = null
): array {
$parameter = $this->getMethodParameter($className, $methodName, $parameterName);
return $parameter->getAttributes($attributeClass);
}
// AttributeReflector implementation
public function hasMethodAttribute(ClassName $className, string $methodName, string $attributeClass): bool
{
$attributes = $this->getMethodAttributes($className, $methodName, $attributeClass);
return ! $attributes->isEmpty();
}
public function getAttributeInstances(ClassName $className, ?string $attributeClass = null): array
{
$attributes = $this->getAttributes($className, $attributeClass);
$instances = [];
foreach ($attributes as $attribute) {
try {
$instances[] = $attribute->newInstance();
} catch (\Throwable) {
// Skip attributes that can't be instantiated
}
}
return $instances;
}
public function getMethodAttributeInstances(ClassName $className, string $methodName, ?string $attributeClass = null): array
{
$attributes = $this->getMethodAttributes($className, $methodName, $attributeClass);
$instances = [];
foreach ($attributes as $attribute) {
try {
$instances[] = $attribute->newInstance();
} catch (\Throwable) {
// Skip attributes that can't be instantiated
}
}
return $instances;
}
// EnumReflector implementation
public function isEnum(ClassName $className): bool
{
return $this->getNativeClass($className)->isEnum();
}
public function getEnumCases(ClassName $className): array
{
if (! $this->isEnum($className)) {
throw new \ReflectionException("Class {$className->getFullyQualified()} is not an enum");
}
// Use ReflectionEnum instead of ReflectionClass for enum-specific methods
/** @var class-string $classNameString */
$classNameString = $className->getFullyQualified();
$reflectionEnum = new \ReflectionEnum($classNameString);
$reflectionCases = $reflectionEnum->getCases();
$cases = [];
foreach ($reflectionCases as $case) {
$caseName = $case->getName();
// Use constant() function to dynamically access enum case
$cases[$caseName] = constant("$classNameString::$caseName");
}
return $cases;
}
public function getEnumCase(ClassName $className, string $caseName): object
{
if (! $this->isEnum($className)) {
throw new \ReflectionException("Class {$className->getFullyQualified()} is not an enum");
}
if (! $this->hasEnumCase($className, $caseName)) {
throw new \ReflectionException("Enum case {$caseName} does not exist in enum {$className->getFullyQualified()}");
}
/** @var class-string $classNameString */
$classNameString = $className->getFullyQualified();
// Use constant() function to dynamically access enum case
return constant("$classNameString::$caseName");
}
public function hasEnumCase(ClassName $className, string $caseName): bool
{
if (! $this->isEnum($className)) {
return false;
}
// Use ReflectionEnum instead of ReflectionClass for enum-specific methods
/** @var class-string $classNameString */
$classNameString = $className->getFullyQualified();
$reflectionEnum = new \ReflectionEnum($classNameString);
// ReflectionEnum has a hasCase method we can use directly
return $reflectionEnum->hasCase($caseName);
}
public function getEnumCaseAttributes(
ClassName $className,
string $caseName,
?string $attributeClass = null
): array {
if (! $this->isEnum($className)) {
throw new \ReflectionException("Class {$className->getFullyQualified()} is not an enum");
}
// Use ReflectionEnum instead of ReflectionClass for enum-specific methods
/** @var class-string $classNameString */
$classNameString = $className->getFullyQualified();
$reflectionEnum = new \ReflectionEnum($classNameString);
// Check if the case exists
if (! $reflectionEnum->hasCase($caseName)) {
throw new \ReflectionException("Enum case {$caseName} does not exist in enum {$className->getFullyQualified()}");
}
// Get the case directly using getCase method
$case = $reflectionEnum->getCase($caseName);
return $case->getAttributes($attributeClass);
}
public function isBackedEnum(ClassName $className): bool
{
if (! $this->isEnum($className)) {
return false;
}
// Use ReflectionEnum instead of ReflectionClass for enum-specific methods
/** @var class-string $classNameString */
$classNameString = $className->getFullyQualified();
$reflectionEnum = new \ReflectionEnum($classNameString);
return $reflectionEnum->isBacked();
}
public function getEnumBackingType(ClassName $className): ?string
{
if (! $this->isBackedEnum($className)) {
return null;
}
// Use ReflectionEnum instead of ReflectionClass for enum-specific methods
/** @var class-string $classNameString */
$classNameString = $className->getFullyQualified();
$reflectionEnum = new \ReflectionEnum($classNameString);
$backingType = $reflectionEnum->getBackingType();
return $backingType ? $backingType->getName() : null;
}
public function getEnumCaseBackingValue(ClassName $className, string $caseName): int|string|null
{
if (! $this->isBackedEnum($className)) {
return null;
}
$case = $this->getEnumCase($className, $caseName);
// For backed enums, the value property contains the backing value
return $case->value;
}
// InstantiationReflector implementation
public function createInstance(ClassName $className, array $args = []): object
{
if (! $this->isInstantiable($className)) {
throw new \ReflectionException("Class {$className->getFullyQualified()} is not instantiable");
}
$class = $this->getNativeClass($className);
return $class->newInstance(...$args);
}
public function createInstanceWithoutConstructor(ClassName $className): object
{
$class = $this->getNativeClass($className);
return $class->newInstanceWithoutConstructor();
}
public function createLazyGhost(ClassName $className, callable $initializer, int $options = 0): object
{
$class = $this->getNativeClass($className);
return $class->newLazyGhost($initializer, $options);
}
public function createLazyProxy(ClassName $className, callable $factory, int $options = 0): object
{
$class = $this->getNativeClass($className);
return $class->newLazyProxy($factory, $options);
}
public function hasConstructor(ClassName $className): bool
{
$class = $this->getNativeClass($className);
return $class->getConstructor() !== null;
}
public function getConstructorParameters(ClassName $className): array
{
$class = $this->getNativeClass($className);
$constructor = $class->getConstructor();
if ($constructor === null) {
return [];
}
return $constructor->getParameters();
}
public function isAbstract(ClassName $className): bool
{
return $this->getNativeClass($className)->isAbstract();
}
public function isFinal(ClassName $className): bool
{
return $this->getNativeClass($className)->isFinal();
}
public function isInternal(ClassName $className): bool
{
return $this->getNativeClass($className)->isInternal();
}
public function isUserDefined(ClassName $className): bool
{
return $this->getNativeClass($className)->isUserDefined();
}
// CacheManager implementation
public function forget(ClassName $className): void
{
$this->cache->forget($className);
}
public function flush(): void
{
$this->cache->flush();
}
public function getStats(): Statistics
{
return $this->cache->getStats();
}
/**
* Get the underlying cache instance
*/
public function getCache(): ReflectionCache
{
return $this->cache;
}
}