244 lines
8.5 KiB
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);
|
|
}
|
|
}
|