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); }