From ee44412cc4179d224ba1a8e073e907ba1fe94d5f Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 19 Jun 2026 18:13:44 +0100 Subject: [PATCH 1/2] test: isolate bug #708 --- test/phpunit/Dispatch/DispatcherTest.php | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/test/phpunit/Dispatch/DispatcherTest.php b/test/phpunit/Dispatch/DispatcherTest.php index b909960b..1f9dc104 100644 --- a/test/phpunit/Dispatch/DispatcherTest.php +++ b/test/phpunit/Dispatch/DispatcherTest.php @@ -242,6 +242,70 @@ public function testGenerateResponse_executesComponentAndPageLogicAndAppliesHead self::assertSame([], $invocations[4]["extraArgs"]); } + public function testGenerateResponse_componentFormDoActionDoesNotRunPageDoAction():void { + $html = << + +
+ +
+ + +
+
+ + HTML; + + $viewModel = new HTMLDocument($html); + $component = $viewModel->querySelector("widget-one"); + self::assertInstanceOf(Element::class, $component); + + $componentAssembly = $this->createAssembly("/tmp/component.php"); + $componentList = new LogicAssemblyComponentList(); + $componentList->addAssemblyComponent($componentAssembly, $component); + + $processor = $this->createMock(ViewModelProcessor::class); + $processor->method("processDynamicPath"); + $processor->method("processPartialContent") + ->willReturn($componentList); + + $invocations = []; + $logicAssembly = $this->createAssembly("/tmp/page.php"); + $logicExecutor = $this->createMock(LogicExecutor::class); + $logicExecutor->method("invoke") + ->willReturnCallback(function(Assembly $assembly, string $name)use(&$invocations, $componentAssembly):\Generator { + $invocations[] = [ + "assembly" => $assembly === $componentAssembly ? "component" : "page", + "name" => $name, + ]; + yield ($assembly === $componentAssembly ? "/tmp/component.php" : "/tmp/page.php") . "::$name()"; + }); + + $sut = $this->createDispatcher( + input: new Input([], [ + "__component" => "widget-one", + "do" => "save-item", + ]), + viewModel: $viewModel, + logicAssembly: $logicAssembly, + viewModelInit: $this->createViewModelInit($processor, true), + logicExecutor: $logicExecutor, + ); + + $response = $sut->generateResponse(); + + self::assertSame(StatusCode::OK, $response->getStatusCode()); + self::assertSame([ + ["assembly" => "component", "name" => "go_before"], + ["assembly" => "component", "name" => "do_save_item"], + ["assembly" => "component", "name" => "go"], + ["assembly" => "component", "name" => "go_after"], + ["assembly" => "page", "name" => "go_before"], + ["assembly" => "page", "name" => "go"], + ["assembly" => "page", "name" => "go_after"], + ], $invocations); + } + public function testProcessResponse_rethrowsComponentThrowableOutsideErrorMode():void { $viewModel = new HTMLDocument(''); $component = $viewModel->querySelector("widget-one"); From ee9a67d6e11c0067f075bffb05a0c5f46ea8a36c Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 19 Jun 2026 18:33:38 +0100 Subject: [PATCH 2/2] fix: check component do function triggers closes #708 --- src/Dispatch/Dispatcher.php | 40 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Dispatch/Dispatcher.php b/src/Dispatch/Dispatcher.php index f24843af..e232b769 100644 --- a/src/Dispatch/Dispatcher.php +++ b/src/Dispatch/Dispatcher.php @@ -55,6 +55,7 @@ */ class Dispatcher { private const LOGIC_EXECUTION_HEADER = "X-Logic-Execution"; + private const COMPONENT_INPUT_NAME = "__component"; private Config $config; private Request $request; @@ -381,20 +382,13 @@ private function handleLogicExecution( $this->recordLogicExecution($functionReference); } -// TODO: No need to have the whole Input class. Just pass a nullable string in called $doMethod, from $input->getString("do") - $input->when("do")->call( - function(InputData $data)use($logicAssembly, $extraArgs) { - $doName = "do_" . str_replace( - "-", - "_", - $data->getString("do"), - ); + if($doAction = $this->getDoActionForLogic($input, $component)) { + $doName = "do_" . str_replace("-", "_", $doAction); - foreach($this->logicExecutor->invoke($logicAssembly, $doName, $extraArgs) as $functionReference) { - $this->recordLogicExecution($functionReference); - } + foreach($this->logicExecutor->invoke($logicAssembly, $doName, $extraArgs) as $functionReference) { + $this->recordLogicExecution($functionReference); } - ); + } foreach($this->logicExecutor->invoke($logicAssembly, "go", $extraArgs) as $functionReference) { $this->recordLogicExecution($functionReference); @@ -405,6 +399,28 @@ function(InputData $data)use($logicAssembly, $extraArgs) { } } + private function getDoActionForLogic(Input $input, ?Element $component):?string { + $doAction = $input->getString("do"); + if($doAction === null || $doAction === "") { + return null; + } + + $submittedComponent = $input->getString(self::COMPONENT_INPUT_NAME); + if($submittedComponent === null || $submittedComponent === "") { + return $doAction; + } + + if(!$component) { + return null; + } + + if(strtolower($component->tagName) !== strtolower($submittedComponent)) { + return null; + } + + return $doAction; + } + /** * @return void */