From f98171dfbfe0b9fce1a1da4f04c10e93b04b554e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Mu=CC=88ller?= Date: Mon, 17 Nov 2025 14:09:23 +0100 Subject: [PATCH] FEATURE: Command to list Aspect targets The new command "flow:aop:aspecttargets" will provide information about found aspects in the system as well as the advices declared and what they target. Additionally introduced interfaces, properties and traits can also be shown. There is a filter to show only a single aspect via -- only-aspect "My\Aspect\ClassAspect" and the various outputs can be switched on or off as required. By default only advices are shown. Note that this is a compiletime command and does not have property mapping available, therefore the boolean arguments are best given as 0 or 1. --- Neos.Flow/Classes/Aop/AspectContainer.php | 8 +- .../Aop/Builder/AspectContainerBuilder.php | 215 +++++++++++++++ .../Classes/Aop/Builder/ProxyClassBuilder.php | 258 ++++-------------- .../Aop/Builder/ProxyableClassesFilter.php | 44 +++ .../Aop/Pointcut/PointcutExpressionParser.php | 54 ++-- .../Classes/Aop/Pointcut/PointcutFilter.php | 39 ++- .../Aop/Pointcut/PointcutFilterComposite.php | 4 +- .../Classes/Command/AopCommandController.php | 152 +++++++++++ Neos.Flow/Classes/Mvc/Controller/Argument.php | 4 +- .../CompileTimeObjectManager.php | 110 +------- .../PackageClassFileProvider.php | 118 ++++++++ Neos.Flow/Classes/Package.php | 1 + .../Method/MethodTargetExpressionParser.php | 4 +- 13 files changed, 626 insertions(+), 385 deletions(-) create mode 100644 Neos.Flow/Classes/Aop/Builder/AspectContainerBuilder.php create mode 100644 Neos.Flow/Classes/Aop/Builder/ProxyableClassesFilter.php create mode 100644 Neos.Flow/Classes/Command/AopCommandController.php create mode 100644 Neos.Flow/Classes/ObjectManagement/PackageClassFileProvider.php diff --git a/Neos.Flow/Classes/Aop/AspectContainer.php b/Neos.Flow/Classes/Aop/AspectContainer.php index c66d8a6b58..971d4848b8 100644 --- a/Neos.Flow/Classes/Aop/AspectContainer.php +++ b/Neos.Flow/Classes/Aop/AspectContainer.php @@ -100,7 +100,7 @@ public function getClassName(): string /** * Returns the advisors which were defined in the aspect * - * @return array Array of \Neos\Flow\Aop\Advisor objects + * @return Advisor[] Array of \Neos\Flow\Aop\Advisor objects */ public function getAdvisors(): array { @@ -110,7 +110,7 @@ public function getAdvisors(): array /** * Returns the interface introductions which were defined in the aspect * - * @return array Array of \Neos\Flow\Aop\InterfaceIntroduction objects + * @return InterfaceIntroduction[] Array of \Neos\Flow\Aop\InterfaceIntroduction objects */ public function getInterfaceIntroductions(): array { @@ -120,7 +120,7 @@ public function getInterfaceIntroductions(): array /** * Returns the property introductions which were defined in the aspect * - * @return array Array of \Neos\Flow\Aop\PropertyIntroduction objects + * @return PropertyIntroduction[] Array of \Neos\Flow\Aop\PropertyIntroduction objects */ public function getPropertyIntroductions(): array { @@ -130,7 +130,7 @@ public function getPropertyIntroductions(): array /** * Returns the trait introductions which were defined in the aspect * - * @return array Array of \Neos\Flow\Aop\TraitIntroduction objects + * @return TraitIntroduction[] Array of \Neos\Flow\Aop\TraitIntroduction objects */ public function getTraitIntroductions(): array { diff --git a/Neos.Flow/Classes/Aop/Builder/AspectContainerBuilder.php b/Neos.Flow/Classes/Aop/Builder/AspectContainerBuilder.php new file mode 100644 index 0000000000..f5c0135cfb --- /dev/null +++ b/Neos.Flow/Classes/Aop/Builder/AspectContainerBuilder.php @@ -0,0 +1,215 @@ +pointcutExpressionParser = $pointcutExpressionParser; + } + + public function injectReflectionService(ReflectionService $reflectionService): void + { + $this->reflectionService = $reflectionService; + } + + public function injectObjectManager(ObjectManagerInterface $objectManager): void + { + $this->objectManager = $objectManager; + } + + /** + * Builds aspect containers by checking reflection for Aspect attribute/annotation + * Note that this has a runtime cache + * + * @return array + * @throws ClassLoadingForReflectionFailedException + * @throws Exception + * @throws InvalidClassException + * @throws InvalidPointcutExpressionException + * @throws InvalidTargetClassException + * @throws \ReflectionException + */ + public function buildFromReflection(): array + { + $actualAspectClassNames = $this->reflectionService->getClassNamesByAnnotation(Flow\Aspect::class); + sort($actualAspectClassNames); + return $this->build($actualAspectClassNames); + } + + /** + * /** + * Checks the annotations of the specified classes for aspect tags + * and creates an aspect with advisors accordingly. + * + * @param class-string[] $classNames Classes to check for aspect tags. + * @return array An array of Aop\AspectContainer for all aspects which were found. + * @throws ClassLoadingForReflectionFailedException + * @throws Exception + * @throws InvalidClassException + * @throws InvalidPointcutExpressionException + * @throws InvalidTargetClassException + * @throws \ReflectionException + */ + protected function build(array $classNames): array + { + $aspectContainers = []; + foreach ($classNames as $aspectClassName) { + $aspectContainers[$aspectClassName] = $this->buildAspectContainer($aspectClassName); + } + return $aspectContainers; + } + + /** + * Creates and returns an aspect from the annotations found in a class which + * is tagged as an aspect. The object acting as an advice will already be + * fetched (and therefore instantiated if necessary). + * + * @param class-string $aspectClassName Name of the class which forms the aspect, contains advices etc. + * @throws Exception + * @throws InvalidTargetClassException + * @throws InvalidPointcutExpressionException + * @throws ClassLoadingForReflectionFailedException + * @throws InvalidClassException + * @throws \ReflectionException + */ + protected function buildAspectContainer(string $aspectClassName): AspectContainer + { + $aspectContainer = new AspectContainer($aspectClassName); + if (!class_exists($aspectClassName)) { + throw new Exception\InvalidTargetClassException(sprintf('The class "%s" is not loadable for AOP proxy building. This is most likely an inconsistency with the caches. Try running `./flow flow:cache:flush` and if that does not help, check the class exists and is correctly namespaced.', $aspectClassName), 1607422151); + } + $methodNames = get_class_methods($aspectClassName); + + foreach ($methodNames as $methodName) { + foreach ($this->reflectionService->getMethodAnnotations($aspectClassName, $methodName) as $annotation) { + $annotationClass = get_class($annotation); + switch ($annotationClass) { + case Flow\Around::class: + assert($annotation instanceof Flow\Around); + $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass), $aspectContainer); + $advice = new AroundAdvice($aspectClassName, $methodName, $this->objectManager); + $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); + $advisor = new Advisor($advice, $pointcut); + $aspectContainer->addAdvisor($advisor); + break; + case Flow\Before::class: + assert($annotation instanceof Flow\Before); + $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass), $aspectContainer); + $advice = new BeforeAdvice($aspectClassName, $methodName, $this->objectManager); + $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); + $advisor = new Advisor($advice, $pointcut); + $aspectContainer->addAdvisor($advisor); + break; + case Flow\AfterReturning::class: + assert($annotation instanceof Flow\AfterReturning); + $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass), $aspectContainer); + $advice = new AfterReturningAdvice($aspectClassName, $methodName, $this->objectManager); + $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); + $advisor = new Advisor($advice, $pointcut); + $aspectContainer->addAdvisor($advisor); + break; + case Flow\AfterThrowing::class: + assert($annotation instanceof Flow\AfterThrowing); + $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass), $aspectContainer); + $advice = new AfterThrowingAdvice($aspectClassName, $methodName, $this->objectManager); + $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); + $advisor = new Advisor($advice, $pointcut); + $aspectContainer->addAdvisor($advisor); + break; + case Flow\After::class: + assert($annotation instanceof Flow\After); + $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass), $aspectContainer); + $advice = new AfterAdvice($aspectClassName, $methodName, $this->objectManager); + $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); + $advisor = new Advisor($advice, $pointcut); + $aspectContainer->addAdvisor($advisor); + break; + case Flow\Pointcut::class: + assert($annotation instanceof Flow\Pointcut); + $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->expression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass), $aspectContainer); + $pointcut = new Pointcut($annotation->expression, $pointcutFilterComposite, $aspectClassName, $methodName); + $aspectContainer->addPointcut($pointcut); + break; + } + } + } + $introduceAnnotation = $this->reflectionService->getClassAnnotation($aspectClassName, Flow\Introduce::class); + if ($introduceAnnotation instanceof Flow\Introduce) { + if ($introduceAnnotation->interfaceName === null && $introduceAnnotation->traitName === null) { + throw new Exception('The introduction in class "' . $aspectClassName . '" does neither contain an interface name nor a trait name, at least one is required.', 1172694761); + } + $pointcutFilterComposite = $this->pointcutExpressionParser->parse($introduceAnnotation->pointcutExpression, $this->renderSourceHint($aspectClassName, (string)$introduceAnnotation->interfaceName, Flow\Introduce::class), $aspectContainer); + $pointcut = new Pointcut($introduceAnnotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); + + if ($introduceAnnotation->interfaceName !== null) { + $introduction = new InterfaceIntroduction($aspectClassName, $introduceAnnotation->interfaceName, $pointcut); + $aspectContainer->addInterfaceIntroduction($introduction); + } + + if ($introduceAnnotation->traitName !== null) { + $introduction = new TraitIntroduction($aspectClassName, $introduceAnnotation->traitName, $pointcut); + $aspectContainer->addTraitIntroduction($introduction); + } + } + + foreach ($this->reflectionService->getClassPropertyNames($aspectClassName) as $propertyName) { + $introduceAnnotation = $this->reflectionService->getPropertyAnnotation($aspectClassName, $propertyName, Flow\Introduce::class); + if ($introduceAnnotation !== null) { + assert($introduceAnnotation instanceof Flow\Introduce); + $pointcutFilterComposite = $this->pointcutExpressionParser->parse($introduceAnnotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $propertyName, Flow\Introduce::class), $aspectContainer); + $pointcut = new Pointcut($introduceAnnotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); + $introduction = new PropertyIntroduction($aspectClassName, $propertyName, $pointcut); + $aspectContainer->addPropertyIntroduction($introduction); + } + } + if (count($aspectContainer->getAdvisors()) < 1 && + count($aspectContainer->getPointcuts()) < 1 && + count($aspectContainer->getInterfaceIntroductions()) < 1 && + count($aspectContainer->getTraitIntroductions()) < 1 && + count($aspectContainer->getPropertyIntroductions()) < 1) { + throw new Exception('The class "' . $aspectClassName . '" is tagged to be an aspect but does not contain advices nor pointcut or introduction declarations.', 1169124534); + } + return $aspectContainer; + } + + /** + * Renders a short message which gives a hint on where the currently parsed pointcut expression was defined. + */ + protected function renderSourceHint(string $aspectClassName, string $methodName, string $tagName): string + { + return sprintf('%s::%s (%s advice)', $aspectClassName, $methodName, $tagName); + } +} diff --git a/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php b/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php index 757b0d72b6..daa319a7f9 100644 --- a/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php +++ b/Neos.Flow/Classes/Aop/Builder/ProxyClassBuilder.php @@ -18,9 +18,9 @@ use Neos\Flow\Aop\AspectContainer; use Neos\Flow\Aop\Exception; use Neos\Flow\Aop\Exception\InvalidPointcutExpressionException; +use Neos\Flow\Aop\Exception\InvalidTargetClassException; use Neos\Flow\Aop\Exception\VoidImplementationException; use Neos\Flow\Aop\Pointcut\Pointcut; -use Neos\Flow\Aop\Pointcut\PointcutExpressionParser; use Neos\Flow\Aop\PropertyIntroduction; use Neos\Flow\Aop\TraitIntroduction; use Neos\Flow\Log\Utility\LogEnvironment; @@ -33,8 +33,6 @@ use Neos\Flow\Reflection\PropertyReflection; use Neos\Flow\Reflection\ReflectionService; use Neos\Flow\Utility\Algorithms; -use Neos\Flow\Utility\Exception as UtilityException; -use Neos\Utility\Exception\FilesException; use Psr\Log\LoggerInterface; /** @@ -47,17 +45,13 @@ class ProxyClassBuilder protected Compiler $compiler; protected ReflectionService $reflectionService; protected LoggerInterface $logger; - protected PointcutExpressionParser $pointcutExpressionParser; protected VariableFrontend $objectConfigurationCache; protected CompileTimeObjectManager $objectManager; - - /** - * Hardcoded list of Flow sub packages (first 15 characters) which must be immune to AOP proxying for security, technical or conceptual reasons. - */ - protected array $excludedSubPackages = ['Neos\Flow\Aop\\', 'Neos\Flow\Cach', 'Neos\Flow\Erro', 'Neos\Flow\Log\\', 'Neos\Flow\Moni', 'Neos\Flow\Obje', 'Neos\Flow\Pack', 'Neos\Flow\Prop', 'Neos\Flow\Refl', 'Neos\Flow\Util', 'Neos\Flow\Vali']; + protected AspectContainerBuilder $aspectContainerBuilder; /** * A registry of all known aspects + * @var AspectContainer[] */ protected array $aspectContainers = []; @@ -79,11 +73,6 @@ public function injectLogger(LoggerInterface $logger): void $this->logger = $logger; } - public function injectPointcutExpressionParser(PointcutExpressionParser $pointcutExpressionParser): void - { - $this->pointcutExpressionParser = $pointcutExpressionParser; - } - #[Flow\Autowiring(false)] public function injectObjectConfigurationCache(VariableFrontend $objectConfigurationCache): void { @@ -105,6 +94,11 @@ public function injectObjectManager(CompileTimeObjectManager $objectManager): vo $this->objectManager = $objectManager; } + public function injectAspectContainerBuilder(AspectContainerBuilder $aspectContainerBuilder): void + { + $this->aspectContainerBuilder = $aspectContainerBuilder; + } + /** * Builds proxy class code which weaves advices into the respective target classes. * @@ -120,26 +114,25 @@ public function injectObjectManager(CompileTimeObjectManager $objectManager): vo * a class which has been matched previously but just didn't have to be proxied, * the latter are kept track of by an "unproxiedClass-*" cache entry. * - * @throws \Neos\Cache\Exception * @throws CannotBuildObjectException - * @throws Exception - * @throws VoidImplementationException * @throws ClassLoadingForReflectionFailedException - * @throws UtilityException + * @throws Exception + * @throws InvalidTargetClassException + * @throws InvalidClassException * @throws InvalidPointcutExpressionException - * @throws FilesException + * @throws VoidImplementationException + * @throws \Neos\Cache\Exception * @throws \ReflectionException - * @throws InvalidClassException */ public function build(): void { $allAvailableClassNamesByPackage = $this->objectManager->getRegisteredClassNames(); - $possibleTargetClassNames = $this->getProxyableClasses($allAvailableClassNamesByPackage); - $actualAspectClassNames = $this->reflectionService->getClassNamesByAnnotation(Flow\Aspect::class); - sort($possibleTargetClassNames); - sort($actualAspectClassNames); - $this->aspectContainers = $this->buildAspectContainers($actualAspectClassNames); + + $this->aspectContainers = $this->aspectContainerBuilder->buildFromReflection(); + + $proxyableClassesFilter = new ProxyableClassesFilter(); + $possibleTargetClassNames = $proxyableClassesFilter->getProxyableClasses($allAvailableClassNamesByPackage, array_keys($this->aspectContainers)); $rebuildEverything = false; if ($this->objectConfigurationCache->has('allAspectClassesUpToDate') === false) { @@ -203,162 +196,10 @@ public function findPointcut(string $aspectClassName, string $pointcutMethodName return false; } - /** - * Determines which of the given classes are potentially proxyable - * and returns their names in an array. - * - * @param array $classNamesByPackage Names of the classes to check - * @return array Names of classes which can be proxied - */ - protected function getProxyableClasses(array $classNamesByPackage): array - { - $proxyableClasses = []; - foreach ($classNamesByPackage as $classNames) { - foreach ($classNames as $className) { - if (in_array(substr($className, 0, 15), $this->excludedSubPackages, true)) { - continue; - } - if ($this->reflectionService->isClassAnnotatedWith($className, Flow\Aspect::class)) { - continue; - } - $proxyableClasses[] = $className; - } - } - return $proxyableClasses; - } - - /** - * /** - * Checks the annotations of the specified classes for aspect tags - * and creates an aspect with advisors accordingly. - * - * @param array $classNames Classes to check for aspect tags. - * @return array An array of Aop\AspectContainer for all aspects which were found. - * @throws Exception - * @throws FilesException - * @throws InvalidPointcutExpressionException - * @throws \ReflectionException - * @throws UtilityException - */ - protected function buildAspectContainers(array $classNames): array - { - $aspectContainers = []; - foreach ($classNames as $aspectClassName) { - $aspectContainers[$aspectClassName] = $this->buildAspectContainer($aspectClassName); - } - return $aspectContainers; - } - - /** - * Creates and returns an aspect from the annotations found in a class which - * is tagged as an aspect. The object acting as an advice will already be - * fetched (and therefore instantiated if necessary). - * - * @param string $aspectClassName Name of the class which forms the aspect, contains advices etc. - * @return AspectContainer The aspect container containing one or more advisors - * @throws Exception - * @throws InvalidPointcutExpressionException - * @throws UtilityException - * @throws FilesException - * @throws \ReflectionException - */ - protected function buildAspectContainer(string $aspectClassName): AspectContainer - { - $aspectContainer = new AspectContainer($aspectClassName); - if (!class_exists($aspectClassName)) { - throw new Exception\InvalidTargetClassException(sprintf('The class "%s" is not loadable for AOP proxy building. This is most likely an inconsistency with the caches. Try running `./flow flow:cache:flush` and if that does not help, check the class exists and is correctly namespaced.', $aspectClassName), 1607422151); - } - $methodNames = get_class_methods($aspectClassName); - - foreach ($methodNames as $methodName) { - foreach ($this->reflectionService->getMethodAnnotations($aspectClassName, $methodName) as $annotation) { - $annotationClass = get_class($annotation); - switch ($annotationClass) { - case Flow\Around::class: - $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); - $advice = new Aop\Advice\AroundAdvice($aspectClassName, $methodName); - $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); - $advisor = new Aop\Advisor($advice, $pointcut); - $aspectContainer->addAdvisor($advisor); - break; - case Flow\Before::class: - $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); - $advice = new Aop\Advice\BeforeAdvice($aspectClassName, $methodName); - $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); - $advisor = new Aop\Advisor($advice, $pointcut); - $aspectContainer->addAdvisor($advisor); - break; - case Flow\AfterReturning::class: - $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); - $advice = new Aop\Advice\AfterReturningAdvice($aspectClassName, $methodName); - $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); - $advisor = new Aop\Advisor($advice, $pointcut); - $aspectContainer->addAdvisor($advisor); - break; - case Flow\AfterThrowing::class: - $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); - $advice = new Aop\Advice\AfterThrowingAdvice($aspectClassName, $methodName); - $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); - $advisor = new Aop\Advisor($advice, $pointcut); - $aspectContainer->addAdvisor($advisor); - break; - case Flow\After::class: - $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); - $advice = new Aop\Advice\AfterAdvice($aspectClassName, $methodName); - $pointcut = new Pointcut($annotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); - $advisor = new Aop\Advisor($advice, $pointcut); - $aspectContainer->addAdvisor($advisor); - break; - case Flow\Pointcut::class: - $pointcutFilterComposite = $this->pointcutExpressionParser->parse($annotation->expression, $this->renderSourceHint($aspectClassName, $methodName, $annotationClass)); - $pointcut = new Pointcut($annotation->expression, $pointcutFilterComposite, $aspectClassName, $methodName); - $aspectContainer->addPointcut($pointcut); - break; - } - } - } - $introduceAnnotation = $this->reflectionService->getClassAnnotation($aspectClassName, Flow\Introduce::class); - if ($introduceAnnotation instanceof Flow\Introduce) { - if ($introduceAnnotation->interfaceName === null && $introduceAnnotation->traitName === null) { - throw new Aop\Exception('The introduction in class "' . $aspectClassName . '" does neither contain an interface name nor a trait name, at least one is required.', 1172694761); - } - $pointcutFilterComposite = $this->pointcutExpressionParser->parse($introduceAnnotation->pointcutExpression, $this->renderSourceHint($aspectClassName, (string)$introduceAnnotation->interfaceName, Flow\Introduce::class)); - $pointcut = new Pointcut($introduceAnnotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); - - if ($introduceAnnotation->interfaceName !== null) { - $introduction = new Aop\InterfaceIntroduction($aspectClassName, $introduceAnnotation->interfaceName, $pointcut); - $aspectContainer->addInterfaceIntroduction($introduction); - } - - if ($introduceAnnotation->traitName !== null) { - $introduction = new TraitIntroduction($aspectClassName, $introduceAnnotation->traitName, $pointcut); - $aspectContainer->addTraitIntroduction($introduction); - } - } - - foreach ($this->reflectionService->getClassPropertyNames($aspectClassName) as $propertyName) { - $introduceAnnotation = $this->reflectionService->getPropertyAnnotation($aspectClassName, $propertyName, Flow\Introduce::class); - if ($introduceAnnotation !== null) { - $pointcutFilterComposite = $this->pointcutExpressionParser->parse($introduceAnnotation->pointcutExpression, $this->renderSourceHint($aspectClassName, $propertyName, Flow\Introduce::class)); - $pointcut = new Pointcut($introduceAnnotation->pointcutExpression, $pointcutFilterComposite, $aspectClassName); - $introduction = new PropertyIntroduction($aspectClassName, $propertyName, $pointcut); - $aspectContainer->addPropertyIntroduction($introduction); - } - } - if (count($aspectContainer->getAdvisors()) < 1 && - count($aspectContainer->getPointcuts()) < 1 && - count($aspectContainer->getInterfaceIntroductions()) < 1 && - count($aspectContainer->getTraitIntroductions()) < 1 && - count($aspectContainer->getPropertyIntroductions()) < 1) { - throw new Aop\Exception('The class "' . $aspectClassName . '" is tagged to be an aspect but does not contain advices nor pointcut or introduction declarations.', 1169124534); - } - return $aspectContainer; - } - /** * Builds methods for a single AOP proxy class for the specified class. * - * @param string $targetClassName Name of the class to create a proxy class file for + * @param class-string $targetClassName Name of the class to create a proxy class file for * @param array $aspectContainers The array of aspect containers from the AOP Framework * @return bool true if the proxy class could be built, false otherwise. * @throws \ReflectionException @@ -379,8 +220,8 @@ public function buildProxyClass(string $targetClassName, array $aspectContainers $methodsFromIntroducedInterfaces = $this->getIntroducedMethodsFromInterfaceIntroductions($interfaceIntroductions); $interceptedMethods = []; - $this->addAdvisedMethodsToInterceptedMethods($interceptedMethods, array_merge($methodsFromTargetClass, $methodsFromIntroducedInterfaces), $targetClassName, $aspectContainers); - $this->addIntroducedMethodsToInterceptedMethods($interceptedMethods, $methodsFromIntroducedInterfaces); + $interceptedMethods = $this->addAdvisedMethodsToInterceptedMethods($interceptedMethods, array_merge($methodsFromTargetClass, $methodsFromIntroducedInterfaces), $targetClassName, $aspectContainers); + $interceptedMethods = $this->addIntroducedMethodsToInterceptedMethods($interceptedMethods, $methodsFromIntroducedInterfaces); if (count($interceptedMethods) < 1 && count($introducedInterfaces) < 1 && count($introducedTraits) < 1 && count($propertyIntroductions) < 1) { return false; @@ -503,8 +344,8 @@ protected function addBuildMethodsAndAdvicesCodeToClass(string $className, Class /** * Returns the methods of the target class. * - * @param string $targetClassName Name of the target class - * @return array Method information with declaring class and method name pairs + * @param class-string $targetClassName Name of the target class + * @return array Method information with declaring class and method name pairs * @throws \ReflectionException */ protected function getMethodsFromTargetClass(string $targetClassName): array @@ -594,13 +435,13 @@ protected function buildMethodsInterceptorCode(string $targetClassName, array $i * Traverses all aspect containers, their aspects and their advisors and adds the * methods and their advices to the (usually empty) array of intercepted methods. * - * @param array &$interceptedMethods An array (empty or not) which contains the names of the intercepted methods and additional information - * @param array $methods An array of class and method names which are matched against the pointcut (class name = name of the class or interface the method was declared) - * @param string $targetClassName Name of the class the pointcut should match with - * @param array &$aspectContainers All aspects to take into consideration - * @return void + * @param array $interceptedMethods An array (empty or not) which contains the names of the intercepted methods and additional information + * @param array $methods An array of class and method names which are matched against the pointcut (class name = name of the class or interface the method was declared) + * @param class-string $targetClassName Name of the class the pointcut should match with + * @param AspectContainer[] $aspectContainers All aspects to take into consideration + * @return array */ - protected function addAdvisedMethodsToInterceptedMethods(array &$interceptedMethods, array $methods, string $targetClassName, array $aspectContainers): void + protected function addAdvisedMethodsToInterceptedMethods(array $interceptedMethods, array $methods, string $targetClassName, array $aspectContainers): array { $pointcutQueryIdentifier = 0; @@ -629,17 +470,19 @@ protected function addAdvisedMethodsToInterceptedMethods(array &$interceptedMeth } } } + + return $interceptedMethods; } /** * Traverses all methods which were introduced by interfaces and adds them to the * intercepted methods array if they didn't exist already. * - * @param array &$interceptedMethods An array (empty or not) which contains the names of the intercepted methods and additional information - * @param array $methodsFromIntroducedInterfaces An array of class and method names from introduced interfaces - * @return void + * @param array $interceptedMethods An array (empty or not) which contains the names of the intercepted methods and additional information + * @param array $methodsFromIntroducedInterfaces An array of class and method names from introduced interfaces + * @return array */ - protected function addIntroducedMethodsToInterceptedMethods(array &$interceptedMethods, array $methodsFromIntroducedInterfaces): void + protected function addIntroducedMethodsToInterceptedMethods(array $interceptedMethods, array $methodsFromIntroducedInterfaces): array { foreach ($methodsFromIntroducedInterfaces as $interfaceAndMethodName) { [$interfaceName, $methodName] = $interfaceAndMethodName; @@ -648,15 +491,17 @@ protected function addIntroducedMethodsToInterceptedMethods(array &$interceptedM $interceptedMethods[$methodName]['declaringClassName'] = $interfaceName; } } + + return $interceptedMethods; } /** * Traverses all aspect containers and returns an array of interface * introductions which match the target class. * - * @param array &$aspectContainers All aspects to take into consideration - * @param string $targetClassName Name of the class the pointcut should match with - * @return array array of interface names + * @param AspectContainer[] $aspectContainers All aspects to take into consideration + * @param class-string $targetClassName Name of the class the pointcut should match with + * @return Aop\InterfaceIntroduction[] array of interface names * @throws \Exception */ protected function getMatchingInterfaceIntroductions(array $aspectContainers, string $targetClassName): array @@ -680,7 +525,7 @@ protected function getMatchingInterfaceIntroductions(array $aspectContainers, st * Traverses all aspect containers and returns an array of property * introductions which match the target class. * - * @param array &$aspectContainers All aspects to take into consideration + * @param AspectContainer[] $aspectContainers All aspects to take into consideration * @param string $targetClassName Name of the class the pointcut should match with * @return array|PropertyIntroduction[] array of property introductions * @throws \Exception @@ -706,15 +551,14 @@ protected function getMatchingPropertyIntroductions(array $aspectContainers, str * Traverses all aspect containers and returns an array of trait * introductions which match the target class. * - * @param array &$aspectContainers All aspects to take into consideration + * @param AspectContainer[] $aspectContainers All aspects to take into consideration * @param string $targetClassName Name of the class the pointcut should match with - * @return array array of trait names + * @return string[] array of trait names * @throws \Exception */ protected function getMatchingTraitNamesFromIntroductions(array $aspectContainers, string $targetClassName): array { $introductions = []; - /** @var AspectContainer $aspectContainer */ foreach ($aspectContainers as $aspectContainer) { if (!$aspectContainer->getCachedTargetClassNameCandidates()->hasClassName($targetClassName)) { continue; @@ -734,8 +578,8 @@ protected function getMatchingTraitNamesFromIntroductions(array $aspectContainer /** * Returns an array of interface names introduced by the given introductions * - * @param array $interfaceIntroductions An array of interface introductions - * @return array Array of interface names + * @param Aop\InterfaceIntroduction[] $interfaceIntroductions An array of interface introductions + * @return string[] Array of interface names */ protected function getInterfaceNamesFromIntroductions(array $interfaceIntroductions): array { @@ -749,8 +593,8 @@ protected function getInterfaceNamesFromIntroductions(array $interfaceIntroducti /** * Returns all methods declared by the introduced interfaces * - * @param array $interfaceIntroductions An array of Aop\InterfaceIntroduction - * @return array An array of method information (interface, method name) + * @param Aop\InterfaceIntroduction[] $interfaceIntroductions An array of Aop\InterfaceIntroduction + * @return array An array of method information (interface, method name) * @throws Aop\Exception */ protected function getIntroducedMethodsFromInterfaceIntroductions(array $interfaceIntroductions): array @@ -770,12 +614,4 @@ protected function getIntroducedMethodsFromInterfaceIntroductions(array $interfa } return $methods; } - - /** - * Renders a short message which gives a hint on where the currently parsed pointcut expression was defined. - */ - protected function renderSourceHint(string $aspectClassName, string $methodName, string $tagName): string - { - return sprintf('%s::%s (%s advice)', $aspectClassName, $methodName, $tagName); - } } diff --git a/Neos.Flow/Classes/Aop/Builder/ProxyableClassesFilter.php b/Neos.Flow/Classes/Aop/Builder/ProxyableClassesFilter.php new file mode 100644 index 0000000000..1c4f8fec36 --- /dev/null +++ b/Neos.Flow/Classes/Aop/Builder/ProxyableClassesFilter.php @@ -0,0 +1,44 @@ + $classNamesByPackage Names of the classes to check + * @param class-string[] $aspectClasses + * @return class-string[] Names of classes which can be proxied + */ + public function getProxyableClasses(array $classNamesByPackage, array $aspectClasses): array + { + $proxyableClasses = []; + foreach ($classNamesByPackage as $classNames) { + foreach ($classNames as $className) { + if (in_array(substr($className, 0, 15), $this->excludedSubPackages, true)) { + continue; + } + if (in_array($className, $aspectClasses, true)) { + continue; + } + $proxyableClasses[] = $className; + } + } + + sort($proxyableClasses); + return $proxyableClasses; + } +} diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutExpressionParser.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutExpressionParser.php index a10cec36d9..dcdd4036d4 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutExpressionParser.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutExpressionParser.php @@ -12,7 +12,7 @@ */ use Neos\Flow\Annotations as Flow; -use Neos\Flow\Aop\Builder\ProxyClassBuilder; +use Neos\Flow\Aop\AspectContainer; use Neos\Flow\Aop\Exception as AopException; use Neos\Flow\Aop\Exception\InvalidPointcutExpressionException; use Neos\Flow\Configuration\ConfigurationManager; @@ -62,34 +62,13 @@ class PointcutExpressionParser /x'; const PATTERN_MATCHMETHODNAMEANDARGUMENTS = '/^(?P.*)\((?P.*)\)$/'; - /** - * @var ProxyClassBuilder - */ - protected $proxyClassBuilder; - - /** - * @var ReflectionService - */ - protected $reflectionService; - - /** - * @var ObjectManagerInterface - */ - protected $objectManager; + protected ReflectionService|null $reflectionService = null; + protected ObjectManagerInterface|null $objectManager = null; /** * @var string */ - protected $sourceHint = ''; - - /** - * @param ProxyClassBuilder $proxyClassBuilder - * @return void - */ - public function injectProxyClassBuilder(ProxyClassBuilder $proxyClassBuilder): void - { - $this->proxyClassBuilder = $proxyClassBuilder; - } + protected string $sourceHint = ''; /** * @param ReflectionService $reflectionService @@ -104,7 +83,7 @@ public function injectReflectionService(ReflectionService $reflectionService): v * @param ObjectManagerInterface $objectManager * @return void */ - public function injectObjectManager(ObjectManagerInterface $objectManager) + public function injectObjectManager(ObjectManagerInterface $objectManager): void { $this->objectManager = $objectManager; } @@ -119,7 +98,7 @@ public function injectObjectManager(ObjectManagerInterface $objectManager) * @throws InvalidPointcutExpressionException * @throws AopException */ - public function parse(string $pointcutExpression, string $sourceHint): PointcutFilterComposite + public function parse(string $pointcutExpression, string $sourceHint, AspectContainer|null $aspectContainer = null): PointcutFilterComposite { $this->sourceHint = $sourceHint; @@ -140,7 +119,10 @@ public function parse(string $pointcutExpression, string $sourceHint): PointcutF } if (strpos($expression, '(') === false) { - $this->parseDesignatorPointcut($operator, $expression, $pointcutFilterComposite); + if ($aspectContainer === null) { + throw new \RuntimeException('AspectContainer must be provided for designator pointcut', 1762974669); + } + $this->parseDesignatorPointcut($operator, $expression, $pointcutFilterComposite, $aspectContainer); } else { $matches = []; $numberOfMatches = preg_match(self::PATTERN_MATCHPOINTCUTDESIGNATOR, $expression, $matches); @@ -260,12 +242,12 @@ protected function parseAnnotationPattern(string &$annotationPattern, array &$an */ protected function parseDesignatorMethod(string $operator, string $signaturePattern, PointcutFilterComposite $pointcutFilterComposite): void { - if (strpos($signaturePattern, '->') === false) { + if (str_contains($signaturePattern, '->') === false) { throw new InvalidPointcutExpressionException('Syntax error: "->" expected in "' . $signaturePattern . '", defined in ' . $this->sourceHint, 1169027339); } $methodVisibility = $this->getVisibilityFromSignaturePattern($signaturePattern); - list($classPattern, $methodPattern) = explode('->', $signaturePattern, 2); - if (strpos($methodPattern, '(') === false) { + [$classPattern, $methodPattern] = explode('->', $signaturePattern, 2); + if (str_contains($methodPattern, '(') === false) { throw new InvalidPointcutExpressionException('Syntax error: "(" expected in "' . $methodPattern . '", defined in ' . $this->sourceHint, 1169144299); } @@ -318,17 +300,17 @@ protected function parseDesignatorWithin(string $operator, string $signaturePatt * @param string $operator The operator * @param string $pointcutExpression The pointcut expression (value of the designator) * @param PointcutFilterComposite $pointcutFilterComposite An instance of the pointcut filter composite. The result (ie. the pointcut filter) will be added to this composite object. + * @param AspectContainer $aspectContainer * @return void * @throws InvalidPointcutExpressionException */ - protected function parseDesignatorPointcut(string $operator, string $pointcutExpression, PointcutFilterComposite $pointcutFilterComposite): void + protected function parseDesignatorPointcut(string $operator, string $pointcutExpression, PointcutFilterComposite $pointcutFilterComposite, AspectContainer $aspectContainer): void { - if (strpos($pointcutExpression, '->') === false) { + if (str_contains($pointcutExpression, '->') === false) { throw new InvalidPointcutExpressionException('Syntax error: "->" expected in "' . $pointcutExpression . '", defined in ' . $this->sourceHint, 1172219205); } - list($aspectClassName, $pointcutMethodName) = explode('->', $pointcutExpression, 2); - $pointcutFilter = new PointcutFilter($aspectClassName, $pointcutMethodName); - $pointcutFilter->injectProxyClassBuilder($this->proxyClassBuilder); + [$aspectClassName, $pointcutMethodName] = explode('->', $pointcutExpression, 2); + $pointcutFilter = new PointcutFilter($aspectClassName, $pointcutMethodName, $aspectContainer); $pointcutFilterComposite->addFilter($operator, $pointcutFilter); } diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutFilter.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutFilter.php index 059819d8b8..e6da0a2533 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutFilter.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutFilter.php @@ -12,8 +12,8 @@ */ use Neos\Flow\Annotations as Flow; +use Neos\Flow\Aop\AspectContainer; use Neos\Flow\Aop\Builder\ClassNameIndex; -use Neos\Flow\Aop\Builder\ProxyClassBuilder; use Neos\Flow\Aop\Exception\UnknownPointcutException; /** @@ -41,11 +41,7 @@ class PointcutFilter implements PointcutFilterInterface */ protected $pointcut; - /** - * A reference to the AOP Proxy ClassBuilder - * @var ProxyClassBuilder - */ - protected $proxyClassBuilder; + protected AspectContainer $aspectContainer; /** * The constructor - initializes the pointcut filter with the name of the pointcut we're referring to @@ -53,21 +49,11 @@ class PointcutFilter implements PointcutFilterInterface * @param string $aspectClassName Name of the aspect class containing the pointcut * @param string $pointcutMethodName Name of the method which acts as an anchor for the pointcut name and expression */ - public function __construct(string $aspectClassName, string $pointcutMethodName) + public function __construct(string $aspectClassName, string $pointcutMethodName, AspectContainer $aspectContainer) { $this->aspectClassName = $aspectClassName; $this->pointcutMethodName = $pointcutMethodName; - } - - /** - * Injects the AOP Proxy Class Builder - * - * @param ProxyClassBuilder $proxyClassBuilder - * @return void - */ - public function injectProxyClassBuilder(ProxyClassBuilder $proxyClassBuilder): void - { - $this->proxyClassBuilder = $proxyClassBuilder; + $this->aspectContainer = $aspectContainer; } /** @@ -83,7 +69,7 @@ public function injectProxyClassBuilder(ProxyClassBuilder $proxyClassBuilder): v public function matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier): bool { if ($this->pointcut === null) { - $this->pointcut = $this->proxyClassBuilder->findPointcut($this->aspectClassName, $this->pointcutMethodName) ?: null; + $this->pointcut = $this->findPointcut($this->pointcutMethodName) ?: null; } if ($this->pointcut === null) { throw new UnknownPointcutException('No pointcut "' . $this->pointcutMethodName . '" found in aspect class "' . $this->aspectClassName . '" .', 1172223694); @@ -109,7 +95,7 @@ public function hasRuntimeEvaluationsDefinition(): bool public function getRuntimeEvaluationsDefinition(): array { if ($this->pointcut === null) { - $this->pointcut = $this->proxyClassBuilder->findPointcut($this->aspectClassName, $this->pointcutMethodName) ?: null; + $this->pointcut = $this->findPointcut($this->pointcutMethodName) ?: null; } if ($this->pointcut === null) { return []; @@ -127,11 +113,22 @@ public function getRuntimeEvaluationsDefinition(): array public function reduceTargetClassNames(ClassNameIndex $classNameIndex): ClassNameIndex { if ($this->pointcut === null) { - $this->pointcut = $this->proxyClassBuilder->findPointcut($this->aspectClassName, $this->pointcutMethodName) ?: null; + $this->pointcut = $this->findPointcut($this->pointcutMethodName); } if ($this->pointcut === null) { return $classNameIndex; } return $this->pointcut->reduceTargetClassNames($classNameIndex); } + + protected function findPointcut(string $methodName): ?Pointcut + { + foreach ($this->aspectContainer->getPointcuts() as $pointcut) { + if ($pointcut->getPointcutMethodName() === $methodName) { + return $pointcut; + } + } + + return null; + } } diff --git a/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterComposite.php b/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterComposite.php index 699e72df63..d9ffef5e4e 100644 --- a/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterComposite.php +++ b/Neos.Flow/Classes/Aop/Pointcut/PointcutFilterComposite.php @@ -27,9 +27,9 @@ class PointcutFilterComposite implements PointcutFilterInterface { /** - * @var array An array of \Neos\Flow\Aop\Pointcut\Pointcut*Filter objects + * @var array An array of \Neos\Flow\Aop\Pointcut\Pointcut*Filter objects */ - protected $filters = []; + protected array $filters = []; /** * @var boolean diff --git a/Neos.Flow/Classes/Command/AopCommandController.php b/Neos.Flow/Classes/Command/AopCommandController.php new file mode 100644 index 0000000000..33c2eda3fe --- /dev/null +++ b/Neos.Flow/Classes/Command/AopCommandController.php @@ -0,0 +1,152 @@ +aspectContainerBuilder = $aspectContainerBuilder; + } + + public function injectPointcutExpressionParser(PointcutExpressionParser $pointcutExpressionParser): void + { + $this->pointcutExpressionParser = $pointcutExpressionParser; + } + + public function injectPackageManager(PackageManager $packageManager): void + { + $this->packageManager = $packageManager; + } + + public function injectReflectionService(ReflectionService $reflectionService): void + { + $this->reflectionService = $reflectionService; + } + + public function injectSettings(array $settings): void + { + $this->objectSettings = $settings['object']; + } + + /** + * @param class-string $onlyAspect Class name of an aspect class to focus on + * @param bool $advisors Output advisors and their targets, this would be all advices declared within the aspect, enabled by default, use 0 to disable + * @param bool $interfaceIntroductions Output interface introductions from this advice and where they are introduced, disabled by default + * @param bool $propertyIntroductions Output property introductions from this advice and where they are introduced, disabled by default + * @param bool $traitIntroductions Output trait introductions from this advice and where they are introduced, disabled by default + * @return void + * @throws \Neos\Flow\Aop\Exception + * @throws \Neos\Flow\Aop\Exception\InvalidPointcutExpressionException + * @throws \Neos\Flow\Aop\Exception\InvalidTargetClassException + * @throws \Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException + * @throws \Neos\Flow\Reflection\Exception\ClassLoadingForReflectionFailedException + * @throws \Neos\Flow\Reflection\Exception\InvalidClassException + * @throws \ReflectionException + */ + public function aspectTargetsCommand(string $onlyAspect = '', bool $advisors = true, bool $interfaceIntroductions = false, bool $propertyIntroductions = false, bool $traitIntroductions = false): void + { + $packageClassFileProvider = new PackageClassFileProvider(); + $packageClassFiles = $packageClassFileProvider->build($this->packageManager->getAvailablePackages(), $this->objectSettings); + + $this->pointcutExpressionParser->injectReflectionService($this->reflectionService); + $this->pointcutExpressionParser->injectObjectManager($this->objectManager); + + $this->aspectContainerBuilder->injectReflectionService($this->reflectionService); + $this->aspectContainerBuilder->injectPointcutExpressionParser($this->pointcutExpressionParser); + $this->aspectContainerBuilder->injectObjectManager($this->objectManager); + $aspectContainers = $this->aspectContainerBuilder->buildFromReflection(); + + $proxyableClassesFilter = new ProxyableClassesFilter(); + $possibleTargetClassNames = $proxyableClassesFilter->getProxyableClasses($packageClassFiles, array_keys($aspectContainers)); + + foreach ($aspectContainers as $packageName => $aspectContainer) { + if ($onlyAspect !== '' && $aspectContainer->getClassName() !== $onlyAspect) { + continue; + } + + $this->outputFormatted('Aspect container: "%s"', [$packageName]); + $classNameIndex = new ClassNameIndex(); + $classNameIndex->setClassNames($possibleTargetClassNames); +// $targetClassNames = $aspectContainer->reduceTargetClassNames($classNameIndex); +// foreach ($aspectContainer->getPointcuts() as $pointcut) { +// $this->outputLine('- Pointcut "%s" applies to:', [$pointcut->getPointcutExpression()]); +// foreach ($pointcut->reduceTargetClassNames($classNameIndex)->getClassNames() as $targetClassName) { +// $this->outputLine('class: "%s"', [$targetClassName]); +// } +// } + + $advisors && $this->outputAdvisors($aspectContainer, $classNameIndex); + $interfaceIntroductions && $this->outputInterfaceIntroductions($aspectContainer, $classNameIndex); + $propertyIntroductions && $this->outputPropertyInjections($aspectContainer, $classNameIndex); + $traitIntroductions && $this->outputTraitIntroductions($aspectContainer, $classNameIndex); + +// foreach ($targetClassNames->getClassNames() as $targetClassName) { +// $this->outputLine('Target class: "%s"', [$targetClassName]); +// } + } + } + + protected function outputAdvisors(AspectContainer $aspectContainer, ClassNameIndex $classNameIndex): void + { + foreach ($aspectContainer->getAdvisors() as $advisor) { + $this->outputLine('- %s with pointcut', [get_class($advisor->getAdvice())]); + $this->outputLine(' %s', [$advisor->getPointcut()->getPointcutExpression()]); + $this->outputLine(' applies to:'); + foreach ($advisor->getPointcut()->reduceTargetClassNames($classNameIndex)->getClassNames() as $targetClassName) { + $this->outputLine(' class: "%s"', [$targetClassName]); + } + } + } + + protected function outputInterfaceIntroductions(AspectContainer $aspectContainer, ClassNameIndex $classNameIndex): void + { + foreach ($aspectContainer->getInterfaceIntroductions() as $interfaceIntroduction) { + $this->outputLine(' - Introducing interface "%s" to:', [$interfaceIntroduction->getInterfaceName()]); + foreach ($interfaceIntroduction->getPointcut()->reduceTargetClassNames($classNameIndex)->getClassNames() as $targetClassName) { + $this->outputLine(' class: "%s"', [$targetClassName]); + } + } + } + + protected function outputPropertyInjections(AspectContainer $aspectContainer, ClassNameIndex $classNameIndex): void + { + foreach ($aspectContainer->getPropertyIntroductions() as $propertyIntroduction) { + $this->outputLine(' - Introducing property "%s" to:', [$propertyIntroduction->getPropertyName()]); + foreach ($propertyIntroduction->getPointcut()->reduceTargetClassNames($classNameIndex)->getClassNames() as $targetClassName) { + $this->outputLine(' class: "%s"', [$targetClassName]); + } + } + } + + protected function outputTraitIntroductions(AspectContainer $aspectContainer, ClassNameIndex $classNameIndex): void + { + foreach ($aspectContainer->getTraitIntroductions() as $traitIntroduction) { + $this->outputLine(' - Introducing trait "%s" to:', [$traitIntroduction->getTraitName()]); + foreach ($traitIntroduction->getPointcut()->reduceTargetClassNames($classNameIndex)->getClassNames() as $targetClassName) { + $this->outputLine(' class: "%s"', [$targetClassName]); + } + } + } +} diff --git a/Neos.Flow/Classes/Mvc/Controller/Argument.php b/Neos.Flow/Classes/Mvc/Controller/Argument.php index c125a2f564..592814aa9b 100644 --- a/Neos.Flow/Classes/Mvc/Controller/Argument.php +++ b/Neos.Flow/Classes/Mvc/Controller/Argument.php @@ -226,8 +226,8 @@ public function setValue($rawValue): Argument } elseif (is_array($rawValue) && isset($rawValue['__type']) && $configuration->getConfigurationValue(ObjectConverter::class, ObjectConverter::CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED) === true) { $this->dataType = $rawValue['__type']; } - $this->value = $this->propertyMapper->convert($rawValue, $this->dataType, $this->getPropertyMappingConfiguration()); - $this->validationResults = $this->propertyMapper->getMessages() ?? new Result(); + $this->value = $this->propertyMapper?->convert($rawValue, $this->dataType, $this->getPropertyMappingConfiguration()) ?? $rawValue; + $this->validationResults = $this->propertyMapper?->getMessages() ?? new Result(); if ($this->validator !== null) { $validationMessages = $this->validator->validate($this->value); $this->validationResults->merge($validationMessages); diff --git a/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php b/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php index 8f678fe7ee..e0596fc237 100644 --- a/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php +++ b/Neos.Flow/Classes/ObjectManagement/CompileTimeObjectManager.php @@ -131,7 +131,9 @@ public function injectLogger(LoggerInterface $logger): void */ public function initialize(array $packages): void { - $this->registeredClassNames = $this->registerClassFiles($packages); + $packageClassFileProvider = new PackageClassFileProvider(); + $packageClassFileProvider->injectLogger($this->logger); + $this->registeredClassNames = $packageClassFileProvider->build($packages, $this->allSettings['Neos']['Flow']['object'] ?? []); $this->reflectionService->buildReflectionData($this->registeredClassNames); $rawCustomObjectConfigurations = $this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_OBJECTS); @@ -200,112 +202,6 @@ public function getClassNamesByScope(int $scope): array return $this->cachedClassNamesByScope[$scope]; } - /** - * Traverses through all class files of the active packages and registers collects the class names as - * "all available class names". If the respective Flow settings say so, also function test classes - * are registered. - * - * For performance reasons this function ignores classes whose name ends with "Exception". - * - * @param array $packages A list of packages to consider - * @return array A list of class names which were discovered in the given packages - * - * @throws InvalidConfigurationTypeException - */ - protected function registerClassFiles(array $packages): array - { - $includeClassesConfiguration = []; - if (isset($this->allSettings['Neos']['Flow']['object']['includeClasses'])) { - if (!is_array($this->allSettings['Neos']['Flow']['object']['includeClasses'])) { - throw new InvalidConfigurationTypeException('The setting "Neos.Flow.object.includeClasses" is invalid, it must be an array if set. Check the syntax in the YAML file.', 1422357285); - } - - $includeClassesConfiguration = $this->allSettings['Neos']['Flow']['object']['includeClasses']; - } - - $availableClassNames = ['' => ['DateTime']]; - - $shouldRegisterFunctionalTestClasses = (bool)($this->allSettings['Neos']['Flow']['object']['registerFunctionalTestClasses'] ?? false); - - foreach ($packages as $packageKey => $package) { - $packageType = (string)$package->getComposerManifest('type'); - if (isset($includeClassesConfiguration[$packageKey]) || ComposerUtility::isFlowPackageType($packageType)) { - foreach ($package->getClassFiles() as $fullClassName => $path) { - if (!str_ends_with($fullClassName, 'Exception')) { - $availableClassNames[$packageKey][] = $fullClassName; - } - } - if ($package instanceof FlowPackageInterface && $shouldRegisterFunctionalTestClasses) { - foreach ($package->getFunctionalTestsClassFiles() as $fullClassName => $path) { - if (!str_ends_with($fullClassName, 'Exception')) { - $availableClassNames[$packageKey][] = $fullClassName; - } - } - } - if (isset($availableClassNames[$packageKey]) && is_array($availableClassNames[$packageKey])) { - $availableClassNames[$packageKey] = array_unique($availableClassNames[$packageKey]); - } - } - } - return $this->filterClassNamesFromConfiguration($availableClassNames, $includeClassesConfiguration); - } - - /** - * Given an array of class names by package key this filters out classes that - * have been configured to be included by object management. - * - * @param array $classNames 2-level array - key of first level is package key, value of second level is classname (FQN) - * @param array $includeClassesConfiguration array of includeClasses configurations - * @return array The input array with all configured to be included in object management added in - * @throws InvalidConfigurationTypeException - */ - protected function filterClassNamesFromConfiguration(array $classNames, array $includeClassesConfiguration): array - { - return $this->applyClassFilterConfiguration($classNames, $includeClassesConfiguration); - } - - /** - * Filters the classnames available for object management by filter expressions that includes classes. - * - * @param array $classNames All classnames per package - * @param array $filterConfiguration The filter configuration to apply - * @return array the remaining class - * @throws InvalidConfigurationTypeException - */ - protected function applyClassFilterConfiguration(array $classNames, array $filterConfiguration): array - { - foreach ($filterConfiguration as $packageKey => $filterExpressions) { - if (!array_key_exists($packageKey, $classNames)) { - $this->logger->debug('The package "' . $packageKey . '" specified in the setting "Neos.Flow.object.includeClasses" was either excluded or is not loaded.'); - continue; - } - if (!is_array($filterExpressions)) { - throw new InvalidConfigurationTypeException('The value given for setting "Neos.Flow.object.includeClasses.\'' . $packageKey . '\'" is invalid. It should be an array of expressions. Check the syntax in the YAML file.', 1422357272); - } - - $classesForPackageUnderInspection = $classNames[$packageKey]; - $classNames[$packageKey] = []; - - foreach ($filterExpressions as $filterExpression) { - $classesForPackageUnderInspection = array_filter( - $classesForPackageUnderInspection, - static function ($className) use ($filterExpression) { - $match = preg_match('/' . $filterExpression . '/', $className); - return $match === 1; - } - ); - $classNames[$packageKey] = array_merge($classNames[$packageKey], $classesForPackageUnderInspection); - $classesForPackageUnderInspection = $classNames[$packageKey]; - } - - if ($classNames[$packageKey] === []) { - unset($classNames[$packageKey]); - } - } - - return $classNames; - } - /** * Builds the objects array which contains information about the registered objects, * their scope, class, built method etc. diff --git a/Neos.Flow/Classes/ObjectManagement/PackageClassFileProvider.php b/Neos.Flow/Classes/ObjectManagement/PackageClassFileProvider.php new file mode 100644 index 0000000000..fd47e3a378 --- /dev/null +++ b/Neos.Flow/Classes/ObjectManagement/PackageClassFileProvider.php @@ -0,0 +1,118 @@ +logger = $logger; + } + + /** + * Traverses through all class files of the active packages and registers collects the class names as + * "all available class names". If the respective Flow settings say so, also function test classes + * are registered. + * + * For performance reasons this function ignores classes whose name ends with "Exception". + * + * @param PackageInterface[] $packages A list of packages to consider + * @param array{"includeClasses"?: mixed|array, "registerFunctionalTestClasses"?: bool} $objectConfigurationSettings + * + * @return array A list of class names which were discovered in the given packages + * + * @throws InvalidConfigurationTypeException + */ + public function build(array $packages, array $objectConfigurationSettings): array + { + $includeClassesConfiguration = []; + if (isset($objectConfigurationSettings['includeClasses'])) { + if (!is_array($objectConfigurationSettings['includeClasses'])) { + throw new InvalidConfigurationTypeException('The setting "Neos.Flow.object.includeClasses" is invalid, it must be an array if set. Check the syntax in the YAML file.', 1422357285); + } + + $includeClassesConfiguration = $objectConfigurationSettings['includeClasses']; + } + + $availableClassNames = ['' => ['DateTime']]; + + $shouldRegisterFunctionalTestClasses = (bool)($objectConfigurationSettings['registerFunctionalTestClasses'] ?? false); + + foreach ($packages as $packageKey => $package) { + $packageType = (string)$package->getComposerManifest('type'); + if (isset($includeClassesConfiguration[$packageKey]) || ComposerUtility::isFlowPackageType($packageType)) { + foreach ($package->getClassFiles() as $fullClassName => $path) { + if (!str_ends_with($fullClassName, 'Exception')) { + $availableClassNames[$packageKey][] = $fullClassName; + } + } + if ($package instanceof FlowPackageInterface && $shouldRegisterFunctionalTestClasses) { + foreach ($package->getFunctionalTestsClassFiles() as $fullClassName => $path) { + if (!str_ends_with($fullClassName, 'Exception')) { + $availableClassNames[$packageKey][] = $fullClassName; + } + } + } + if (isset($availableClassNames[$packageKey])) { + $availableClassNames[$packageKey] = array_unique($availableClassNames[$packageKey]); + } + } + } + return $this->applyClassFilterConfiguration($availableClassNames, $includeClassesConfiguration); + } + + /** + * Filters the classnames available for object management by filter expressions that includes classes. + * + * @param array $classNames All classnames per package + * @param array> $filterConfiguration The filter configuration to apply + * @return array the remaining classes + * @throws InvalidConfigurationTypeException + */ + protected function applyClassFilterConfiguration(array $classNames, array $filterConfiguration): array + { + foreach ($filterConfiguration as $packageKey => $filterExpressions) { + if (!array_key_exists($packageKey, $classNames)) { + $this->logger?->debug('The package "' . $packageKey . '" specified in the setting "Neos.Flow.object.includeClasses" was either excluded or is not loaded.'); + continue; + } + if (!is_array($filterExpressions)) { + throw new InvalidConfigurationTypeException('The value given for setting "Neos.Flow.object.includeClasses.\'' . $packageKey . '\'" is invalid. It should be an array of expressions. Check the syntax in the YAML file.', 1422357272); + } + + $classesForPackageUnderInspection = $classNames[$packageKey]; + $classNames[$packageKey] = []; + + foreach ($filterExpressions as $filterExpression) { + $classesForPackageUnderInspection = array_filter( + $classesForPackageUnderInspection, + static function ($className) use ($filterExpression) { + $match = preg_match('/' . $filterExpression . '/', $className); + return $match === 1; + } + ); + $classNames[$packageKey] = array_merge($classNames[$packageKey], $classesForPackageUnderInspection); + $classesForPackageUnderInspection = $classNames[$packageKey]; + } + + if ($classNames[$packageKey] === []) { + unset($classNames[$packageKey]); + } + } + + return $classNames; + } +} diff --git a/Neos.Flow/Classes/Package.php b/Neos.Flow/Classes/Package.php index f6ad59cbfb..71564c9813 100644 --- a/Neos.Flow/Classes/Package.php +++ b/Neos.Flow/Classes/Package.php @@ -67,6 +67,7 @@ public function boot(Core\Bootstrap $bootstrap) $bootstrap->registerCompiletimeCommand('neos.flow:core:*'); $bootstrap->registerCompiletimeCommand('neos.flow:cache:flush'); $bootstrap->registerCompiletimeCommand('neos.flow:package:rescan'); + $bootstrap->registerCompiletimeCommand('neos.flow:aop:aspecttargets'); $dispatcher = $bootstrap->getSignalSlotDispatcher(); diff --git a/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodTargetExpressionParser.php b/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodTargetExpressionParser.php index dec1266201..7a1a33af12 100644 --- a/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodTargetExpressionParser.php +++ b/Neos.Flow/Classes/Security/Authorization/Privilege/Method/MethodTargetExpressionParser.php @@ -12,6 +12,7 @@ */ use Neos\Flow\Annotations as Flow; +use Neos\Flow\Aop\AspectContainer; use Neos\Flow\Aop\Exception\InvalidPointcutExpressionException; use Neos\Flow\Aop\Pointcut\PointcutExpressionParser; use Neos\Flow\Aop\Pointcut\PointcutFilterComposite; @@ -30,11 +31,10 @@ class MethodTargetExpressionParser extends PointcutExpressionParser * @param string $operator The operator * @param string $pointcutExpression The pointcut expression (value of the designator) * @param PointcutFilterComposite $pointcutFilterComposite An instance of the pointcut filter composite. The result (ie. the pointcut filter) will be added to this composite object. - * @param array &$trace * @return void * @throws InvalidPointcutExpressionException */ - protected function parseDesignatorPointcut(string $operator, string $pointcutExpression, PointcutFilterComposite $pointcutFilterComposite, array &$trace = []): void + protected function parseDesignatorPointcut(string $operator, string $pointcutExpression, PointcutFilterComposite $pointcutFilterComposite, AspectContainer $aspectContainer): void { throw new InvalidPointcutExpressionException('The given method privilege target matcher contained an expression for a named pointcut. This not supported! Given expression: "' . $pointcutExpression . '".', 1222014591); }