From 9f389af220ee1aee6e5961649e0f8096adab4325 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 20 Dec 2025 10:36:43 +0400 Subject: [PATCH] feat: produce deprecation when use activity method without the activity attribute --- phpunit.xml.dist | 4 + .../Declaration/Prototype/Prototype.php | 2 +- .../Declaration/Reader/ActivityReader.php | 7 -- src/Internal/Workflow/ActivityProxy.php | 30 ++++-- testing/src/DeprecationCollector.php | 33 +++++++ testing/src/DeprecationMessage.php | 15 +++ tests/Acceptance/App/RuntimeBuilder.php | 3 + .../Extra/Activity/ActivityMethodTest.php | 93 +++++++++++++++++++ 8 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 testing/src/DeprecationCollector.php create mode 100644 testing/src/DeprecationMessage.php create mode 100644 tests/Acceptance/Extra/Activity/ActivityMethodTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a982abbf3..b901245be 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,6 +9,10 @@ stopOnFailure="false" stopOnError="false" stderr="true" + failOnDeprecation="true" + failOnPhpunitDeprecation="true" + stopOnDeprecation="true" + displayDetailsOnPhpunitDeprecations="true" displayDetailsOnIncompleteTests="true" displayDetailsOnSkippedTests="true" displayDetailsOnTestsThatTriggerDeprecations="true" diff --git a/src/Internal/Declaration/Prototype/Prototype.php b/src/Internal/Declaration/Prototype/Prototype.php index bbf693679..47a0c2b08 100644 --- a/src/Internal/Declaration/Prototype/Prototype.php +++ b/src/Internal/Declaration/Prototype/Prototype.php @@ -58,7 +58,7 @@ private static function matchClass(PrototypeInterface $prototype, string $class) { $reflection = $prototype->getClass(); - return $reflection && $reflection->getName() === \trim($class, '\\'); + return $reflection->getName() === \trim($class, '\\'); } private static function matchMethod(PrototypeInterface $prototype, string $method): bool diff --git a/src/Internal/Declaration/Reader/ActivityReader.php b/src/Internal/Declaration/Reader/ActivityReader.php index 541fa7b57..c97c22a4c 100644 --- a/src/Internal/Declaration/Reader/ActivityReader.php +++ b/src/Internal/Declaration/Reader/ActivityReader.php @@ -111,13 +111,6 @@ private function getMethodGroups(ClassNode $graph, \ReflectionMethod $root): arr $contextualRetry = $contextualRetry ? $retry->mergeWith($contextualRetry) : $retry; } - // - // In the future, activity methods are available only in - // those classes that contain the attribute: - // - // - #[ActivityInterface] - // - #[LocalActivityInterface] - // $interface = $this->reader->firstClassMetadata($ctx->getReflection(), ActivityInterface::class); if ($interface === null) { diff --git a/src/Internal/Workflow/ActivityProxy.php b/src/Internal/Workflow/ActivityProxy.php index 22528988d..9165edccf 100644 --- a/src/Internal/Workflow/ActivityProxy.php +++ b/src/Internal/Workflow/ActivityProxy.php @@ -12,6 +12,7 @@ namespace Temporal\Internal\Workflow; use React\Promise\PromiseInterface; +use Temporal\Activity\ActivityMethod; use Temporal\Activity\ActivityOptionsInterface; use Temporal\Interceptor\WorkflowOutboundCalls\ExecuteActivityInput; use Temporal\Interceptor\WorkflowOutboundCalls\ExecuteLocalActivityInput; @@ -61,13 +62,24 @@ public function __construct( */ public function __call(string $method, array $args = []): PromiseInterface { - $handler = $this->findPrototypeByHandlerNameOrFail($method); - $type = $handler->getHandler()->getReturnType(); - $options = $this->options->mergeWith($handler->getMethodRetry()); + $prototype = $this->findPrototypeByHandlerNameOrFail($method); + $type = $prototype->getHandler()->getReturnType(); + $options = $this->options->mergeWith($prototype->getMethodRetry()); - $args = Reflection::orderArguments($handler->getHandler(), $args); + $args = Reflection::orderArguments($prototype->getHandler(), $args); - return $handler->isLocalActivity() + if (!$prototype->getHandler()->getAttributes(ActivityMethod::class, \ReflectionAttribute::IS_INSTANCEOF)) { + \trigger_error( + \sprintf( + 'Using implicit activity methods is deprecated. Explicitly mark activity method %s with #[%s] attribute instead.', + $prototype->getHandler()->getDeclaringClass()->getName() . '::' . $method, + ActivityMethod::class, + ), + \E_USER_DEPRECATED, + ); + } + + return $prototype->isLocalActivity() // Run local activity through an interceptor pipeline ? $this->callsInterceptor->with( fn(ExecuteLocalActivityInput $input): PromiseInterface => $this->ctx @@ -77,11 +89,11 @@ public function __call(string $method, array $args = []): PromiseInterface 'executeLocalActivity', )( new ExecuteLocalActivityInput( - $handler->getID(), + $prototype->getID(), $args, $options, $type, - $handler->getHandler(), + $prototype->getHandler(), ) ) @@ -94,11 +106,11 @@ public function __call(string $method, array $args = []): PromiseInterface 'executeActivity', )( new ExecuteActivityInput( - $handler->getID(), + $prototype->getID(), $args, $options, $type, - $handler->getHandler(), + $prototype->getHandler(), ) ); } diff --git a/testing/src/DeprecationCollector.php b/testing/src/DeprecationCollector.php new file mode 100644 index 000000000..4a49c5017 --- /dev/null +++ b/testing/src/DeprecationCollector.php @@ -0,0 +1,33 @@ + 'withAttribute'])] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult('array'); + self::assertEquals(1, $result['result']); + self::assertCount(0, $result['deprecations'], \print_r($result['deprecations'], true)); + } + + public function testMethodWithoutAttribute( + #[Stub('Extra_Activity_ActivityMethod', args: ['method' => 'withoutAttribute'])] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult('array'); + self::assertEquals(2, $result['result']); + self::assertCount(1, $result['deprecations']); + self::assertEquals( + \sprintf( + 'Using implicit activity methods is deprecated. Explicitly mark activity method %s with #[%s] attribute instead.', + TestActivity::class . '::withoutAttribute', + ActivityMethod::class, + ), + $result['deprecations'][0]['message'], + ); + } + + public function testMagicMethodIsIgnored( + #[Stub('Extra_Activity_ActivityMethod', args: ['method' => '__invoke'])] + WorkflowStubInterface $stub, + ): void { + $this->expectException(WorkflowFailedException::class); + $stub->getResult(type: 'int'); + } +} + + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Extra_Activity_ActivityMethod")] + public function handle(string $method) + { + $activityStub = Workflow::newActivityStub( + TestActivity::class, + Activity\ActivityOptions::new()->withScheduleToCloseTimeout(10), + ); + $result = yield $activityStub->{$method}(); + + return [ + 'result' => $result, + 'deprecations' => DeprecationCollector::getAll(), + ]; + } +} + +#[Activity\ActivityInterface(prefix: 'Extra_Activity_ActivityMethod.')] +class TestActivity +{ + #[ActivityMethod] + public function withAttribute() + { + return 1; + } + + public function withoutAttribute() + { + return 2; + } + + public function __invoke() + { + return 3; + } +}