diff --git a/src/Controller/Component/SearchComponent.php b/src/Controller/Component/SearchComponent.php index 717ff63b..5bb24098 100644 --- a/src/Controller/Component/SearchComponent.php +++ b/src/Controller/Component/SearchComponent.php @@ -12,6 +12,7 @@ use Cake\Form\Form; use Cake\Utility\Hash; use Closure; +use RuntimeException; use UnexpectedValueException; /** @@ -44,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 */ @@ -55,6 +61,7 @@ class SearchComponent extends Component 'modelClass' => null, 'formClass' => null, 'autoloadHelper' => true, + 'strictMode' => false, 'events' => [ 'Controller.startup' => 'startup', 'Controller.beforeRender' => 'beforeRender', @@ -161,16 +168,38 @@ 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) { + 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')) { + 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\')` 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..9dc10d2a 100644 --- a/tests/TestCase/Controller/Component/SearchComponentTest.php +++ b/tests/TestCase/Controller/Component/SearchComponentTest.php @@ -7,10 +7,12 @@ use Cake\Event\Event; use Cake\Http\Response; use Cake\Http\ServerRequest; +use Cake\ORM\Table; use Cake\Routing\RouteBuilder; 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 +551,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', + ]), + ); + + // Use a plain Table without the Search behavior + $articles = $this->getTableLocator()->get('Articles', [ + 'className' => Table::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', + ]), + ); + + // Use a plain Table without the Search behavior + $articles = $this->getTableLocator()->get('Articles', [ + 'className' => Table::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(); + } }