From 6d052a9cde69353e51d9c0c52eb9ce2b500565ed Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 31 Jul 2026 15:55:57 -0400 Subject: [PATCH] =?UTF-8?q?FOUR-32497:=20[Octane]=20MEDIUM=20=E2=80=94=20A?= =?UTF-8?q?ccumulating=20State=20"pmFunctions"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ProcessMaker/Models/FormalExpression.php | 10 ++ .../Models/FormalExpressionOctaneTest.php | 162 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 tests/unit/ProcessMaker/Models/FormalExpressionOctaneTest.php diff --git a/ProcessMaker/Models/FormalExpression.php b/ProcessMaker/Models/FormalExpression.php index 838944d60e..e91bbb4eac 100644 --- a/ProcessMaker/Models/FormalExpression.php +++ b/ProcessMaker/Models/FormalExpression.php @@ -56,6 +56,16 @@ protected function initFormalExpression() public function registerPMFunction($name, callable $callable) { static::$pmFunctions[$name] = $callable; + + // Also register into the current ExpressionLanguage instance if initialized + if ($this->feelExpression instanceof ExpressionLanguage) { + $this->feelExpression->register( + $name, + function () { + }, + $callable + ); + } } /** diff --git a/tests/unit/ProcessMaker/Models/FormalExpressionOctaneTest.php b/tests/unit/ProcessMaker/Models/FormalExpressionOctaneTest.php new file mode 100644 index 0000000000..6a5eaf5ce1 --- /dev/null +++ b/tests/unit/ProcessMaker/Models/FormalExpressionOctaneTest.php @@ -0,0 +1,162 @@ +setLanguage('FEEL'); + $formalExp1->setBody('customFn("test") == "test"'); + + // Use reflection to call registerPMFunction (simulating a package registering) + $reflection = new \ReflectionClass($formalExp1); + $method = $reflection->getMethod('registerPMFunction'); + $method->setAccessible(true); + $method->invoke($formalExp1, 'customFn', function ($arguments, $arg) { + return (string) $arg; + }); + + // Verify the function was registered + $staticProperty = $reflection->getProperty('pmFunctions'); + $staticProperty->setAccessible(true); + $this->assertArrayHasKey('customFn', $staticProperty->getValue()); + $initialCount = count($staticProperty->getValue()); + + // Simulate request 2: create a new instance in the same worker + // The static $pmFunctions should still contain the previously registered function + // but should not grow unbounded + $formalExp2 = new FormalExpression(); + $formalExp2->setLanguage('FEEL'); + + $reflection2 = new \ReflectionClass($formalExp2); + $staticProperty2 = $reflection2->getProperty('pmFunctions'); + $staticProperty2->setAccessible(true); + $pmFunctionsAfterRequest2 = $staticProperty2->getValue(); + + // The functions should persist (static across instances in same worker) + $this->assertArrayHasKey('customFn', $pmFunctionsAfterRequest2); + + // But the count should not have grown unexpectedly + $this->assertCount($initialCount, $pmFunctionsAfterRequest2); + + // Simulate request 3: register another custom function + $method2 = $reflection2->getMethod('registerPMFunction'); + $method2->setAccessible(true); + $method2->invoke($formalExp2, 'anotherFn', function ($arguments, $arg) { + return strtoupper((string) $arg); + }); + + $countAfterRequest3 = count($staticProperty2->getValue()); + $this->assertArrayHasKey('anotherFn', $staticProperty2->getValue()); + + // The registry grew by exactly 1 + $this->assertEquals($initialCount + 1, $countAfterRequest3); + } + + /** + * Test that the built-in system functions are always registered and + * available regardless of static state. + */ + public function test_built_in_functions_are_always_available(): void + { + $formalExp = new FormalExpression(); + $formalExp->setLanguage('FEEL'); + $formalExp->setBody('date("Y") > 1900'); + $this->assertTrue($formalExp([])); + } + + /** + * Test that a custom registered function works correctly. + */ + public function test_custom_pm_function_is_evaluable(): void + { + $formalExp = new FormalExpression(); + $formalExp->setLanguage('FEEL'); + $formalExp->setBody('greet("World") == "Hello, World!"'); + + $reflection = new \ReflectionClass($formalExp); + $method = $reflection->getMethod('registerPMFunction'); + $method->setAccessible(true); + $method->invoke($formalExp, 'greet', function ($arguments, $name) { + return 'Hello, ' . $name . '!'; + }); + + $this->assertTrue($formalExp([])); + } + + /** + * Test that duplicate function registration overwrites the previous one + * rather than causing conflicts. + */ + public function test_registering_same_function_twice_overwrites(): void + { + $formalExp = new FormalExpression(); + $formalExp->setLanguage('FEEL'); + + $reflection = new \ReflectionClass($formalExp); + $method = $reflection->getMethod('registerPMFunction'); + $method->setAccessible(true); + + // Register with first implementation + $method->invoke($formalExp, 'double', function ($arguments, $x) { + return $x * 2; + }); + + // Overwrite with second implementation + $method->invoke($formalExp, 'double', function ($arguments, $x) { + return $x * 3; + }); + + $formalExp->setBody('double(2) == 6'); + $this->assertTrue($formalExp([])); + } + + /** + * Test that pmFunctions remains bounded and does not grow with each + * FormalExpression instantiation. + */ + public function test_pm_functions_static_array_does_not_grow_with_instances(): void + { + $reflection = new \ReflectionClass(FormalExpression::class); + $staticProperty = $reflection->getProperty('pmFunctions'); + $staticProperty->setAccessible(true); + + // Clear any existing functions to get a clean baseline + $staticProperty->setValue([]); + + // Create multiple instances - the static array should not grow just + // from instantiating FormalExpression (only when registerPMFunction is called) + $initialCount = count($staticProperty->getValue()); + + for ($i = 0; $i < 10; $i++) { + $exp = new FormalExpression(); + $exp->setLanguage('FEEL'); + } + + $this->assertCount($initialCount, $staticProperty->getValue()); + } +}