From af1b217b9df0998e2f58c19627e59d944b72a081 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 28 Jan 2026 19:06:02 +0100 Subject: [PATCH 1/5] Add deprecation warnings for misconfigured SearchComponent --- src/Controller/Component/SearchComponent.php | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Controller/Component/SearchComponent.php b/src/Controller/Component/SearchComponent.php index 717ff63b..1ca692b8 100644 --- a/src/Controller/Component/SearchComponent.php +++ b/src/Controller/Component/SearchComponent.php @@ -167,10 +167,32 @@ public function beforeRender(): void */ $model = $controller->fetchTable($this->getConfig('modelClass')); } catch (UnexpectedValueException $e) { + deprecationWarning( + '5.0.0', + sprintf( + 'SearchComponent on `%s::%s()` could not load table: %s. ' + . 'Set the `modelClass` config option to the correct table class.', + get_class($controller), + $controller->getRequest()->getParam('action'), + $e->getMessage(), + ), + ); + return; } if (!$model->behaviors()->has('Search')) { + deprecationWarning( + '5.0.0', + sprintf( + 'SearchComponent on `%s::%s()`: Table `%s` does not have the Search behavior loaded. ' + . 'Make sure to call `addBehavior(\'Search.Search\')` before the render phase.', + get_class($controller), + $controller->getRequest()->getParam('action'), + get_class($model), + ), + ); + return; } From 7042552f6e451c7561557d234d8c5c1e433ec9ca Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 28 Jan 2026 19:11:41 +0100 Subject: [PATCH 2/5] Add use function import for deprecationWarning --- src/Controller/Component/SearchComponent.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/Component/SearchComponent.php b/src/Controller/Component/SearchComponent.php index 1ca692b8..68cfdcaa 100644 --- a/src/Controller/Component/SearchComponent.php +++ b/src/Controller/Component/SearchComponent.php @@ -13,6 +13,7 @@ use Cake\Utility\Hash; use Closure; use UnexpectedValueException; +use function Cake\Core\deprecationWarning; /** * SearchComponent Component. From 19562bb28e31593b866d23deb977218167ce9118 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 29 Jan 2026 09:39:02 +0100 Subject: [PATCH 3/5] Refactor to strictMode config instead of deprecation warnings Replace deprecation warnings with a `strictMode` config option (default `false`) that throws RuntimeException when enabled. This is BC-safe: - strictMode=false (default): silently skips as before, allowing the component to be loaded in AppController where not all controllers have a searchable table - strictMode=true: throws meaningful exceptions when the model cannot be fetched or the Search behavior is not loaded, catching misconfiguration early In a future major version, strictMode can become the default. --- src/Controller/Component/SearchComponent.php | 30 +++--- .../Component/SearchComponentTest.php | 102 ++++++++++++++++++ 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/src/Controller/Component/SearchComponent.php b/src/Controller/Component/SearchComponent.php index 68cfdcaa..5bb24098 100644 --- a/src/Controller/Component/SearchComponent.php +++ b/src/Controller/Component/SearchComponent.php @@ -12,8 +12,8 @@ use Cake\Form\Form; use Cake\Utility\Hash; use Closure; +use RuntimeException; use UnexpectedValueException; -use function Cake\Core\deprecationWarning; /** * SearchComponent Component. @@ -45,6 +45,11 @@ class SearchComponent extends Component * - `autoloadHelper` : Whether to autoload the SearchHelper for search actions, default `true`. * Use `false` to disable automatic loading or set it to an array to configure the helper. * E.g. `'events' => ['Controller.beforeRender' => false]` + * - `strictMode` : When `true`, throws exceptions if the model cannot be fetched or the + * Search behavior is not loaded. When `false` (default), silently skips. + * This is useful when the component is loaded in AppController but not all controllers + * have a searchable table. Set to `true` in controller-specific configurations to catch + * misconfiguration early. * * @var array */ @@ -56,6 +61,7 @@ class SearchComponent extends Component 'modelClass' => null, 'formClass' => null, 'autoloadHelper' => true, + 'strictMode' => false, 'events' => [ 'Controller.startup' => 'startup', 'Controller.beforeRender' => 'beforeRender', @@ -162,37 +168,37 @@ public function beforeRender(): void } $controller = $this->getController(); + $strictMode = $this->getConfig('strictMode'); + try { /** * @var \Cake\ORM\Table $model */ $model = $controller->fetchTable($this->getConfig('modelClass')); } catch (UnexpectedValueException $e) { - deprecationWarning( - '5.0.0', - sprintf( + if ($strictMode) { + throw new RuntimeException(sprintf( 'SearchComponent on `%s::%s()` could not load table: %s. ' . 'Set the `modelClass` config option to the correct table class.', get_class($controller), $controller->getRequest()->getParam('action'), $e->getMessage(), - ), - ); + ), 0, $e); + } return; } if (!$model->behaviors()->has('Search')) { - deprecationWarning( - '5.0.0', - sprintf( + if ($strictMode) { + throw new RuntimeException(sprintf( 'SearchComponent on `%s::%s()`: Table `%s` does not have the Search behavior loaded. ' - . 'Make sure to call `addBehavior(\'Search.Search\')` before the render phase.', + . 'Make sure to call `addBehavior(\'Search.Search\')` in the table\'s `initialize()` method.', get_class($controller), $controller->getRequest()->getParam('action'), get_class($model), - ), - ); + )); + } return; } diff --git a/tests/TestCase/Controller/Component/SearchComponentTest.php b/tests/TestCase/Controller/Component/SearchComponentTest.php index 91382c70..9afc316e 100644 --- a/tests/TestCase/Controller/Component/SearchComponentTest.php +++ b/tests/TestCase/Controller/Component/SearchComponentTest.php @@ -11,6 +11,7 @@ use Cake\Routing\Router; use Cake\TestSuite\TestCase; use ReflectionProperty; +use RuntimeException; use Search\Controller\Component\SearchComponent; use Search\Test\TestApp\Form\SearchForm; use Search\Test\TestApp\Model\Table\ArticlesTable; @@ -549,4 +550,105 @@ public function testAutoloadHelperWithConfig() $this->assertSame('Search.Search', $helpers['Search']['className']); $this->assertSame(['foo'], $helpers['Search']['additionalBlacklist']); } + + /** + * Test that without strictMode, missing model silently skips. + * + * @return void + */ + public function testBeforeRenderWithoutModelSilentlySkips(): void + { + $controller = new Controller( + $this->Controller->getRequest()->withAttribute('params', [ + 'controller' => 'NonExistent', + 'action' => 'index', + ]), + ); + $reflection = new ReflectionProperty(Controller::class, 'defaultTable'); + $reflection->setValue($controller, null); + + $search = new SearchComponent($controller->components()); + $search->beforeRender(); + + $viewVars = $controller->viewBuilder()->getVars(); + $this->assertArrayNotHasKey('_isSearch', $viewVars); + } + + /** + * Test that without strictMode, missing Search behavior silently skips. + * + * @return void + */ + public function testBeforeRenderWithoutBehaviorSilentlySkips(): void + { + $this->Controller->setRequest( + $this->Controller->getRequest()->withAttribute('params', [ + 'controller' => 'Articles', + 'action' => 'index', + ]), + ); + + // Table exists but Search behavior is NOT loaded + $articles = $this->getTableLocator()->get('Articles', [ + 'className' => ArticlesTable::class, + ]); + $this->Controller->getTableLocator()->set('Articles', $articles); + + $this->Search->beforeRender(); + + $viewVars = $this->Controller->viewBuilder()->getVars(); + $this->assertArrayNotHasKey('_isSearch', $viewVars); + } + + /** + * Test that strictMode throws exception when model cannot be fetched. + * + * @return void + */ + public function testStrictModeThrowsOnMissingModel(): void + { + $controller = new Controller( + $this->Controller->getRequest()->withAttribute('params', [ + 'controller' => 'NonExistent', + 'action' => 'index', + ]), + ); + $reflection = new ReflectionProperty(Controller::class, 'defaultTable'); + $reflection->setValue($controller, null); + + $search = new SearchComponent($controller->components(), [ + 'strictMode' => true, + ]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('could not load table'); + $search->beforeRender(); + } + + /** + * Test that strictMode throws exception when Search behavior is not loaded. + * + * @return void + */ + public function testStrictModeThrowsOnMissingBehavior(): void + { + $this->Controller->setRequest( + $this->Controller->getRequest()->withAttribute('params', [ + 'controller' => 'Articles', + 'action' => 'index', + ]), + ); + + // Table exists but Search behavior is NOT loaded + $articles = $this->getTableLocator()->get('Articles', [ + 'className' => ArticlesTable::class, + ]); + $this->Controller->getTableLocator()->set('Articles', $articles); + + $this->Search->setConfig('strictMode', true); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('does not have the Search behavior loaded'); + $this->Search->beforeRender(); + } } From 6b9c9aa473812efa85d1eedccceb1a6c4d1f6ad4 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 29 Jan 2026 09:47:47 +0100 Subject: [PATCH 4/5] Fix tests: use plain Table for missing behavior tests ArticlesTable::initialize() auto-loads the Search behavior, so use a plain Table class instead when testing the missing behavior scenario. --- .../Controller/Component/SearchComponentTest.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/TestCase/Controller/Component/SearchComponentTest.php b/tests/TestCase/Controller/Component/SearchComponentTest.php index 9afc316e..42116af2 100644 --- a/tests/TestCase/Controller/Component/SearchComponentTest.php +++ b/tests/TestCase/Controller/Component/SearchComponentTest.php @@ -5,6 +5,7 @@ use Cake\Controller\Controller; use Cake\Event\Event; +use Cake\ORM\Table; use Cake\Http\Response; use Cake\Http\ServerRequest; use Cake\Routing\RouteBuilder; @@ -588,9 +589,9 @@ public function testBeforeRenderWithoutBehaviorSilentlySkips(): void ]), ); - // Table exists but Search behavior is NOT loaded + // Use a plain Table without the Search behavior $articles = $this->getTableLocator()->get('Articles', [ - 'className' => ArticlesTable::class, + 'className' => Table::class, ]); $this->Controller->getTableLocator()->set('Articles', $articles); @@ -639,9 +640,9 @@ public function testStrictModeThrowsOnMissingBehavior(): void ]), ); - // Table exists but Search behavior is NOT loaded + // Use a plain Table without the Search behavior $articles = $this->getTableLocator()->get('Articles', [ - 'className' => ArticlesTable::class, + 'className' => Table::class, ]); $this->Controller->getTableLocator()->set('Articles', $articles); From e758c87f6a3026f7b087dab4e324949b3c53d2f9 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 29 Jan 2026 09:50:03 +0100 Subject: [PATCH 5/5] Fix import order for CS --- tests/TestCase/Controller/Component/SearchComponentTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase/Controller/Component/SearchComponentTest.php b/tests/TestCase/Controller/Component/SearchComponentTest.php index 42116af2..9dc10d2a 100644 --- a/tests/TestCase/Controller/Component/SearchComponentTest.php +++ b/tests/TestCase/Controller/Component/SearchComponentTest.php @@ -5,9 +5,9 @@ use Cake\Controller\Controller; use Cake\Event\Event; -use Cake\ORM\Table; use Cake\Http\Response; use Cake\Http\ServerRequest; +use Cake\ORM\Table; use Cake\Routing\RouteBuilder; use Cake\Routing\Router; use Cake\TestSuite\TestCase;