├── .semver ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Controller └── Component │ └── PrgComponent.php ├── Docs ├── Documentation │ ├── Configuration.md │ ├── Examples.md │ ├── Installation.md │ ├── Named-Parameters-vs-Query-Strings.md │ ├── Overview.md │ └── Post-Redirect-Get.md ├── Home.md └── Tutorials │ └── Quick-Start.md ├── LICENSE.txt ├── Locale ├── deu │ └── LC_MESSAGES │ │ └── search.po ├── fre │ └── LC_MESSAGES │ │ └── search.po ├── por │ └── LC_MESSAGES │ │ └── search.po ├── rus │ └── LC_MESSAGES │ │ └── search.po ├── search.pot └── spa │ └── LC_MESSAGES │ └── search.po ├── Model └── Behavior │ └── SearchableBehavior.php ├── README.md ├── Test ├── Case │ ├── AllSearchTest.php │ ├── Controller │ │ └── Component │ │ │ └── PrgComponentTest.php │ └── Model │ │ └── Behavior │ │ └── SearchableBehaviorTest.php └── Fixture │ ├── ArticleFixture.php │ ├── PostFixture.php │ ├── TagFixture.php │ └── TaggedFixture.php └── composer.json /.semver: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 2 3 | :minor: 5 4 | :patch: 1 5 | :special: '' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | env: 9 | global: 10 | - PLUGIN_NAME=Search 11 | - DB=mysql 12 | - REQUIRE="phpunit/phpunit:3.7.31" 13 | 14 | matrix: 15 | - DB=mysql CAKE_VERSION=master 16 | - DB=pgsql CAKE_VERSION=master 17 | 18 | matrix: 19 | include: 20 | - php: 5.3 21 | env: 22 | - CAKE_VERSION=master 23 | - COVERALLS=1 24 | - php: 5.4 25 | env: 26 | - CAKE_VERSION=master 27 | - COVERALLS=1 28 | - php: 5.5 29 | env: 30 | - CAKE_VERSION=master 31 | - COVERALLS=1 32 | 33 | before_script: 34 | - git clone https://github.com/burzum/travis.git --depth 1 ../travis 35 | - ../travis/before_script.sh 36 | - if [ "$PHPCS" != 1 ]; then 37 | echo " 38 | require_once APP . DS . 'vendor' . DS . 'phpunit' . DS . 'phpunit' . DS . 'PHPUnit' . DS . 'Autoload.php'; 39 | " >> ../cakephp/app/Config/bootstrap.php; 40 | fi 41 | 42 | script: 43 | - ../travis/script.sh 44 | 45 | after_success: 46 | - ../travis/after_success.sh 47 | 48 | notifications: 49 | email: false 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Release 2.5.1 5 | ------------- 6 | 7 | https://github.com/CakeDC/search/tree/2.5.1 8 | 9 | * [8aba388](https://github.com/cakedc/search/commit/8aba388) Update Overview.md 10 | 11 | Release 2.5.0 12 | ------------- 13 | 14 | https://github.com/CakeDC/search/tree/2.5.0 15 | 16 | * [2638972](https://github.com/cakedc/search/commit/2638972) Refs #157 Changing the plugins defaults from named parameters to query strings 17 | * [fdac4c3](https://github.com/cakedc/search/commit/fdac4c3) CS Fix 18 | * [8cdb841](https://github.com/cakedc/search/commit/8cdb841) Update Installation.md 19 | 20 | Release 2.4.1 21 | ------------- 22 | 23 | https://github.com/CakeDC/search/tree/2.4.1 24 | 25 | * [23f24a6](https://github.com/cakedc/search/commit/23f24a6) Updating travis 26 | * [9fd58af](https://github.com/cakedc/search/commit/9fd58af) Updating .travis 27 | * [1d4d78c](https://github.com/cakedc/search/commit/1d4d78c) Updating PostFixture.php 28 | * [66d45c3](https://github.com/cakedc/search/commit/66d45c3) Updating the ArticleFixture.php 29 | 30 | Release 2.4.0 31 | ------------- 32 | 33 | https://github.com/CakeDC/search/tree/2.4.0 34 | 35 | * [ad94745](https://github.com/cakedc/search/commit/ad94745) improve merging of strict key based options 36 | * [251d233](https://github.com/cakedc/search/commit/251d233) Changing $this->_defaults['model'] to $this->_defaults['presetForm']['model'] 37 | * [bd9a129](https://github.com/cakedc/search/commit/bd9a129) Add missing bracket 38 | * [e91e48a](https://github.com/cakedc/search/commit/e91e48a) Add support for ilike to SearchableBehavior 39 | * [b122dc7](https://github.com/cakedc/search/commit/b122dc7) Add case-insensitive support for Postgres 40 | * [6689123](https://github.com/cakedc/search/commit/6689123) Making the 2nd arg of the PrgComponent constructor optional as it should be 41 | * [27abbf1](https://github.com/cakedc/search/commit/27abbf1) Refs https://github.com/CakeDC/search/pull/144 fixing the undefined var $settings, changing it to $this->_defaults 42 | * [a3c4c3c](https://github.com/cakedc/search/commit/a3c4c3c) Avoid Undefined index error due to empty criteria of "type = lookup" 43 | 44 | Release 2.3.0 45 | ------------- 46 | 47 | https://github.com/CakeDC/search/tree/2.3.0 48 | 49 | * [4b7cfdb](https://github.com/CakeDC/search/commit/4b7cfdb) Made the prg component initialization configurable, see https://github.com/CakeDC/search/issues/138 50 | * [2fd5d97](https://github.com/CakeDC/search/commit/2fd5d97) Changing Set to Hash in an App::uses() call 51 | * [eabe675](https://github.com/CakeDC/search/commit/eabe675) Updating the deprecated Set to Hash 52 | * [afc8f5d](https://github.com/CakeDC/search/commit/afc8f5d) Fixes https://github.com/CakeDC/search/pull/133 53 | * [f72a97b](https://github.com/CakeDC/search/commit/f72a97b) Field value is actually null 54 | * [c4df770](https://github.com/CakeDC/search/commit/c4df770) Fix emptyValue for query strings, before it was only possible for named params. 55 | * [aa87da7](https://github.com/CakeDC/search/commit/aa87da7) Renaming the test file that triggers all tests 56 | * [6ae2212](https://github.com/CakeDC/search/commit/6ae2212) Fixing the filterArgs initialization 57 | * [57a4ddc](https://github.com/CakeDC/search/commit/57a4ddc) Fixes a missing word, see https://github.com/CakeDC/search/pull/115 58 | * [24f8520](https://github.com/CakeDC/search/commit/24f8520) Fixed one test and removed another that isn't necessary anymore since always allowing wildcards 59 | * [6fbf596](https://github.com/CakeDC/search/commit/6fbf596) Partly reconstructed testSubQueryEmptyCondition() 60 | * [eadc9a8](https://github.com/CakeDC/search/commit/eadc9a8) Replaced skipIf() with markTestSkipped() 61 | * [f26cbb0](https://github.com/CakeDC/search/commit/f26cbb0) General DocBlock, comment and code cleanup in PrgComponentTest 62 | * [a45388e](https://github.com/CakeDC/search/commit/a45388e) Removed unused variable in SearchableBehavior 63 | * [872499d](https://github.com/CakeDC/search/commit/872499d) General DocBlock, comment and code cleanup of test suite 64 | * [30d69cb](https://github.com/CakeDC/search/commit/30d69cb) General DocBlock, comment and code cleanup of fixtures 65 | * [07d812f](https://github.com/CakeDC/search/commit/07d812f) Fixed @license part order in all files 66 | * [90c9916](https://github.com/CakeDC/search/commit/90c9916) General DocBlock, comment and code cleanup of SearchableBehaviorTest 67 | * [363cdc7](https://github.com/CakeDC/search/commit/363cdc7) Removed Behaviors->detach, replaced Behaviors->attach by Behaviors->load() 68 | * [e3afc10](https://github.com/CakeDC/search/commit/e3afc10) Always allow custom wildcards -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | This repository follows the [CakeDC Plugin Standard](http://cakedc.com/plugin-standard). If you'd like to contribute new features, enhancements or bug fixes to the plugin, please read our [Contribution Guidelines](http://cakedc.com/contribution-guidelines) for detailed instructions. 5 | -------------------------------------------------------------------------------- /Controller/Component/PrgComponent.php: -------------------------------------------------------------------------------- 1 | 'results'); 29 | * array('search' => array('controller' => 'results'); 30 | * 31 | * @var array actions 32 | */ 33 | public $actions = array(); 34 | 35 | /** 36 | * Enables encoding on all presetVar fields 37 | * 38 | * @var boolean 39 | */ 40 | public $encode = false; 41 | 42 | /** 43 | * If the current request is an actual search (at least one search value present) 44 | * 45 | * @var boolean 46 | */ 47 | public $isSearch = false; 48 | 49 | /** 50 | * Parsed params of current request 51 | * 52 | * @var array 53 | */ 54 | protected $_parsedParams = array(); 55 | 56 | /** 57 | * Default options 58 | * 59 | * @var array 60 | */ 61 | protected $_defaults = array( 62 | 'callback' => 'initialize', 63 | 'commonProcess' => array( 64 | 'formName' => null, 65 | 'keepPassed' => true, 66 | 'action' => null, 67 | 'modelMethod' => 'validateSearch', 68 | 'allowedParams' => array(), 69 | 'paramType' => 'querystring', 70 | 'filterEmpty' => false 71 | ), 72 | 'presetForm' => array( 73 | 'model' => null, 74 | 'paramType' => 'querystring' 75 | ) 76 | ); 77 | 78 | /** 79 | * Constructor 80 | * 81 | * @param ComponentCollection $collection 82 | * @param array $settings 83 | */ 84 | public function __construct(ComponentCollection $collection, $settings = array()) { 85 | $this->_defaults = Hash::merge($this->_defaults, array( 86 | 'commonProcess' => (array)Configure::read('Search.Prg.commonProcess'), 87 | 'presetForm' => (array)Configure::read('Search.Prg.presetForm'), 88 | ), $settings); 89 | } 90 | 91 | /** 92 | * Called after the Controller::beforeFilter() and before the controller action 93 | * 94 | * @param Controller $controller Controller with components to startup 95 | * @return void 96 | * @link http://book.cakephp.org/2.0/en/controllers/components.html#Component::startup 97 | */ 98 | public function startup(Controller $controller) { 99 | if ($this->_defaults['callback'] === 'startup') { 100 | $this->init($controller); 101 | } 102 | } 103 | 104 | /** 105 | * Called before the Controller::beforeFilter(). 106 | * 107 | * @param Controller $controller Controller with components to initialize 108 | * @return void 109 | */ 110 | public function initialize(Controller $controller) { 111 | if ($this->_defaults['callback'] === 'initialize') { 112 | $this->init($controller); 113 | } 114 | } 115 | 116 | /** 117 | * Initializes the component based on the controller 118 | * 119 | * @param controller $controller 120 | * @return void 121 | */ 122 | public function init(Controller $controller) { 123 | $this->controller = $controller; 124 | 125 | // fix for not throwing warnings 126 | if (!isset($this->controller->presetVars)) { 127 | $this->controller->presetVars = true; 128 | } 129 | 130 | $model = $this->controller->modelClass; 131 | if (!empty($this->_defaults['presetForm']['model'])) { 132 | $model = $this->_defaults['presetForm']['model']; 133 | } 134 | 135 | if ($this->controller->presetVars === true) { 136 | // auto-set the presetVars based on search definitions in model 137 | $this->controller->presetVars = array(); 138 | $filterArgs = array(); 139 | if (!empty($this->controller->$model->filterArgs)) { 140 | $filterArgs = $this->controller->$model->filterArgs; 141 | } 142 | 143 | foreach ($filterArgs as $key => $arg) { 144 | if ($args = $this->_parseFromModel($arg, $key)) { 145 | $this->controller->presetVars[] = $args; 146 | } 147 | } 148 | } 149 | foreach ($this->controller->presetVars as $key => $field) { 150 | if ($field === true) { 151 | if (isset($this->controller->$model->filterArgs[$key])) { 152 | $field = $this->_parseFromModel($this->controller->$model->filterArgs[$key], $key); 153 | } else { 154 | $field = array('type' => 'value'); 155 | } 156 | } 157 | if (!isset($field['field'])) { 158 | $field['field'] = $key; 159 | } 160 | $this->controller->presetVars[$key] = $field; 161 | } 162 | } 163 | 164 | /** 165 | * Populates controller->request->data with allowed values from the named/passed get params 166 | * 167 | * Fields in $controller::$presetVars that have a type of 'lookup' the foreignKey value will be inserted 168 | * 169 | * 1) 'lookup' 170 | * Is used for autocomplete selectors 171 | * For auto-complete we have hidden field with value and autocomplete text box 172 | * Component fills text part on id from hidden field 173 | * 2) 'value' 174 | * The value as it is entered in form 175 | * 3) 'checkbox' 176 | * Allows to pass several values internally encoded as string 177 | * 178 | * 1 uses field, model, formField, and modelField 179 | * 2, 3 need only field parameter 180 | * 181 | * @param array $options 182 | * @return void 183 | */ 184 | public function presetForm($options) { 185 | if (!is_array($options)) { 186 | $options = array('model' => $options); 187 | } 188 | extract(Hash::merge($this->_defaults['presetForm'], $options)); 189 | 190 | if ($paramType === 'named') { 191 | $args = $this->controller->passedArgs; 192 | } else { 193 | $args = $this->controller->request->query; 194 | } 195 | 196 | $parsedParams = array(); 197 | $data = array($model => array()); 198 | foreach ($this->controller->presetVars as $field) { 199 | if (!isset($args[$field['field']])) { 200 | continue; 201 | } 202 | 203 | if ($paramType === 'named' && ($this->encode || !empty($field['encode']))) { 204 | // Its important to set it also back to the controllers passed args! 205 | $fieldContent = str_replace(array('-', '_'), array('/', '='), $args[$field['field']]); 206 | $args[$field['field']] = base64_decode($fieldContent); 207 | } 208 | 209 | switch ($field['type']) { 210 | case 'lookup': 211 | if (!empty($args[$field['field']])) { 212 | $searchModel = $field['model']; 213 | $this->controller->loadModel($searchModel); 214 | $this->controller->{$searchModel}->recursive = -1; 215 | $result = $this->controller->{$searchModel}->findById($args[$field['field']]); 216 | $parsedParams[$field['field']] = $args[$field['field']]; 217 | $parsedParams[$field['formField']] = $result[$searchModel][$field['modelField']]; 218 | $data[$model][$field['field']] = $args[$field['field']]; 219 | $data[$model][$field['formField']] = $result[$searchModel][$field['modelField']]; 220 | } 221 | break; 222 | 223 | case 'checkbox': 224 | $values = explode('|', $args[$field['field']]); 225 | $parsedParams[$field['field']] = $values; 226 | $data[$model][$field['field']] = $values; 227 | break; 228 | case 'value': 229 | $parsedParams[$field['field']] = $args[$field['field']]; 230 | $data[$model][$field['field']] = $args[$field['field']]; 231 | break; 232 | } 233 | 234 | if (isset($data[$model][$field['field']]) && $data[$model][$field['field']] !== '') { 235 | $this->isSearch = true; 236 | } 237 | 238 | if (isset($data[$model][$field['field']]) && $data[$model][$field['field']] === '' && isset($field['emptyValue'])) { 239 | $data[$model][$field['field']] = $field['emptyValue']; 240 | } 241 | } 242 | 243 | $this->controller->request->data = $data; 244 | $this->_parsedParams = $parsedParams; 245 | // deprecated, don't use controller's parsedData or passedArgs anymore. 246 | $this->controller->parsedData = $this->_parsedParams; 247 | foreach ($this->controller->parsedData as $key => $value) { 248 | $this->controller->passedArgs[$key] = $value; 249 | } 250 | $this->controller->set('isSearch', $this->isSearch); 251 | } 252 | 253 | /** 254 | * Return the parsed params of the current search request 255 | * 256 | * @return array Params 257 | */ 258 | public function parsedParams() { 259 | return $this->_parsedParams; 260 | } 261 | 262 | /** 263 | * Restores form params for checkboxes and other url encoded params 264 | * 265 | * @param array 266 | * @return array 267 | */ 268 | public function serializeParams(array &$data) { 269 | foreach ($this->controller->presetVars as $field) { 270 | if ($field['type'] === 'checkbox') { 271 | if (array_key_exists($field['field'], $data)) { 272 | $values = join('|', (array)$data[$field['field']]); 273 | } else { 274 | $values = ''; 275 | } 276 | $data[$field['field']] = $values; 277 | } 278 | 279 | if ($this->_defaults['commonProcess']['paramType'] === 'named' && ($this->encode || !empty($field['encode']))) { 280 | $fieldContent = $data[$field['field']]; 281 | $tmp = base64_encode($fieldContent); 282 | // replace chars base64 uses that would mess up the url 283 | $tmp = str_replace(array('/', '='), array('-', '_'), $tmp); 284 | $data[$field['field']] = $tmp; 285 | } 286 | if (!empty($field['empty']) && isset($data[$field['field']]) && $data[$field['field']] === '') { 287 | unset($data[$field['field']]); 288 | } 289 | } 290 | return $data; 291 | } 292 | 293 | /** 294 | * Connect named arguments 295 | * 296 | * @param array $data 297 | * @param array $exclude 298 | * @return void 299 | */ 300 | public function connectNamed($data = null, array $exclude = array()) { 301 | if (!isset($data)) { 302 | $data = $this->controller->passedArgs; 303 | } 304 | 305 | if (!is_array($data)) { 306 | return; 307 | } 308 | 309 | foreach ($data as $key => $value) { 310 | if (!is_numeric($key) && !in_array($key, $exclude)) { 311 | Router::connectNamed(array($key)); 312 | } 313 | } 314 | } 315 | 316 | /** 317 | * Exclude 318 | * 319 | * Removes key/values from $array based on $exclude 320 | * 321 | * @param array Array of data to be filtered 322 | * @param array Array of keys to exclude from other $array 323 | * @return array 324 | */ 325 | public function exclude(array $array, array $exclude) { 326 | $data = array(); 327 | foreach ($array as $key => $value) { 328 | if (is_numeric($key) || !in_array($key, $exclude)) { 329 | $data[$key] = $value; 330 | } 331 | } 332 | return $data; 333 | } 334 | 335 | /** 336 | * Common search method 337 | * 338 | * Handles processes common to all PRG forms 339 | * 340 | * - Handles validation of post data 341 | * - converting post data into named params 342 | * - Issuing redirect(), and connecting named parameters before redirect 343 | * - Setting named parameter form data to view 344 | * 345 | * @param string $modelName - Name of the model class being used for the prg form 346 | * @param array $options Optional parameters: 347 | * - string formName - name of the form involved in the prg 348 | * - string action - The action to redirect to. Defaults to the current action 349 | * - mixed modelMethod - If not false a string that is the model method that will be used to process the data 350 | * - array allowedParams - An array of additional top level route params that should be included in the params processed 351 | * - array excludedParams - An array of named/query params that should be excluded from the redirect url 352 | * - string paramType - 'named' if you want to used named params or 'querystring' is you want to use query string 353 | * @return void 354 | */ 355 | public function commonProcess($modelName = null, array $options = array()) { 356 | $defaults = array( 357 | 'excludedParams' => array('page'), 358 | ); 359 | $defaults = Hash::merge($defaults, $this->_defaults['commonProcess']); 360 | extract(Hash::merge($defaults, $options)); 361 | 362 | $paramType = strtolower($paramType); 363 | 364 | if (empty($modelName)) { 365 | $modelName = $this->controller->modelClass; 366 | } 367 | 368 | if (empty($formName)) { 369 | $formName = $modelName; 370 | } 371 | 372 | if (empty($action)) { 373 | $action = $this->controller->action; 374 | } 375 | 376 | if (!empty($this->controller->request->data)) { 377 | $this->controller->{$modelName}->set($this->controller->request->data); 378 | $valid = true; 379 | if ($modelMethod !== false) { 380 | $valid = $this->controller->{$modelName}->{$modelMethod}(); 381 | } 382 | 383 | if ($valid) { 384 | $params = $this->controller->request->params['named']; 385 | if ($keepPassed) { 386 | $params = array_merge($this->controller->request->params['pass'], $params); 387 | } 388 | 389 | $searchParams = $this->controller->request->data[$modelName]; 390 | $this->serializeParams($searchParams); 391 | 392 | if ($paramType === 'named') { 393 | $params = array_merge($params, $searchParams); 394 | $params = $this->exclude($params, $excludedParams); 395 | if ($filterEmpty) { 396 | $params = Hash::filter($params); 397 | } 398 | 399 | $params = $this->_filter($params); 400 | 401 | $this->connectNamed($params, array()); 402 | 403 | } else { 404 | $searchParams = array_merge($this->controller->request->query, $searchParams); 405 | $searchParams = $this->exclude($searchParams, $excludedParams); 406 | if ($filterEmpty) { 407 | $searchParams = Hash::filter($searchParams); 408 | } 409 | 410 | $searchParams = $this->_filter($searchParams); 411 | 412 | $this->connectNamed($searchParams, array()); 413 | $params['?'] = $searchParams; 414 | } 415 | 416 | $params['action'] = $action; 417 | 418 | foreach ($allowedParams as $key) { 419 | if (isset($this->controller->request->params[$key])) { 420 | $params[$key] = $this->controller->request->params[$key]; 421 | } 422 | } 423 | 424 | $this->controller->redirect($params); 425 | } else { 426 | $this->controller->Session->setFlash(__d('search', 'Please correct the errors below.')); 427 | } 428 | } elseif (($paramType === 'named' && !empty($this->controller->passedArgs)) || 429 | ($paramType === 'querystring' && !empty($this->controller->request->query)) 430 | ) { 431 | $this->connectNamed($this->controller->passedArgs, array()); 432 | $this->presetForm(array('model' => $formName, 'paramType' => $paramType)); 433 | } 434 | } 435 | 436 | /** 437 | * Filter params based on emptyValue. 438 | * 439 | * @param array $params Params 440 | * @return array Params 441 | */ 442 | protected function _filter(array $params) { 443 | foreach ($this->controller->presetVars as $key => $presetVar) { 444 | $field = $key; 445 | if (!empty($presetVar['field'])) { 446 | $field = $presetVar['field']; 447 | } 448 | if (!isset($params[$field])) { 449 | continue; 450 | } 451 | if (!isset($presetVar['emptyValue']) || $presetVar['emptyValue'] !== $params[$field]) { 452 | continue; 453 | } 454 | $params[$field] = null; 455 | } 456 | return $params; 457 | } 458 | 459 | /** 460 | * Parse the configs from the Model (to keep things dry) 461 | * 462 | * @param array $arg 463 | * @param mixed $key 464 | * @return array 465 | */ 466 | protected function _parseFromModel(array $arg, $key = null) { 467 | if (isset($arg['preset']) && !$arg['preset']) { 468 | return array(); 469 | } 470 | if (isset($arg['presetType'])) { 471 | $arg['type'] = $arg['presetType']; 472 | unset($arg['presetType']); 473 | } elseif (!isset($arg['type']) || in_array($arg['type'], array('expression', 'query', 'subquery', 'like', 'type', 'ilike'))) { 474 | $arg['type'] = 'value'; 475 | } 476 | 477 | if (isset($arg['name']) || is_numeric($key)) { 478 | $field = $arg['name']; 479 | } else { 480 | $field = $key; 481 | } 482 | $res = array('field' => $field, 'type' => $arg['type']); 483 | if (!empty($arg['encode'])) { 484 | $res['encode'] = $arg['encode']; 485 | } 486 | $res = array_merge($arg, $res); 487 | return $res; 488 | } 489 | 490 | } 491 | -------------------------------------------------------------------------------- /Docs/Documentation/Configuration.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | Behavior and Model Configuration 5 | -------------------------------- 6 | 7 | All search fields must be configured in the ```Model::$filterArgs``` property as an array. 8 | 9 | Each filter record should contain an array with several keys: 10 | 11 | * **name:** The parameter stored in ```Model::$data```. The "name" used to search in a field (can be ommited if the key is the name). 12 | * **type:** One of supported search types described below. 13 | * **field:** Real field name used for search should be used. 14 | * **method:** Model method name or behavior used to generate expression, subquery or query. 15 | * **allowEmpty:** Optional parameter used to allow generating conditions even if the filter field value is empty. It is often used when you specifically allow a lookup for an empty string or if conditions need to be generated based on several other fields. 16 | 17 | Supported Types of Search 18 | ------------------------- 19 | 20 | * **like:** Type of search used when you need to search using the "LIKE" SQL keyword. 21 | * **value:** Useful when you need to perform exact comparisons. For example, when using a select box as your filter. 22 | * **expression:** Useful if you want to add condition that will generate by some method, and condition field contain several parameter like in previous sample used for 'range'. Field here contains 'Article.views BETWEEN ? AND ?' and Article::makeRangeCondition returns array of two values. 23 | * **subquery:** Useful if you want to add condition that looks like "FIELD IN (SUBQUERY)", where "SUBQUERY" is generated by the method declared in this filter configuration. 24 | * **query:** Most universal type of search. In this case method should return array (that contains condition of any complexity). Returned condition will be merged with the search conditions. 25 | 26 | Component Configuration 27 | ----------------------- 28 | 29 | The Prg component can be configured to start in the startup or initialize callback of the component. 30 | 31 | * **callback:** Must be ```startup``` or ```initialize```, by default it is ```initialize```. Choose ```startup``` if you need to initialize model related settings in another component in the initialize callback. 32 | 33 | Controller Configuration 34 | ------------------------ 35 | 36 | All search fields parameters need to be configured in the ```Controller::$presetVars``` array - if you didn't yet in the model. 37 | 38 | Each preset variable is an array that that contains some of the following keys: 39 | 40 | * **field:** Field that defined in the view search form. 41 | * **type:** One of the search types described above 42 | * **value:** Should be used for values that don't require any processing, 43 | * **checkbox:** Used for checkbox fields in the view (Prg component packs and unpacks checkbox values when it is passed through the get named action). 44 | * **lookup:** This type should be used when you have for example an auto-complete lookup field implemented in your view. This lookup field is a text field, and also you'll have a hidden field for the id value. In this case the component will fill both, text and id values. 45 | * **model:** Parameter that specifies what model is used in ```Request::$data``` for this field. 46 | * **formField:** Field in the form that contains text and will be populated using Model.modelField based on field value. 47 | * **modelField:** Field in the model that contains text and will be used to fill the formField in the view. 48 | * **encode:** Boolean, by default false. If you want to use search strings in URL's with special characters like % or / you need to use encoding. 49 | * **empty:** Boolean, by default false. If you want to omit this field in the PRG url if no value has been given (shorter urls). 50 | 51 | **Note:** Those can also be configured in the model itself (to keep it DRY). You can then set ```public $presetVar = true;``` then in the controller to use the model ones (see the example above). You can still use define the keys here where you want to overwrite certain settings. When using named params instead of query strings it is recommended to always use ```encode => true``` in combination with search strings (custom text input) to avoid url-breaking. 52 | -------------------------------------------------------------------------------- /Docs/Documentation/Examples.md: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | An example of how to implement complex search conditions in your application. 5 | 6 | Here is the model code. 7 | 8 | ```php 9 | class Article extends AppModel { 10 | 11 | public $actsAs = array( 12 | 'Search.Searchable' 13 | ); 14 | 15 | public $belongsTo = array( 16 | 'User' 17 | ); 18 | 19 | public $hasAndBelongsToMany = array( 20 | 'Tag' => array( 21 | 'with' => 'Tagged' 22 | ) 23 | ); 24 | 25 | public $filterArgs = array( 26 | 'title' => array( 27 | 'type' => 'like' 28 | ), 29 | 'status' => array( 30 | 'type' => 'value' 31 | ), 32 | 'blog_id' => array( 33 | 'type' => 'lookup', 34 | 'formField' => 'blog_input', 35 | 'modelField' => 'title', 36 | 'model' => 'Blog' 37 | ), 38 | 'search' => array( 39 | 'type' => 'like', 40 | 'field' => 'Article.description' 41 | ), 42 | 'range' => array( 43 | 'type' => 'expression', 44 | 'method' => 'makeRangeCondition', 45 | 'field' => 'Article.views BETWEEN ? AND ?' 46 | ), 47 | 'username' => array( 48 | 'type' => 'like', 'field' => array( 49 | 'User.username', 50 | 'UserInfo.first_name' 51 | ) 52 | ), 53 | 'tags' => array( 54 | 'type' => 'subquery', 55 | 'method' => 'findByTags', 56 | 'field' => 'Article.id' 57 | ), 58 | 'filter' => array( 59 | 'type' => 'query', 60 | 'method' => 'orConditions' 61 | ), 62 | 'year' => array( 63 | 'type' => 'query', 64 | 'method' => 'yearRange' 65 | ), 66 | 'enhanced_search' => array( 67 | 'type' => 'like', 68 | 'encode' => true, 69 | 'before' => false, 70 | 'after' => false, 71 | 'field' => array( 72 | 'ThisModel.name', 'OtherModel.name' 73 | ) 74 | ), 75 | ); 76 | 77 | public function findByTags($data = array()) { 78 | $this->Tagged->Behaviors->attach('Containable', array( 79 | 'autoFields' => false 80 | ) 81 | ); 82 | 83 | $this->Tagged->Behaviors->attach('Search.Searchable'); 84 | $query = $this->Tagged->getQuery('all', array( 85 | 'conditions' => array( 86 | 'Tag.name' => $data['tags'] 87 | ), 88 | 'fields' => array( 89 | 'foreign_key' 90 | ), 91 | 'contain' => array( 92 | 'Tag' 93 | ) 94 | )); 95 | return $query; 96 | } 97 | 98 | // Or conditions with like 99 | public function orConditions($data = array()) { 100 | $filter = $data['filter']; 101 | $condition = array( 102 | 'OR' => array( 103 | $this->alias . '.title LIKE' => '%' . $filter . '%', 104 | $this->alias . '.body LIKE' => '%' . $filter . '%', 105 | ) 106 | ); 107 | return $condition; 108 | } 109 | 110 | // Turns 2000 - 2014 into a search between these two years 111 | public function yearRange($data = array()) { 112 | if (strpos($data['year'], ' - ') !== false){ 113 | $tmp = explode(' - ', $data['year']); 114 | $tmp[0] = $tmp[0] . '-01-01'; 115 | $tmp[1] = $tmp[1] . '-12-31'; 116 | return $tmp; 117 | } else { 118 | return array($data['year'] . '-01-01', $data['year']."-12-31"); 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | Associated snippet for the controller class. 125 | 126 | ```php 127 | class ArticlesController extends AppController { 128 | 129 | public $components = array( 130 | 'Search.Prg' 131 | ); 132 | 133 | public function find() { 134 | $this->Prg->commonProcess(); 135 | $this->Paginator->settings['conditions'] = $this->Article->parseCriteria($this->Prg->parsedParams()); 136 | $this->set('articles', $this->Paginator->paginate()); 137 | } 138 | } 139 | ``` 140 | 141 | or verbose (overriding the model configuration) 142 | 143 | ```php 144 | class ArticlesController extends AppController { 145 | 146 | public $components = array( 147 | 'Search.Prg' 148 | ); 149 | 150 | // This will override the model config 151 | public $presetVars = array( 152 | 'title' => array( 153 | 'type' => 'value' 154 | ), 155 | 'status' => array( 156 | 'type' => 'checkbox' 157 | ), 158 | 'blog_id' => array( 159 | 'type' => 'lookup', 160 | 'formField' => 'blog_input', 161 | 'modelField' => 'title', 162 | 'model' => 'Blog' 163 | ) 164 | ); 165 | 166 | public function find() { 167 | $this->Prg->commonProcess(); 168 | $this->Paginator->settings['conditions'] = $this->Article->parseCriteria($this->Prg->parsedParams()); 169 | $this->set('articles', $this->Paginator->paginate()); 170 | } 171 | } 172 | ``` 173 | 174 | The ```find.ctp``` view is the same as ```index.ctp``` with the addition of the search form. 175 | 176 | ```php 177 | echo $this->Form->create('Article', array( 178 | 'url' => array_merge( 179 | array( 180 | 'action' => 'find' 181 | ), 182 | $this->params['pass'] 183 | ) 184 | ) 185 | ); 186 | echo $this->Form->input('title', array( 187 | 'div' => false 188 | ) 189 | ); 190 | echo $this->Form->input('year', array( 191 | 'div' => false 192 | ) 193 | ); 194 | echo $this->Form->input('blog_id', array( 195 | 'div' => false, 196 | 'options' => $blogs 197 | ) 198 | ); 199 | echo $this->Form->input('status', array( 200 | 'div' => false, 201 | 'multiple' => 'checkbox', 202 | 'options' => array( 203 | 'open', 'closed' 204 | ) 205 | ) 206 | ); 207 | echo $this->Form->input('username', array( 208 | 'div' => false 209 | ) 210 | ); 211 | echo $this->Form->submit(__('Search'), array( 212 | 'div' => false 213 | ) 214 | ); 215 | echo $this->Form->end(); 216 | ``` 217 | 218 | In this example the search by OR condition is shown. For this purpose we defined the method ```orConditions()``` and added the filter method. 219 | 220 | ```php 221 | array( 222 | 'name' => 'filter', 223 | 'type' => 'query', 224 | 'method' => 'orConditions' 225 | ) 226 | ``` 227 | 228 | Advanced Usage 229 | -------------- 230 | 231 | ```php 232 | public $filterArgs = array( 233 | 234 | // match results with `%searchstring`: 235 | 'search_exact_beginning' => array( 236 | 'type' => 'like', 237 | 'encode' => true, 238 | 'before' => true, 239 | 'after' => false 240 | ), 241 | 242 | // match results with `searchstring%`: 243 | 'search_exact_end' => array( 244 | 'type' => 'like', 245 | 'encode' => true, 246 | 'before' => false, 247 | 'after' => true 248 | ), 249 | 250 | // match results with `__searchstring%`: 251 | 'search_special_like' => array( 252 | 'type' => 'like', 253 | 'encode' => true, 254 | 'before' => '__', 255 | 'after' => '%' 256 | ), 257 | 258 | // use custom wildcards in the frontend (instead of * and ?): 259 | 'search_custom_like' => array( 260 | 'type' => 'like', 261 | 'encode' => true, 262 | 'before' => false, 263 | 'after' => false, 264 | 'wildcardAny' => '%', 'wildcardOne' => '_' 265 | ), 266 | 267 | // use and/or connectors ('First + Second, Third'): 268 | 'search_with_connectors' => array( 269 | 'type' => 'like', 270 | 'field' => 'Article.title', 271 | 'connectorAnd' => '+', 'connectorOr' => ',' 272 | ) 273 | ); 274 | ``` 275 | 276 | Default Values to Allow Search for "Not Any of The Below" 277 | --------------------------------------------------------- 278 | 279 | Let's say we have categories and a dropdown list to select any of those or "empty = ignore this filter". But what if we also want to have an option to find all non-categorized items? With "default 0 NOT NULL" fields this works as we can use 0 here explicitly. 280 | 281 | ```php 282 | $categories = $this->Model->Category->find('list'); 283 | 284 | // before passing it on to the view (the key will be 0, not '' as the ignore-filter key will be) 285 | array_unshift($categories, '- not categorized -'); 286 | ``` 287 | 288 | But for char(36) foreign keys or "default NULL" fields this doesn't work. The posted empty string will result in the omitting of the rule. That's where ```emptyValue``` comes into play. 289 | 290 | ```php 291 | // controller 292 | public $presetVars = array( 293 | 'category_id' => array( 294 | 'allowEmpty' => true, 295 | 'emptyValue' => '0', 296 | ); 297 | ); 298 | ``` 299 | 300 | This way we assign '' for 0, and "ignore" for '' on POST, and the opposite for ```presetForm()```. 301 | 302 | Note: This only works if you use ```allowEmpty``` here. If you fail to do that it will always trigger the lookup here. 303 | 304 | Default Values to Allow Search in Default Case 305 | ---------------------------------------------- 306 | 307 | The filterArgs property in your model. 308 | 309 | ```php 310 | public $filterArgs = array( 311 | 'some_related_table_id' => array( 312 | 'type' => 'value', 313 | 'defaultValue' => 'none' 314 | ) 315 | ); 316 | ``` 317 | 318 | This will always trigger the filter for it (looking for string ```none``` in the table field). 319 | 320 | Full Example for Model/Controller Configuration with Overriding 321 | --------------------------------------------------------------- 322 | 323 | This goes in a model. 324 | 325 | ```php 326 | public $filterArgs = array( 327 | 'some_related_table_id' => array( 328 | 'type' => 'value' 329 | ), 330 | 'search'=> array( 331 | 'type' => 'like', 332 | 'encode' => true, 333 | 'before' => false, 334 | 'after' => false, 335 | 'field' => array( 336 | 'ThisModel.name', 337 | 'OtherModel.name' 338 | ) 339 | ), 340 | 'name'=> array( 341 | 'type' => 'query', 342 | 'method' => 'searchNameCondition' 343 | ) 344 | ); 345 | 346 | public function searchNameCondition($data = array()) { 347 | $filter = $data['name']; 348 | $conditions = array( 349 | 'OR' => array( 350 | $this->alias . '.name LIKE' => '' . $this->formatLike($filter) . '', 351 | $this->alias . '.invoice_number LIKE' => '' . $this->formatLike($filter) . '', 352 | ) 353 | ); 354 | return $conditions; 355 | } 356 | ``` 357 | 358 | In your controller. 359 | 360 | ```php 361 | public $presetVars = array( 362 | 'some_related_table_id' => true, 363 | 'search' => true, 364 | // overriding/extending the model defaults 365 | 'name'=> array( 366 | 'type' => 'value', 367 | 'encode' => true 368 | ), 369 | ); 370 | ``` 371 | 372 | Search example with wildcards in the view for field `search` 20??BE* => matches 2011BES and 2012BETR etc. 373 | 374 | -------------------------------------------------------------------------------- /Docs/Documentation/Installation.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | To install the plugin, place the files in a directory labelled "Search/" in your "app/Plugin/" directory. 5 | 6 | Then, include the following line in your `app/Config/bootstrap.php` to load the plugin in your application. 7 | 8 | ``` 9 | CakePlugin::load('Search'); 10 | ``` 11 | 12 | Git Submodule 13 | ------------- 14 | 15 | If you're using git for version control, you may want to add the **Search** plugin as a submodule on your repository. To do so, run the following command from the base of your repository: 16 | 17 | ``` 18 | git submodule add git@github.com:CakeDC/search.git app/Plugin/Search 19 | ``` 20 | 21 | After doing so, you will see the submodule in your changes pending, plus the file ```.gitmodules```. Simply commit and push to your repository. 22 | 23 | To initialize the submodule(s) run the following command: 24 | 25 | ``` 26 | git submodule update --init --recursive 27 | ``` 28 | 29 | To retrieve the latest updates to the plugin, assuming you're using the ```master``` branch, go to ```app/Plugin/Search``` and run the following command: 30 | 31 | ``` 32 | git pull origin master 33 | ``` 34 | 35 | If you're using another branch, just change "master" for the branch you are currently using. 36 | 37 | If any updates are added, go back to the base of your own repository, commit and push your changes. This will update your repository to point to the latest updates to the plugin. 38 | 39 | Composer 40 | -------- 41 | 42 | The plugin also provides a "composer.json" file, to easily use the plugin through the Composer dependency manager. 43 | -------------------------------------------------------------------------------- /Docs/Documentation/Named-Parameters-vs-Query-Strings.md: -------------------------------------------------------------------------------- 1 | Named Params vs Querystring 2 | =========================== 3 | 4 | With the release of version **2.5.0** of the **Search** plugin the settings for the `Prg` component have been changed to use `querystring` by default instead of `named`. 5 | 6 | To use query string parameters before **2.5.0** you would have had to use these configuration settings for the component: 7 | 8 | ```php 9 | public $components = array( 10 | 'Search.Prg' => array( 11 | 'commonProcess' => array('paramType' => 'querystring'), 12 | 'presetForm' => array('paramType' => 'querystring') 13 | ) 14 | ); 15 | ``` 16 | 17 | If you just upgraded to **2.5.0** or higher, and you're not already using query string parameters, you'll have to set the configuration of the `Prg` component in your app back to use `named` parameters. This is also valid if you want to favor `named` parameters over `querystring`. 18 | 19 | ```php 20 | public $components = array( 21 | 'Search.Prg' => array( 22 | 'commonProcess' => array('paramType' => 'named'), 23 | 'presetForm' => array('paramType' => 'named') 24 | ) 25 | ); 26 | ``` 27 | 28 | Why Query String instead of Named parameters? 29 | -------------------------------------------- 30 | 31 | Using the [query string](http://en.wikipedia.org/wiki/Query_string) part of the URI is the correct way of passing parameters. Historically, `named` parameters were an alternative to passing arguments as part of the URL in *CakePHP*. 32 | 33 | When you pass `named` parameters that contain special characters like `/` or `&`, they potentially break the URL. You would have to manually encode and decode these all the time. They also violate the [HTTP](http://tools.ietf.org/html/rfc3986#section-2.2) specification, as they are specific to *CakePHP*, and are no longer supported in version **3.0** of the framework. 34 | -------------------------------------------------------------------------------- /Docs/Documentation/Overview.md: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | PRG Component Features 5 | ---------------------- 6 | 7 | The [Prg Component](../../Controller/Component/PrgComponent.php) implements the PRG pattern so you can use it separately from search tasks when you need it. 8 | 9 | The component maintains passed and named parameters or query string variables that come as POST parameters and transform it to the named during redirect, and sets Controller::data back if the GET method was used during component call. 10 | 11 | Most importantly the component acts as the glue between your app and the searchable behavior. 12 | 13 | You can attach the component to your controller. Here is an example using defaults already set in the component itself. 14 | 15 | ```php 16 | public $components = array('Search.Prg' => array( 17 | // options for preset form method 18 | 'presetForm' => array( 19 | // 'paramType' can be 'named' or 'querystring' 20 | 'paramType' => 'named', 21 | // 'model' can be 'null' or a default model name 22 | 'model' => null 23 | ), 24 | // options for commonProcess method 25 | 'commonProcess' => array( 26 | 'formName' => null, 27 | 'keepPassed' => true, 28 | 'action' => null, 29 | 'modelMethod' => 'validateSearch', 30 | 'allowedParams' => array(), 31 | // 'paramType' can be 'named' or 'querystring' 32 | 'paramType' => 'named', 33 | 'filterEmpty' => false 34 | ) 35 | )); 36 | ``` 37 | 38 | PrgComponent::presetForm Options 39 | -------------------------------- 40 | 41 | * **paramType:** ```named``` or ```querystring```, by default ```named```. 42 | * **model:** Model name or null, by default ```null```. 43 | 44 | PrgComponent::commonProcess Options 45 | ----------------------------------- 46 | 47 | The ```commonProcess()``` method defined in the Prg component allows you to inject search in any controller with just 1-2 lines of additional code. You should pass the model name that is used for search. By default it is ```Controller::$modelClass```. 48 | 49 | Additional options parameters. 50 | 51 | * **form:** Search form name. 52 | * **keepPassed:** Parameter that describes if you need to merge ```passedArgs``` to the url where you will be redirected to after post. 53 | * **action:** Sometimes you want to have different actions for post and get. In this case you can define get action using this parameter. 54 | * **modelMethod:** Method used to filter named parameters, passed from the form. By default it is validateSearch, and it defined in Searchable behavior. 55 | -------------------------------------------------------------------------------- /Docs/Documentation/Post-Redirect-Get.md: -------------------------------------------------------------------------------- 1 | POST-Redirect-GET 2 | ================= 3 | 4 | Post/Redirect/Get (PRG) is a common design pattern for web developers to help avoid certain duplicate form submissions and allow user agents to behave more intuitively with bookmarks and the refresh button. 5 | 6 | When a web form is submitted to a server through an HTTP POST request, a web user that attempts to refresh the server response in certain user agents can cause the contents of the original HTTP POST request to be resubmitted, possibly causing undesired results. 7 | 8 | To avoid this problem, it is possible to use the PRG pattern instead of returning the web page directly. The POST operation returns a redirection command, instructing the browser to load a different page (or same page) using an HTTP GET request. 9 | 10 | See the [Wikipedia article](http://en.wikipedia.org/wiki/Post/Redirect/Get) for more information. 11 | -------------------------------------------------------------------------------- /Docs/Home.md: -------------------------------------------------------------------------------- 1 | Home 2 | ==== 3 | 4 | The **Search** plugin enables developers to quickly implement the [POST-Redirect-GET](Documentation/Post-Redirect-Get.md) pattern. It also provides you with a paginate-able search in any controller. It supports simple methods to build search conditions inside models using strict and non-strict comparing, but also allows you to implement any kind of complex type of search conditions. 5 | 6 | Requirements 7 | ------------ 8 | 9 | * CakePHP 2.5+ 10 | * PHP 5.2.8+ 11 | 12 | Documentation 13 | ------------- 14 | 15 | * [Overview](Documentation/Overview.md) 16 | * [Installation](Documentation/Installation.md) 17 | * [Named Parameters vs Query Strings](Documentation/Named-Parameters-vs-Query-Strings.md) 18 | * [Configuration](Documentation/Configuration.md) 19 | * [POST-Redirect-GET Pattern](Documentation/Post-Redirect-Get.md) 20 | * [Examples](Documentation/Examples.md) 21 | 22 | Tutorials 23 | --------- 24 | 25 | * [Quick Start](Tutorials/Quick-Start.md) 26 | -------------------------------------------------------------------------------- /Docs/Tutorials/Quick-Start.md: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | This quick start guide will help you get ready to use the **Search** plugin with your application. 5 | 6 | Add the [Prg](../../Controller/Component/PrgComponent.php) component to the controller and call the component methods to process POST and GET. You can debug the paginator settings to see what the component does there, for example: 7 | 8 | ```php 9 | class UsersController extends AppController { 10 | 11 | public $components = array( 12 | 'Search.Prg' 13 | ); 14 | 15 | public function index() { 16 | $this->Prg->commonProcess(); 17 | $this->Paginator->settings['conditions'] = $this->User->parseCriteria($this->Prg->parsedParams()); 18 | $this->set('users', $this->Paginator->paginate()); 19 | } 20 | } 21 | ``` 22 | 23 | For the previous example, in your User model, attach the [Searchable](../../Model/Behavior/SearchableBehavior.php) behavior and configure the ```$filterArgs``` property for the fields you want to make searchable. 24 | 25 | ```php 26 | class User extends AppModel { 27 | 28 | public $actsAs = array( 29 | 'Search.Searchable' 30 | ); 31 | 32 | public $filterArgs = array( 33 | 'username' => array( 34 | 'type' => 'like', 35 | 'field' => 'username' 36 | ), 37 | 'email' => array( 38 | 'type' => 'like', 39 | 'field' => 'email' 40 | ), 41 | 'active' => array( 42 | 'type' => 'value' 43 | ) 44 | ); 45 | 46 | } 47 | ``` 48 | 49 | There is no need to make any additional changes in your view, only make sure that your form includes the fields defined in your ```$filterArgs``` property, for example: 50 | 51 | ```php 52 | Form->create(); 54 | echo $this->Form->input('username'); 55 | echo $this->Form->input('email'); 56 | echo $this->Form->input('active', array( 57 | 'type' => 'checkbox' 58 | )); 59 | echo $this->Form->submit(__('Submit')); 60 | echo $this->Form->end(); 61 | ?> 62 | ``` 63 | 64 | For more complex examples see the [Examples](../Documentation/Examples.md) section of the documentation. 65 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2007 - 2014 4 | Cake Development Corporation 5 | 1785 E. Sahara Avenue, Suite 490-423 6 | Las Vegas, Nevada 89104 7 | http://cakedc.com 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a 10 | copy of this software and associated documentation files (the "Software"), 11 | to deal in the Software without restriction, including without limitation 12 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 13 | and/or sell copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Locale/deu/LC_MESSAGES/search.po: -------------------------------------------------------------------------------- 1 | # LANGUAGE translation of the CakePHP Categories plugin 2 | # 3 | # Copyright 2010, Cake Development Corporation (http://cakedc.com) 4 | # 5 | # Licensed under The MIT License 6 | # Redistributions of files must retain the above copyright notice. 7 | # 8 | # @copyright Copyright 2010, Cake Development Corporation (http://cakedc.com) 9 | # @license http://www.opensource.org/licenses/mit-license.php MIT License 10 | # 11 | msgid "" 12 | msgstr "" 13 | "Project-Id-Version: CakePHP Search Plugin\n" 14 | "POT-Creation-Date: 2010-09-15 15:33+0200\n" 15 | "PO-Revision-Date: 2010-09-23 21:53+0100\n" 16 | "Last-Translator: Florian Krämer \n" 17 | "Language-Team: Cake Development Corporation \n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Plural-Forms: nplurals=2; plural=(n!=1);\n" 22 | "X-Poedit-Language: GERMAN\n" 23 | "X-Poedit-Country: GERMANY\n" 24 | "X-Poedit-SourceCharset: utf-8\n" 25 | 26 | #: /controllers/components/prg.php:245 27 | msgid "Please correct the errors below." 28 | msgstr "Bitte korrigieren Sie die unten stehenden Fehler." 29 | 30 | -------------------------------------------------------------------------------- /Locale/fre/LC_MESSAGES/search.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: CakePHP Search Plugin\n" 4 | "POT-Creation-Date: 2010-09-15 15:33+0200\n" 5 | "PO-Revision-Date: \n" 6 | "Last-Translator: Pierre MARTIN \n" 7 | "Language-Team: CakeDC \n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "X-Poedit-Language: French\n" 12 | 13 | #: /controllers/components/prg.php:245 14 | msgid "Please correct the errors below." 15 | msgstr "Veuillez corriger les erreurs ci-dessous." 16 | 17 | -------------------------------------------------------------------------------- /Locale/por/LC_MESSAGES/search.po: -------------------------------------------------------------------------------- 1 | # LANGUAGE translation of the CakePHP Categories plugin 2 | # 3 | # Copyright 2010, Cake Development Corporation (http://cakedc.com) 4 | # 5 | # Licensed under The MIT License 6 | # Redistributions of files must retain the above copyright notice. 7 | # 8 | # @copyright Copyright 2010, Cake Development Corporation (http://cakedc.com) 9 | # @license http://www.opensource.org/licenses/mit-license.php MIT License 10 | # 11 | msgid "" 12 | msgstr "" 13 | "Project-Id-Version: CakePHP Search Plugin\n" 14 | "POT-Creation-Date: 2010-09-15 15:33+0200\n" 15 | "PO-Revision-Date: 2010-09-23 18:59-0300\n" 16 | "Last-Translator: Renan Gonçalves \n" 17 | "Language-Team: CakeDC \n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Plural-Forms: nplurals=2;plural=n>1;\n" 22 | "X-Poedit-Language: Portuguese\n" 23 | "X-Poedit-Country: BRAZIL\n" 24 | 25 | #: /controllers/components/prg.php:245 26 | msgid "Please correct the errors below." 27 | msgstr "Por favor, corrija os erros abaixo." 28 | 29 | -------------------------------------------------------------------------------- /Locale/rus/LC_MESSAGES/search.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: CakePHP Search Plugin\n" 4 | "POT-Creation-Date: 2010-09-15 17:33+0200\n" 5 | "PO-Revision-Date: \n" 6 | "Last-Translator: Evgeny Tomenko \n" 7 | "Language-Team: CakeDC\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "X-Poedit-Language: Russian\n" 12 | "Plural-Forms: plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10< =4 && (n%100<10 or n%100>=20) ? 1 : 2)\n" 13 | 14 | #: /controllers/components/prg.php:245 15 | msgid "Please correct the errors below." 16 | msgstr "Пожалуйста исправьте следующие ошибки:" 17 | 18 | -------------------------------------------------------------------------------- /Locale/search.pot: -------------------------------------------------------------------------------- 1 | # LANGUAGE translation of the CakePHP Categories plugin 2 | # 3 | # Copyright 2010, Cake Development Corporation (http://cakedc.com) 4 | # 5 | # Licensed under The MIT License 6 | # Redistributions of files must retain the above copyright notice. 7 | # 8 | # @copyright Copyright 2010, Cake Development Corporation (http://cakedc.com) 9 | # @license http://www.opensource.org/licenses/mit-license.php MIT License 10 | # 11 | # 12 | "Project-Id-Version: PROJECT VERSION\n" 13 | "POT-Creation-Date: 2010-09-15 15:33+0200\n" 14 | "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n" 15 | "Last-Translator: NAME \n" 16 | "Language-Team: LANGUAGE \n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=utf-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 21 | 22 | #: /controllers/components/prg.php:245 23 | msgid "Please correct the errors below." 24 | msgstr "" 25 | 26 | -------------------------------------------------------------------------------- /Locale/spa/LC_MESSAGES/search.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: Search CakePHP plugin\n" 4 | "POT-Creation-Date: \n" 5 | "PO-Revision-Date: \n" 6 | "Last-Translator: José Lorenzo Rodríguez \n" 7 | "Language-Team: CakeDC \n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "X-Poedit-Language: Spanish\n" 12 | 13 | # LANGUAGE translation of the CakePHP Categories plugin 14 | # 15 | # Copyright 2010, Cake Development Corporation (http://cakedc.com) 16 | # 17 | # Licensed under The MIT License 18 | # Redistributions of files must retain the above copyright notice. 19 | # 20 | # @copyright Copyright 2010, Cake Development Corporation (http://cakedc.com) 21 | # @license http://www.opensource.org/licenses/mit-license.php MIT License 22 | # 23 | # 24 | #: /controllers/components/prg.php:245 25 | msgid "Please correct the errors below." 26 | msgstr "Por favor corrija los errores que se indican" 27 | 28 | -------------------------------------------------------------------------------- /Model/Behavior/SearchableBehavior.php: -------------------------------------------------------------------------------- 1 | user can enter wildcards himself) 24 | * - ilike: auto add % wildcard to beginning, end or both (both false => user can enter wildcards himself) 25 | * - connectorAnd: the character between search terms to specify an "and" relationship (binds stronger than or, similar to * and + in math) 26 | * - connectorOr: the character between search terms to specify an "or" relationship 27 | * 28 | * @var array 29 | */ 30 | protected $_defaults = array( 31 | 'wildcardAny' => '*', //on windows/unix/mac/google/... thats the default one 32 | 'wildcardOne' => '?', //on windows/unix/mac thats the default one 33 | 'like' => array('before' => true, 'after' => true), 34 | 'ilike' => array('before' => true, 'after' => true), 35 | 'connectorAnd' => null, 36 | 'connectorOr' => null, 37 | ); 38 | 39 | /** 40 | * Configuration of model 41 | * 42 | * @param Model $Model 43 | * @param array $config 44 | * @return void 45 | */ 46 | public function setup(Model $Model, $config = array()) { 47 | $this->_defaults = (array)Configure::read('Search.Searchable') + $this->_defaults; 48 | $this->settings[$Model->alias] = $config + $this->_defaults; 49 | } 50 | 51 | /** 52 | * Prepares the filter args based on the model information and calls 53 | * Model::getFilterArgs if present to set up the filterArgs with proper model 54 | * aliases. 55 | * 56 | * @param Model $Model 57 | * @return boolean|array 58 | */ 59 | public function setupFilterArgs(Model $Model) { 60 | if (method_exists($Model, 'getFilterArgs')) { 61 | $Model->getFilterArgs(); 62 | } 63 | if (empty($Model->filterArgs)) { 64 | return false; 65 | } 66 | foreach ($Model->filterArgs as $key => $val) { 67 | if (!isset($val['name'])) { 68 | $Model->filterArgs[$key]['name'] = $key; 69 | } 70 | if (!isset($val['field'])) { 71 | $Model->filterArgs[$key]['field'] = $Model->filterArgs[$key]['name']; 72 | } 73 | if (!isset($val['type'])) { 74 | $Model->filterArgs[$key]['type'] = 'value'; 75 | } 76 | } 77 | return $Model->filterArgs; 78 | } 79 | 80 | /** 81 | * parseCriteria 82 | * parses the GET data and returns the conditions for the find('all')/paginate 83 | * we are just going to test if the params are legit 84 | * 85 | * @param Model $Model 86 | * @param array $data Criteria of key->value pairs from post/named parameters 87 | * @return array Array of conditions that express the conditions needed for the search 88 | */ 89 | public function parseCriteria(Model $Model, $data) { 90 | $this->setupFilterArgs($Model); 91 | $conditions = array(); 92 | 93 | foreach ($Model->filterArgs as $field) { 94 | // If this field was not passed and a default value exists, use that instead. 95 | if (!array_key_exists($field['name'], $data) && array_key_exists('defaultValue', $field)) { 96 | $data[$field['name']] = $field['defaultValue']; 97 | } 98 | 99 | if (in_array($field['type'], array('like'))) { 100 | $this->_addCondLike($Model, $conditions, $data, $field, 'LIKE'); 101 | } elseif (in_array($field['type'], array('ilike'))) { 102 | $this->_addCondLike($Model, $conditions, $data, $field, 'ILIKE'); 103 | } elseif (in_array($field['type'], array('value', 'lookup'))) { 104 | $this->_addCondValue($Model, $conditions, $data, $field); 105 | } elseif ($field['type'] === 'expression') { 106 | $this->_addCondExpression($Model, $conditions, $data, $field); 107 | } elseif ($field['type'] === 'query') { 108 | $this->_addCondQuery($Model, $conditions, $data, $field); 109 | } elseif ($field['type'] === 'subquery') { 110 | $this->_addCondSubquery($Model, $conditions, $data, $field); 111 | } 112 | } 113 | return $conditions; 114 | } 115 | 116 | /** 117 | * Validate search 118 | * 119 | * @param Model $Model 120 | * @param null $data 121 | * @return boolean always true 122 | */ 123 | public function validateSearch(Model $Model, $data = null) { 124 | if (!empty($data)) { 125 | $Model->set($data); 126 | } 127 | $keys = array_keys($Model->data[$Model->alias]); 128 | foreach ($keys as $key) { 129 | if (empty($Model->data[$Model->alias][$key])) { 130 | unset($Model->data[$Model->alias][$key]); 131 | } 132 | } 133 | return true; 134 | } 135 | 136 | /** 137 | * filter retrieving variables only that present in Model::filterArgs 138 | * 139 | * @param Model $Model 140 | * @param array $vars 141 | * @return array, filtered args 142 | */ 143 | public function passedArgs(Model $Model, $vars) { 144 | $this->setupFilterArgs($Model); 145 | 146 | $result = array(); 147 | foreach ($vars as $var => $val) { 148 | if (in_array($var, Hash::extract($Model->filterArgs, '{n}.name'))) { 149 | $result[$var] = $val; 150 | } 151 | } 152 | return $result; 153 | } 154 | 155 | /** 156 | * Generates a query string using the same API Model::find() uses, calling the beforeFind process for the model 157 | * 158 | * @param Model $Model 159 | * @param string $type Type of find operation (all / first / count / neighbors / list / threaded) 160 | * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks) 161 | * @return array Array of records 162 | * @link http://book.cakephp.org/view/1018/find 163 | */ 164 | public function getQuery(Model $Model, $type = 'first', $query = array()) { 165 | $Model->findQueryType = $type; 166 | $Model->id = $Model->getID(); 167 | $query = $Model->buildQuery($type, $query); 168 | $this->findQueryType = null; 169 | return $this->_queryGet($Model, $query); 170 | } 171 | 172 | /** 173 | * Clear all associations 174 | * 175 | * @param Model $Model 176 | * @param boolean $reset 177 | * @return void 178 | */ 179 | public function unbindAllModels(Model $Model, $reset = false) { 180 | $assocs = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); 181 | $unbind = array(); 182 | foreach ($assocs as $assoc) { 183 | $unbind[$assoc] = array_keys($Model->{$assoc}); 184 | } 185 | $Model->unbindModel($unbind, $reset); 186 | } 187 | 188 | /** 189 | * For custom queries inside the model 190 | * example "makePhoneCondition": $cond = array('OR' => array_merge($this->condIlike('cell_number', $filter), $this->condIlike('landline_number', $filter, array('before' => false)))); 191 | * 192 | * @param Model $Model 193 | * @param $name 194 | * @param $data 195 | * @param array $field 196 | * @return array of conditions 197 | */ 198 | public function condIlike(Model $Model, $name, $data, $field = array()) { 199 | $conditions = array(); 200 | $field['name'] = $name; 201 | if (!is_array($data)) { 202 | $data = array($name => $data); 203 | } 204 | if (!isset($field['field'])) { 205 | $field['field'] = $field['name']; 206 | } 207 | return $this->_addCondLike($Model, $conditions, $data, $field, 'ILIKE'); 208 | } 209 | 210 | /** 211 | * For custom queries inside the model 212 | * example "makePhoneCondition": $cond = array('OR' => array_merge($this->condLike('cell_number', $filter), $this->condLike('landline_number', $filter, array('before' => false)))); 213 | * 214 | * @param Model $Model 215 | * @param $name 216 | * @param $data 217 | * @param array $field 218 | * @return array of conditions 219 | */ 220 | public function condLike(Model $Model, $name, $data, $field = array()) { 221 | $conditions = array(); 222 | $field['name'] = $name; 223 | if (!is_array($data)) { 224 | $data = array($name => $data); 225 | } 226 | if (!isset($field['field'])) { 227 | $field['field'] = $field['name']; 228 | } 229 | return $this->_addCondLike($Model, $conditions, $data, $field, 'LIKE'); 230 | } 231 | 232 | /** 233 | * Replace substitutions with original wildcards 234 | * but first, escape the original wildcards in the text to use them as normal search text 235 | * 236 | * @param Model $Model 237 | * @param $data 238 | * @param array $options 239 | * @return string queryLikeString 240 | */ 241 | public function formatLike(Model $Model, $data, $options = array()) { 242 | $options = array_merge($this->settings[$Model->alias], $options); 243 | $from = $to = $substFrom = $substTo = array(); 244 | if ($options['wildcardAny'] !== '%') { 245 | $from[] = '%'; 246 | $to[] = '\%'; 247 | $substFrom[] = $options['wildcardAny']; 248 | $substTo[] = '%'; 249 | } 250 | if ($options['wildcardOne'] !== '_') { 251 | $from[] = '_'; 252 | $to[] = '\_'; 253 | $substFrom[] = $options['wildcardOne']; 254 | $substTo[] = '_'; 255 | } 256 | if (!empty($from)) { 257 | // escape first 258 | $data = str_replace($from, $to, $data); 259 | // replace wildcards 260 | $data = str_replace($substFrom, $substTo, $data); 261 | } 262 | return $data; 263 | } 264 | 265 | /** 266 | * Return the current chars for querying LIKE/ILIKE statements on this model 267 | * 268 | * @param Model $Model Reference to the model 269 | * @param array $options 270 | * @return array, [one=>..., any=>...] 271 | */ 272 | public function getWildcards(Model $Model, $options = array()) { 273 | $options = array_merge($this->settings[$Model->alias], $options); 274 | return array('any' => $options['wildcardAny'], 'one' => $options['wildcardOne']); 275 | } 276 | 277 | /** 278 | * Add Conditions based on fuzzy comparison 279 | * 280 | * @param Model $Model Reference to the model 281 | * @param array $conditions existing Conditions collected for the model 282 | * @param array $data Array of data used in search query 283 | * @param array $field Field definition information 284 | * @return array Conditions 285 | */ 286 | protected function _addCondLike(Model $Model, &$conditions, $data, $field, $likeMethod = 'LIKE') { 287 | $settingName = strtolower($likeMethod); 288 | if (!is_array($this->settings[$Model->alias][$settingName])) { 289 | $this->settings[$Model->alias][$settingName] = array('before' => $this->settings[$Model->alias][$settingName], 'after' => $this->settings[$Model->alias][$settingName]); 290 | } 291 | $field = array_merge($this->settings[$Model->alias][$settingName], $field); 292 | if (empty($data[$field['name']])) { 293 | return $conditions; 294 | } 295 | $fieldNames = (array)$field['field']; 296 | 297 | $cond = array(); 298 | foreach ($fieldNames as $fieldName) { 299 | if (strpos($fieldName, '.') === false) { 300 | $fieldName = $Model->alias . '.' . $fieldName; 301 | } 302 | 303 | if ($field['before'] === true) { 304 | $field['before'] = '%'; 305 | } 306 | if ($field['after'] === true) { 307 | $field['after'] = '%'; 308 | } 309 | 310 | $options = $this->settings[$Model->alias]; 311 | $from = $to = $substFrom = $substTo = array(); 312 | if ($options['wildcardAny'] !== '%') { 313 | $from[] = '%'; 314 | $to[] = '\%'; 315 | $from[] = $options['wildcardAny']; 316 | $to[] = '%'; 317 | } 318 | if ($options['wildcardOne'] !== '_') { 319 | $from[] = '_'; 320 | $to[] = '\_'; 321 | $from[] = $options['wildcardOne']; 322 | $to[] = '_'; 323 | } 324 | $value = $data[$field['name']]; 325 | if (!empty($from)) { 326 | $value = str_replace($from, $to, $value); 327 | } 328 | 329 | if (!empty($field['connectorAnd']) || !empty($field['connectorOr'])) { 330 | $cond[] = $this->_connectedLike($value, $field, $fieldName, $likeMethod); 331 | } else { 332 | $cond[$fieldName . " " . $likeMethod] = $field['before'] . $value . $field['after']; 333 | } 334 | } 335 | if (count($cond) > 1) { 336 | if (isset($conditions['OR'])) { 337 | $conditions[]['OR'] = $cond; 338 | } else { 339 | $conditions['OR'] = $cond; 340 | } 341 | } else { 342 | $conditions = array_merge($conditions, $cond); 343 | } 344 | return $conditions; 345 | } 346 | 347 | /** 348 | * Form AND/OR query array using CakeText::tokenize to separate 349 | * search terms by or/and connectors. 350 | * 351 | * @param mixed $value 352 | * @param array $field 353 | * @param string $fieldName 354 | * @return array Conditions 355 | */ 356 | protected function _connectedLike($value, $field, $fieldName, $likeMethod = 'LIKE') { 357 | $or = array(); 358 | if (Configure::version() < '2.7') { 359 | $orValues = String::tokenize($value, $field['connectorOr']); 360 | } else { 361 | $orValues = CakeText::tokenize($value, $field['connectorOr']); 362 | } 363 | foreach ($orValues as $orValue) { 364 | if (Configure::version() < '2.7') { 365 | $andValues = String::tokenize($orValue, $field['connectorAnd']); 366 | } else { 367 | $andValues = CakeText::tokenize($orValue, $field['connectorAnd']); 368 | } 369 | $and = array(); 370 | foreach ($andValues as $andValue) { 371 | $and[] = array($fieldName . " " . $likeMethod => $field['before'] . $andValue . $field['after']); 372 | } 373 | 374 | $or[] = array('AND' => $and); 375 | } 376 | 377 | return array('OR' => $or); 378 | } 379 | 380 | /** 381 | * Add Conditions based on exact comparison 382 | * 383 | * @param Model $Model Reference to the model 384 | * @param array $conditions existing Conditions collected for the model 385 | * @param array $data Array of data used in search query 386 | * @param array $field Field definition information 387 | * @return array of conditions 388 | */ 389 | protected function _addCondValue(Model $Model, &$conditions, $data, $field) { 390 | $fieldNames = (array)$field['field']; 391 | $fieldValue = isset($data[$field['name']]) ? $data[$field['name']] : null; 392 | 393 | $cond = array(); 394 | foreach ($fieldNames as $fieldName) { 395 | if (strpos($fieldName, '.') === false) { 396 | $fieldName = $Model->alias . '.' . $fieldName; 397 | } 398 | if (is_array($fieldValue) && empty($fieldValue)) { 399 | continue; 400 | } 401 | if (!is_array($fieldValue) && ($fieldValue === null || $fieldValue === '' && empty($field['allowEmpty']))) { 402 | continue; 403 | } 404 | 405 | if (is_array($fieldValue) || !is_array($fieldValue) && (string)$fieldValue !== '') { 406 | $cond[$fieldName] = $fieldValue; 407 | } elseif (isset($data[$field['name']]) && !empty($field['allowEmpty'])) { 408 | $schema = $Model->schema($field['name']); 409 | if (isset($schema) && ($schema['default'] !== null || !empty($schema['null']))) { 410 | $cond[$fieldName] = $schema['default']; 411 | } elseif (!empty($fieldValue)) { 412 | $cond[$fieldName] = $fieldValue; 413 | } else { 414 | $cond[$fieldName] = $fieldValue; 415 | } 416 | } 417 | } 418 | if (count($cond) > 1) { 419 | if (isset($conditions['OR'])) { 420 | $conditions[]['OR'] = $cond; 421 | } else { 422 | $conditions['OR'] = $cond; 423 | } 424 | } else { 425 | $conditions = array_merge($conditions, $cond); 426 | } 427 | return $conditions; 428 | } 429 | 430 | /** 431 | * Add Conditions based expressions to search conditions. 432 | * 433 | * @param Model $Model Instance of AppModel 434 | * @param array $conditions Existing conditions. 435 | * @param array $data Data for a field. 436 | * @param array $field Info for field. 437 | * @return array of conditions modified by this method 438 | */ 439 | protected function _addCondExpression(Model $Model, &$conditions, $data, $field) { 440 | $fieldName = $field['field']; 441 | 442 | if ((method_exists($Model, $field['method']) || $this->_checkBehaviorMethods($Model, $field['method'])) && (!empty($field['allowEmpty']) || !empty($data[$field['name']]) || (isset($data[$field['name']]) && (string)$data[$field['name']] !== ''))) { 443 | $fieldValues = $Model->{$field['method']}($data, $field); 444 | if (!empty($conditions[$fieldName]) && is_array($conditions[$fieldName])) { 445 | $conditions[$fieldName] = array_unique(array_merge(array($conditions[$fieldName]), array($fieldValues))); 446 | } else { 447 | $conditions[$fieldName] = $fieldValues; 448 | } 449 | } 450 | return $conditions; 451 | } 452 | 453 | /** 454 | * Add Conditions based query to search conditions. 455 | * 456 | * @param Model $Model Instance of AppModel 457 | * @param array $conditions Existing conditions. 458 | * @param array $data Data for a field. 459 | * @param array $field Info for field. 460 | * @return array of conditions modified by this method 461 | */ 462 | protected function _addCondQuery(Model $Model, &$conditions, $data, $field) { 463 | if ((method_exists($Model, $field['method']) || $this->_checkBehaviorMethods($Model, $field['method'])) && (!empty($field['allowEmpty']) || !empty($data[$field['name']]) || (isset($data[$field['name']]) && (string)$data[$field['name']] !== ''))) { 464 | $conditionsAdd = $Model->{$field['method']}($data, $field); 465 | // if our conditions function returns something empty, nothing to merge in 466 | if (!empty($conditionsAdd)) { 467 | $conditions = Hash::merge($conditions, (array)$conditionsAdd); 468 | } 469 | } 470 | return $conditions; 471 | } 472 | 473 | /** 474 | * Add Conditions based subquery to search conditions. 475 | * 476 | * @param Model $Model Instance of AppModel 477 | * @param array $conditions Existing conditions. 478 | * @param array $data Data for a field. 479 | * @param array $field Info for field. 480 | * @return array of conditions modified by this method 481 | */ 482 | protected function _addCondSubquery(Model $Model, &$conditions, $data, $field) { 483 | $fieldName = $field['field']; 484 | if ((method_exists($Model, $field['method']) || $this->_checkBehaviorMethods($Model, $field['method'])) && (!empty($field['allowEmpty']) || !empty($data[$field['name']]) || (isset($data[$field['name']]) && (string)$data[$field['name']] !== ''))) { 485 | $subquery = $Model->{$field['method']}($data, $field); 486 | // if our subquery function returns something empty, nothing to merge in 487 | if (!empty($subquery)) { 488 | $conditions[] = $Model->getDataSource()->expression("$fieldName in ($subquery)"); 489 | } 490 | } 491 | return $conditions; 492 | } 493 | 494 | /** 495 | * Helper method for getQuery. 496 | * extension of dbo source method. Create association query. 497 | * 498 | * @param Model $Model 499 | * @param array $queryData 500 | * @return string 501 | */ 502 | protected function _queryGet(Model $Model, $queryData = array()) { 503 | /** @var DboSource $db */ 504 | $db = $Model->getDataSource(); 505 | $queryData = $this->_scrubQueryData($queryData); 506 | $recursive = null; 507 | $byPass = false; 508 | $null = null; 509 | $linkedModels = array(); 510 | 511 | if (isset($queryData['recursive'])) { 512 | $recursive = $queryData['recursive']; 513 | } 514 | 515 | if ($recursive !== null) { 516 | $Model->recursive = $recursive; 517 | } 518 | 519 | if (!empty($queryData['fields'])) { 520 | $byPass = true; 521 | $queryData['fields'] = $db->fields($Model, null, $queryData['fields']); 522 | } else { 523 | $queryData['fields'] = $db->fields($Model); 524 | } 525 | 526 | $_associations = $Model->associations(); 527 | 528 | if ($Model->recursive == -1) { 529 | $_associations = array(); 530 | } elseif ($Model->recursive == 0) { 531 | unset($_associations[2], $_associations[3]); 532 | } 533 | 534 | foreach ($_associations as $type) { 535 | foreach ($Model->{$type} as $assoc => $assocData) { 536 | $linkModel = $Model->{$assoc}; 537 | $external = isset($assocData['external']); 538 | 539 | $linkModel->getDataSource(); 540 | if ($Model->useDbConfig === $linkModel->useDbConfig) { 541 | if ($byPass) { 542 | $assocData['fields'] = false; 543 | } 544 | if ($db->generateAssociationQuery($Model, $linkModel, $type, $assoc, $assocData, $queryData, $external, $null) === true) { 545 | $linkedModels[$type . '/' . $assoc] = true; 546 | } 547 | } 548 | } 549 | } 550 | 551 | return trim($db->generateAssociationQuery($Model, null, null, null, null, $queryData, false, $null)); 552 | } 553 | 554 | /** 555 | * Private helper method to remove query metadata in given data array. 556 | * 557 | * @param array $data 558 | * @return array 559 | */ 560 | protected function _scrubQueryData($data) { 561 | static $base = null; 562 | if ($base === null) { 563 | $base = array_fill_keys(array('conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group'), array()); 564 | } 565 | return (array)$data + $base; 566 | } 567 | 568 | /** 569 | * Check if model have some method in attached behaviors 570 | * 571 | * @param Model $Model 572 | * @param string $method 573 | * @return boolean, true if method exists in attached and enabled behaviors 574 | */ 575 | protected function _checkBehaviorMethods(Model $Model, $method) { 576 | $behaviors = $Model->Behaviors->enabled(); 577 | $count = count($behaviors); 578 | $found = false; 579 | for ($i = 0; $i < $count; $i++) { 580 | $name = $behaviors[$i]; 581 | $methods = get_class_methods($Model->Behaviors->{$name}); 582 | $check = array_flip($methods); 583 | $found = isset($check[$method]); 584 | if ($found) { 585 | return true; 586 | } 587 | } 588 | return $found; 589 | } 590 | 591 | } 592 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CakeDC Search Plugin 2 | ======================== 3 | 4 | [![Bake Status](https://secure.travis-ci.org/CakeDC/search.png?branch=master)](http://travis-ci.org/CakeDC/search) 5 | [![Downloads](https://poser.pugx.org/CakeDC/search/d/total.png)](https://packagist.org/packages/CakeDC/search) 6 | [![Latest Version](https://poser.pugx.org/CakeDC/search/v/stable.png)](https://packagist.org/packages/CakeDC/search) 7 | 8 | This **Search** plugin enables developers to quickly implement the [POST-Redirect-GET](Docs/Documentation/Post-Redirect-Get.md) pattern. 9 | 10 | The Search plugin is an easy way to implement PRG in your application, and provides you with a paginate-able search in any controller. It supports simple methods to search inside models using strict and non-strict comparing, but also allows you to implement any complex type of searching. 11 | 12 | * **PRG Component:** The component will turn GET parameters into POST to populate a form and vice versa. 13 | * **Search Behaviour:** The behavior will generate search conditions passed in the provided GET parameters. 14 | 15 | This is *not* a Search Engine or Index 16 | -------------------------------------- 17 | 18 | As mentioned before, this plugin helps you to implement searching for data using the [PRG](Docs/Documentation/Post-Redirect-Get.md) pattern. It is **not** in any way a search engine implementation or search index builder, although it can be used to search an index such as *Elastic Search* or *Sphinx*. 19 | 20 | Requirements 21 | ------------ 22 | 23 | * CakePHP 2.5+ 24 | * PHP 5.2.8+ 25 | 26 | Documentation 27 | ------------- 28 | 29 | For documentation, as well as tutorials, see the [Docs](Docs/Home.md) directory of this repository. 30 | 31 | Support 32 | ------- 33 | 34 | For bugs and feature requests, please use the [issues](https://github.com/CakeDC/search/issues) section of this repository. 35 | 36 | Commercial support is also available, [contact us](http://cakedc.com/contact) for more information. 37 | 38 | Contributing 39 | ------------ 40 | 41 | This repository follows the [CakeDC Plugin Standard](http://cakedc.com/plugin-standard). If you'd like to contribute new features, enhancements or bug fixes to the plugin, please read our [Contribution Guidelines](http://cakedc.com/contribution-guidelines) for detailed instructions. 42 | 43 | License 44 | ------- 45 | 46 | Copyright 2007-2014 Cake Development Corporation (CakeDC). All rights reserved. 47 | 48 | Licensed under the [MIT](http://www.opensource.org/licenses/mit-license.php) License. Redistributions of the source code included in this repository must retain the copyright notice found in each file. 49 | -------------------------------------------------------------------------------- /Test/Case/AllSearchTest.php: -------------------------------------------------------------------------------- 1 | addTestDirectory($path . DS . 'Controller' . DS . 'Component'); 26 | $Suite->addTestDirectory($path . DS . 'Model' . DS . 'Behavior'); 27 | return $Suite; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Test/Case/Controller/Component/PrgComponentTest.php: -------------------------------------------------------------------------------- 1 | array( 50 | 'commonProcess' => array('paramType' => 'named'), 51 | 'presetForm' => array('paramType' => 'named') 52 | ), 53 | 'Session' 54 | ); 55 | 56 | /** 57 | * beforeFilter 58 | * 59 | * @return void 60 | */ 61 | public function beforeFilter() { 62 | parent::beforeFilter(); 63 | $this->Prg->actions = array( 64 | 'search' => array( 65 | 'controller' => 'Posts', 66 | 'action' => 'result' 67 | ) 68 | ); 69 | } 70 | 71 | /** 72 | * Overwrite redirect 73 | * 74 | * @param string $url The URL to redirect to 75 | * @param string $status Not used 76 | * @param bool|string $exit Not used 77 | * @return void 78 | */ 79 | public function redirect($url, $status = null, $exit = true) { 80 | $this->redirectUrl = $url; 81 | } 82 | 83 | } 84 | 85 | /** 86 | * Posts Options Test Controller 87 | */ 88 | class PostOptionsTestController extends PostsTestController { 89 | 90 | /** 91 | * Components 92 | * 93 | * @var array 94 | */ 95 | public $components = array( 96 | 'Search.Prg' => array( 97 | 'commonProcess' => array( 98 | 'form' => 'Post', 99 | 'modelMethod' => false, 100 | 'allowedParams' => array('lang'))), 101 | 'Session' 102 | ); 103 | } 104 | 105 | /** 106 | * PRG Component Test 107 | */ 108 | class PrgComponentTest extends CakeTestCase { 109 | 110 | /** 111 | * Load relevant fixtures 112 | * 113 | * @var array 114 | */ 115 | public $fixtures = array('plugin.search.post'); 116 | 117 | /** 118 | * Setup test controller 119 | * 120 | * @return void 121 | */ 122 | public function setUp() { 123 | parent::setUp(); 124 | 125 | Configure::delete('Search'); 126 | 127 | $this->Controller = new PostsTestController(new CakeRequest(), new CakeResponse()); 128 | $this->Controller->constructClasses(); 129 | $this->Controller->startupProcess(); 130 | $this->Controller->request->params = array( 131 | 'named' => array(), 132 | 'pass' => array(), 133 | 'url' => array() 134 | ); 135 | $this->Controller->request->query = array(); 136 | } 137 | 138 | /** 139 | * Release test controller 140 | * 141 | * @return void 142 | */ 143 | public function tearDown() { 144 | unset($this->Controller); 145 | ClassRegistry::flush(); 146 | 147 | parent::tearDown(); 148 | } 149 | 150 | /** 151 | * Test options 152 | * 153 | * @return void 154 | */ 155 | public function testOptions() { 156 | $this->Controller->presetVars = array(); 157 | $this->Controller->action = 'search'; 158 | $this->Controller->request->data = array( 159 | 'Post' => array( 160 | 'title' => 'test' 161 | ) 162 | ); 163 | 164 | $this->Controller->Prg->commonProcess('Post'); 165 | $expected = array( 166 | 'title' => 'test', 167 | 'action' => 'search' 168 | ); 169 | $this->assertEquals($expected, $this->Controller->redirectUrl); 170 | 171 | $this->Controller->request->params = array_merge( 172 | $this->Controller->request->params, 173 | array( 174 | 'lang' => 'en', 175 | ) 176 | ); 177 | $this->Controller->Prg->commonProcess('Post', array('allowedParams' => array('lang'))); 178 | $expected = array( 179 | 'title' => 'test', 180 | 'action' => 'search', 181 | 'lang' => 'en' 182 | ); 183 | $this->assertEquals($expected, $this->Controller->redirectUrl); 184 | 185 | $this->Controller->presetVars = array( 186 | array('field' => 'title', 'type' => 'value') 187 | ); 188 | $this->Controller->Prg->commonProcess('Post', array('paramType' => 'querystring')); 189 | $expected = array('action' => 'search', '?' => array('title' => 'test')); 190 | $this->assertEquals($expected, $this->Controller->redirectUrl); 191 | } 192 | 193 | /** 194 | * Test presetForm() 195 | * 196 | * @return void 197 | */ 198 | public function testPresetForm() { 199 | $this->Controller->presetVars = array( 200 | array( 201 | 'field' => 'title', 202 | 'type' => 'value' 203 | ), 204 | array( 205 | 'field' => 'checkbox', 206 | 'type' => 'checkbox' 207 | ), 208 | array( 209 | 'field' => 'lookup', 210 | 'type' => 'lookup', 211 | 'formField' => 'lookup_input', 212 | 'modelField' => 'title', 213 | 'model' => 'Post' 214 | ) 215 | ); 216 | $this->Controller->passedArgs = array( 217 | 'title' => 'test', 218 | 'checkbox' => 'test|test2|test3', 219 | 'lookup' => '1' 220 | ); 221 | $this->Controller->beforeFilter(); 222 | 223 | $this->Controller->Prg->presetForm('Post'); 224 | $expected = array( 225 | 'Post' => array( 226 | 'title' => 'test', 227 | 'checkbox' => array( 228 | 0 => 'test', 229 | 1 => 'test2', 230 | 2 => 'test3'), 231 | 'lookup' => 1, 232 | 'lookup_input' => 'First Post' 233 | ) 234 | ); 235 | $this->assertEquals($expected, $this->Controller->request->data); 236 | 237 | $this->Controller->data = array(); 238 | $this->Controller->passedArgs = array(); 239 | $this->Controller->request->query = array( 240 | 'title' => 'test', 241 | 'checkbox' => 'test|test2|test3', 242 | 'lookup' => '1' 243 | ); 244 | $this->Controller->beforeFilter(); 245 | 246 | $this->Controller->Prg->presetForm(array( 247 | 'model' => 'Post', 'paramType' => 'querystring' 248 | ) 249 | ); 250 | $this->assertTrue($this->Controller->Prg->isSearch); 251 | $this->assertEquals($expected['Post'], $this->Controller->Prg->parsedParams()); 252 | 253 | // Deprecated 254 | $this->assertEquals($expected, $this->Controller->data); 255 | } 256 | 257 | /** 258 | * Test presetForm() when passed args are empty 259 | * 260 | * @return void 261 | */ 262 | public function testPresetFormEmpty() { 263 | $this->Controller->presetVars = array( 264 | array( 265 | 'field' => 'title', 266 | 'type' => 'value' 267 | ), 268 | array( 269 | 'field' => 'checkbox', 270 | 'type' => 'checkbox' 271 | ), 272 | array( 273 | 'field' => 'lookup', 274 | 'type' => 'lookup', 275 | 'formField' => 'lookup_input', 276 | 'modelField' => 'title', 277 | 'model' => 'Post' 278 | ) 279 | ); 280 | $this->Controller->passedArgs = array( 281 | 'page' => '2' 282 | ); 283 | $this->Controller->beforeFilter(); 284 | 285 | $this->Controller->Prg->presetForm('Post'); 286 | $expected = array( 287 | 'Post' => array() 288 | ); 289 | $this->assertEquals($expected, $this->Controller->request->data); 290 | 291 | $this->Controller->data = array(); 292 | $this->Controller->passedArgs = array(); 293 | $this->Controller->request->query = array( 294 | 'page' => '2' 295 | ); 296 | $this->Controller->beforeFilter(); 297 | 298 | $this->Controller->Prg->presetForm(array( 299 | 'model' => 'Post', 'paramType' => 'querystring' 300 | ) 301 | ); 302 | $this->assertFalse($this->Controller->Prg->isSearch); 303 | $this->assertEquals($expected['Post'], $this->Controller->Prg->parsedParams()); 304 | 305 | // Deprecated 306 | $this->assertEquals($expected, $this->Controller->data); 307 | } 308 | 309 | /** 310 | * Test search on integer when zero is entered 311 | * 312 | * This test checks that the search on an integer type field in the database 313 | * works correctly when a 0 (zero) is entered in the form. 314 | * 315 | * @return void 316 | * @link http://github.com/CakeDC/Search/issues#issue/3 317 | */ 318 | public function testPresetFormWithIntegerField() { 319 | $this->Controller->presetVars = array( 320 | array( 321 | 'field' => 'views', 322 | 'type' => 'value' 323 | ) 324 | ); 325 | $this->Controller->passedArgs = array( 326 | 'views' => '0' 327 | ); 328 | $this->Controller->beforeFilter(); 329 | 330 | $this->Controller->Prg->presetForm('Post'); 331 | $expected = array( 332 | 'Post' => array( 333 | 'views' => '0' 334 | ) 335 | ); 336 | $this->assertEquals($expected, $this->Controller->request->data); 337 | $this->assertTrue($this->Controller->Prg->isSearch); 338 | $this->assertEquals($expected['Post'], $this->Controller->Prg->parsedParams()); 339 | } 340 | 341 | /** 342 | * Test serializeParams() 343 | * 344 | * @return void 345 | */ 346 | public function testSerializeParams() { 347 | $this->Controller->presetVars = array( 348 | array( 349 | 'field' => 'options', 350 | 'type' => 'checkbox' 351 | ) 352 | ); 353 | $testData = array( 354 | 'options' => array( 355 | 0 => 'test1', 1 => 'test2', 2 => 'test3' 356 | ) 357 | ); 358 | $result = $this->Controller->Prg->serializeParams($testData); 359 | $this->assertEquals(array('options' => 'test1|test2|test3'), $result); 360 | 361 | $testData = array('options' => ''); 362 | 363 | $result = $this->Controller->Prg->serializeParams($testData); 364 | $this->assertEquals(array('options' => ''), $result); 365 | 366 | $testData = array(); 367 | $result = $this->Controller->Prg->serializeParams($testData); 368 | $this->assertEquals(array('options' => ''), $result); 369 | } 370 | 371 | /** 372 | * Test connectNamed() 373 | * 374 | * @return void 375 | */ 376 | public function testConnectNamed() { 377 | $this->Controller->passedArgs = array( 378 | 'title' => 'test'); 379 | $this->assertNull($this->Controller->Prg->connectNamed()); 380 | $this->assertNull($this->Controller->Prg->connectNamed(1)); 381 | } 382 | 383 | /** 384 | * Test exclude() 385 | * 386 | * @return void 387 | */ 388 | public function testExclude() { 389 | $this->Controller->request->params['named'] = array(); 390 | 391 | $array = array('foo' => 'test', 'bar' => 'test', 'test' => 'test'); 392 | $exclude = array('bar', 'test'); 393 | $result = $this->Controller->Prg->exclude($array, $exclude); 394 | $this->assertEquals(array('foo' => 'test'), $result); 395 | 396 | $array = array('foo' => 'test', 'bar' => 'test', 'test' => 'test', 397 | 0 => 'passed', 1 => 'passed_again' 398 | ); 399 | $exclude = array('bar', 'test'); 400 | $result = $this->Controller->Prg->exclude($array, $exclude); 401 | $this->assertEquals(array('foo' => 'test', 0 => 'passed', 1 => 'passed_again'), $result); 402 | } 403 | 404 | /** 405 | * Test commonProcess() 406 | * 407 | * @return void 408 | */ 409 | public function testCommonProcess() { 410 | $this->Controller->request->params['named'] = array(); 411 | $this->Controller->presetVars = array(); 412 | $this->Controller->action = 'search'; 413 | $this->Controller->request->data = array( 414 | 'Post' => array( 415 | 'title' => 'test' 416 | ) 417 | ); 418 | $this->Controller->Prg->commonProcess('Post', array( 419 | 'form' => 'Post', 420 | 'modelMethod' => false 421 | ) 422 | ); 423 | $expected = array( 424 | 'title' => 'test', 425 | 'action' => 'search' 426 | ); 427 | $this->assertEquals($expected, $this->Controller->redirectUrl); 428 | 429 | $this->Controller->Prg->commonProcess(null, array( 430 | 'modelMethod' => false 431 | ) 432 | ); 433 | $expected = array( 434 | 'title' => 'test', 435 | 'action' => 'search' 436 | ); 437 | $this->assertEquals($expected, $this->Controller->redirectUrl); 438 | 439 | $this->Controller->Post->filterArgs = array( 440 | array('name' => 'title', 'type' => 'value') 441 | ); 442 | $this->Controller->Prg->commonProcess('Post'); 443 | $expected = array( 444 | 'title' => 'test', 445 | 'action' => 'search' 446 | ); 447 | $this->assertEquals($expected, $this->Controller->redirectUrl); 448 | } 449 | 450 | /** 451 | * Test commonProcess() with presetVars not empty 452 | * 453 | * Fixing warning when checking undefined $presetVar['name']. 454 | * 455 | * @return void 456 | */ 457 | public function testCommonProcessWithPresetVarsNotEmpty() { 458 | $this->Controller->request->params['named'] = array(); 459 | $this->Controller->presetVars = array('title' => array('type' => 'value')); 460 | 461 | $this->Controller->action = 'search'; 462 | $this->Controller->request->data = array( 463 | 'Post' => array( 464 | 'title' => 'test' 465 | ) 466 | ); 467 | $this->Controller->Prg->commonProcess('Post'); 468 | $expected = array( 469 | 'title' => 'test', 470 | 'action' => 'search' 471 | ); 472 | $this->assertEquals($expected, $this->Controller->redirectUrl); 473 | } 474 | 475 | /** 476 | * Test commonProcess() with 'allowedParams' set 477 | * 478 | * @return void 479 | */ 480 | public function testCommonProcessAllowedParams() { 481 | $this->Controller->request->params = array_merge( 482 | $this->Controller->request->params, 483 | array( 484 | 'named' => array(), 485 | 'lang' => 'en', 486 | ) 487 | ); 488 | $this->Controller->presetVars = array(); 489 | $this->Controller->action = 'search'; 490 | $this->Controller->request->data = array( 491 | 'Post' => array( 492 | 'title' => 'test' 493 | ) 494 | ); 495 | $this->Controller->Prg->commonProcess('Post', array( 496 | 'form' => 'Post', 497 | 'modelMethod' => false, 498 | 'allowedParams' => array('lang') 499 | ) 500 | ); 501 | $expected = array( 502 | 'title' => 'test', 503 | 'action' => 'search', 504 | 'lang' => 'en' 505 | ); 506 | $this->assertEquals($expected, $this->Controller->redirectUrl); 507 | } 508 | 509 | /** 510 | * Test commonProcess() when resetting 'named' 511 | * 512 | * @return void 513 | */ 514 | public function testCommonProcessResetNamed() { 515 | $this->Controller->request->params = array_merge( 516 | $this->Controller->request->params, 517 | array( 518 | 'named' => array('page' => 2, 'sort' => 'name', 'direction' => 'asc'), 519 | 'lang' => 'en', 520 | ) 521 | ); 522 | $this->Controller->presetVars = array(); 523 | $this->Controller->action = 'search'; 524 | $this->Controller->request->data = array( 525 | 'Post' => array( 526 | 'title' => 'test', 527 | 'foo' => '', 528 | 'bar' => '' 529 | ) 530 | ); 531 | $this->Controller->Prg->commonProcess('Post', array( 532 | 'form' => 'Post', 533 | 'modelMethod' => false, 534 | 'allowedParams' => array('lang') 535 | ) 536 | ); 537 | $expected = array( 538 | 'sort' => 'name', 539 | 'direction' => 'asc', 540 | 'title' => 'test', 541 | 'foo' => '', 542 | 'bar' => '', 543 | 'action' => 'search', 544 | 'lang' => 'en' 545 | ); 546 | $this->assertEquals($expected, $this->Controller->redirectUrl); 547 | } 548 | 549 | /** 550 | * Test commonProcess() when 'filterEmpty' = true 551 | * 552 | * @return void 553 | */ 554 | public function testCommonProcessFilterEmpty() { 555 | $this->Controller->request->params = array_merge( 556 | $this->Controller->request->params, 557 | array( 558 | 'named' => array(), 559 | 'lang' => 'en', 560 | ) 561 | ); 562 | $this->Controller->presetVars = array(); 563 | $this->Controller->action = 'search'; 564 | $this->Controller->request->data = array( 565 | 'Post' => array( 566 | 'title' => 'test', 567 | 'foo' => '', 568 | 'bar' => '' 569 | ) 570 | ); 571 | $this->Controller->Prg->commonProcess('Post', array( 572 | 'form' => 'Post', 573 | 'modelMethod' => false, 574 | 'filterEmpty' => true, 575 | 'allowedParams' => array('lang') 576 | ) 577 | ); 578 | $expected = array( 579 | 'title' => 'test', 580 | 'action' => 'search', 581 | 'lang' => 'en' 582 | ); 583 | $this->assertEquals($expected, $this->Controller->redirectUrl); 584 | } 585 | 586 | /** 587 | * Test commonProcess() with special characters 588 | * 589 | * @return void 590 | */ 591 | public function testCommonProcessSpecialChars() { 592 | $this->Controller->request->params = array_merge( 593 | $this->Controller->request->params, 594 | array( 595 | 'named' => array(), 596 | 'lang' => 'en', 597 | ) 598 | ); 599 | $this->Controller->presetVars = array(); 600 | $this->Controller->action = 'search'; 601 | $this->Controller->request->data = array( 602 | 'Post' => array( 603 | 'title' => 'test/slashes?!', 604 | 'foo' => '', 605 | 'bar' => '' 606 | ) 607 | ); 608 | $this->Controller->Prg->commonProcess('Post', array( 609 | 'form' => 'Post', 610 | 'modelMethod' => false, 611 | 'filterEmpty' => true, 612 | 'allowedParams' => array('lang') 613 | ) 614 | ); 615 | $expected = array( 616 | 'title' => 'test/slashes?!', 617 | 'action' => 'search', 618 | 'lang' => 'en' 619 | ); 620 | $this->assertEquals($expected, $this->Controller->redirectUrl); 621 | 622 | $url = Router::url($this->Controller->redirectUrl); 623 | $expected = '/search/title:test%2Fslashes%3F%21/lang:en'; 624 | $this->assertEquals($expected, $url); 625 | } 626 | 627 | /** 628 | * Test commonProcess() with 'paramType' = 'querystring' 629 | * 630 | * @return void 631 | */ 632 | public function testCommonProcessQuerystring() { 633 | $this->Controller->request->params = array_merge( 634 | $this->Controller->request->params, 635 | array( 636 | 'named' => array(), 637 | 'lang' => 'en', 638 | ) 639 | ); 640 | $this->Controller->presetVars = array(); 641 | $this->Controller->action = 'search'; 642 | $this->Controller->request->data = array( 643 | 'Post' => array( 644 | 'title' => 'test', 645 | 'foo' => '', 646 | 'bar' => '' 647 | ) 648 | ); 649 | $this->Controller->Prg->commonProcess('Post', array( 650 | 'form' => 'Post', 651 | 'modelMethod' => false, 652 | 'paramType' => 'querystring', 653 | 'allowedParams' => array('lang') 654 | ) 655 | ); 656 | $expected = array( 657 | '?' => array('title' => 'test', 'foo' => '', 'bar' => ''), 658 | 'action' => 'search', 659 | 'lang' => 'en' 660 | ); 661 | $this->assertEquals($expected, $this->Controller->redirectUrl); 662 | } 663 | 664 | /** 665 | * Test commonProcess() with 'paramType' = 'querystring' and special characters 666 | * 667 | * @return void 668 | */ 669 | public function testCommonProcessQuerystringSpecialChars() { 670 | $this->Controller->request->params = array_merge( 671 | $this->Controller->request->params, 672 | array( 673 | 'named' => array(), 674 | 'lang' => 'en', 675 | ) 676 | ); 677 | $this->Controller->presetVars = array(); 678 | $this->Controller->action = 'search'; 679 | $this->Controller->request->data = array( 680 | 'Post' => array( 681 | 'title' => 'test/slashes?!', 682 | 'foo' => '', 683 | 'bar' => '' 684 | ) 685 | ); 686 | $this->Controller->Prg->commonProcess('Post', array( 687 | 'form' => 'Post', 688 | 'modelMethod' => false, 689 | 'filterEmpty' => true, 690 | 'paramType' => 'querystring', 691 | 'allowedParams' => array('lang') 692 | ) 693 | ); 694 | $expected = array( 695 | '?' => array('title' => 'test/slashes?!'), 696 | 'action' => 'search', 697 | 'lang' => 'en' 698 | ); 699 | $this->assertEquals($expected, $this->Controller->redirectUrl); 700 | 701 | $url = Router::url($this->Controller->redirectUrl); 702 | $expected = '/search/lang:en?title=test%2Fslashes%3F%21'; 703 | $this->assertEquals($expected, $url); 704 | } 705 | 706 | /** 707 | * Test commonProcess() with 'paramType' = 'querystring' and pagination 708 | * 709 | * @return void 710 | */ 711 | public function testCommonProcessQuerystringPagination() { 712 | $this->Controller->request->query = array( 713 | 'sort' => 'created', 714 | 'direction' => 'asc', 715 | 'page' => 3, 716 | ); 717 | $this->Controller->request->params = array_merge( 718 | $this->Controller->request->params, 719 | array( 720 | 'named' => array(), 721 | 'lang' => 'en', 722 | ) 723 | ); 724 | $this->Controller->presetVars = array(); 725 | $this->Controller->action = 'search'; 726 | $this->Controller->request->data = array( 727 | 'Post' => array( 728 | 'title' => 'test', 729 | 'foo' => '', 730 | 'bar' => '' 731 | ) 732 | ); 733 | $this->Controller->Prg->commonProcess('Post', array( 734 | 'form' => 'Post', 735 | 'modelMethod' => false, 736 | 'paramType' => 'querystring', 737 | 'allowedParams' => array('lang') 738 | ) 739 | ); 740 | $expected = array( 741 | '?' => array('title' => 'test', 'foo' => '', 'bar' => '', 742 | 'sort' => 'created', 'direction' => 'asc' 743 | ), 744 | 'action' => 'search', 745 | 'lang' => 'en' 746 | ); 747 | $this->assertEquals($expected, $this->Controller->redirectUrl); 748 | } 749 | 750 | /** 751 | * Test commonProcess() with 'paramType' = 'querystring' and 'filterEmpty' = true 752 | * 753 | * @return void 754 | */ 755 | public function testCommonProcessQuerystringFilterEmpty() { 756 | $this->Controller->request->params = array_merge( 757 | $this->Controller->request->params, 758 | array( 759 | 'named' => array(), 760 | 'lang' => 'en', 761 | ) 762 | ); 763 | $this->Controller->presetVars = array(); 764 | $this->Controller->action = 'search'; 765 | $this->Controller->request->data = array( 766 | 'Post' => array( 767 | 'title' => 'test', 768 | 'foo' => '', 769 | 'bar' => '' 770 | ) 771 | ); 772 | 773 | $this->Controller->Prg->commonProcess('Post', array( 774 | 'form' => 'Post', 775 | 'modelMethod' => false, 776 | 'filterEmpty' => true, 777 | 'paramType' => 'querystring', 778 | 'allowedParams' => array('lang') 779 | ) 780 | ); 781 | $expected = array( 782 | '?' => array('title' => 'test'), 783 | 'action' => 'search', 784 | 'lang' => 'en' 785 | ); 786 | $this->assertEquals($expected, $this->Controller->redirectUrl); 787 | } 788 | 789 | /** 790 | * Test commonProcess() with GET 791 | * 792 | * @return void 793 | */ 794 | public function testCommonProcessGet() { 795 | $this->Controller->action = 'search'; 796 | $this->Controller->presetVars = array( 797 | array('field' => 'title', 'type' => 'value') 798 | ); 799 | $this->Controller->request->data = array(); 800 | $this->Controller->Post->filterArgs = array( 801 | array('name' => 'title', 'type' => 'value') 802 | ); 803 | $this->Controller->request->params['named'] = array('title' => 'test'); 804 | $this->Controller->passedArgs = array_merge( 805 | $this->Controller->request->params['named'], 806 | $this->Controller->request->params['pass'] 807 | ); 808 | $this->Controller->Prg->commonProcess('Post'); 809 | 810 | $this->assertTrue($this->Controller->Prg->isSearch); 811 | $expected = array('Post' => array('title' => 'test')); 812 | $this->assertEquals($expected['Post'], $this->Controller->Prg->parsedParams()); 813 | $this->assertEquals($expected, $this->Controller->request->data); 814 | } 815 | 816 | /** 817 | * Test commonProcess() with GET and string keys 818 | * 819 | * @return void 820 | */ 821 | public function testCommonProcessGetWithStringKeys() { 822 | $this->Controller->action = 'search'; 823 | $this->Controller->presetVars = array( 824 | 'title' => array('type' => 'value') 825 | ); 826 | $this->Controller->Post->filterArgs = array( 827 | 'title' => array('type' => 'value') 828 | ); 829 | 830 | $this->Controller->Prg->__construct($this->Controller->Components, array()); 831 | $this->Controller->Prg->initialize($this->Controller); 832 | $this->Controller->request->data = array(); 833 | 834 | $this->Controller->request->params['named'] = array('title' => 'test'); 835 | $this->Controller->passedArgs = array_merge( 836 | $this->Controller->request->params['named'], 837 | $this->Controller->request->params['pass'] 838 | ); 839 | $this->Controller->Prg->commonProcess('Post'); 840 | $expected = array('Post' => array('title' => 'test')); 841 | $this->assertEquals($expected['Post'], $this->Controller->Prg->parsedParams()); 842 | $this->assertEquals($expected, $this->Controller->request->data); 843 | } 844 | 845 | /** 846 | * Test commonProcess() with GET and string keys (short notation) 847 | * 848 | * @return void 849 | */ 850 | public function testCommonProcessGetWithStringKeysShort() { 851 | $this->Controller->action = 'search'; 852 | $this->Controller->presetVars = array( 853 | 'title' => true 854 | ); 855 | $this->Controller->Post->filterArgs = array( 856 | 'title' => array('type' => 'value') 857 | ); 858 | 859 | $this->Controller->Prg->__construct($this->Controller->Components, array()); 860 | $this->Controller->Prg->initialize($this->Controller); 861 | $this->Controller->request->data = array(); 862 | 863 | $this->Controller->request->params['named'] = array('title' => 'test'); 864 | $this->Controller->passedArgs = array_merge( 865 | $this->Controller->request->params['named'], 866 | $this->Controller->request->params['pass'] 867 | ); 868 | $this->Controller->Prg->commonProcess('Post'); 869 | $expected = array('Post' => array('title' => 'test')); 870 | $this->assertEquals($expected['Post'], $this->Controller->Prg->parsedParams()); 871 | $this->assertEquals($expected, $this->Controller->request->data); 872 | } 873 | 874 | /** 875 | * Test serializeParams() with encoding 876 | * 877 | * @return void 878 | */ 879 | public function testSerializeParamsWithEncoding() { 880 | $this->Controller->action = 'search'; 881 | $this->Controller->presetVars = array( 882 | array('field' => 'title', 'type' => 'value', 'encode' => true)); 883 | $this->Controller->request->data = array(); 884 | $this->Controller->Post->filterArgs = array( 885 | array('name' => 'title', 'type' => 'value') 886 | ); 887 | $this->Controller->Prg->encode = true; 888 | $test = array('title' => 'Something new'); 889 | $result = $this->Controller->Prg->serializeParams($test); 890 | $this->assertEquals($this->_urlEncode('Something new'), $result['title']); 891 | 892 | $test = array('title' => 'ef?'); 893 | $result = $this->Controller->Prg->serializeParams($test); 894 | $this->assertEquals($this->_urlEncode('ef?'), $result['title']); 895 | } 896 | 897 | /** 898 | * Replace the base64encoded values 899 | * 900 | * Replace the base64encoded values that could harm the url (/ and =) with harmless characters 901 | * 902 | * @param $str 903 | * @return string 904 | */ 905 | protected function _urlEncode($str) { 906 | return str_replace(array('/', '='), array('-', '_'), base64_encode($str)); 907 | } 908 | 909 | /** 910 | * Test serializeParams() with encoding 911 | * 912 | * @return void 913 | */ 914 | public function testSerializeParamsWithEncodingAndSpace() { 915 | $this->Controller->action = 'search'; 916 | $this->Controller->presetVars = array( 917 | array('field' => 'title', 'type' => 'value', 'encode' => true)); 918 | $this->Controller->request->data = array(); 919 | $this->Controller->Post->filterArgs = array( 920 | array('name' => 'title', 'type' => 'value') 921 | ); 922 | $this->Controller->Prg->encode = true; 923 | $testData = $test = array('title' => 'Something new'); 924 | $result = $this->Controller->Prg->serializeParams($test); 925 | $this->assertEquals($this->_urlEncode('Something new'), $result['title']); 926 | 927 | $this->Controller->passedArgs = $result; 928 | $this->Controller->Prg->presetForm('Post'); 929 | $expected = array('Post' => $testData); 930 | $this->assertEquals($expected['Post'], $this->Controller->Prg->parsedParams()); 931 | $this->assertEquals($expected, $this->Controller->request->data); 932 | } 933 | 934 | /** 935 | * Test presetForm() with encoded parameters 936 | * 937 | * @return void 938 | */ 939 | public function testPresetFormWithEncodedParams() { 940 | $this->Controller->presetVars = array( 941 | array( 942 | 'field' => 'title', 943 | 'type' => 'value' 944 | ), 945 | array( 946 | 'field' => 'checkbox', 947 | 'type' => 'checkbox' 948 | ), 949 | array( 950 | 'field' => 'lookup', 951 | 'type' => 'lookup', 952 | 'formField' => 'lookup_input', 953 | 'modelField' => 'title', 954 | 'model' => 'Post' 955 | ) 956 | ); 957 | $this->Controller->passedArgs = array( 958 | 'title' => $this->_urlEncode('test'), 959 | 'checkbox' => $this->_urlEncode('test|test2|test3'), 960 | 'lookup' => $this->_urlEncode('1') 961 | ); 962 | 963 | $this->Controller->beforeFilter(); 964 | 965 | $this->Controller->Prg->encode = true; 966 | $this->Controller->Prg->presetForm('Post'); 967 | $expected = array( 968 | 'Post' => array( 969 | 'title' => 'test', 970 | 'checkbox' => array( 971 | 0 => 'test', 972 | 1 => 'test2', 973 | 2 => 'test3'), 974 | 'lookup' => 1, 975 | 'lookup_input' => 'First Post' 976 | ) 977 | ); 978 | $this->assertEquals($expected, $this->Controller->request->data); 979 | $this->assertTrue($this->Controller->Prg->isSearch); 980 | $this->assertEquals($expected['Post'], $this->Controller->Prg->parsedParams()); 981 | } 982 | 983 | /** 984 | * Test commonProcess() with empty value 985 | * 986 | * @return void 987 | */ 988 | public function testCommonProcessGetWithEmptyValue() { 989 | $this->Controller->request->params = array_merge( 990 | $this->Controller->request->params, 991 | array( 992 | 'named' => array(), 993 | 'category_id' => '0', 994 | ) 995 | ); 996 | $this->Controller->presetVars = array( 997 | array( 998 | 'field' => 'category_id', 999 | 'name' => 'category_id', 1000 | 'type' => 'value', 1001 | 'allowEmpty' => true, 1002 | 'emptyValue' => '0', 1003 | ), 1004 | array( 1005 | 'field' => 'checkbox', 1006 | 'name' => 'checkbox', 1007 | 'type' => 'checkbox' 1008 | ), 1009 | ); 1010 | $this->Controller->action = 'search'; 1011 | $this->Controller->request->data = array( 1012 | 'Post' => array( 1013 | 'category_id' => '0', 1014 | 'foo' => '' 1015 | ) 1016 | ); 1017 | $this->Controller->Prg->commonProcess('Post', array( 1018 | 'form' => 'Post', 1019 | 'modelMethod' => false, 1020 | 'filterEmpty' => true 1021 | ) 1022 | ); 1023 | $expected = array( 1024 | 'action' => 'search', 1025 | 'category_id' => null 1026 | ); 1027 | $this->assertEquals($expected, $this->Controller->redirectUrl); 1028 | } 1029 | 1030 | /** 1031 | * Test commonProcess() with empty value 1032 | * 1033 | * @return void 1034 | */ 1035 | public function testCommonProcessGetWithEmptyValueQueryStrings() { 1036 | $this->Controller->presetVars = array( 1037 | array( 1038 | 'field' => 'category_id', 1039 | 'name' => 'category_id', 1040 | 'type' => 'value', 1041 | 'allowEmpty' => true, 1042 | 'emptyValue' => '0', 1043 | ), 1044 | array( 1045 | 'field' => 'checkbox', 1046 | 'name' => 'checkbox', 1047 | 'type' => 'checkbox' 1048 | ), 1049 | ); 1050 | 1051 | $this->Controller->action = 'search'; 1052 | $this->Controller->request->data = array( 1053 | 'Post' => array( 1054 | 'category_id' => '0', 1055 | 'checkbox' => 'x' 1056 | ) 1057 | ); 1058 | 1059 | $this->Controller->Prg->commonProcess('Post', array( 1060 | 'form' => 'Post', 1061 | 'paramType' => 'querystring', 1062 | 'filterEmpty' => true 1063 | ) 1064 | ); 1065 | 1066 | $expected = array( 1067 | '?' => array( 1068 | 'category_id' => null, 1069 | 'checkbox' => 'x' 1070 | ), 1071 | 'action' => 'search' 1072 | ); 1073 | $this->assertEquals($expected, $this->Controller->redirectUrl); 1074 | } 1075 | 1076 | /** 1077 | * Test presetForm() with empty value 1078 | * 1079 | * @return void 1080 | */ 1081 | public function testPresetFormWithEmptyValue() { 1082 | $this->Controller->presetVars = array( 1083 | array( 1084 | 'field' => 'category_id', 1085 | 'type' => 'value', 1086 | 'allowEmpty' => true, 1087 | 'emptyValue' => '0', 1088 | ), 1089 | array( 1090 | 'field' => 'checkbox', 1091 | 'type' => 'checkbox', 1092 | 'allowEmpty' => true, 1093 | ), 1094 | ); 1095 | $this->Controller->passedArgs = array( 1096 | 'category_id' => '', 1097 | ); 1098 | $this->Controller->beforeFilter(); 1099 | 1100 | $this->Controller->Prg->encode = true; 1101 | $this->Controller->Prg->presetForm(array('model' => 'Post')); 1102 | $expected = array( 1103 | 'Post' => array( 1104 | 'category_id' => '0' 1105 | ) 1106 | ); 1107 | $this->assertEquals($expected, $this->Controller->request->data); 1108 | $this->assertFalse($this->Controller->Prg->isSearch); 1109 | 1110 | $expected = array( 1111 | 'Post' => array( 1112 | 'category_id' => '' 1113 | ) 1114 | ); 1115 | $this->assertEquals($expected['Post'], $this->Controller->Prg->parsedParams()); 1116 | } 1117 | 1118 | /** 1119 | * Test presetForm() with empty value and 1120 | * 1121 | * @return void 1122 | */ 1123 | public function testPresetFormWithEmptyValueAndIsSearch() { 1124 | $this->Controller->presetVars = array( 1125 | array( 1126 | 'field' => 'category_id', 1127 | 'type' => 'value', 1128 | 'allowEmpty' => true, 1129 | 'emptyValue' => '0', 1130 | ), 1131 | array( 1132 | 'field' => 'checkbox', 1133 | 'type' => 'checkbox' 1134 | ), 1135 | ); 1136 | $this->Controller->passedArgs = array( 1137 | 'category_id' => '', 1138 | 'checkbox' => $this->_urlEncode('test|test2|test3'), 1139 | ); 1140 | $this->Controller->beforeFilter(); 1141 | 1142 | $this->Controller->Prg->encode = true; 1143 | $this->Controller->Prg->presetForm(array('model' => 'Post')); 1144 | $expected = array( 1145 | 'Post' => array( 1146 | 'category_id' => '0', 1147 | 'checkbox' => array( 1148 | 0 => 'test', 1149 | 1 => 'test2', 1150 | 2 => 'test3' 1151 | ) 1152 | ) 1153 | ); 1154 | $this->assertEquals($expected, $this->Controller->request->data); 1155 | $this->assertTrue($this->Controller->Prg->isSearch); 1156 | 1157 | $expected = array( 1158 | 'Post' => array( 1159 | 'category_id' => '', 1160 | 'checkbox' => array( 1161 | 0 => 'test', 1162 | 1 => 'test2', 1163 | 2 => 'test3' 1164 | ) 1165 | ) 1166 | ); 1167 | $this->assertEquals($expected['Post'], $this->Controller->Prg->parsedParams()); 1168 | } 1169 | 1170 | } 1171 | -------------------------------------------------------------------------------- /Test/Case/Model/Behavior/SearchableBehaviorTest.php: -------------------------------------------------------------------------------- 1 | alias . '.views > 10'; 38 | break; 39 | case 'comments': 40 | $cond = $Model->alias . '.comments > 10'; 41 | break; 42 | } 43 | return (array)$cond; 44 | } 45 | 46 | } 47 | 48 | /** 49 | * Tag test model 50 | */ 51 | class Tag extends CakeTestModel { 52 | } 53 | 54 | /** 55 | * Tagged test model 56 | */ 57 | class Tagged extends CakeTestModel { 58 | 59 | /** 60 | * Table to use 61 | * 62 | * @var string 63 | */ 64 | public $useTable = 'tagged'; 65 | 66 | /** 67 | * Belongs To Associations 68 | * 69 | * @var array 70 | */ 71 | public $belongsTo = array('Tag'); 72 | 73 | } 74 | 75 | /** 76 | * Article test model 77 | * 78 | * Contains various find and condition methods used by the tests below. 79 | */ 80 | class Article extends CakeTestModel { 81 | 82 | /** 83 | * Attach the SearchableBehavior by default 84 | * 85 | * @var array 86 | */ 87 | public $actsAs = array('Search.Searchable'); 88 | 89 | /** 90 | * HABTM associations 91 | * 92 | * @var array 93 | */ 94 | public $hasAndBelongsToMany = array('Tag' => array('with' => 'Tagged')); 95 | 96 | /** 97 | * Find by tags 98 | * 99 | * @param array $data 100 | * @return array 101 | */ 102 | public function findByTags($data = array()) { 103 | $this->Tagged->Behaviors->attach('Containable', array('autoFields' => false)); 104 | $this->Tagged->Behaviors->attach('Search.Searchable'); 105 | $conditions = array(); 106 | if (!empty($data['tags'])) { 107 | $conditions = array('Tag.name' => $data['tags']); 108 | } 109 | $this->Tagged->order = null; 110 | $query = $this->Tagged->getQuery('all', array( 111 | 'conditions' => $conditions, 112 | 'fields' => array('foreign_key'), 113 | 'contain' => array('Tag') 114 | )); 115 | return $query; 116 | } 117 | 118 | /** 119 | * Makes an array of range numbers that matches the ones on the interface. 120 | * 121 | * @param $data 122 | * @param null $field 123 | * @return array 124 | */ 125 | public function makeRangeCondition($data, $field = null) { 126 | if (is_string($data)) { 127 | $input = $data; 128 | } 129 | if (is_array($data)) { 130 | if (!empty($field['name'])) { 131 | $input = $data[$field['name']]; 132 | } else { 133 | $input = $data['range']; 134 | } 135 | } 136 | switch ($input) { 137 | case '10': 138 | return array(0, 10); 139 | case '100': 140 | return array(11, 100); 141 | case '1000': 142 | return array(101, 1000); 143 | default: 144 | return array(0, 0); 145 | } 146 | } 147 | 148 | /** 149 | * orConditions 150 | * 151 | * @param array $data 152 | * @return array 153 | */ 154 | public function orConditions($data = array()) { 155 | $filter = $data['filter']; 156 | $cond = array( 157 | 'OR' => array( 158 | $this->alias . '.title LIKE' => '%' . $filter . '%', 159 | $this->alias . '.body LIKE' => '%' . $filter . '%', 160 | )); 161 | return $cond; 162 | } 163 | 164 | public function or2Conditions($data = array()) { 165 | $filter = $data['filter2']; 166 | $cond = array( 167 | 'OR' => array( 168 | $this->alias . '.field1 LIKE' => '%' . $filter . '%', 169 | $this->alias . '.field2 LIKE' => '%' . $filter . '%', 170 | )); 171 | return $cond; 172 | } 173 | 174 | } 175 | 176 | /** 177 | * SearchableTestCase 178 | */ 179 | class SearchableBehaviorTest extends CakeTestCase { 180 | 181 | /** 182 | * Article test model 183 | * 184 | * @var 185 | */ 186 | public $Article; 187 | 188 | /** 189 | * Load relevant fixtures 190 | * 191 | * @var array 192 | */ 193 | public $fixtures = array( 194 | 'plugin.search.article', 195 | 'plugin.search.tag', 196 | 'plugin.search.tagged', 197 | 'core.user' 198 | ); 199 | 200 | /** 201 | * Load Article test model 202 | * 203 | * @return void 204 | */ 205 | public function setUp() { 206 | parent::setUp(); 207 | 208 | $this->Article = ClassRegistry::init('Article'); 209 | } 210 | 211 | /** 212 | * Release Article test model 213 | * 214 | * @return void 215 | */ 216 | public function tearDown() { 217 | parent::tearDown(); 218 | 219 | unset($this->Article); 220 | } 221 | 222 | /** 223 | * Test getWildcards() 224 | * 225 | * @return void 226 | */ 227 | public function testGetWildcards() { 228 | $result = $this->Article->getWildcards(); 229 | $expected = array('any' => '*', 'one' => '?'); 230 | $this->assertSame($expected, $result); 231 | 232 | $this->Article->Behaviors->Searchable->settings['Article']['wildcardAny'] = false; 233 | $this->Article->Behaviors->Searchable->settings['Article']['wildcardOne'] = false; 234 | $result = $this->Article->getWildcards(); 235 | $expected = array('any' => false, 'one' => false); 236 | $this->assertSame($expected, $result); 237 | 238 | $this->Article->Behaviors->Searchable->settings['Article']['wildcardAny'] = '%'; 239 | $this->Article->Behaviors->Searchable->settings['Article']['wildcardOne'] = '_'; 240 | $result = $this->Article->getWildcards(); 241 | $expected = array('any' => '%', 'one' => '_'); 242 | $this->assertSame($expected, $result); 243 | } 244 | 245 | /** 246 | * Test 'value' filter type 247 | * 248 | * @return void 249 | * @link http://github.com/CakeDC/Search/issues#issue/3 250 | */ 251 | public function testValueCondition() { 252 | $this->Article->filterArgs = array( 253 | array('name' => 'slug', 'type' => 'value')); 254 | $this->Article->Behaviors->load('Search.Searchable'); 255 | $data = array(); 256 | $result = $this->Article->parseCriteria($data); 257 | $this->assertEquals(array(), $result); 258 | 259 | $data = array('slug' => 'first_article'); 260 | $result = $this->Article->parseCriteria($data); 261 | $expected = array('Article.slug' => 'first_article'); 262 | $this->assertEquals($expected, $result); 263 | 264 | $this->Article->filterArgs = array( 265 | array('name' => 'fakeslug', 'type' => 'value', 'field' => 'Article2.slug')); 266 | $this->Article->Behaviors->load('Search.Searchable'); 267 | $data = array('fakeslug' => 'first_article'); 268 | $result = $this->Article->parseCriteria($data); 269 | $expected = array('Article2.slug' => 'first_article'); 270 | $this->assertEquals($expected, $result); 271 | 272 | // Testing http://github.com/CakeDC/Search/issues#issue/3 273 | $this->Article->filterArgs = array( 274 | array('name' => 'views', 'type' => 'value')); 275 | $this->Article->Behaviors->load('Search.Searchable'); 276 | $data = array('views' => '0'); 277 | $result = $this->Article->parseCriteria($data); 278 | $this->assertEquals(array('Article.views' => 0), $result); 279 | 280 | $this->Article->filterArgs = array( 281 | array('name' => 'views', 'type' => 'value')); 282 | $this->Article->Behaviors->load('Search.Searchable'); 283 | $data = array('views' => 0); 284 | $result = $this->Article->parseCriteria($data); 285 | $this->assertEquals(array('Article.views' => 0), $result); 286 | 287 | $this->Article->filterArgs = array( 288 | array('name' => 'views', 'type' => 'value')); 289 | $this->Article->Behaviors->load('Search.Searchable'); 290 | $data = array('views' => ''); 291 | $result = $this->Article->parseCriteria($data); 292 | $this->assertEquals(array(), $result); 293 | 294 | // Multiple fields + cross model searches 295 | $this->Article->filterArgs = array( 296 | 'faketitle' => array('type' => 'value', 'field' => array('title', 'User.name')) 297 | ); 298 | $this->Article->Behaviors->load('Search.Searchable'); 299 | $data = array('faketitle' => 'First'); 300 | $result = $this->Article->parseCriteria($data); 301 | $expected = array('OR' => array('Article.title' => 'First', 'User.name' => 'First')); 302 | $this->assertEquals($expected, $result); 303 | 304 | // Multiple select dropdown 305 | $this->Article->filterArgs = array( 306 | 'fakesource' => array('type' => 'value') 307 | ); 308 | $this->Article->Behaviors->load('Search.Searchable'); 309 | $data = array('fakesource' => array(5, 9)); 310 | $result = $this->Article->parseCriteria($data); 311 | $expected = array('Article.fakesource' => array(5, 9)); 312 | $this->assertEquals($expected, $result); 313 | } 314 | 315 | /** 316 | * Test 'like' filter type 317 | * 318 | * @return void 319 | */ 320 | public function testLikeCondition() { 321 | $this->Article->filterArgs = array( 322 | array('name' => 'title', 'type' => 'like')); 323 | $this->Article->Behaviors->load('Search.Searchable'); 324 | 325 | $data = array(); 326 | $result = $this->Article->parseCriteria($data); 327 | $this->assertEquals(array(), $result); 328 | 329 | $data = array('title' => 'First'); 330 | $result = $this->Article->parseCriteria($data); 331 | $expected = array('Article.title LIKE' => '%First%'); 332 | $this->assertEquals($expected, $result); 333 | 334 | $this->Article->filterArgs = array( 335 | array('name' => 'faketitle', 'type' => 'like', 'field' => 'Article.title')); 336 | $this->Article->Behaviors->load('Search.Searchable'); 337 | 338 | $data = array('faketitle' => 'First'); 339 | $result = $this->Article->parseCriteria($data); 340 | $expected = array('Article.title LIKE' => '%First%'); 341 | $this->assertEquals($expected, $result); 342 | 343 | // Wildcards should be treated as normal text 344 | $this->Article->filterArgs = array( 345 | array('name' => 'faketitle', 'type' => 'like', 'field' => 'Article.title') 346 | ); 347 | $this->Article->Behaviors->load('Search.Searchable'); 348 | $data = array('faketitle' => '%First_'); 349 | $result = $this->Article->parseCriteria($data); 350 | $expected = array('Article.title LIKE' => '%\%First\_%'); 351 | $this->assertEquals($expected, $result); 352 | 353 | // Working with like settings 354 | $this->Article->Behaviors->Searchable->settings['Article']['like']['before'] = false; 355 | $result = $this->Article->parseCriteria($data); 356 | $expected = array('Article.title LIKE' => '\%First\_%'); 357 | $this->assertEquals($expected, $result); 358 | 359 | $this->Article->Behaviors->Searchable->settings['Article']['like']['after'] = false; 360 | $result = $this->Article->parseCriteria($data); 361 | $expected = array('Article.title LIKE' => '\%First\_'); 362 | $this->assertEquals($expected, $result); 363 | 364 | // Now custom like should be possible 365 | $data = array('faketitle' => '*First?'); 366 | $this->Article->Behaviors->Searchable->settings['Article']['like']['after'] = false; 367 | $result = $this->Article->parseCriteria($data); 368 | $expected = array('Article.title LIKE' => '%First_'); 369 | $this->assertEquals($expected, $result); 370 | 371 | $data = array('faketitle' => 'F?rst'); 372 | $this->Article->Behaviors->Searchable->settings['Article']['like']['before'] = true; 373 | $this->Article->Behaviors->Searchable->settings['Article']['like']['after'] = true; 374 | $result = $this->Article->parseCriteria($data); 375 | $expected = array('Article.title LIKE' => '%F_rst%'); 376 | $this->assertEquals($expected, $result); 377 | 378 | $data = array('faketitle' => 'F*t'); 379 | $this->Article->Behaviors->Searchable->settings['Article']['like']['before'] = true; 380 | $this->Article->Behaviors->Searchable->settings['Article']['like']['after'] = true; 381 | $result = $this->Article->parseCriteria($data); 382 | $expected = array('Article.title LIKE' => '%F%t%'); 383 | $this->assertEquals($expected, $result); 384 | 385 | // now we try the default wildcards % and _ 386 | $data = array('faketitle' => '*First?'); 387 | $this->Article->Behaviors->Searchable->settings['Article']['like']['before'] = false; 388 | $this->Article->Behaviors->Searchable->settings['Article']['like']['after'] = false; 389 | $this->Article->Behaviors->Searchable->settings['Article']['wildcardAny'] = '%'; 390 | $this->Article->Behaviors->Searchable->settings['Article']['wildcardOne'] = '_'; 391 | $result = $this->Article->parseCriteria($data); 392 | $expected = array('Article.title LIKE' => '*First?'); 393 | $this->assertEquals($expected, $result); 394 | 395 | // Now it is possible and makes sense to allow wildcards in between (custom wildcard use case) 396 | $data = array('faketitle' => '%Fi_st_'); 397 | $result = $this->Article->parseCriteria($data); 398 | $expected = array('Article.title LIKE' => '%Fi_st_'); 399 | $this->assertEquals($expected, $result); 400 | 401 | // Shortcut disable/enable like before/after 402 | $data = array('faketitle' => '%First_'); 403 | $this->Article->Behaviors->Searchable->settings['Article']['like'] = false; 404 | $result = $this->Article->parseCriteria($data); 405 | $expected = array('Article.title LIKE' => '%First_'); 406 | $this->assertEquals($expected, $result); 407 | 408 | // Multiple OR fields per field 409 | $this->Article->filterArgs = array( 410 | array('name' => 'faketitle', 'type' => 'like', 'field' => array('title', 'descr')) 411 | ); 412 | $this->Article->Behaviors->load('Search.Searchable'); 413 | $data = array('faketitle' => 'First'); 414 | $this->Article->Behaviors->Searchable->settings['Article']['like'] = true; 415 | $result = $this->Article->parseCriteria($data); 416 | $expected = array('OR' => array('Article.title LIKE' => '%First%', 417 | 'Article.descr LIKE' => '%First%') 418 | ); 419 | $this->assertEquals($expected, $result); 420 | 421 | // Set before => false dynamically 422 | $this->Article->filterArgs = array( 423 | array('name' => 'faketitle', 424 | 'type' => 'like', 425 | 'field' => array('title', 'descr'), 426 | 'before' => false 427 | ) 428 | ); 429 | $this->Article->Behaviors->load('Search.Searchable'); 430 | $data = array('faketitle' => 'First'); 431 | $result = $this->Article->parseCriteria($data); 432 | $expected = array('OR' => array('Article.title LIKE' => 'First%', 433 | 'Article.descr LIKE' => 'First%') 434 | ); 435 | $this->assertEquals($expected, $result); 436 | 437 | // Manually define the before/after type 438 | $this->Article->filterArgs = array( 439 | array('name' => 'faketitle', 'type' => 'like', 'field' => array('title'), 440 | 'before' => '_', 'after' => '_') 441 | ); 442 | $this->Article->Behaviors->load('Search.Searchable'); 443 | $data = array('faketitle' => 'First'); 444 | $result = $this->Article->parseCriteria($data); 445 | $expected = array('Article.title LIKE' => '_First_'); 446 | $this->assertEquals($expected, $result); 447 | 448 | // Cross model searches + named keys (shorthand) 449 | $this->Article->bindModel(array('belongsTo' => array('User'))); 450 | $this->Article->filterArgs = array( 451 | 'faketitle' => array('type' => 'like', 'field' => array('title', 'User.name'), 452 | 'before' => false, 'after' => true) 453 | ); 454 | $this->Article->Behaviors->load('Search.Searchable'); 455 | $data = array('faketitle' => 'First'); 456 | $result = $this->Article->parseCriteria($data); 457 | $expected = array('OR' => array('Article.title LIKE' => 'First%', 'User.name LIKE' => 'First%')); 458 | $this->assertEquals($expected, $result); 459 | 460 | // With already existing or conditions + named keys (shorthand) 461 | $this->Article->filterArgs = array( 462 | 'faketitle' => array('type' => 'like', 'field' => array('title', 'User.name'), 463 | 'before' => false, 'after' => true), 464 | 'otherfaketitle' => array('type' => 'like', 'field' => array('descr', 'comment'), 465 | 'before' => false, 'after' => true) 466 | ); 467 | $this->Article->Behaviors->load('Search.Searchable'); 468 | 469 | $data = array('faketitle' => 'First', 'otherfaketitle' => 'Second'); 470 | $result = $this->Article->parseCriteria($data); 471 | $expected = array( 472 | 'OR' => array('Article.title LIKE' => 'First%', 'User.name LIKE' => 'First%'), 473 | array('OR' => array( 474 | 'Article.descr LIKE' => 'Second%', 475 | 'Article.comment LIKE' => 'Second%') 476 | ) 477 | ); 478 | $this->assertEquals($expected, $result); 479 | 480 | // Wildcards and and/or connectors 481 | $this->Article->Behaviors->unload('Search.Searchable'); 482 | $this->Article->filterArgs = array( 483 | array('name' => 'faketitle', 'type' => 'like', 'field' => 'Article.title', 484 | 'connectorAnd' => '+', 'connectorOr' => ',', 'before' => true, 'after' => true) 485 | ); 486 | $this->Article->Behaviors->load('Search.Searchable'); 487 | $data = array('faketitle' => 'First%+Second%, Third%'); 488 | $result = $this->Article->parseCriteria($data); 489 | $expected = array(0 => array('OR' => array( 490 | array('AND' => array( 491 | array('Article.title LIKE' => '%First\%%'), 492 | array('Article.title LIKE' => '%Second\%%'), 493 | )), 494 | array('AND' => array( 495 | array('Article.title LIKE' => '%Third\%%') 496 | )), 497 | ))); 498 | $this->assertEquals($expected, $result); 499 | } 500 | 501 | /** 502 | * Test 'subquery' filter type 503 | * 504 | * @return void 505 | */ 506 | public function testSubQueryCondition() { 507 | if ($this->db->config['datasource'] !== 'Database/Mysql') { 508 | $this->markTestSkipped('Test requires mysql db.'); 509 | } 510 | $database = $this->db->config['database']; 511 | 512 | $this->Article->filterArgs = array( 513 | array('name' => 'tags', 'type' => 'subquery', 'method' => 'findByTags', 'field' => 'Article.id') 514 | ); 515 | 516 | $data = array(); 517 | $result = $this->Article->parseCriteria($data); 518 | $this->assertEquals(array(), $result); 519 | 520 | $data = array('tags' => 'Cake'); 521 | $result = $this->Article->parseCriteria($data); 522 | $expression = $this->Article->getDatasource()->expression( 523 | 'Article.id in (SELECT `Tagged`.`foreign_key` FROM `' . 524 | $database . '`.`' . $this->Article->tablePrefix . 'tagged` AS `Tagged` LEFT JOIN `' . 525 | $database . '`.`' . $this->Article->tablePrefix . 526 | 'tags` AS `Tag` ON (`Tagged`.`tag_id` = `Tag`.`id`) WHERE `Tag`.`name` = \'Cake\')' 527 | ); 528 | $expected = array($expression); 529 | $this->assertEquals($expected, $result); 530 | } 531 | 532 | /** 533 | * Test 'subquery' filter type when 'allowEmpty' = true 534 | * 535 | * @return void 536 | */ 537 | public function testSubQueryEmptyCondition() { 538 | if ($this->db->config['datasource'] !== 'Database/Mysql') { 539 | $this->markTestSkipped('Test requires mysql db.'); 540 | } 541 | $database = $this->db->config['database']; 542 | 543 | // Old syntax 544 | $this->Article->filterArgs = array( 545 | array('name' => 'tags', 'type' => 'subquery', 'method' => 'findByTags', 546 | 'field' => 'Article.id', 'allowEmpty' => true 547 | ) 548 | ); 549 | 550 | $data = array('tags' => 'Cake'); 551 | $this->Article->parseCriteria($data); 552 | $expression = $this->Article->getDatasource()->expression( 553 | 'Article.id in (SELECT `Tagged`.`foreign_key` FROM `' . 554 | $database . '`.`' . $this->Article->tablePrefix . 'tagged` AS `Tagged` LEFT JOIN `' . 555 | $database . '`.`' . $this->Article->tablePrefix . 556 | 'tags` AS `Tag` ON (`Tagged`.`tag_id` = `Tag`.`id`) WHERE `Tag`.`name` = \'Cake\')' 557 | ); 558 | $expected = array($expression); 559 | 560 | // New syntax 561 | $this->Article->filterArgs = array( 562 | 'tags' => array('type' => 'subquery', 'method' => 'findByTags', 563 | 'field' => 'Article.id', 'allowEmpty' => true 564 | ) 565 | ); 566 | $this->Article->Behaviors->load('Search.Searchable'); 567 | 568 | $result = $this->Article->parseCriteria($data); 569 | $this->assertEquals($expected, $result); 570 | } 571 | 572 | /** 573 | * Test 'query' filter type with one orConditions method 574 | * 575 | * Uses ``Article::orConditions()``. 576 | * 577 | * @return void 578 | */ 579 | public function testQueryOneOrConditions() { 580 | $this->Article->filterArgs = array( 581 | array('name' => 'filter', 'type' => 'query', 'method' => 'orConditions')); 582 | 583 | $data = array(); 584 | $result = $this->Article->parseCriteria($data); 585 | $this->assertEquals(array(), $result); 586 | 587 | $data = array('filter' => 'ticl'); 588 | $result = $this->Article->parseCriteria($data); 589 | $expected = array('OR' => array( 590 | 'Article.title LIKE' => '%ticl%', 591 | 'Article.body LIKE' => '%ticl%')); 592 | $this->assertEquals($expected, $result); 593 | } 594 | 595 | /** 596 | * Test 'query' filter type with two orConditions methods 597 | * 598 | * Uses ``Article::orConditions()`` and ``Article::or2Conditions()``. 599 | * 600 | * @return void 601 | */ 602 | public function testQueryOrTwoOrConditions() { 603 | $this->Article->filterArgs = array( 604 | array('name' => 'filter', 'type' => 'query', 'method' => 'orConditions'), 605 | array('name' => 'filter2', 'type' => 'query', 'method' => 'or2Conditions')); 606 | 607 | $data = array(); 608 | $result = $this->Article->parseCriteria($data); 609 | $this->assertEquals(array(), $result); 610 | 611 | $data = array('filter' => 'ticl', 'filter2' => 'test'); 612 | $result = $this->Article->parseCriteria($data); 613 | $expected = array('OR' => array( 614 | 'Article.title LIKE' => '%ticl%', 615 | 'Article.body LIKE' => '%ticl%', 616 | 'Article.field1 LIKE' => '%test%', 617 | 'Article.field2 LIKE' => '%test%')); 618 | $this->assertEquals($expected, $result); 619 | } 620 | 621 | /** 622 | * Test 'query' filter type with behavior condition method 623 | * 624 | * Uses ``FilterBehavior::FilterBehavior::mostFilterConditions()``. 625 | * 626 | * @return void 627 | */ 628 | public function testQueryWithBehaviorCondition() { 629 | $this->Article->Behaviors->load('Filter'); 630 | $this->Article->filterArgs = array( 631 | array('name' => 'filter', 'type' => 'query', 'method' => 'mostFilterConditions')); 632 | 633 | $data = array(); 634 | $result = $this->Article->parseCriteria($data); 635 | $this->assertEquals(array(), $result); 636 | 637 | $data = array('filter' => 'views'); 638 | $result = $this->Article->parseCriteria($data); 639 | $expected = array('Article.views > 10'); 640 | $this->assertEquals($expected, $result); 641 | } 642 | 643 | /** 644 | * Test 'expression' filter type 645 | * 646 | * Uses ``Article::makeRangeCondition()`` and 647 | * a non-existent one. 648 | * 649 | * @return void 650 | */ 651 | public function testExpressionCallCondition() { 652 | $this->Article->filterArgs = array( 653 | array('name' => 'range', 'type' => 'expression', 'method' => 'makeRangeCondition', 654 | 'field' => 'Article.views BETWEEN ? AND ?') 655 | ); 656 | $data = array(); 657 | $result = $this->Article->parseCriteria($data); 658 | $this->assertEquals(array(), $result); 659 | 660 | $data = array('range' => '10'); 661 | $result = $this->Article->parseCriteria($data); 662 | $expected = array('Article.views BETWEEN ? AND ?' => array(0, 10)); 663 | $this->assertEquals($expected, $result); 664 | 665 | $this->Article->filterArgs = array( 666 | array('name' => 'range', 'type' => 'expression', 'method' => 'testThatInBehaviorMethodNotDefined', 667 | 'field' => 'Article.views BETWEEN ? AND ?') 668 | ); 669 | $data = array('range' => '10'); 670 | $result = $this->Article->parseCriteria($data); 671 | $this->assertEquals(array(), $result); 672 | } 673 | 674 | /** 675 | * Test 'query' filter type with 'defaultValue' set 676 | * 677 | * @return void 678 | */ 679 | public function testDefaultValue() { 680 | $this->Article->filterArgs = array( 681 | 'range' => array('type' => 'expression', 'defaultValue' => '100', 'method' => 'makeRangeCondition', 682 | 'field' => 'Article.views BETWEEN ? AND ?') 683 | ); 684 | $this->Article->Behaviors->load('Search.Searchable'); 685 | 686 | $data = array(); 687 | $result = $this->Article->parseCriteria($data); 688 | $expected = array( 689 | 'Article.views BETWEEN ? AND ?' => array(11, 100)); 690 | $this->assertEquals($expected, $result); 691 | } 692 | 693 | /** 694 | * Test unbindAllModels() 695 | * 696 | * @return void 697 | */ 698 | public function testUnbindAll() { 699 | $this->Article->unbindAllModels(); 700 | $this->assertEquals(array(), $this->Article->belongsTo); 701 | $this->assertEquals(array(), $this->Article->hasMany); 702 | $this->assertEquals(array(), $this->Article->hasAndBelongsToMany); 703 | $this->assertEquals(array(), $this->Article->hasOne); 704 | } 705 | 706 | /** 707 | * Test validateSearch() 708 | * 709 | * @return void 710 | */ 711 | public function testValidateSearch() { 712 | $this->Article->filterArgs = array(); 713 | $data = array('Article' => array('title' => 'Last Article')); 714 | $this->Article->set($data); 715 | $this->Article->validateSearch(); 716 | $this->assertEquals($data, $this->Article->data); 717 | 718 | $this->Article->validateSearch($data); 719 | $this->assertEquals($data, $this->Article->data); 720 | 721 | $data = array('Article' => array('title' => '')); 722 | $this->Article->validateSearch($data); 723 | $expected = array('Article' => array()); 724 | $this->assertEquals($expected, $this->Article->data); 725 | } 726 | 727 | /** 728 | * Test passedArgs() 729 | * 730 | * @return void 731 | */ 732 | public function testPassedArgs() { 733 | $this->Article->filterArgs = array( 734 | array('name' => 'slug', 'type' => 'value')); 735 | $data = array('slug' => 'first_article', 'filter' => 'myfilter'); 736 | $result = $this->Article->passedArgs($data); 737 | $expected = array('slug' => 'first_article'); 738 | $this->assertEquals($expected, $result); 739 | } 740 | 741 | /** 742 | * Test getQuery() 743 | * 744 | * @return void 745 | */ 746 | public function testGetQuery() { 747 | if ($this->db->config['datasource'] !== 'Database/Mysql') { 748 | $this->markTestSkipped('Test requires mysql db.'); 749 | } 750 | $database = $this->db->config['database']; 751 | 752 | $conditions = array('Article.id' => 1); 753 | $result = $this->Article->getQuery('all', array( 754 | 'conditions' => $conditions, 755 | 'order' => 'title', 756 | 'page' => 2, 757 | 'limit' => 2, 758 | 'fields' => array('id', 'title') 759 | )); 760 | $expected = 'SELECT `Article`.`id`, `Article`.`title` FROM `' . 761 | $database . '`.`' . $this->Article->tablePrefix . 762 | 'articles` AS `Article` WHERE `Article`.`id` = 1 ORDER BY `title` ASC LIMIT 2, 2'; 763 | $this->assertEquals($expected, $result); 764 | 765 | $this->Article->Tagged->Behaviors->attach('Search.Searchable'); 766 | $conditions = array('Tagged.tag_id' => 1); 767 | $this->Article->Tagged->recursive = -1; 768 | $order = array('Tagged.id' => 'ASC'); 769 | $result = $this->Article->Tagged->getQuery('first', compact('conditions', 'order')); 770 | $expected = 'SELECT `Tagged`.`id`, `Tagged`.`foreign_key`, `Tagged`.`tag_id`, ' . 771 | '`Tagged`.`model`, `Tagged`.`language`, `Tagged`.`created`, `Tagged`.`modified` ' . 772 | 'FROM `' . $database . '`.`' . $this->Article->tablePrefix . 773 | 'tagged` AS `Tagged` WHERE `Tagged`.`tag_id` = \'1\' ORDER BY `Tagged`.`id` ASC LIMIT 1'; 774 | $this->assertEquals($expected, $result); 775 | } 776 | 777 | /** 778 | * Test whether 'allowEmpty' will be respected 779 | * 780 | * @return void 781 | */ 782 | public function testAllowEmptyWithNullValues() { 783 | // Author is just empty, created will be mapped against schema default (NULL) 784 | // and slug omitted as its NULL already 785 | $this->Article->filterArgs = array( 786 | 'title' => array( 787 | 'name' => 'title', 788 | 'type' => 'like', 789 | 'field' => 'Article.title', 790 | 'allowEmpty' => true 791 | ), 792 | 'author' => array( 793 | 'name' => 'author', 794 | 'type' => 'value', 795 | 'field' => 'Article.author', 796 | 'allowEmpty' => true 797 | ), 798 | 'created' => array( 799 | 'name' => 'created', 800 | 'type' => 'value', 801 | 'field' => 'Article.created', 802 | 'allowEmpty' => true 803 | ), 804 | 'slug' => array( 805 | 'name' => 'slug', 806 | 'type' => 'value', 807 | 'field' => 'Article.slug', 808 | 'allowEmpty' => true 809 | ), 810 | ); 811 | $data = array('title' => 'first', 'author' => '', 'created' => '', 'slug' => null); 812 | $expected = array( 813 | 'Article.title LIKE' => '%first%', 814 | 'Article.author' => '', 815 | 'Article.created' => null, 816 | ); 817 | $result = $this->Article->parseCriteria($data); 818 | $this->assertSame($expected, $result); 819 | } 820 | 821 | } 822 | -------------------------------------------------------------------------------- /Test/Fixture/ArticleFixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'integer', 'key' => 'primary'), 24 | 'title' => array('type' => 'string', 'null' => false), 25 | 'body' => array('type' => 'text', 'null' => false), 26 | 'slug' => array('type' => 'string', 'null' => false), 27 | 'views' => array('type' => 'integer', 'null' => false), 28 | 'comments' => array('type' => 'integer', 'null' => false, 'default' => '0', 'length' => 10), 29 | 'created' => array('datetime', 'null' => true, 'default' => null), 30 | 'updated' => array('datetime', 'null' => true, 'default' => null) 31 | ); 32 | 33 | /** 34 | * Records 35 | * 36 | * @var array $records 37 | */ 38 | public $records = array( 39 | array('id' => 1, 'title' => 'First Article', 'body' => 'First Article', 40 | 'slug' => 'first_article', 'views' => 2, 'comments' => 1, 41 | 'created' => '2007-03-18 10:39:23', 'updated' => '2007-03-18 10:41:31' 42 | ), 43 | array('id' => 2, 'title' => 'Second Article', 'body' => 'Second Article', 44 | 'slug' => 'second_article', 'views' => 1, 'comments' => 2, 45 | 'created' => '2007-03-18 10:41:23', 'updated' => '2007-03-18 10:43:31' 46 | ), 47 | array('id' => 3, 'title' => 'Third Article', 'body' => 'Third Article', 48 | 'slug' => 'third_article', 'views' => 2, 'comments' => 3, 49 | 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31' 50 | ), 51 | ); 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Test/Fixture/PostFixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'integer', 'key' => 'primary'), 24 | 'title' => array('type' => 'string', 'null' => false), 25 | 'slug' => array('type' => 'string', 'null' => false), 26 | 'views' => array('type' => 'integer', 'null' => false), 27 | 'comments' => array('type' => 'integer', 'null' => false, 'default' => '0', 'length' => 10), 28 | 'created' => array('type' => 'datetime', 'null' => true, 'default' => null), 29 | 'updated' => array('type' => 'datetime', 'null' => true, 'default' => null), 30 | ); 31 | 32 | /** 33 | * Records 34 | * 35 | * @var array $records 36 | */ 37 | public $records = array( 38 | array('id' => 1, 'title' => 'First Post', 39 | 'slug' => 'first_post', 'views' => 2, 'comments' => 1, 40 | 'created' => '2007-03-18 10:39:23', 'updated' => '2007-03-18 10:41:31' 41 | ), 42 | array('id' => 2, 'title' => 'Second Post', 43 | 'slug' => 'second_post', 'views' => 1, 'comments' => 2, 44 | 'created' => '2007-03-18 10:41:23', 'updated' => '2007-03-18 10:43:31' 45 | ), 46 | array('id' => 3, 'title' => 'Third Post', 47 | 'slug' => 'third_post', 'views' => 2, 'comments' => 3, 48 | 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31' 49 | ), 50 | ); 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Test/Fixture/TagFixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'string', 'null' => false, 'default' => null, 'length' => 36, 'key' => 'primary'), 31 | 'identifier' => array('type' => 'string', 'null' => true, 'default' => null, 'length' => 30, 'key' => 'index'), 32 | 'name' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 30), 33 | 'keyname' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 30), 34 | 'weight' => array('type' => 'integer', 'null' => false, 'default' => 0, 'length' => 2), 35 | 'created' => array('type' => 'datetime', 'null' => true, 'default' => null), 36 | 'modified' => array('type' => 'datetime', 'null' => true, 'default' => null), 37 | 'indexes' => array( 38 | 'PRIMARY' => array('column' => 'id', 'unique' => 1), 39 | 'UNIQUE_TAG' => array('column' => array('identifier', 'keyname'), 'unique' => 1) 40 | ) 41 | ); 42 | 43 | /** 44 | * Records 45 | * 46 | * @var array $records 47 | */ 48 | public $records = array( 49 | array( 50 | 'id' => 1, 51 | 'identifier' => null, 52 | 'name' => 'CakePHP', 53 | 'keyname' => 'cakephp', 54 | 'weight' => 2, 55 | 'created' => '2008-06-02 18:18:11', 56 | 'modified' => '2008-06-02 18:18:37'), 57 | array( 58 | 'id' => 2, 59 | 'identifier' => null, 60 | 'name' => 'CakeDC', 61 | 'keyname' => 'cakedc', 62 | 'weight' => 2, 63 | 'created' => '2008-06-01 18:18:15', 64 | 'modified' => '2008-06-01 18:18:15'), 65 | array( 66 | 'id' => 3, 67 | 'identifier' => null, 68 | 'name' => 'CakeDC', 69 | 'keyname' => 'cakedc', 70 | 'weight' => 2, 71 | 'created' => '2008-06-01 18:18:15', 72 | 'modified' => '2008-06-01 18:18:15', 73 | ) 74 | ); 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Test/Fixture/TaggedFixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'string', 'null' => false, 'default' => null, 'length' => 36, 'key' => 'primary'), 31 | 'foreign_key' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 36), 32 | 'tag_id' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 36), 33 | 'model' => array('type' => 'string', 'null' => false, 'default' => null, 'key' => 'index'), 34 | 'language' => array('type' => 'string', 'null' => true, 'default' => null, 'length' => 6), 35 | 'created' => array('type' => 'datetime', 'null' => true, 'default' => null), 36 | 'modified' => array('type' => 'datetime', 'null' => true, 'default' => null), 37 | 'indexes' => array( 38 | 'PRIMARY' => array('column' => 'id', 'unique' => 1), 39 | 'UNIQUE_TAGGING' => array('column' => array('model', 'foreign_key', 'tag_id', 'language'), 'unique' => 1), 40 | 'INDEX_TAGGED' => array('column' => 'model', 'unique' => 0), 41 | 'INDEX_LANGUAGE' => array('column' => 'language', 'unique' => 0) 42 | ) 43 | ); 44 | 45 | /** 46 | * Records 47 | * 48 | * @var array $records 49 | */ 50 | public $records = array( 51 | array( 52 | 'id' => '49357f3f-c464-461f-86ac-a85d4a35e6b6', 53 | 'foreign_key' => 1, 54 | 'tag_id' => 1, // CakePHP 55 | 'model' => 'Article', 56 | 'language' => 'eng', 57 | 'created' => '2008-12-02 12:32:31 ', 58 | 'modified' => '2008-12-02 12:32:31', 59 | ), 60 | array( 61 | 'id' => '49357f3f-c66c-4300-a128-a85d4a35e6b6', 62 | 'foreign_key' => 1, 63 | 'tag_id' => 2, // CakeDC 64 | 'model' => 'Article', 65 | 'language' => 'eng', 66 | 'created' => '2008-12-02 12:32:31 ', 67 | 'modified' => '2008-12-02 12:32:31', 68 | ), 69 | array( 70 | 'id' => '493dac81-1b78-4fa1-a761-43ef4a35e6b2', 71 | 'foreign_key' => 2, 72 | 'tag_id' => '49357f3f-17a0-4c42-af78-a85d4a35e6b6', // CakeDC 73 | 'model' => 'Article', 74 | 'language' => 'eng', 75 | 'created' => '2008-12-02 12:32:31 ', 76 | 'modified' => '2008-12-02 12:32:31', 77 | ), 78 | ); 79 | 80 | } 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakedc/search", 3 | "description": "Search Plugin for CakePHP", 4 | "type": "cakephp-plugin", 5 | "keywords": ["cakephp", "search", "prg", "post-redirect-get", "filter"], 6 | "homepage": "http://github.com/CakeDC/search", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Cake Development Corporation", 11 | "email": "team@cakedc.com", 12 | "homepage": "http://cakedc.com" 13 | } 14 | ], 15 | "support": { 16 | "email": "team@cakedc.com", 17 | "issues": "https://github.com/CakeDC/search/issues", 18 | "forum": "http://stackoverflow.com/tags/cakedc", 19 | "wiki": "https://github.com/CakeDC/search/blob/master/Docs/Home.md", 20 | "irc": "irc://irc.freenode.org/cakephp", 21 | "source": "https://github.com/CakeDC/search" 22 | }, 23 | "require": { 24 | "php": ">=5.3.0", 25 | "composer/installers": "*" 26 | }, 27 | "extra": { 28 | "installer-name": "Search" 29 | } 30 | } 31 | --------------------------------------------------------------------------------