Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/Controller/Component/SearchComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Cake\Form\Form;
use Cake\Utility\Hash;
use Closure;
use RuntimeException;
use UnexpectedValueException;

/**
Expand Down Expand Up @@ -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<string, mixed>
*/
Expand All @@ -55,6 +61,7 @@ class SearchComponent extends Component
'modelClass' => null,
'formClass' => null,
'autoloadHelper' => true,
'strictMode' => false,
'events' => [
'Controller.startup' => 'startup',
'Controller.beforeRender' => 'beforeRender',
Expand Down Expand Up @@ -161,16 +168,38 @@ public function beforeRender(): void
}

$controller = $this->getController();
$strictMode = $this->getConfig('strictMode');

try {
/**
* @var \Cake\ORM\Table<array{Search: \Search\Model\Behavior\SearchBehavior}> $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;
}

Expand Down
103 changes: 103 additions & 0 deletions tests/TestCase/Controller/Component/SearchComponentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}