Skip to content

Commit

Permalink
Merge pull request #126 from ndm2/multi-value-support
Browse files Browse the repository at this point in the history
Add multi-value search support.
  • Loading branch information
ADmad authored Sep 22, 2016
2 parents 45896ee + 82e70df commit 9b33d87
Show file tree
Hide file tree
Showing 9 changed files with 843 additions and 70 deletions.
93 changes: 92 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ well. In your table classes `initialize()` method call the `searchManager()`
method, it will return a search manager instance. You can now add filters to the
manager by chaining them. The first arg of the `add()` method is the field, the
second the filter using the dot notation of cake to load filters from plugins.
The third one is an array of filter specific options.
The third one is an array of filter specific options. Please refer to
[the Options section](#options) for an explanation of the available options
supported by the different filters.

```php
use Search\Manager;
Expand Down Expand Up @@ -216,6 +218,95 @@ easily create the search results you need. Use:
``>``, ``<``, ``>=`` and ``<=``)
- ``Callback`` to produce results using your own custom callable function

### Options

#### All filters

The following options are supported by all filters.

- `field` (`string`, defaults to the name passed to the first argument of the
add filter method) The name of the field to use for searching. Use this option
if you need to use a name in your forms that doesn't match the actual field name.

- `name` (`string`, defaults to the name passed to the first argument of the add
filter method) The name of the field to look up in the request data. Use this
option if you need to configure the name of the filter differently than the name
of the field, in cases where you can't use the `field` option, for example when it
is being used to define multiple fields, which is supported by the `Like` filter.

- `alwaysRun` (`bool`, defaults to `false`) Defines whether the filter should always
run, irrespectively of whether the corresponding field exists in the request data.

- `filterEmpty` (`bool`, defaults to `false`) Defines whether the filter should not
run in case the corresponding field in the request is empty. Refer to
[the Optional fields section](#optional-fields) for additional details.

The following options are supported by all filters except `Callback` and `Finder`.

- `aliasField` (`bool`, defaults to `true`) Defines whether the field name should
be aliased with respect to the alias used by the table class to which the behavior
is attached to.

- `defaultValue` (`mixed`, defaults to `null`) The default value that is being
used in case the value passed for the corresponding field is invalid or missing.

#### `Compare`

- `operator` (`string`, defaults to `>=`) The operator to use for comparison. Valid
values are `>=`, `<=`, `>` and `<`.

#### `Like`

- `multiValue` (`bool`, defaults to `false`) Defines whether the filter accepts
multiple values. If disabled, and multiple values are being passed, the filter
will fall back to using the default value defined by the `defaultValue` option.

- `field` (`string|array`, defaults to ) The name of the field to use for
searching. Works like the base `field` option but also accepts multiple field
names as an array. When defining multiple fields, the search term is going to
be looked up in all the given fields, using the conditional operator defined by
the `fieldMode` option.

- `before` (`bool`, defaults to `false`) Whether to automatically add a wildcard
*before* the search term.

- `after` (`bool`, defaults to `false`) Whether to automatically add a wildcard
*after* the search term.

- `mode` (`string`, default to `or`) **This options is deprecated**, please use
`fieldMode` instead.

- `fieldMode` (`string`, defaults to `or`) The conditional mode to use when
matching against multiple fields.

- `valueMode` (`string`, defaults to `or`) The conditional mode to use when
searching for multiple values.

- `comparison` (`string`, defaults to `LIKE`) The comparison operator to use.

- `wildcardAny` (`string`, defaults to `*`) Defines the string that should be
treated as a _any_ wildcard in case it is being encountered in the search term.
The behavior will internally replace this with the appropriate `LIKE`
compatible wildcard. This is useful if you want to pass wildcards inside of the
search term, while still being able to use the actual wildcard character inside
of the search term so that it is being treated as a part of the term. For example
a search term of `* has reached 100%` would be converted to `% has reached 100\%`.

- `wildcardOne` (`string`, defaults to `?`) Defines the string that should be
treated as a _one_ wildcard in case it is being encountered in the search term.
Behaves similar to `wildcardAny`, that is, the actual `LIKE` compatible wildcard
(`_`) is being escaped in case used the search term.

#### `Value`

- `multiValue` (`bool`, defaults to `false`) Defines whether the filter accepts
multiple values. If disabled, and multiple values are being passed, the filter
will fall back to using the default value defined by the `defaultValue` option.

- `mode` (`string`, possible values are `or` and `and`, defaults to `or`) The
conditional mode to use when searching for multiple values.


## Optional fields

Sometimes you might want to search your data based on two of three inputs in
Expand Down
113 changes: 99 additions & 14 deletions src/Model/Behavior/SearchBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,23 +62,13 @@ public function findSearch(Query $query, array $options)
'to be nested under key "search" in find() options.'
);
}

$filters = $this->_getAllFilters(Hash::get($options, 'collection', 'default'));
$params = (array)$options['search'];
$params = Hash::flatten($params);
$params = array_intersect_key(Hash::filter($params), $filters);

$this->_isSearch = false;
foreach ($filters as $filter) {
$filter->args($params);
$filter->query($query);
$params = $this->_flattenParams((array)$options['search']);
$params = $this->_extractParams($params, $filters);

if (!$filter->skip()) {
$this->_isSearch = true;
}
$filter->process();
}

return $query;
return $this->_processFilters($filters, $params, $query);
}

/**
Expand Down Expand Up @@ -120,6 +110,77 @@ public function searchManager()
return $this->_manager;
}

/**
* Extracts all parameters for wich a filter with a matching field
* name exists.
*
* @param array $params The parameters array to extract from.
* @param \Search\Model\Filter\Base[] $filters The filters to match against.
* @return array The extracted parameters.
*/
protected function _extractParams($params, $filters)
{
return array_intersect_key(Hash::filter($params), $filters);
}

/**
* Flattens a parameters array, so that possible aliased parameter
* keys that are provided in a nested fashion, are being grouped
* using flat keys.
*
* ### Example:
*
* The following parameters array:
*
* ```
* [
* 'Alias' => [
* 'field' => 'value'
* 'otherField' => [
* 'value',
* 'otherValue'
* ]
* ],
* 'field' => 'value'
* ]
* ```
*
* would return as
*
* ```
* [
* 'Alias.field' => 'value',
* 'Alias.otherField' => [
* 'value',
* 'otherValue'
* ],
* 'field' => 'value'
* ]
* ```
*
* @param array $params The parameters array to flatten.
* @return array The flattened parameters array.
*/
protected function _flattenParams($params)
{
$flattened = [];
foreach ($params as $key => $value) {
if (is_array($value)) {
foreach ($value as $childKey => $childValue) {
if (!is_numeric($childKey)) {
$flattened[$key . '.' . $childKey] = $childValue;
} else {
$flattened[$key][$childKey] = $childValue;
}
}
} else {
$flattened[$key] = $value;
}
}

return $flattened;
}

/**
* Gets all filters by the default or given collection from the search manager
*
Expand All @@ -137,4 +198,28 @@ protected function _getAllFilters($collection = 'default')

return $manager->getFilters($collection);
}

/**
* Processes the given filters.
*
* @param \Search\Model\Filter\Base[] $filters The filters to process.
* @param array $params The parameters to pass to the filters.
* @param \Cake\ORM\Query $query The query to pass to the filters.
* @return \Cake\ORM\Query The query processed by the filters.
*/
protected function _processFilters($filters, $params, $query)
{
$this->_isSearch = false;
foreach ($filters as $filter) {
$filter->args($params);
$filter->query($query);

if (!$filter->skip()) {
$this->_isSearch = true;
}
$filter->process();
}

return $query;
}
}
15 changes: 13 additions & 2 deletions src/Model/Filter/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ public function __construct($name, Manager $manager, array $config = [])
'validate' => [],
'alwaysRun' => false,
'filterEmpty' => false,
'defaultValue' => null
'defaultValue' => null,
'multiValue' => false,
];

$this->config($config + $defaults);
Expand Down Expand Up @@ -169,7 +170,17 @@ public function skip()
*/
public function value()
{
return isset($this->_args[$this->name()]) ? $this->_args[$this->name()] : $this->_config['defaultValue'];
$value = $this->_config['defaultValue'];
if (isset($this->_args[$this->name()])) {
$passedValue = $this->_args[$this->name()];
if (!is_array($passedValue) ||
$this->config('multiValue')
) {
return $passedValue;
}
}

return $value;
}

/**
Expand Down
67 changes: 53 additions & 14 deletions src/Model/Filter/Like.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php
namespace Search\Model\Filter;

use Search\Manager;

class Like extends Base
{

Expand All @@ -12,12 +14,31 @@ class Like extends Base
protected $_defaultConfig = [
'before' => false,
'after' => false,
'mode' => 'or',
'mode' => null,
'fieldMode' => 'or',
'valueMode' => 'or',
'comparison' => 'LIKE',
'wildcardAny' => '*',
'wildcardOne' => '?',
];

/**
* {@inheritDoc}
*
* @param string $name Name.
* @param \Search\Manager $manager Manager.
* @param array $config Config.
*/
public function __construct($name, Manager $manager, array $config = [])
{
parent::__construct($name, $manager, $config);

$mode = $this->config('mode');
if ($mode !== null) {
$this->config('fieldMode', $mode);
}
}

/**
* Process a LIKE condition ($x LIKE $y).
*
Expand All @@ -29,31 +50,49 @@ public function process()
return;
}

$comparison = $this->config('comparison');
$valueMode = $this->config('valueMode');
$value = $this->value();
$isMultiValue = is_array($value);

$conditions = [];
foreach ($this->fields() as $field) {
$left = $field . ' ' . $this->config('comparison');
$right = $this->_wildcards($this->value());

$conditions[] = [$left => $right];
$left = $field . ' ' . $comparison;
if ($isMultiValue) {
$valueConditions = [];
foreach ($value as $val) {
$right = $this->_wildcards($val);
if ($right !== false) {
$valueConditions[] = [$left => $right];
}
}
if (!empty($valueConditions)) {
$conditions[] = [$valueMode => $valueConditions];
}
} else {
$right = $this->_wildcards($value);
if ($right !== false) {
$conditions[] = [$left => $right];
}
}
}

$this->query()->andWhere([$this->config('mode') => $conditions]);
if (!empty($conditions)) {
$this->query()->andWhere([$this->config('fieldMode') => $conditions]);
}
}

/**
* Wrap wild cards around the value.
*
* @param string $value Value.
* @return string
* @param string $value Value.
* @return string|false Either the wildcard decorated input value, or `false` when
* encountering a non-string value.
*/
protected function _wildcards($value)
{
if (is_array($value)) {
foreach ($value as $k => $v) {
$value[$k] = $this->_wildcards($v);
}

return $value;
if (!is_string($value)) {
return false;
}

$value = $this->_formatWildcards($value);
Expand Down
Loading

0 comments on commit 9b33d87

Please sign in to comment.