|
27 | 27 | use Go\ParserReflection\ReflectionFileNamespace; |
28 | 28 | use Go\ParserReflection\ReflectionMethod; |
29 | 29 | use Go\Proxy\ClassProxyGenerator; |
| 30 | +use Go\Proxy\EnumProxyGenerator; |
30 | 31 | use Go\Proxy\FunctionProxyGenerator; |
31 | 32 | use Go\Proxy\TraitProxyGenerator; |
| 33 | +use PhpParser\Node\Stmt\EnumCase; |
32 | 34 |
|
33 | 35 | /** |
34 | 36 | * Main transformer that performs weaving of aspects into the source code |
@@ -95,8 +97,8 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum |
95 | 97 | foreach ($namespaces as $namespace) { |
96 | 98 | $classes = $namespace->getClasses(); |
97 | 99 | foreach ($classes as $class) { |
98 | | - // Skip interfaces, enums, and aspects — these cannot be proxied as traits |
99 | | - if ($class->isInterface() || $class->isEnum() || in_array(Aspect::class, $class->getInterfaceNames(), true)) { |
| 100 | + // Skip interfaces and aspects — enums are now supported via EnumProxyGenerator |
| 101 | + if ($class->isInterface() || in_array(Aspect::class, $class->getInterfaceNames(), true)) { |
100 | 102 | continue; |
101 | 103 | } |
102 | 104 | $wasClassProcessed = $this->processSingleClass( |
@@ -146,10 +148,14 @@ private function processSingleClass( |
146 | 148 | $newFqcn = ($class->getNamespaceName() !== '' ? $class->getNamespaceName() . '\\' : '') . $newClassName; |
147 | 149 |
|
148 | 150 | // For traits: rename the trait (legacy approach, TraitProxyGenerator generates a child trait). |
| 151 | + // For enums: convert the enum body to a trait (cases extracted to proxy enum by EnumProxyGenerator). |
149 | 152 | // For classes: convert the class body to a trait (new trait-based engine). |
150 | 153 | if ($class->isTrait()) { |
151 | 154 | $this->adjustOriginalClass($class, $advices, $metadata, $newClassName); |
152 | 155 | $childProxyGenerator = new TraitProxyGenerator($class, $newFqcn, $advices, $this->useParameterWidening); |
| 156 | + } elseif ($class->isEnum()) { |
| 157 | + $this->convertEnumToTrait($class, $advices, $metadata, $newClassName); |
| 158 | + $childProxyGenerator = new EnumProxyGenerator($class, $newFqcn, $advices, $this->useParameterWidening); |
153 | 159 | } else { |
154 | 160 | $this->convertClassToTrait($class, $advices, $metadata, $newClassName); |
155 | 161 | $childProxyGenerator = new ClassProxyGenerator($class, $newFqcn, $advices, $this->useParameterWidening); |
@@ -372,6 +378,127 @@ private function convertClassToTrait( |
372 | 378 | $this->stripOverrideAttributeFromInterceptedMethods($class, $advices, $streamMetaData); |
373 | 379 | } |
374 | 380 |
|
| 381 | + /** |
| 382 | + * Convert an enum declaration into a trait for the trait-based AOP engine. |
| 383 | + * |
| 384 | + * Performs the following token-stream modifications in-place: |
| 385 | + * - Changes 'enum' keyword text to 'trait' |
| 386 | + * - Renames the enum to $newClassName (__AopProxied suffix) |
| 387 | + * - Removes the backed type (': string' / ': int') and any 'implements ...' clause |
| 388 | + * - Removes all enum case declarations from the body (cases live in the proxy enum instead) |
| 389 | + * |
| 390 | + * @param array<string, array<string, array<string>>> $advices List of class advices (sorted advice IDs) |
| 391 | + */ |
| 392 | + private function convertEnumToTrait( |
| 393 | + ReflectionClass $class, |
| 394 | + array $advices, |
| 395 | + StreamMetaData $streamMetaData, |
| 396 | + string $newClassName |
| 397 | + ): void { |
| 398 | + $classNode = $class->getNode(); |
| 399 | + if ($classNode === null) { |
| 400 | + return; |
| 401 | + } |
| 402 | + $position = $classNode->getAttribute('startTokenPos'); |
| 403 | + if (!is_int($position)) { |
| 404 | + return; |
| 405 | + } |
| 406 | + |
| 407 | + $classNameFound = false; |
| 408 | + |
| 409 | + do { |
| 410 | + if (!isset($streamMetaData->tokenStream[$position])) { |
| 411 | + ++$position; |
| 412 | + continue; |
| 413 | + } |
| 414 | + |
| 415 | + $token = $streamMetaData->tokenStream[$position]; |
| 416 | + |
| 417 | + if (!$classNameFound) { |
| 418 | + // Rewrite 'enum' keyword to 'trait' |
| 419 | + if ($token->id === T_ENUM) { |
| 420 | + $streamMetaData->tokenStream[$position]->text = 'trait'; |
| 421 | + ++$position; |
| 422 | + continue; |
| 423 | + } |
| 424 | + // First T_STRING after the keyword is the enum name — rename it |
| 425 | + if ($token->id === T_STRING) { |
| 426 | + $streamMetaData->tokenStream[$position]->text = $newClassName; |
| 427 | + $classNameFound = true; |
| 428 | + ++$position; |
| 429 | + continue; |
| 430 | + } |
| 431 | + } else { |
| 432 | + // After the enum name: strip backed type (': string/int') and 'implements ...' up to '{' |
| 433 | + if ($token->text === '{') { |
| 434 | + break; |
| 435 | + } |
| 436 | + // Keep whitespace tokens to preserve original brace placement |
| 437 | + if ($token->id !== T_WHITESPACE) { |
| 438 | + unset($streamMetaData->tokenStream[$position]); |
| 439 | + } |
| 440 | + } |
| 441 | + |
| 442 | + ++$position; |
| 443 | + } while (true); |
| 444 | + |
| 445 | + // Remove all enum case declarations from the trait body. |
| 446 | + // Cases cannot exist in traits; they are re-declared in the proxy enum by EnumProxyGenerator. |
| 447 | + // The trailing whitespace token (newline + indent) after each case is intentionally kept so |
| 448 | + // that subsequent methods remain at their original line numbers in the woven file, which is |
| 449 | + // required for XDebug breakpoints to map correctly (see CLAUDE.md). |
| 450 | + foreach ($classNode->stmts as $stmt) { |
| 451 | + if (!($stmt instanceof EnumCase)) { |
| 452 | + continue; |
| 453 | + } |
| 454 | + $start = $stmt->getAttribute('startTokenPos'); |
| 455 | + $end = $stmt->getAttribute('endTokenPos'); |
| 456 | + if (!is_int($start) || !is_int($end)) { |
| 457 | + continue; |
| 458 | + } |
| 459 | + // Remove the case tokens only (not the trailing whitespace/newline). |
| 460 | + // Keeping the trailing whitespace preserves blank lines in place of the removed case, |
| 461 | + // so the line numbers of all following methods are unchanged. |
| 462 | + for ($pos = $start; $pos <= $end; $pos++) { |
| 463 | + unset($streamMetaData->tokenStream[$pos]); |
| 464 | + } |
| 465 | + } |
| 466 | + |
| 467 | + // Remove 'final' from all intercepted enum methods — final methods cannot be overridden in |
| 468 | + // the proxy enum. Only intercepted methods need stripping; unintercepted final methods are |
| 469 | + // not overridden by the proxy and can safely remain final in the trait. |
| 470 | + foreach ($class->getMethods(ReflectionMethod::IS_FINAL) as $finalMethod) { |
| 471 | + if ($finalMethod->getDeclaringClass()->name !== $class->name) { |
| 472 | + continue; |
| 473 | + } |
| 474 | + $hasDynamicAdvice = isset($advices[AspectContainer::METHOD_PREFIX][$finalMethod->name]); |
| 475 | + $hasStaticAdvice = isset($advices[AspectContainer::STATIC_METHOD_PREFIX][$finalMethod->name]); |
| 476 | + if (!$hasDynamicAdvice && !$hasStaticAdvice) { |
| 477 | + continue; |
| 478 | + } |
| 479 | + $methodNode = $finalMethod->getNode(); |
| 480 | + $position = $methodNode->getAttribute('startTokenPos'); |
| 481 | + if (!is_int($position)) { |
| 482 | + continue; |
| 483 | + } |
| 484 | + do { |
| 485 | + if (isset($streamMetaData->tokenStream[$position])) { |
| 486 | + $token = $streamMetaData->tokenStream[$position]; |
| 487 | + if ($token->id === T_FINAL) { |
| 488 | + unset($streamMetaData->tokenStream[$position], $streamMetaData->tokenStream[$position + 1]); |
| 489 | + break; |
| 490 | + } |
| 491 | + } |
| 492 | + ++$position; |
| 493 | + } while (true); |
| 494 | + } |
| 495 | + |
| 496 | + // Strip #[\Override] from intercepted methods to prevent fatal errors on the alias. |
| 497 | + // PHP copies attributes to alias names (e.g. __aop__label), and since __aop__label has |
| 498 | + // no matching parent method, #[\Override] on the alias would be a fatal error. |
| 499 | + $this->stripOverrideAttributeFromInterceptedMethods($class, $advices, $streamMetaData); |
| 500 | + } |
| 501 | + |
375 | 502 | /** |
376 | 503 | * Removes #[\Override] attribute groups from all intercepted methods in the token stream. |
377 | 504 | * |
|
0 commit comments