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
24 changes: 24 additions & 0 deletions docs/additional-docs.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Additional documentation

## Using a Form class for your search form

While not mandatory, you can configure the `SearchComponent` to use a
[Form class](https://book.cakephp.org/5/en/core-libraries/form.html) for your search form.

```php
$this->loadComponent('Search.Search', [
'formClass' => 'MySearch', // Or use the FQCN: App\Form\MySearch::class
]);
```

The search component will auto set a `$searchForm` view variable containing the
form instance, for your search actions. You can use the form in your template as:

```php
echo $this->Form->create($searchForm, ['valueSources' => 'query', 'data']);
// Add your search fields here
```

Using a form class has the advantage of being able to define validation rules
and form field types. Validation will be done when the form is submitted
and in case of validation errors the component will not perform a redirect with
the query params, but instead the view will be rendered with validation errors.

## Persisting the Query String

Persisting the query string can be done with the `queryStringWhitelist` option.
Expand Down
2 changes: 0 additions & 2 deletions phpcs.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<?xml version="1.0"?>
<ruleset name="friendsofcake-search">
<config name="installed_paths" value="vendor/cakephp/cakephp-codesniffer/CakePHP"/>

<rule ref="CakePHP"/>

<exclude-pattern>*/tests/comparisons/*</exclude-pattern>
Expand Down
55 changes: 50 additions & 5 deletions src/Controller/Component/SearchComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

use Cake\Controller\Component;
use Cake\Controller\ComponentRegistry;
use Cake\Core\App;
use Cake\Core\Configure;
use Cake\Core\Exception\CakeException;
use Cake\Event\EventInterface;
use Cake\Form\Form;
use Cake\Utility\Hash;
use Closure;
use UnexpectedValueException;
Expand Down Expand Up @@ -35,6 +38,7 @@ class SearchComponent extends Component
* - `modelClass` : Configure the controller's modelClass to be used for the query, used to
* populate the _isSearch view variable to allow for a reset button, for example.
* Set to false to disable the auto-setting of the view variable.
* - `formClass` : The form class to use for the search form. Default `null`.
* - `events`: List of events this component listens to. You can disable an
* event by setting it to false.
* E.g. `'events' => ['Controller.beforeRender' => false]`
Expand All @@ -47,6 +51,7 @@ class SearchComponent extends Component
'queryStringBlacklist' => ['_csrfToken', '_Token'],
'emptyValues' => [],
'modelClass' => null,
'formClass' => null,
'events' => [
'Controller.startup' => 'startup',
'Controller.beforeRender' => 'beforeRender',
Expand Down Expand Up @@ -83,13 +88,30 @@ public function implementedEvents(): array
*/
public function startup(EventInterface $event): void
{
if (!$this->getController()->getRequest()->is('post') || !$this->_isSearchAction()) {
if (!$this->_isSearchAction()) {
return;
}

$url = $this->getController()->getRequest()->getPath();
$form = $this->getSearchForm();

if (!$this->getController()->getRequest()->is('post')) {
if ($form !== null) {
$this->getController()->set('searchForm', $form);
}

return;
}

$params = (array)$this->getController()->getRequest()->getData();
Comment thread
dereuromark marked this conversation as resolved.

if ($form !== null && !$form->validate($params)) {
$this->getController()->set('searchForm', $form);

return;
}

$params = $this->_filterParams();
$url = $this->getController()->getRequest()->getPath();
$params = $this->_filterParams($params);
if ($params) {
$params = Hash::expand($params);
$url .= '?' . http_build_query($params);
Expand All @@ -98,6 +120,28 @@ public function startup(EventInterface $event): void
$event->setResult($this->_registry->getController()->redirect($url));
}

/**
* Returns the search form instance if a `formClass` has been configured.
*
* @throws \Cake\Core\Exception\CakeException When the configured form does not exist.
* @return \Cake\Form\Form|null
*/
protected function getSearchForm(): ?Form
{
$fc = $this->getConfig('formClass');
if (!is_string($fc)) {
return null;
}

/** @var class-string<\Cake\Form\Form>|null $formClass */
$formClass = App::className($fc, 'Form', 'Form');
if ($formClass === null) {
throw new CakeException(sprintf('The form class `%s` could not be found.', $fc));
}

return new $formClass();
}

/**
* Populates the $_isSearch view variable based on the current request.
*
Expand Down Expand Up @@ -147,11 +191,12 @@ protected function _isSearchAction(): bool
/**
* Filters the params from POST data and merges in the whitelisted query string ones.
*
* @param array $params The params to filter.
* @return array
*/
protected function _filterParams(): array
protected function _filterParams(array $params): array

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dereuromark This is a breaking signature change but I think it's acceptable for a protected method in a new minor release.

{
$params = Hash::filter((array)$this->getController()->getRequest()->getData());
$params = Hash::filter($params);

foreach ((array)$this->getConfig('queryStringBlacklist') as $field) {
unset($params[$field]);
Expand Down
19 changes: 19 additions & 0 deletions tests/TestApp/Form/SearchForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);

namespace Search\Test\TestApp\Form;

use Cake\Form\Form;
use Cake\Validation\Validator;

class SearchForm extends Form
{
public function validationDefault(Validator $validator): Validator
{
$validator
->allowEmptyString('q')
->minLength('q', 3, 'Search query must be at least 3 characters long');

return $validator;
}
}
52 changes: 51 additions & 1 deletion tests/TestCase/Controller/Component/SearchComponentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Cake\TestSuite\TestCase;
use ReflectionProperty;
use Search\Controller\Component\SearchComponent;
use Search\Test\TestApp\Form\SearchForm;

class SearchComponentTest extends TestCase
{
Expand Down Expand Up @@ -43,7 +44,6 @@ public function setUp(): void

$this->Controller = new Controller($request);
$reflection = new ReflectionProperty(Controller::class, 'defaultTable');
$reflection->setAccessible(true);
$reflection->setValue($this->Controller, 'Articles');

$this->Search = new SearchComponent($this->Controller->components());
Expand Down Expand Up @@ -359,6 +359,56 @@ public function testIsSearchTrue()
$this->assertTrue($viewVars['_isSearch']);
}

public function testGetWithForm(): void
{
$request = $this->Controller->getRequest()
->withAttribute('params', [
'controller' => 'Posts',
'action' => 'index',
])
->withRequestTarget('/Posts')
->withEnv('REQUEST_METHOD', 'GET');

$this->Search->setConfig('formClass', SearchForm::class);

$this->Controller->setRequest($request);
$event = new Event('Controller.startup', $this->Controller);
$this->Search->startup($event);
$this->assertNull($event->getResult());

$this->assertInstanceOf(SearchForm::class, $this->Controller->viewBuilder()->getVar('searchForm'));
}

public function testPostWithFormValidation(): void
{
$request = $this->Controller->getRequest()
->withAttribute('params', [
'controller' => 'Posts',
'action' => 'index',
])
->withRequestTarget('/Posts')
->withData('q', 'ab') // too short, validation should fail
->withEnv('REQUEST_METHOD', 'POST');

$this->Search->setConfig('formClass', SearchForm::class);

$this->Controller->setRequest($request);
$event = new Event('Controller.startup', $this->Controller);
$this->Search->startup($event);
$this->assertNull($event->getResult());

/** @var \Cake\Form\Form $form */
$form = $this->Controller->viewBuilder()->getVar('searchForm');
$this->assertInstanceOf(SearchForm::class, $form);

$errors = [
'q' => [
'minLength' => 'Search query must be at least 3 characters long',
],
];
$this->assertEquals($errors, $form->getErrors());
}

/**
* @return void
*/
Expand Down