$arguments * @param array $additionalData */ public function __construct( public ClassName $className, public string $attributeClass, public AttributeTarget $target, public ?MethodName $methodName = null, public ?string $propertyName = null, public array $arguments = [], public ?FilePath $filePath = null, public array $additionalData = [] ) { } public function isClassAttribute(): bool { return $this->target === AttributeTarget::TARGET_CLASS; } public function isMethodAttribute(): bool { return $this->target === AttributeTarget::METHOD; } public function isPropertyAttribute(): bool { return $this->target === AttributeTarget::PROPERTY; } /** * Create from legacy array format (for cache compatibility) * @param array $data */ public static function fromArray(array $data): self { // Handle both new DiscoveredAttribute format and old AttributeMapping format $classData = $data['class'] ?? $data['className'] ?? ''; if (is_object($classData)) { // Check if it's a complete ClassName object or incomplete class if ($classData instanceof ClassName) { $className = $classData; } elseif (is_object($classData) && method_exists($classData, 'getFullyQualified')) { try { $className = ClassName::create($classData->getFullyQualified()); } catch (\Throwable) { // Fallback: try to extract from object properties or use empty string $className = ClassName::create(''); } } else { // Incomplete class or other object - skip or use fallback $className = ClassName::create(''); } } else { $className = ClassName::create($classData); } $attributeClass = $data['attribute_class'] ?? $data['attributeClass'] ?? $data['attribute'] ?? ''; $target = AttributeTarget::from($data['target_type'] ?? $data['target'] ?? 'class'); $methodName = null; $methodData = $data['method'] ?? $data['methodName'] ?? null; if ($methodData !== null) { if (is_object($methodData)) { // Check if it's a complete MethodName object if ($methodData instanceof MethodName) { $methodName = $methodData; } elseif (method_exists($methodData, 'toString')) { try { $methodName = MethodName::create($methodData->toString()); } catch (\Throwable) { $methodName = null; } } } else { $methodName = MethodName::create($methodData); } } $filePath = null; $fileData = $data['file'] ?? $data['filePath'] ?? null; if (! empty($fileData)) { if (is_object($fileData)) { // Check if it's a complete FilePath object if ($fileData instanceof FilePath) { $filePath = $fileData; } elseif (method_exists($fileData, 'toString')) { try { $filePath = FilePath::create($fileData->toString()); } catch (\Throwable) { $filePath = null; } } } else { $filePath = FilePath::create($fileData); } } $arguments = $data['arguments'] ?? []; $additionalData = $data; // Remove standard fields from additionalData unset($additionalData['class'], $additionalData['attribute'], $additionalData['attribute_class']); unset($additionalData['target'], $additionalData['target_type'], $additionalData['method']); unset($additionalData['file'], $additionalData['arguments']); return new self( className: $className, attributeClass: $attributeClass, target: $target, methodName: $methodName, propertyName: $data['property'] ?? null, arguments: $arguments, filePath: $filePath, additionalData: $additionalData ); } /** * Get unique identifier for deduplication */ public function getUniqueId(): string { $base = $this->className->getFullyQualified() . '::' . $this->attributeClass; if ($this->methodName !== null) { $base .= '::' . $this->methodName->toString(); } elseif ($this->propertyName !== null) { $base .= '::$' . $this->propertyName; } return $base; } /** * Create an instance of the attribute class with the stored arguments */ public function createAttributeInstance(): ?object { try { // Use PHP 8's named arguments with unpacking return new $this->attributeClass(...$this->arguments); } catch (\Throwable) { return null; } } /** * Get memory footprint estimate */ public function getMemoryFootprint(): Byte { $bytes = strlen($this->className->getFullyQualified()) + strlen($this->attributeClass) + ($this->methodName ? strlen($this->methodName->toString()) : 0) + ($this->propertyName ?? 0) + ($this->filePath ? strlen($this->filePath->toString()) : 0) + strlen($this->target->value) + (count($this->arguments) * 50) + // Rough estimate (count($this->additionalData) * 30); // Rough estimate return Byte::fromBytes($bytes); } /** * Convert to array for backwards compatibility * @deprecated Use object properties instead */ public function toArray(): array { $classString = $this->className->getFullyQualified(); $data = [ 'class' => $classString, 'attribute_class' => $this->attributeClass, 'target_type' => $this->target->value, ]; if ($this->methodName !== null) { $data['method'] = $this->methodName->toString(); } if ($this->propertyName !== null) { $data['property'] = $this->propertyName; } if (! empty($this->arguments)) { $data['arguments'] = $this->arguments; } if ($this->filePath !== null) { $data['file'] = $this->filePath->toString(); } // Sanitize additionalData to prevent object references from overwriting our string conversions $sanitizedAdditionalData = []; foreach ($this->additionalData as $key => $value) { // Skip keys that we've already handled to prevent object overwrites if (in_array($key, ['class', 'method', 'property', 'file', 'attribute_class', 'target_type'], true)) { continue; } // Convert objects to strings or skip them if (is_object($value)) { if (method_exists($value, 'toString')) { $sanitizedAdditionalData[$key] = $value->toString(); } elseif (method_exists($value, '__toString')) { $sanitizedAdditionalData[$key] = (string)$value; } elseif (method_exists($value, 'getFullyQualified')) { $sanitizedAdditionalData[$key] = $value->getFullyQualified(); } elseif ($value instanceof \BackedEnum) { // Handle PHP backed enums (like Method::GET) $sanitizedAdditionalData[$key] = $value->value; } elseif ($value instanceof \UnitEnum) { // Handle PHP unit enums $sanitizedAdditionalData[$key] = $value->name; } } else { $sanitizedAdditionalData[$key] = $value; } } return array_merge($data, $sanitizedAdditionalData); } }