Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Neos.Flow/Classes/Aop/Builder/AbstractMethodInterceptorBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ protected function buildSavedConstructorParametersCode(?string $className = null
* @param string|null $declaringClassName Name of the declaring class. This is usually the same as the $targetClassName. However, it is the introduction interface for introduced methods.
* @return string PHP code to be used in the method interceptor
*/
protected function buildAdvicesCode(array $groupedAdvices, ?string $methodName = null, ?string $targetClassName = null, ?string $declaringClassName = null): string
protected function buildAdvicesCode(array $groupedAdvices, ?string $methodName, ?string $targetClassName, ?string $declaringClassName, ?string $declaredReturnType): string
{
$advicesCode = $this->buildMethodArgumentsArrayCode($declaringClassName, $methodName, ($methodName === '__construct'));

Expand Down Expand Up @@ -184,9 +184,17 @@ protected function buildAdvicesCode(array $groupedAdvices, ?string $methodName =
$adviceChain = $adviceChains[\'Neos\Flow\Aop\Advice\AroundAdvice\'];
$adviceChain->rewind();
$joinPoint = new \Neos\Flow\Aop\JoinPoint($this, \'' . $targetClassName . '\', \'' . $methodName . '\', $methodArguments, $adviceChain);
';
if ($declaredReturnType === 'never') {
$advicesCode .= '
$adviceChain->proceed($joinPoint);
';
} else {
$advicesCode .= '
$result = $adviceChain->proceed($joinPoint);
$methodArguments = $joinPoint->getMethodArguments();
';
}
} else {
$advicesCode .= '
$joinPoint = new \Neos\Flow\Aop\JoinPoint($this, \'' . $targetClassName . '\', \'' . $methodName . '\', $methodArguments);
Expand All @@ -195,7 +203,7 @@ protected function buildAdvicesCode(array $groupedAdvices, ?string $methodName =
';
}

if (isset($groupedAdvices[\Neos\Flow\Aop\Advice\AfterReturningAdvice::class])) {
if (isset($groupedAdvices[\Neos\Flow\Aop\Advice\AfterReturningAdvice::class]) && $declaredReturnType !== 'never') {
$advicesCode .= '
if (isset($this->Flow_Aop_Proxy_targetMethodsAndGroupedAdvices[\'' . $methodName . '\'][\'Neos\Flow\Aop\Advice\AfterReturningAdvice\'])) {
$advices = $this->Flow_Aop_Proxy_targetMethodsAndGroupedAdvices[\'' . $methodName . '\'][\'Neos\Flow\Aop\Advice\AfterReturningAdvice\'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function build(string $methodName, array $methodMetaInformation, string $
$proxyMethod = $this->compiler->getProxyClass($targetClassName)->getConstructor();

$groupedAdvices = $methodMetaInformation[$methodName]['groupedAdvices'];
$advicesCode = $this->buildAdvicesCode($groupedAdvices, $methodName, $targetClassName, $declaringClassName);
$advicesCode = $this->buildAdvicesCode($groupedAdvices, $methodName, $targetClassName, $declaringClassName, null);

$proxyMethod->addPreParentCallCode(<<<PHP
if (isset(\$this->Flow_Aop_Proxy_methodIsInAdviceMode['{$methodName}'])) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public function build(string $methodName, array $methodMetaInformation, string $
}

$declaringClassName = $methodMetaInformation[$methodName]['declaringClassName'];
$declaredReturnType = ($declaringClassName !== null) ? $this->reflectionService->getMethodDeclaredReturnType($declaringClassName, $methodName) : null;
$proxyMethod = $this->compiler->getProxyClass($targetClassName)->getMethod($methodName);
if ($proxyMethod->getVisibility() === ProxyMethodGenerator::VISIBILITY_PRIVATE) {
throw new Exception(sprintf('The %s cannot build interceptor code for private method %s::%s(). Please change the scope to at least protected or adjust the pointcut expression in the corresponding aspect.', __CLASS__, $targetClassName, $methodName), 1593070574);
Expand All @@ -49,7 +50,8 @@ public function build(string $methodName, array $methodMetaInformation, string $
}

$groupedAdvices = $methodMetaInformation[$methodName]['groupedAdvices'];
$advicesCode = $this->buildAdvicesCode($groupedAdvices, $methodName, $targetClassName, $declaringClassName);
$advicesCode = $this->buildAdvicesCode($groupedAdvices, $methodName, $targetClassName, $declaringClassName, $declaredReturnType);
$neverThrowCode = $declaredReturnType === 'never' ? 'throw new \RuntimeException(\'Possible bug in around advice proxy code for method ' . $targetClassName . '::' . $methodName . '() with return type "never". This point should never be reached. 👻\', 1761038455);' : '';

$proxyMethod->addPreParentCallCode(<<<PHP
if (isset(\$this->Flow_Aop_Proxy_methodIsInAdviceMode['{$methodName}'])) {
Expand All @@ -65,6 +67,7 @@ public function build(string $methodName, array $methodMetaInformation, string $
}
unset(\$this->Flow_Aop_Proxy_methodIsInAdviceMode['{$methodName}']);
}
{$neverThrowCode}
PHP);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php
namespace Neos\Flow\Tests\Functional\Aop\Fixtures;

/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface;

/**
* An aspect for testing methods with never return type
*
* @Flow\Aspect
*/
class NeverReturnTypeTestingAspect
{
/**
* A before advice should work with never return type
*
* @Flow\Before("method(Neos\Flow\Tests\Functional\Aop\Fixtures\TargetClassWithNeverReturnType->methodThatThrows())")
* @param JoinPointInterface $joinPoint
* @return void
*/
public function beforeNeverReturningMethod(JoinPointInterface $joinPoint): void
{
$proxy = $joinPoint->getProxy();
assert($proxy instanceof TargetClassWithNeverReturnType);
$proxy->beforeAdviceWasInvoked = true;
}

/**
* An after throwing advice should work with never return type
*
* @Flow\AfterThrowing("method(Neos\Flow\Tests\Functional\Aop\Fixtures\TargetClassWithNeverReturnType->methodThatThrows())")
* @param JoinPointInterface $joinPoint
* @return void
*/
public function afterThrowingNeverReturningMethod(JoinPointInterface $joinPoint): void
{
$proxy = $joinPoint->getProxy();
assert($proxy instanceof TargetClassWithNeverReturnType);
$proxy->afterThrowingAdviceWasInvoked = true;
}

/**
*
* @Flow\Around("method(Neos\Flow\Tests\Functional\Aop\Fixtures\TargetClassWithNeverReturnType->aroundAdvicedMethodThatThrows())")
* @param JoinPointInterface $joinPoint
* @return void
*/
public function aroundNeverReturningMethod(JoinPointInterface $joinPoint): void
{
$proxy = $joinPoint->getProxy();
assert($proxy instanceof TargetClassWithNeverReturnType);
try {
$joinPoint->getAdviceChain()->proceed($joinPoint);
} catch (\RuntimeException $exception) {
$proxy->aroundAdviceWasInvoked = true;
throw $exception;
}
}

/**
* An after returning advice makes no sense for never return type - but let's test what happens
*
* @Flow\AfterReturning("method(Neos\Flow\Tests\Functional\Aop\Fixtures\TargetClassWithNeverReturnType->methodThatExits())")
* @return void
*/
public function afterReturningNeverReturningMethod(): void
{
throw new \LogicException('AfterReturning advice should not be invoked for never-returning methods');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
namespace Neos\Flow\Tests\Functional\Aop\Fixtures;

/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

/**
* A target class for testing the AOP framework with never return type
*/
class TargetClassWithNeverReturnType
{
public bool $beforeAdviceWasInvoked = false;
public bool $afterThrowingAdviceWasInvoked = false;
public bool $aroundAdviceWasInvoked = false;

public function methodThatExits(): never
{
exit(42);
}

public function methodThatThrows(): never
{
throw new \RuntimeException('This method always throws', 1761036727);
}

public function aroundAdvicedMethodThatThrows(): never
{
throw new \RuntimeException('This method also always throws', 1761036724);
}
}
33 changes: 33 additions & 0 deletions Neos.Flow/Tests/Functional/Aop/FrameworkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* source code.
*/

use Neos\Flow\Tests\Functional\Aop\Fixtures\TargetClassWithNeverReturnType;
use Neos\Flow\Tests\Functional\Aop\Fixtures\TargetClassWithPhp7Features;
use Neos\Flow\Tests\Functional\Aop\Fixtures\TargetClassWithPhp8Features;
use Neos\Flow\Tests\FunctionalTestCase;
Expand Down Expand Up @@ -391,4 +392,36 @@ public function methodsWithReturnPhp8SimpleReturnTypesAreGeneratedCorrectly(): v
$this->expectExceptionCode(1686132896);
$targetClass->alwaysNever();
}

/**
* @test
*/
public function methodWithNeverReturnTypeCanBeAdvised(): void
{
$targetClass = new TargetClassWithNeverReturnType();

try {
$targetClass->methodThatThrows();
} catch (\Exception) {
} finally {
self::assertTrue($targetClass->beforeAdviceWasInvoked, 'Before advice should be invoked for never-returning method methodThatThrows()');
self::assertTrue($targetClass->afterThrowingAdviceWasInvoked, 'AfterThrowing advice should be invoked for never-returning method methodThatThrows()');
}
}

/**
* @test
*/
public function methodWithNeverReturnTypeCanBeAroundAdvised(): void
{
$target = new TargetClassWithNeverReturnType();

try {
$target->aroundAdvicedMethodThatThrows();
} catch (\Exception $e) {
self::assertSame(1761036724, $e->getCode());
} finally {
self::assertTrue($target->aroundAdviceWasInvoked, 'Around advice should be invoked for never-returning method aroundAdvicedMethodThatThrows()');
}
}
}