stmts as $class) { if ($class instanceof Node\Stmt\ClassLike) { $this->visitorMetadata->classLike = get_class($class); } } } return null; } public function enterNode(Node $node) { switch ($node) { case $node instanceof ClassMethod: $this->shouldRewrite = $this->shouldRewrite($node); break; } return null; } public function leaveNode(Node $node) { switch ($node) { case $node instanceof ClassMethod: if (! $this->shouldRewrite($node)) { return $node; } // Rewrite the method to proxy call method. return $this->rewriteMethod($node); case $node instanceof Trait_: // If the node is trait and php version >= 7.3, it can `use ProxyTrait` like class. case $node instanceof Enum_: // If the node is enum and php version >= 8.1, it can `use ProxyTrait` like class. case $node instanceof Class_ && ! $node->isAnonymous(): // Add use proxy traits. $stmts = $node->stmts; if ($stmt = $this->buildProxyCallTraitUseStatement()) { array_unshift($stmts, $stmt); } $node->stmts = $stmts; unset($stmts); return $node; case $node instanceof MagicConstFunction: // Rewrite __FUNCTION__ to $__function__ variable. if ($this->shouldRewrite) { return new Variable('__function__'); } break; case $node instanceof MagicConstMethod: // Rewrite __METHOD__ to $__method__ variable. if ($this->shouldRewrite) { return new Variable('__method__'); } break; case $node instanceof MagicConstDir: // Rewrite __DIR__ as the real directory path if ($file = Composer::getLoader()->findFile($this->visitorMetadata->className)) { return new String_(dirname(realpath($file))); } break; case $node instanceof MagicConstFile: // Rewrite __FILE__ to the real file path if ($file = Composer::getLoader()->findFile($this->visitorMetadata->className)) { return new String_(realpath($file)); } break; } return null; } /** * Build `use ProxyTrait;`. */ private function buildProxyCallTraitUseStatement(): ?TraitUse { $traits = []; foreach ($this->proxyTraits as $proxyTrait) { if (! is_string($proxyTrait) || ! trait_exists($proxyTrait)) { continue; } // Add backslash prefix if the proxy trait does not start with backslash. $proxyTrait[0] !== '\\' && $proxyTrait = '\\' . $proxyTrait; $traits[] = new Name($proxyTrait); } if (empty($traits)) { return null; } return new TraitUse($traits); } /** * Rewrite a normal class method to a proxy call method, * include normal class method and static method. */ private function rewriteMethod(ClassMethod $node): ClassMethod { // Build the static proxy call method base on the original method. $shouldReturn = true; $returnType = $node->getReturnType(); if ($returnType instanceof Identifier && $returnType->name === 'void') { $shouldReturn = false; } $staticCall = new StaticCall(new Name('self'), '__proxyCall', [ // __CLASS__ new Arg($this->getMagicConst()), // __FUNCTION__ new Arg(new MagicConstFunction()), // ['order' => ['param1', 'param2'], 'keys' => compact('param1', 'param2'), 'variadic' => 'param2'] new Arg($this->getArguments($node->getParams())), // A closure that wrapped original method code. new Arg(new Closure([ 'params' => $this->filterModifier($node->getParams()), 'uses' => [ new Variable('__function__'), new Variable('__method__'), ], 'stmts' => $node->stmts, ])), ]); $stmts = $this->unshiftMagicMethods([]); if ($shouldReturn) { $stmts[] = new Return_($staticCall); } else { $stmts[] = new Expression($staticCall); } $node->stmts = $stmts; return $node; } /** * @param Node\Param[] $params * @return Node\Param[] */ private function filterModifier(array $params): array { return array_map(function (Node\Param $param) { $tempParam = clone $param; $tempParam->flags &= ~Class_::VISIBILITY_MODIFIER_MASK & ~Class_::MODIFIER_READONLY; return $tempParam; }, $params); } /** * @param Node\Param[] $params */ private function getArguments(array $params): Array_ { if (empty($params)) { return new Array_([ new ArrayItem( key: new String_('keys'), value: new Array_([], ['kind' => Array_::KIND_SHORT]), ), ], ['kind' => Array_::KIND_SHORT]); } // ['param1', 'param2', ...] $methodParamsList = new Array_( array_map(fn (Node\Param $param) => new ArrayItem(new String_($param->var->name)), $params), ['kind' => Array_::KIND_SHORT] ); return new Array_([ new ArrayItem( key: new String_('order'), value: $methodParamsList, ), new ArrayItem( key: new String_('keys'), value: new FuncCall(new Name('compact'), [new Arg($methodParamsList)]) ), new ArrayItem( key: new String_('variadic'), value: $this->getVariadicParamName($params), )], ['kind' => Array_::KIND_SHORT]); } /** * @param Node\Param[] $params */ private function getVariadicParamName(array $params): String_ { foreach ($params as $param) { if ($param->variadic) { return new String_($param->var->name); } } return new String_(''); } private function unshiftMagicMethods(array $stmts = []): array { $magicConstFunction = new Expression(new Assign(new Variable('__function__'), new MagicConstFunction())); $magicConstMethod = new Expression(new Assign(new Variable('__method__'), new MagicConstMethod())); array_unshift($stmts, $magicConstFunction, $magicConstMethod); return $stmts; } private function getMagicConst(): Node\Scalar\MagicConst { return match ($this->visitorMetadata->classLike) { Trait_::class => new MagicConstTrait(), default => new MagicConstClass(), }; } private function shouldRewrite(ClassMethod $node): bool { if ($this->visitorMetadata->classLike == Node\Stmt\Interface_::class || $node->isAbstract()) { return false; } $rewriteCollection = Aspect::parse($this->visitorMetadata->className); return $rewriteCollection->shouldRewrite($node->name->toString()); } }