Skip to content
Closed
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
23 changes: 23 additions & 0 deletions docs/additional-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@ You can use `queryStringBlacklist` option of `SearchComponent` to set an array o
form fields that should not end up in the query when extracting params from POST
request and redirecting.

## Extra Parameters

The search manager, by default, only processes params for which filters are defined
and discards any other query string parameters passed to the search finder.
The `extraParams` config allows you to preserve additional query string
parameters that don't have corresponding search filters defined. This is useful
for implementing filters which are dependent on more than one field.

Configure `extraParams` in your Table's `initialize()` method when adding the behavior:

```php
$this->addBehavior('Search.Search', [
'extraParams' => ['extra_field'],
]);

$this->searchManager()
->callback('field', function ($query, $args, $filter) {
// $args will contain both 'field' and 'extra_field' keys when using the
// search finder like $model->find('search', search: $this->request->getQueryParams())
// for a URL like /somepath?field=foo&extra_field=bar
});
```

## Emptiness based on more than one field.
If you need to determine `emptyValues` dynamically or based on multiple fields
(e.g. price range min/max), you can use closures for it and pass this to the `SearchComponent` config:
Expand Down
113 changes: 0 additions & 113 deletions docs/filters-and-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,119 +78,6 @@ $searchManager->callback('category_id', [

----------

## Multi-field Search Callbacks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IMO those are still valid strategies still for specific use cases, where those are specific to only one filter.

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.

Using an array is a valid use case, but the empty callback is surely a hack I don't want to promote once we have a proper config based solution.


When using callback filters that need to access values from multiple form fields,
be aware that **the `$args` parameter only contains values for fields that have
configured filters**. This means if you have a callback that depends on multiple
fields, but only one field has the "real" filter logic, the other field's value
won't be available in `$args` unless you configure a filter for it.

There are two approaches to handle this:

### Approach 1: Nested Array Fields (Recommended)

Group related fields under a single parent key in your form. This way, you only need
one filter and all sub-values are automatically available.

**Form fields:**
```php
echo $this->Form->control('search.field_a');
echo $this->Form->control('search.field_b');
```

**Filter configuration:**
```php
$this->callback('search', [
'callback' => function (SelectQuery $query, array $args, $filter) {
$fieldA = $args['search']['field_a'] ?? null;
$fieldB = $args['search']['field_b'] ?? null;

if (!$fieldA || !$fieldB) {
return false;
}

// Your custom query logic using both field values
$query->where([
'Model.field_a' => $fieldA,
'Model.field_b' => $fieldB,
]);

return true;
},
]);
```

**Pros:**
- Only need one filter
- No dummy filters required
- Semantically groups related fields
- Cleaner configuration

**Cons:**
- More complex form field names (`search[field_a]` vs `field_a`)
- URL query string is nested (`?search[field_a]=x&search[field_b]=y`)
- Less flexible if fields aren't semantically related

### Approach 2: Dummy Filters for Separate Fields

Keep fields independent but create dummy filters to make values available.

**Form fields:**
```php
echo $this->Form->control('field_a');
echo $this->Form->control('field_b');
```

**Filter configuration:**
```php
// Dummy filter to ensure 'field_b' value is available in $args
$this->callback('field_b', [
'callback' => function (SelectQuery $query, array $args, $filter) {
// Return false to not modify the query, but make $args['field_b'] available
return false;
},
]);

// The actual search logic that uses both 'field_a' and 'field_b'
$this->callback('field_a', [
'callback' => function (SelectQuery $query, array $args, $filter) {
// Now $args['field_b'] is available thanks to the dummy filter above
$fieldA = $args['field_a'] ?? null;
$fieldB = $args['field_b'] ?? null;

if (!$fieldA || !$fieldB) {
return false;
}

// Your custom query logic using both field values
$query->where([
'Model.field_a' => $fieldA,
'Model.field_b' => $fieldB,
]);

return true;
},
]);
```

**Pros:**
- Simple, flat field names
- Clean URL structure (`?field_a=x&field_b=y`)
- Fields remain conceptually independent
- More flexibility for unrelated fields

**Cons:**
- Requires dummy filter workaround
- Less obvious that fields are related
- More verbose configuration

**Which approach to use?**
- Use **nested arrays** when fields are semantically related (e.g., date range with start/end, tag search with options)
- Use **separate fields with dummy filters** when fields are independent but happen to be used together in one callback

----------

## Options

### All filters
Expand Down
18 changes: 16 additions & 2 deletions src/Model/Behavior/SearchBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ class SearchBehavior extends Behavior
/**
* Default config for the behavior.
*
* You can overwrite default empty values using emptyValues key
* when initializing the behavior
* - `emptyValues`: Values that should be considered empty and filtered out
* from search parameters
* - `extraParams`: Additional parameters that should be preserved even if
* no filter is defined for them.
* - `collectionClass`: Custom filter collection class to use for search filters
*
* @var array<string, mixed>
*/
Expand All @@ -36,6 +39,7 @@ class SearchBehavior extends Behavior
'searchParams' => 'searchParams',
],
'emptyValues' => ['', false, null],
'extraParams' => [],
'collectionClass' => null,
];

Expand Down Expand Up @@ -90,4 +94,14 @@ protected function _emptyValues(): ?array
{
return $this->getConfig('emptyValues');
}

/**
* Return the extra params.
*
* @return array
*/
protected function _extraParams(): array
{
return $this->getConfig('extraParams', []);
}
}
11 changes: 11 additions & 0 deletions src/Model/SearchTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public function findSearch(
if ($emptyValues !== null) {
$this->processor()->setEmptyValues($emptyValues);
}
$this->processor()->setExtraParams($this->_extraParams());

$this->_isSearch = $this->processor()->process(
$filters,
Expand Down Expand Up @@ -141,4 +142,14 @@ protected function _emptyValues(): ?array
{
return null;
}

/**
* Return the extra params.
*
* @return array
*/
protected function _extraParams(): array
{
return [];
}
}
29 changes: 28 additions & 1 deletion src/Processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ class Processor
*/
protected array $_emptyValues = ['', false, null];

/**
* Extra query string params to be preserved even if no filter has been set
* for them.
*
* @var array
*/
protected array $_extraParams = [];

/**
* Processes the given filters.
*
Expand Down Expand Up @@ -67,6 +75,20 @@ public function setEmptyValues(array $emptyValues)
return $this;
}

/**
* Set extra query string params to be preserved even if no filter has been set
* for them.
*
* @param array $extraParams Extra params list.
* @return $this
*/
public function setExtraParams(array $extraParams)
{
$this->_extraParams = $extraParams;

return $this;
}

/**
* Get params from query string to be used for filtering.
*
Expand Down Expand Up @@ -157,6 +179,11 @@ protected function _extractParams(array $params, FilterCollectionInterface $filt
return !in_array($val, $emptyValues, true);
});

return array_intersect_key($nonEmptyParams, iterator_to_array($filters));
$params = array_intersect_key($nonEmptyParams, iterator_to_array($filters));
if ($this->_extraParams) {
$params += array_intersect_key($nonEmptyParams, array_flip($this->_extraParams));
}

return $params;
}
}
23 changes: 23 additions & 0 deletions tests/TestCase/Model/Behavior/SearchBehaviorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Search\Test\TestCase\Model\Behavior;

use Cake\ORM\Query\SelectQuery;
use Cake\TestSuite\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use Search\Manager;
Expand Down Expand Up @@ -315,4 +316,26 @@ public function testSearchManager()
$manager = $this->Articles->searchManager();
$this->assertInstanceOf('\Search\Manager', $manager);
}

public function testExtraParams(): void
{
$this->Articles->getBehavior('Search')->setConfig('extraParams', ['extra_field']);

$result = [];
$manager = $this->Articles->searchManager();
$manager->callback('field1', [
'callback' => function (SelectQuery $query, array $args) use (&$result) {
$result = $args;

return true;
},
]);

$this->Articles->find('search', search: ['field1' => 'foo', 'extra_field' => 'bar']);

$this->assertSame([
'field1' => 'foo',
'extra_field' => 'bar',
], $result);
}
}