Files
michaelschiemer/src/Framework/Discovery/ValueObjects/DiscoveredAttribute.php

244 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Discovery\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\Filesystem\ValueObjects\FilePath;
use Attribute;
/**
* Represents a discovered attribute with all its metadata
*/
final readonly class DiscoveredAttribute
{
/**
* @param array<string, mixed> $arguments
* @param array<string, mixed> $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<string, mixed> $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);
}
}