├── locale ├── fre │ └── LC_MESSAGES │ │ └── search.po ├── rus │ └── LC_MESSAGES │ │ └── search.po └── search.pot ├── license.txt ├── tests ├── fixtures │ ├── post_fixture.php │ ├── article_fixture.php │ ├── tag_fixture.php │ └── tagged_fixture.php └── cases │ ├── components │ └── prg.test.php │ └── behaviors │ └── searchable.test.php ├── controllers └── components │ └── prg.php ├── readme.md └── models └── behaviors └── searchable.php /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/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 MIT License (http://www.opensource.org/licenses/mit-license.php) 10 | # 11 | #, fuzzy 12 | msgid "" 13 | msgstr "" 14 | "Project-Id-Version: PROJECT VERSION\n" 15 | "POT-Creation-Date: 2010-09-15 15:33+0200\n" 16 | "PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n" 17 | "Last-Translator: NAME \n" 18 | "Language-Team: LANGUAGE \n" 19 | "MIME-Version: 1.0\n" 20 | "Content-Type: text/plain; charset=utf-8\n" 21 | "Content-Transfer-Encoding: 8bit\n" 22 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 23 | 24 | #: /controllers/components/prg.php:245 25 | msgid "Please correct the errors below." 26 | msgstr "" 27 | 28 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2009-2010 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. -------------------------------------------------------------------------------- /tests/fixtures/post_fixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'integer', 'key' => 'primary'), 34 | 'title' => array('type' => 'string', 'null' => false), 35 | 'slug' => array('type' => 'string', 'null' => false), 36 | 'views' => array('type' => 'integer', 'null' => false), 37 | 'comments' => array('type' => 'integer', 'null' => false, 'default' => '0', 'length' => 10), 38 | 'created' => 'datetime', 39 | 'updated' => 'datetime'); 40 | 41 | /** 42 | * records property 43 | * 44 | * @var array 45 | */ 46 | public $records = array( 47 | array('id' => 1, 'title' => 'First Post', 'slug' => 'first_post', 'views' => 2, 'comments' => 1, 'created' => '2007-03-18 10:39:23', 'updated' => '2007-03-18 10:41:31'), 48 | array('id' => 2, 'title' => 'Second Post', 'slug' => 'second_post', 'views' => 1, 'comments' => 2, 'created' => '2007-03-18 10:41:23', 'updated' => '2007-03-18 10:43:31'), 49 | array('id' => 3, 'title' => 'Third Post', 'slug' => 'third_post', 'views' => 2, 'comments' => 3, 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31')); 50 | } 51 | -------------------------------------------------------------------------------- /tests/fixtures/article_fixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'integer', 'key' => 'primary'), 34 | 'title' => array('type' => 'string', 'null' => false), 35 | 'body' => array('type' => 'text', 'null' => false), 36 | 'slug' => array('type' => 'string', 'null' => false), 37 | 'views' => array('type' => 'integer', 'null' => false), 38 | 'comments' => array('type' => 'integer', 'null' => false, 'default' => '0', 'length' => 10), 39 | 'created' => 'datetime', 40 | 'updated' => 'datetime'); 41 | 42 | /** 43 | * records property 44 | * 45 | * @var array 46 | */ 47 | public $records = array( 48 | array('id' => 1, 'title' => 'First Article', 'body' => 'First Article', 'slug' => 'first_article', 'views' => 2, 'comments' => 1, 'created' => '2007-03-18 10:39:23', 'updated' => '2007-03-18 10:41:31'), 49 | array('id' => 2, 'title' => 'Second Article', 'body' => 'Second Article', 'slug' => 'second_article', 'views' => 1, 'comments' => 2, 'created' => '2007-03-18 10:41:23', 'updated' => '2007-03-18 10:43:31'), 50 | array('id' => 3, 'title' => 'Third Article', 'body' => 'Third Article', 'slug' => 'third_article', 'views' => 2, 'comments' => 3, 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31')); 51 | } 52 | -------------------------------------------------------------------------------- /tests/fixtures/tag_fixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36, 'key' => 'primary'), 41 | 'identifier' => array('type' => 'string', 'null' => true, 'default' => NULL, 'length' => 30, 'key' => 'index'), 42 | 'name' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 30), 43 | 'keyname' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 30), 44 | 'weight' => array('type' => 'integer', 'null' => false, 'default' => 0, 'length' => 2), 45 | 'created' => array('type' => 'datetime', 'null' => true, 'default' => NULL), 46 | 'modified' => array('type' => 'datetime', 'null' => true, 'default' => NULL), 47 | 'indexes' => array( 48 | 'PRIMARY' => array('column' => 'id', 'unique' => 1), 49 | 'UNIQUE_TAG' => array('column' => array('identifier', 'keyname'), 'unique' => 1) 50 | ) 51 | ); 52 | 53 | /** 54 | * Records 55 | * 56 | * @var array $records 57 | */ 58 | public $records = array( 59 | array( 60 | 'id' => 1, 61 | 'identifier' => null, 62 | 'name' => 'CakePHP', 63 | 'keyname' => 'cakephp', 64 | 'weight' => 2, 65 | 'created' => '2008-06-02 18:18:11', 66 | 'modified' => '2008-06-02 18:18:37'), 67 | array( 68 | 'id' => 2, 69 | 'identifier' => null, 70 | 'name' => 'CakeDC', 71 | 'keyname' => 'cakedc', 72 | 'weight' => 2, 73 | 'created' => '2008-06-01 18:18:15', 74 | 'modified' => '2008-06-01 18:18:15'), 75 | array( 76 | 'id' => 3, 77 | 'identifier' => null, 78 | 'name' => 'CakeDC', 79 | 'keyname' => 'cakedc', 80 | 'weight' => 2, 81 | 'created' => '2008-06-01 18:18:15', 82 | 'modified' => '2008-06-01 18:18:15')); 83 | } 84 | -------------------------------------------------------------------------------- /tests/fixtures/tagged_fixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36, 'key' => 'primary'), 41 | 'foreign_key' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36), 42 | 'tag_id' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36), 43 | 'model' => array('type' => 'string', 'null' => false, 'default' => NULL, 'key' => 'index'), 44 | 'language' => array('type' => 'string', 'null' => true, 'default' => NULL, 'length' => 6), 45 | 'created' => array('type' => 'datetime', 'null' => true, 'default' => NULL), 46 | 'modified' => array('type' => 'datetime', 'null' => true, 'default' => NULL), 47 | 'indexes' => array( 48 | 'PRIMARY' => array('column' => 'id', 'unique' => 1), 49 | 'UNIQUE_TAGGING' => array('column' => array('model', 'foreign_key', 'tag_id', 'language'), 'unique' => 1), 50 | 'INDEX_TAGGED' => array('column' => 'model', 'unique' => 0), 51 | 'INDEX_LANGUAGE' => array('column' => 'language', 'unique' => 0) 52 | ) 53 | ); 54 | 55 | /** 56 | * Records 57 | * 58 | * @var array $records 59 | */ 60 | public $records = array( 61 | array( 62 | 'id' => '49357f3f-c464-461f-86ac-a85d4a35e6b6', 63 | 'foreign_key' => 1, 64 | 'tag_id' => 1, //cakephp 65 | 'model' => 'Article', 66 | 'language' => 'eng', 67 | 'created' => '2008-12-02 12:32:31 ', 68 | 'modified' => '2008-12-02 12:32:31', 69 | ), 70 | array( 71 | 'id' => '49357f3f-c66c-4300-a128-a85d4a35e6b6', 72 | 'foreign_key' => 1, 73 | 'tag_id' => 2, //cakedc 74 | 'model' => 'Article', 75 | 'language' => 'eng', 76 | 'created' => '2008-12-02 12:32:31 ', 77 | 'modified' => '2008-12-02 12:32:31', 78 | ), 79 | array( 80 | 'id' => '493dac81-1b78-4fa1-a761-43ef4a35e6b2', 81 | 'foreign_key' => 2, 82 | 'tag_id' => '49357f3f-17a0-4c42-af78-a85d4a35e6b6', //cakedc 83 | 'model' => 'Article', 84 | 'language' => 'eng', 85 | 'created' => '2008-12-02 12:32:31 ', 86 | 'modified' => '2008-12-02 12:32:31', 87 | ), 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /controllers/components/prg.php: -------------------------------------------------------------------------------- 1 | 'results'); 28 | * array('search' => array('controller' => 'results'); 29 | * 30 | * @var array actions 31 | */ 32 | public $actions = array(); 33 | 34 | /** 35 | * Enables encoding on all presetVar fields 36 | * 37 | * @var boolean 38 | */ 39 | public $encode = false; 40 | 41 | /** 42 | * Intialize Callback 43 | * 44 | * @param object Controller object 45 | */ 46 | public function initialize(&$controller) { 47 | $this->controller = $controller; 48 | } 49 | 50 | /** 51 | * Poplulates controller->data with allowed values from the named/passed get params 52 | * 53 | * Fields in $controller::$presetVars that have a type of 'lookup' the foreignKey value will be inserted 54 | * 55 | * 1) 'lookup' 56 | * Is used for autocomplete selectors 57 | * For autocomplete we have hidden field with value and autocomplete text box 58 | * Component fills text part on id from hidden field 59 | * 2) 'value' 60 | * The value as it is entered in form 61 | * 3) 'checkbox' 62 | * Allows to pass several values internaly encoded as string 63 | * 64 | * 1 use field, model, formField, and modelField 65 | * 2, 3 need only field parameter 66 | * 67 | * @param array 68 | */ 69 | public function presetForm($model) { 70 | $data = array($model => array()); 71 | $args = $this->controller->passedArgs; 72 | 73 | foreach ($this->controller->presetVars as $field) { 74 | if ($this->encode == true || isset($field['encode']) && $field['encode'] == true) { 75 | // Its important to set it also back to the controllers passed args! 76 | $name = $field['field']; 77 | if (isset($args[$name])) { 78 | $this->controller->passedArgs[$name] = $args[$name] = pack('H*', $args[$name]); 79 | } 80 | } 81 | 82 | if ($field['type'] == 'lookup') { 83 | if (isset($args[$field['field']])) { 84 | $searchModel = $field['model']; 85 | $this->controller->loadModel($searchModel); 86 | $this->controller->{$searchModel}->recursive = -1; 87 | $result = $this->controller->{$searchModel}->findById($args[$field['field']]); 88 | $data[$model][$field['field']] = $args[$field['field']]; 89 | $data[$model][$field['formField']] = $result[$searchModel][$field['modelField']]; 90 | } 91 | } 92 | 93 | if ($field['type'] == 'checkbox') { 94 | if (isset($args[$field['field']])) { 95 | $values = split('\|', $args[$field['field']]); 96 | $data[$model][$field['field']] = $values; 97 | } 98 | } 99 | 100 | if ($field['type'] == 'value') { 101 | if (isset($args[$field['field']])) { 102 | $data[$model][$field['field']] = $args[$field['field']]; 103 | } 104 | } 105 | } 106 | 107 | $this->controller->data = $data; 108 | $this->controller->parsedData = $data; 109 | } 110 | 111 | /** 112 | * Restores form params for checkboxs and other url encoded params 113 | * 114 | * @param array 115 | */ 116 | public function serializeParams(&$data) { 117 | foreach ($this->controller->presetVars as $field) { 118 | if ($field['type'] == 'checkbox') { 119 | if (is_array($data[$field['field']])) { 120 | $values = join('|', $data[$field['field']]); 121 | } else { 122 | $values = ''; 123 | } 124 | $data[$field['field']] = $values; 125 | } 126 | 127 | if ($this->encode == true || isset($field['encode']) && $field['encode'] == true) { 128 | $data[$field['field']] = bin2hex($data[$field['field']]); 129 | } 130 | } 131 | return $data; 132 | } 133 | 134 | /** 135 | * Connect named arguments 136 | * 137 | * @param array $data 138 | * @param array $exclude 139 | * @return void 140 | */ 141 | public function connectNamed($data = null, $exclude = array()) { 142 | if (!isset($data)) { 143 | $data = $this->controller->passedArgs; 144 | } 145 | 146 | if (!is_array($data)) { 147 | return; 148 | } 149 | 150 | foreach ($data as $key => $value) { 151 | if (!is_numeric($key) && !in_array($key, $exclude)) { 152 | Router::connectNamed(array($key)); 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * Exclude 159 | * 160 | * Removes key/values from $array based on $exclude 161 | 162 | * @param array Array of data to be filtered 163 | * @param array Array of keys to exclude from other $array 164 | * @return array 165 | */ 166 | public function exclude($array, $exclude) { 167 | $data = array(); 168 | foreach ($array as $key => $value) { 169 | if (!is_numeric($key) && !in_array($key, $exclude)) { 170 | $data[$key] = $value; 171 | } 172 | } 173 | return $data; 174 | } 175 | 176 | /** 177 | * Common search method 178 | * 179 | * Handles processes common to all PRG forms 180 | * 181 | * - Handles validation of post data 182 | * - converting post data into named params 183 | * - Issuing redirect(), and connecting named parameters before redirect 184 | * - Setting named parameter form data to view 185 | * 186 | * @param string $modelName Name of the model class being used for the prg form 187 | * @param array $options Optional parameters: 188 | * - string form Name of the form involved in the prg 189 | * - string action The action to redirect to. Defaults to the current action 190 | * - mixed modelMethod If not false a string that is the model method that will be used to process the data 191 | * @return void 192 | */ 193 | public function commonProcess($modelName = null, $options = array()) { 194 | $defaults = array( 195 | 'form' => null, 196 | 'keepPassed' => true, 197 | 'action' => null, 198 | 'modelMethod' => 'validateSearch'); 199 | extract(Set::merge($defaults, $options)); 200 | 201 | if (empty($modelName)) { 202 | $modelName = $this->controller->modelClass; 203 | } 204 | 205 | if (empty($formName)) { 206 | $formName = $modelName; 207 | } 208 | 209 | if (empty($action)) { 210 | $action = $this->controller->action; 211 | } 212 | 213 | if (!empty($this->controller->data)) { 214 | $this->controller->{$modelName}->data = $this->controller->data; 215 | $valid = true; 216 | if ($modelMethod !== false) { 217 | $valid = $this->controller->{$modelName}->{$modelMethod}(); 218 | } 219 | 220 | if ($valid) { 221 | $passed = $this->controller->params['pass']; 222 | $params = $this->controller->data[$modelName]; 223 | $params = $this->exclude($params, array()); 224 | 225 | if ($keepPassed) { 226 | $params = array_merge($passed, $params); 227 | } 228 | 229 | $this->serializeParams($params); 230 | $this->connectNamed($params, array()); 231 | $params['action'] = $action; 232 | $params = array_merge($this->controller->params['named'], $params); 233 | $this->controller->redirect($params); 234 | } else { 235 | $this->controller->Session->setFlash(__d('search', 'Please correct the errors below.', true)); 236 | } 237 | } 238 | 239 | if (empty($this->controller->data) && !empty($this->controller->passedArgs)) { 240 | $this->connectNamed($this->controller->passedArgs, array()); 241 | $this->presetForm($formName); 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Search Plugin for CakePHP # 2 | 3 | The Search plugin allows you to make any kind of data searchable, enabling you to implement a robust searching rapidly. 4 | 5 | The Search plugin is an easy way to include search into your application, and provides you with a paginate-able search in any controller. 6 | 7 | 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. 8 | 9 | ## Sample of usage ## 10 | 11 | An example of how to implement complex searching in your application. 12 | 13 | Model code: 14 | 15 | class Article extends AppModel { 16 | 17 | public $actsAs = array('Search.Searchable'); 18 | public $belongsTo = array('User'); 19 | public $hasAndBelongsToMany = array('Tag' => array('with' => 'Tagged')); 20 | 21 | public $filterArgs = array( 22 | array('name' => 'title', 'type' => 'like'), 23 | array('name' => 'status', 'type' => 'value'), 24 | array('name' => 'blog_id', 'type' => 'value'), 25 | array('name' => 'search', 'type' => 'like', 'field' => 'Article.description'), 26 | array('name' => 'range', 'type' => 'expression', 'method' => 'makeRangeCondition', 'field' => 'Article.views BETWEEN ? AND ?'), 27 | array('name' => 'username', 'type' => 'like', 'field' => 'User.username'), 28 | array('name' => 'tags', 'type' => 'subquery', 'method' => 'findByTags', 'field' => 'Article.id'), 29 | array('name' => 'filter', 'type' => 'query', 'method' => 'orConditions'), 30 | ); 31 | 32 | public function findByTags($data = array()) { 33 | $this->Tagged->Behaviors->attach('Containable', array('autoFields' => false)); 34 | $this->Tagged->Behaviors->attach('Search.Searchable'); 35 | $query = $this->Tagged->getQuery('all', array( 36 | 'conditions' => array('Tag.name' => $data['tags']), 37 | 'fields' => array('foreign_key'), 38 | 'contain' => array('Tag') 39 | )); 40 | return $query; 41 | } 42 | 43 | public function orConditions($data = array()) { 44 | $filter = $data['filter']; 45 | $cond = array( 46 | 'OR' => array( 47 | $this->alias . '.title LIKE' => '%' . $filter . '%', 48 | $this->alias . '.body LIKE' => '%' . $filter . '%', 49 | )); 50 | return $cond; 51 | } 52 | } 53 | 54 | Associated snippet for the controller class: 55 | 56 | class ArticlesController extends AppController { 57 | public $components = array('Search.Prg'); 58 | 59 | public $presetVars = array( 60 | array('field' => 'title', 'type' => 'value'), 61 | array('field' => 'status', 'type' => 'checkbox'), 62 | array('field' => 'blog_id', 'type' => 'lookup', 'formField' => 'blog_input', 'modelField' => 'title', 'model' => 'Blog')); 63 | 64 | public function find() { 65 | $this->Prg->commonProcess(); 66 | $this->paginate['conditions'] = $this->Article->parseCriteria($this->passedArgs); 67 | $this->set('articles', $this->paginate()); 68 | } 69 | } 70 | 71 | The `find.ctp` view is the same as `index.ctp` with the addition of the search form: 72 | 73 | echo $this->Form->create('Article', array( 74 | 'url' => array_merge(array('action' => 'find'), $this->params['pass']) 75 | )); 76 | echo $this->Form->input('title', array('div' => false)); 77 | echo $this->Form->input('blog_id', array('div' => false, 'options' => $blogs)); 78 | echo $this->Form->input('status', array('div' => false, 'multiple' => 'checkbox', 'options' => array('open', 'closed'))); 79 | echo $this->Form->input('username', array('div' => false)); 80 | echo $this->Form->submit(__('Search', true), array('div' => false)); 81 | echo $this->Form->end(); 82 | 83 | In this example on model level shon example of search by OR condition. For this purpose defined method orConditions and added filter arg `array('name' => 'filter', 'type' => 'query', 'method' => 'orConditions')`. 84 | 85 | ## Behavior and Model configuration ## 86 | 87 | All search fields need to be configured in the Model::filterArgs array. 88 | 89 | Each filter record should contain array with several keys: 90 | 91 | * name - the parameter stored in Model::data. In the example above the 'search' name used to search in the Article.description field. 92 | * type - one of supported search types described below. 93 | * field - Real field name used for search should be used. 94 | * method - model method name or behavior used to generate expression, subquery or query. 95 | 96 | ### Supported types of search ### 97 | 98 | * 'like' or 'string'. This type of search used when you need to search using 'LIKE' sql keyword. 99 | * 'value' or 'int'. This type of search very useful when you need exact compare. So if you have select box in your view as a filter than you definitely should use value type. 100 | * 'expression' type 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. 101 | * 'subquery' type useful if you want to add condition that looks like FIELD IN (SUBQUERY), where SUBQUERY generated by method declared in this filter configuration. 102 | * 'query' most universal type of search. In this case method should return array(that contain condition of any complexity). Returned condition will joined to whole search conditions. 103 | 104 | ## Post, redirect, get concept ## 105 | 106 | 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. 107 | 108 | 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. To avoid this problem possible to use the PRG pattern instead of returning a 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. See the [Wikipedia article](http://en.wikipedia.org/wiki/Post/Redirect/Get) for more information. 109 | 110 | ## PRG Component features ## 111 | 112 | The Prg component implements the PRG pattern so you can use it separately from search tasks when you need it. 113 | 114 | The component maintains passed and named parameters 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. 115 | 116 | Most importantly the component acts as the glue between your app and the searchable behavior. 117 | 118 | ### Controller configuration ### 119 | 120 | All search fields parameters need to configure in the Controller::presetVars array. 121 | 122 | Each preset variable is a array record that contains next keys: 123 | 124 | * field - field that defined in the view search form. 125 | * type - one of search types: 126 | * value - should used for value that does not require any processing, 127 | * checkbox - used for checkbox fields in view (Prg component pack and unpack checkbox values when pass it through the get named action). 128 | * lookup - this type used when you have autocomplete lookup field implemented in your view. This lookup field is a text field, and also you have hidden field id value. In this case component will fill both text and id values. 129 | * model - param that specifies what model used in Controller::data at a key for this field. 130 | * formField - field in the form that contain text, and will populated using model.modelField based on field value. 131 | * modelField - field in the model that contain text, and will used to fill formField in view. 132 | * 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 133 | 134 | ### Prg::commonProcess method usage ### 135 | 136 | The `commonProcess` method defined in the Prg component allows you to inject search in any index controller with just 1-2 lines of additional code. 137 | 138 | You should pass model name that used for search. By default it is default Controller::modelClass model. 139 | 140 | Additional options parameters: 141 | 142 | * form - search form name. 143 | * keepPassed - parameter that describe if you need to merge passedArgs to Get url where you will Redirect after Post 144 | * action - sometimes you want to have different actions for post and get. In this case you can define get action using this parameter. 145 | * modelMethod - method, used to filter named parameters, passed from form. By default it is validateSearch, and it defined in Searchable behavior. 146 | 147 | ## Requirements ## 148 | 149 | * PHP version: PHP 5.2+ 150 | * CakePHP version: Cakephp 1.3 Stable 151 | 152 | ## Support ## 153 | 154 | For support and feature request, please visit the [Search Plugin Support Site](http://cakedc.lighthouseapp.com/projects/59618-search-plugin/). 155 | 156 | For more information about our Professional CakePHP Services please visit the [Cake Development Corporation website](http://cakedc.com). 157 | 158 | ## License ## 159 | 160 | Copyright 2009-2010, [Cake Development Corporation](http://cakedc.com) 161 | 162 | Licensed under [The MIT License](http://www.opensource.org/licenses/mit-license.php)
163 | Redistributions of files must retain the above copyright notice. 164 | 165 | ## Copyright ### 166 | 167 | Copyright 2009-2010
168 | [Cake Development Corporation](http://cakedc.com)
169 | 1785 E. Sahara Avenue, Suite 490-423
170 | Las Vegas, Nevada 89104
171 | http://cakedc.com
172 | -------------------------------------------------------------------------------- /tests/cases/components/prg.test.php: -------------------------------------------------------------------------------- 1 | Prg->actions = array( 75 | 'search' => array( 76 | 'controller' => 'Posts', 77 | 'action' => 'result')); 78 | } 79 | 80 | /** 81 | * Overwrite redirect 82 | * 83 | * @param string $url 84 | * @param string $status 85 | * @param string $exit 86 | * @return void 87 | */ 88 | public function redirect($url, $status = NULL, $exit = true) { 89 | $this->redirectUrl = $url; 90 | } 91 | } 92 | 93 | /** 94 | * PRG Component Test 95 | * 96 | * @package search 97 | * @subpackage search.tests.cases.components 98 | */ 99 | class PrgComponentTest extends CakeTestCase { 100 | 101 | /** 102 | * Fixtures 103 | * 104 | * @var array 105 | */ 106 | public $fixtures = array('plugin.search.Post'); 107 | 108 | /** 109 | * startTest 110 | * 111 | * @return void 112 | */ 113 | function startTest() { 114 | $this->Controller = new PostsTestController(); 115 | $this->Controller->constructClasses(); 116 | $this->Controller->params = array( 117 | 'named' => array(), 118 | 'pass' => array(), 119 | 'url' => array()); 120 | } 121 | 122 | /** 123 | * endTest 124 | * 125 | * @return void 126 | */ 127 | function endTest() { 128 | unset($this->Controller); 129 | ClassRegistry::flush(); 130 | } 131 | 132 | /** 133 | * test 134 | * 135 | * @return void 136 | */ 137 | public function testPresetForm() { 138 | $this->Controller->presetVars = array( 139 | array( 140 | 'field' => 'title', 141 | 'type' => 'value'), 142 | array( 143 | 'field' => 'checkbox', 144 | 'type' => 'checkbox'), 145 | array( 146 | 'field' => 'lookup', 147 | 'type' => 'lookup', 148 | 'formField' => 'lookup_input', 149 | 'modelField' => 'title', 150 | 'model' => 'Post')); 151 | $this->Controller->passedArgs = array( 152 | 'title' => 'test', 153 | 'checkbox' => 'test|test2|test3', 154 | 'lookup' => '1'); 155 | $this->Controller->Component->init($this->Controller); 156 | $this->Controller->Component->initialize($this->Controller); 157 | $this->Controller->beforeFilter(); 158 | ClassRegistry::addObject('view', new View($this->Controller)); 159 | 160 | $this->Controller->Prg->presetForm('Post'); 161 | $expected = array( 162 | 'Post' => array( 163 | 'title' => 'test', 164 | 'checkbox' => array( 165 | 0 => 'test', 166 | 1 => 'test2', 167 | 2 => 'test3'), 168 | 'lookup' => 1, 169 | 'lookup_input' => 'First Post')); 170 | $this->assertEqual($this->Controller->data, $expected); 171 | } 172 | 173 | /** 174 | * This test checks that the search on an integer type field in the database 175 | * works correctly when a 0 (zero) is entered in the form. 176 | * 177 | * @return void 178 | * @link http://github.com/CakeDC/Search/issues#issue/3 179 | */ 180 | public function testPresetFormWithIntegerField() { 181 | $this->Controller->presetVars = array( 182 | array( 183 | 'field' => 'views', 184 | 'type' => 'value')); 185 | $this->Controller->passedArgs = array( 186 | 'views' => '0'); 187 | $this->Controller->Component->init($this->Controller); 188 | $this->Controller->Component->initialize($this->Controller); 189 | $this->Controller->beforeFilter(); 190 | ClassRegistry::addObject('view', new View($this->Controller)); 191 | 192 | $this->Controller->Prg->presetForm('Post'); 193 | $expected = array( 194 | 'Post' => array( 195 | 'views' => '0')); 196 | $this->assertEqual($this->Controller->data, $expected); 197 | } 198 | 199 | /** 200 | * testFixFormValues 201 | * 202 | * @return void 203 | */ 204 | public function testSerializeParams() { 205 | $this->Controller->presetVars = array( 206 | array( 207 | 'field' => 'options', 208 | 'type' => 'checkbox')); 209 | 210 | $this->Controller->Component->init($this->Controller); 211 | $this->Controller->Component->initialize($this->Controller); 212 | 213 | $testData = array( 214 | 'options' => array( 215 | 0 => 'test1', 1 => 'test2', 2 => 'test3')); 216 | 217 | $result = $this->Controller->Prg->serializeParams($testData); 218 | $this->assertEqual($result, array('options' => 'test1|test2|test3')); 219 | 220 | $testData = array('options' => ''); 221 | 222 | $result = $this->Controller->Prg->serializeParams($testData); 223 | $this->assertEqual($result, array('options' => '')); 224 | } 225 | 226 | /** 227 | * testConnectNamed 228 | * 229 | * @return void 230 | */ 231 | public function testConnectNamed() { 232 | $this->Controller->passedArgs = array( 233 | 'title' => 'test'); 234 | $this->Controller->Component->init($this->Controller); 235 | $this->Controller->Component->initialize($this->Controller); 236 | $this->assertFalse($this->Controller->Prg->connectNamed()); 237 | $this->assertFalse($this->Controller->Prg->connectNamed(1)); 238 | } 239 | 240 | /** 241 | * testExclude 242 | * 243 | * @return void 244 | */ 245 | public function testExclude() { 246 | $this->Controller->params['named'] = array(); 247 | $this->Controller->Component->init($this->Controller); 248 | $this->Controller->Component->initialize($this->Controller); 249 | 250 | $array = array('foo' => 'test', 'bar' => 'test', 'test' => 'test'); 251 | $exclude = array('bar', 'test'); 252 | $this->assertEqual($this->Controller->Prg->exclude($array, $exclude), array('foo' => 'test')); 253 | } 254 | 255 | /** 256 | * testCommonProcess 257 | * 258 | * @return void 259 | */ 260 | public function testCommonProcess() { 261 | $this->Controller->params['named'] = array(); 262 | $this->Controller->Component->init($this->Controller); 263 | $this->Controller->Component->initialize($this->Controller); 264 | $this->Controller->presetVars = array(); 265 | $this->Controller->action = 'search'; 266 | $this->Controller->data = array( 267 | 'Post' => array( 268 | 'title' => 'test')); 269 | $this->Controller->Prg->commonProcess('Post', array( 270 | 'form' => 'Post', 271 | 'modelMethod' => false)); 272 | $this->assertEqual($this->Controller->redirectUrl, array( 273 | 'title' => 'test', 274 | 'action' => 'search')); 275 | 276 | $this->Controller->Prg->commonProcess(null, array( 277 | 'modelMethod' => false)); 278 | $this->assertEqual($this->Controller->redirectUrl, array( 279 | 'title' => 'test', 280 | 'action' => 'search')); 281 | 282 | $this->Controller->Post->filterArgs = array( 283 | array('name' => 'title', 'type' => 'value')); 284 | $this->Controller->Prg->commonProcess('Post'); 285 | $this->assertEqual($this->Controller->redirectUrl, array( 286 | 'title' => 'test', 287 | 'action' => 'search')); 288 | } 289 | 290 | /** 291 | * testCommonProcessGet 292 | * 293 | * @return void 294 | */ 295 | public function testCommonProcessGet() { 296 | $this->Controller->Component->init($this->Controller); 297 | $this->Controller->Component->initialize($this->Controller); 298 | $this->Controller->action = 'search'; 299 | $this->Controller->presetVars = array( 300 | array('field' => 'title', 'type' => 'value')); 301 | $this->Controller->data = array(); 302 | $this->Controller->Post->filterArgs = array( 303 | array('name' => 'title', 'type' => 'value')); 304 | $this->Controller->params['named'] = array('title' => 'test'); 305 | $this->Controller->passedArgs = array_merge($this->Controller->params['named'], $this->Controller->params['pass']); 306 | $this->Controller->Prg->commonProcess('Post'); 307 | $this->assertEqual($this->Controller->data, array('Post' => array('title' => 'test'))); 308 | } 309 | 310 | /** 311 | * testSerializeParamsWithEncoding 312 | * 313 | * @return void 314 | */ 315 | public function testSerializeParamsWithEncoding() { 316 | $this->Controller->Component->init($this->Controller); 317 | $this->Controller->Component->initialize($this->Controller); 318 | $this->Controller->action = 'search'; 319 | $this->Controller->presetVars = array( 320 | array('field' => 'title', 'type' => 'value', 'encode' => true)); 321 | $this->Controller->data = array(); 322 | $this->Controller->Post->filterArgs = array( 323 | array('name' => 'title', 'type' => 'value')); 324 | 325 | $this->Controller->Prg->encode = true; 326 | $test = array('title' => 'Something new'); 327 | $result = $this->Controller->Prg->serializeParams($test); 328 | $this->assertEqual($result['title'], bin2hex('Something new')); 329 | } 330 | 331 | /** 332 | * testPresetFormWithEncodedParams 333 | * 334 | * @return void 335 | */ 336 | public function testPresetFormWithEncodedParams() { 337 | $this->Controller->presetVars = array( 338 | array( 339 | 'field' => 'title', 340 | 'type' => 'value'), 341 | array( 342 | 'field' => 'checkbox', 343 | 'type' => 'checkbox'), 344 | array( 345 | 'field' => 'lookup', 346 | 'type' => 'lookup', 347 | 'formField' => 'lookup_input', 348 | 'modelField' => 'title', 349 | 'model' => 'Post')); 350 | $this->Controller->passedArgs = array( 351 | 'title' => bin2hex('test'), 352 | 'checkbox' => bin2hex('test|test2|test3'), 353 | 'lookup' => bin2hex('1')); 354 | $this->Controller->Component->init($this->Controller); 355 | $this->Controller->Component->initialize($this->Controller); 356 | $this->Controller->beforeFilter(); 357 | ClassRegistry::addObject('view', new View($this->Controller)); 358 | 359 | $this->Controller->Prg->encode = true; 360 | $this->Controller->Prg->presetForm('Post'); 361 | $expected = array( 362 | 'Post' => array( 363 | 'title' => 'test', 364 | 'checkbox' => array( 365 | 0 => 'test', 366 | 1 => 'test2', 367 | 2 => 'test3'), 368 | 'lookup' => 1, 369 | 'lookup_input' => 'First Post')); 370 | $this->assertEqual($this->Controller->data, $expected); 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /tests/cases/behaviors/searchable.test.php: -------------------------------------------------------------------------------- 1 | alias . '.views > 10'; 37 | break; 38 | case 'comments': 39 | $cond = $Model->alias . '.comments > 10'; 40 | break; 41 | } 42 | return (array)$cond; 43 | } 44 | } 45 | 46 | /** 47 | * Tag model 48 | * 49 | * @package search 50 | * @subpackage search.tests.cases.behaviors 51 | */ 52 | class Tag extends CakeTestModel { 53 | } 54 | 55 | /** 56 | * Tagged model 57 | * 58 | * @package search 59 | * @subpackage search.tests.cases.behaviors 60 | */ 61 | class Tagged extends CakeTestModel { 62 | 63 | /** 64 | * Table to use 65 | * 66 | * @var string 67 | */ 68 | public $useTable = 'tagged'; 69 | 70 | /** 71 | * Belongs To Assocaitions 72 | * 73 | * @var array 74 | */ 75 | public $belongsTo = array('Tag'); 76 | } 77 | 78 | /** 79 | * Article model 80 | * 81 | * @package search 82 | * @subpackage search.tests.cases.behaviors 83 | */ 84 | class Article extends CakeTestModel { 85 | 86 | /** 87 | * Behaviors 88 | * 89 | * @var array 90 | */ 91 | public $actsAs = array('Search.Searchable'); 92 | 93 | /** 94 | * HABTM associations 95 | * 96 | * @var array 97 | */ 98 | public $hasAndBelongsToMany = array('Tag' => array('with' => 'Tagged')); 99 | 100 | /** 101 | * Find by tags 102 | * 103 | * @param string $data 104 | * @return array 105 | */ 106 | public function findByTags($data = array()) { 107 | $this->Tagged->Behaviors->attach('Containable', array('autoFields' => false)); 108 | $this->Tagged->Behaviors->attach('Search.Searchable'); 109 | $query = $this->Tagged->getQuery('all', array( 110 | 'conditions' => array('Tag.name' => $data['tags']), 111 | 'fields' => array('foreign_key'), 112 | 'contain' => array('Tag') 113 | )); 114 | return $query; 115 | } 116 | 117 | /** 118 | * Makes an array of range numbers that matches the ones on the interface. 119 | * 120 | * @return array 121 | */ 122 | public function makeRangeCondition($data, $field = null) { 123 | if (is_string($data)) { 124 | $input = $data; 125 | } 126 | if (is_array($data)) { 127 | if (!empty($field['name'])) { 128 | $input = $data[$field['name']]; 129 | } else { 130 | $input = $data['range']; 131 | } 132 | } 133 | switch ($input) { 134 | case '10': 135 | return array(0, 10); 136 | case '100': 137 | return array(11, 100); 138 | case '1000': 139 | return array(101, 1000); 140 | default: 141 | return array(0, 0); 142 | } 143 | } 144 | 145 | /** 146 | * orConditions 147 | * 148 | * @param array $data 149 | * @return array 150 | */ 151 | public function orConditions($data = array()) { 152 | $filter = $data['filter']; 153 | $cond = array( 154 | 'OR' => array( 155 | $this->alias . '.title LIKE' => '%' . $filter . '%', 156 | $this->alias . '.body LIKE' => '%' . $filter . '%', 157 | )); 158 | return $cond; 159 | } 160 | } 161 | 162 | /** 163 | * SearchableTestCase 164 | * 165 | * @package search 166 | * @subpackage search.tests.cases.behaviors 167 | */ 168 | class SearchableTestCase extends CakeTestCase { 169 | 170 | /** 171 | * Fixtures used in the SessionTest 172 | * 173 | * @var array 174 | */ 175 | var $fixtures = array('plugin.search.article', 'plugin.search.tag', 'plugin.search.tagged'); 176 | 177 | /** 178 | * startTest 179 | * 180 | * @return void 181 | */ 182 | public function startTest() { 183 | $this->Article = ClassRegistry::init('Article'); 184 | } 185 | 186 | /** 187 | * endTest 188 | * 189 | * @return void 190 | */ 191 | public function endTest() { 192 | unset($this->Article); 193 | } 194 | 195 | /** 196 | * testValueCondition 197 | * 198 | * @return void 199 | * @link http://github.com/CakeDC/Search/issues#issue/3 200 | */ 201 | public function testValueCondition() { 202 | $this->Article->filterArgs = array( 203 | array('name' => 'slug', 'type' => 'value')); 204 | 205 | $data = array(); 206 | $result = $this->Article->parseCriteria($data); 207 | $this->assertEqual($result, array()); 208 | 209 | $data = array('slug' => 'first_article'); 210 | $result = $this->Article->parseCriteria($data); 211 | $expected = array('Article.slug' => 'first_article'); 212 | $this->assertEqual($result, $expected); 213 | 214 | $this->Article->filterArgs = array( 215 | array('name' => 'fakeslug', 'type' => 'value', 'field' => 'Article2.slug')); 216 | $data = array('fakeslug' => 'first_article'); 217 | $result = $this->Article->parseCriteria($data); 218 | $expected = array('Article2.slug' => 'first_article'); 219 | $this->assertEqual($result, $expected); 220 | 221 | // Testing http://github.com/CakeDC/Search/issues#issue/3 222 | $this->Article->filterArgs = array( 223 | array('name' => 'views', 'type' => 'value')); 224 | $data = array('views' => '0'); 225 | $result = $this->Article->parseCriteria($data); 226 | $this->assertEqual($result, array('Article.views' => 0)); 227 | 228 | $this->Article->filterArgs = array( 229 | array('name' => 'views', 'type' => 'value')); 230 | $data = array('views' => 0); 231 | $result = $this->Article->parseCriteria($data); 232 | $this->assertEqual($result, array('Article.views' => 0)); 233 | 234 | $this->Article->filterArgs = array( 235 | array('name' => 'views', 'type' => 'value')); 236 | $data = array('views' => ''); 237 | $result = $this->Article->parseCriteria($data); 238 | $this->assertEqual($result, array()); 239 | } 240 | 241 | /** 242 | * testLikeCondition 243 | * 244 | * @return void 245 | */ 246 | public function testLikeCondition() { 247 | $this->Article->filterArgs = array( 248 | array('name' => 'title', 'type' => 'like')); 249 | 250 | $data = array(); 251 | $result = $this->Article->parseCriteria($data); 252 | $this->assertEqual($result, array()); 253 | 254 | $data = array('title' => 'First'); 255 | $result = $this->Article->parseCriteria($data); 256 | $expected = array('Article.title LIKE' => '%First%'); 257 | $this->assertEqual($result, $expected); 258 | 259 | $this->Article->filterArgs = array( 260 | array('name' => 'faketitle', 'type' => 'like', 'field' => 'Article.title')); 261 | $data = array('faketitle' => 'First'); 262 | $result = $this->Article->parseCriteria($data); 263 | $expected = array('Article.title LIKE' => '%First%'); 264 | $this->assertEqual($result, $expected); 265 | } 266 | 267 | /** 268 | * testSubQueryCondition 269 | * 270 | * @return void 271 | */ 272 | public function testSubQueryCondition() { 273 | $this->Article->filterArgs = array( 274 | array('name' => 'tags', 'type' => 'subquery', 'method' => 'findByTags', 'field' => 'Article.id')); 275 | 276 | $data = array(); 277 | $result = $this->Article->parseCriteria($data); 278 | $this->assertEqual($result, array()); 279 | 280 | $data = array('tags' => 'Cake'); 281 | $result = $this->Article->parseCriteria($data); 282 | $expected = array(array("Article.id in (SELECT `Tagged`.`foreign_key` FROM `tagged` AS `Tagged` LEFT JOIN `tags` AS `Tag` ON (`Tagged`.`tag_id` = `Tag`.`id`) WHERE `Tag`.`name` = 'Cake' )")); 283 | $this->assertEqual($result, $expected); 284 | } 285 | 286 | /** 287 | * testQueryOrExample 288 | * 289 | * @return void 290 | */ 291 | public function testQueryOrExample() { 292 | $this->Article->filterArgs = array( 293 | array('name' => 'filter', 'type' => 'query', 'method' => 'orConditions')); 294 | 295 | $data = array(); 296 | $result = $this->Article->parseCriteria($data); 297 | $this->assertEqual($result, array()); 298 | 299 | $data = array('filter' => 'ticl'); 300 | $result = $this->Article->parseCriteria($data); 301 | $expected = array('OR' => array( 302 | 'Article.title LIKE' => '%ticl%', 303 | 'Article.body LIKE' => '%ticl%')); 304 | $this->assertEqual($result, $expected); 305 | } 306 | 307 | /** 308 | * testQueryWithBehaviorCallCondition 309 | * 310 | * @return void 311 | */ 312 | public function testQueryWithBehaviorCallCondition() { 313 | $this->Article->Behaviors->attach('Filter'); 314 | $this->Article->filterArgs = array( 315 | array('name' => 'filter', 'type' => 'query', 'method' => 'mostFilterConditions')); 316 | 317 | $data = array(); 318 | $result = $this->Article->parseCriteria($data); 319 | $this->assertEqual($result, array()); 320 | 321 | $data = array('filter' => 'views'); 322 | $result = $this->Article->parseCriteria($data); 323 | $expected = array('Article.views > 10'); 324 | $this->assertEqual($result, $expected); 325 | } 326 | 327 | /** 328 | * testExpressionCallCondition 329 | * 330 | * @return void 331 | */ 332 | public function testExpressionCallCondition() { 333 | $this->Article->filterArgs = array( 334 | array('name' => 'range', 'type' => 'expression', 'method' => 'makeRangeCondition', 'field' => 'Article.views BETWEEN ? AND ?')); 335 | $data = array(); 336 | $result = $this->Article->parseCriteria($data); 337 | $this->assertEqual($result, array()); 338 | 339 | $data = array('range' => '10'); 340 | $result = $this->Article->parseCriteria($data); 341 | $expected = array('Article.views BETWEEN ? AND ?' => array(0, 10)); 342 | $this->assertEqual($result, $expected); 343 | 344 | $this->Article->filterArgs = array( 345 | array('name' => 'range', 'type' => 'expression', 'method' => 'testThatInBehaviorMethodNotDefined', 'field' => 'Article.views BETWEEN ? AND ?')); 346 | $data = array('range' => '10'); 347 | $result = $this->Article->parseCriteria($data); 348 | $this->assertEqual($result, array()); 349 | } 350 | 351 | /** 352 | * testUnbindAll 353 | * 354 | * @return void 355 | */ 356 | public function testUnbindAll() { 357 | $this->Article->unbindAllModels(); 358 | $this->assertEqual($this->Article->belongsTo, array()); 359 | $this->assertEqual($this->Article->hasMany, array()); 360 | $this->assertEqual($this->Article->hasAndBelongsToMany, array()); 361 | $this->assertEqual($this->Article->hasOne, array()); 362 | } 363 | 364 | /** 365 | * testValidateSearch 366 | * 367 | * @return void 368 | */ 369 | public function testValidateSearch() { 370 | $this->Article->filterArgs = array(); 371 | $data = array('Article' => array('title' => 'Last Article')); 372 | $this->Article->set($data); 373 | $this->Article->validateSearch(); 374 | $this->assertEqual($this->Article->data, $data); 375 | 376 | $this->Article->validateSearch($data); 377 | $this->assertEqual($this->Article->data, $data); 378 | 379 | $data = array('Article' => array('title' => '')); 380 | $this->Article->validateSearch($data); 381 | $expected = array('Article' => array()); 382 | $this->assertEqual($this->Article->data, $expected); 383 | } 384 | 385 | /** 386 | * testPassedArgs 387 | * 388 | * @return void 389 | */ 390 | public function testPassedArgs() { 391 | $this->Article->filterArgs = array( 392 | array('name' => 'slug', 'type' => 'value')); 393 | $data = array('slug' => 'first_article', 'filter' => 'myfilter'); 394 | $result = $this->Article->passedArgs($data); 395 | $expected = array('slug' => 'first_article'); 396 | $this->assertEqual($result, $expected); 397 | } 398 | 399 | /** 400 | * testGetQuery 401 | * 402 | * @return void 403 | */ 404 | public function testGetQuery() { 405 | $conditions = array('Article.id' => 1); 406 | $result = $this->Article->getQuery($conditions, array('id', 'title')); 407 | $expected = 'SELECT `Article`.`id`, `Article`.`title` FROM `articles` AS `Article` WHERE `Article`.`id` = 1 LIMIT 1'; 408 | $this->assertEqual($result, $expected); 409 | 410 | $result = $this->Article->getQuery('all', array('conditions' => $conditions, 'order' => 'title', 'page' => 2, 'limit' => 2, 'fields' => array('id', 'title'))); 411 | $expected = 'SELECT `Article`.`id`, `Article`.`title` FROM `articles` AS `Article` WHERE `Article`.`id` = 1 ORDER BY `title` ASC LIMIT 2, 2'; 412 | $this->assertEqual($result, $expected); 413 | 414 | $this->Article->Tagged->Behaviors->attach('Search.Searchable'); 415 | $conditions = array('Tagged.tag_id' => 1); 416 | $result = $this->Article->Tagged->recursive = -1; 417 | $result = $this->Article->Tagged->getQuery($conditions); 418 | $expected = "SELECT `Tagged`.`id`, `Tagged`.`foreign_key`, `Tagged`.`tag_id`, `Tagged`.`model`, `Tagged`.`language`, `Tagged`.`created`, `Tagged`.`modified` FROM `tagged` AS `Tagged` WHERE `Tagged`.`tag_id` = '1' LIMIT 1"; 419 | $this->assertEqual($result, $expected); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /models/behaviors/searchable.php: -------------------------------------------------------------------------------- 1 | settings[$model->alias] = array_merge($this->_defaults, $config); 42 | } 43 | 44 | /** 45 | * parseCriteria 46 | * parses the GET data and returns the conditions for the find('all')/paginate 47 | * we are just going to test if the params are legit 48 | * 49 | * @param array $data Criteria of key->value pairs from post/named parameters 50 | * @return array Array of conditions that express the conditions needed for the search. 51 | */ 52 | public function parseCriteria(Model $model, $data) { 53 | $conditions = array(); 54 | foreach ($model->filterArgs as $field) { 55 | if (in_array($field['type'], array('string', 'like'))) { 56 | $this->_addCondLike($model, $conditions, $data, $field); 57 | } elseif (in_array($field['type'], array('int', 'value'))) { 58 | $this->_addCondValue($model, $conditions, $data, $field); 59 | } elseif ($field['type'] == 'expression') { 60 | $this->_addCondExpression($model, $conditions, $data, $field); 61 | } elseif ($field['type'] == 'query') { 62 | $this->_addCondQuery($model, $conditions, $data, $field); 63 | } elseif ($field['type'] == 'subquery') { 64 | $this->_addCondSubquery($model, $conditions, $data, $field); 65 | } 66 | } 67 | return $conditions; 68 | } 69 | 70 | /** 71 | * Validate search 72 | * 73 | * @param object Model 74 | * @return boolean always true 75 | */ 76 | public function validateSearch(Model $model, $data = null) { 77 | if (!empty($data)) { 78 | $model->set($data); 79 | } 80 | $keys = array_keys($model->data[$model->alias]); 81 | foreach ($keys as $key) { 82 | if (empty($model->data[$model->alias][$key])) { 83 | unset($model->data[$model->alias][$key]); 84 | } 85 | } 86 | return true; 87 | } 88 | 89 | /** 90 | * filter retrieving variables only that present in Model::filterArgs 91 | * 92 | * @param object Model 93 | * @param array $vars 94 | * @return array, filtered args 95 | */ 96 | public function passedArgs(Model $model, $vars) { 97 | $result = array(); 98 | foreach ($vars as $var => $val) { 99 | if (in_array($var, Set::extract($model->filterArgs, '{n}.name'))) { 100 | $result[$var] = $val; 101 | } 102 | } 103 | return $result; 104 | } 105 | 106 | /** 107 | * Method to generated DML SQL queries using find* style. 108 | * 109 | * Specifying 'fields' for new-notation 'list': 110 | * - If no fields are specified, then 'id' is used for key and Model::$displayField is used for value. 111 | * - If a single field is specified, 'id' is used for key and specified field is used for value. 112 | * - If three fields are specified, they are used (in order) for key, value and group. 113 | * - Otherwise, first and second fields are used for key and value. 114 | * 115 | * @param array $conditions SQL conditions array, or type of find operation (all / first / count / neighbors / list / threaded) 116 | * @param mixed $fields Either a single string of a field name, or an array of field names, or options for matching 117 | * @param string $order SQL ORDER BY conditions (e.g. "price DESC" or "name ASC") 118 | * @param integer $recursive The number of levels deep to fetch associated records 119 | * @return string SQL query string. 120 | * @link http://book.cakephp.org/view/449/find 121 | */ 122 | public function getQuery(Model $model, $conditions = null, $fields = array(), $order = null, $recursive = null) { 123 | if (!is_string($conditions) || (is_string($conditions) && !array_key_exists($conditions, $model->_findMethods))) { 124 | $type = 'first'; 125 | $query = compact('conditions', 'fields', 'order', 'recursive'); 126 | } else { 127 | list($type, $query) = array($conditions, $fields); 128 | } 129 | 130 | $db =& ConnectionManager::getDataSource($model->useDbConfig); 131 | $model->findQueryType = $type; 132 | $model->id = $model->getID(); 133 | 134 | $query = array_merge( 135 | array( 136 | 'conditions' => null, 'fields' => null, 'joins' => array(), 137 | 'limit' => null, 'offset' => null, 'order' => null, 'page' => null, 138 | 'group' => null, 'callbacks' => true 139 | ), 140 | (array)$query 141 | ); 142 | 143 | if ($type != 'all') { 144 | if ($model->_findMethods[$type] === true) { 145 | $query = $model->{'_find' . ucfirst($type)}('before', $query); 146 | } 147 | } 148 | 149 | if (!is_numeric($query['page']) || intval($query['page']) < 1) { 150 | $query['page'] = 1; 151 | } 152 | if ($query['page'] > 1 && !empty($query['limit'])) { 153 | $query['offset'] = ($query['page'] - 1) * $query['limit']; 154 | } 155 | if ($query['order'] === null && $model->order !== null) { 156 | $query['order'] = $model->order; 157 | } 158 | $query['order'] = array($query['order']); 159 | 160 | 161 | if ($query['callbacks'] === true || $query['callbacks'] === 'before') { 162 | $return = $model->Behaviors->trigger($model, 'beforeFind', array($query), array( 163 | 'break' => true, 'breakOn' => false, 'modParams' => true 164 | )); 165 | $query = (is_array($return)) ? $return : $query; 166 | 167 | if ($return === false) { 168 | return null; 169 | } 170 | 171 | $return = $model->beforeFind($query); 172 | $query = (is_array($return)) ? $return : $query; 173 | 174 | if ($return === false) { 175 | return null; 176 | } 177 | } 178 | return $this->__queryGet($model, $query, $recursive); 179 | } 180 | 181 | /** 182 | * Clear all associations 183 | * 184 | * @param AppModel $model 185 | * @param bool $reset 186 | */ 187 | public function unbindAllModels(Model $model, $reset = false) { 188 | $assocs = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'); 189 | $unbind = array(); 190 | foreach ($assocs as $assoc) { 191 | $unbind[$assoc] = array_keys($model->{$assoc}); 192 | } 193 | $model->unbindModel($unbind, $reset); 194 | } 195 | 196 | /** 197 | * Add Conditions based on fuzzy comparison 198 | * 199 | * @param AppModel $model Reference to the model 200 | * @param array $conditions existing Conditions collected for the model 201 | * @param array $data Array of data used in search query 202 | * @param array $field Field definition information 203 | * @return array of conditions. 204 | */ 205 | protected function _addCondLike(Model $model, &$conditions, $data, $field) { 206 | $fieldName = $field['name']; 207 | if (isset($field['field'])) { 208 | $fieldName = $field['field']; 209 | } 210 | if (strpos($fieldName, '.') === false) { 211 | $fieldName = $model->alias . '.' . $fieldName; 212 | } 213 | if (!empty($data[$field['name']])) { 214 | $conditions[$fieldName . " LIKE"] = "%" . $data[$field['name']] . "%"; 215 | } 216 | return $conditions; 217 | } 218 | 219 | /** 220 | * Add Conditions based on exact comparison 221 | * 222 | * @param AppModel $model Reference to the model 223 | * @param array $conditions existing Conditions collected for the model 224 | * @param array $data Array of data used in search query 225 | * @param array $field Field definition information 226 | * @return array of conditions. 227 | */ 228 | protected function _addCondValue(Model $model, &$conditions, $data, $field) { 229 | $fieldName = $field['name']; 230 | if (isset($field['field'])) { 231 | $fieldName = $field['field']; 232 | } 233 | if (strpos($fieldName, '.') === false) { 234 | $fieldName = $model->alias . '.' . $fieldName; 235 | } 236 | if (!empty($data[$field['name']]) || (isset($data[$field['name']]) && ($data[$field['name']] === 0 || $data[$field['name']] === '0'))) { 237 | $conditions[$fieldName] = $data[$field['name']]; 238 | } 239 | return $conditions; 240 | } 241 | 242 | /** 243 | * Add Conditions based query to search conditions. 244 | * 245 | * @param Object $model Instance of AppModel 246 | * @param array $conditions Existing conditions. 247 | * @param array $data Data for a field. 248 | * @param array $field Info for field. 249 | * @return array of conditions modified by this method. 250 | */ 251 | protected function _addCondQuery(Model $model, &$conditions, $data, $field) { 252 | if ((method_exists($model, $field['method']) || $this->__checkBehaviorMethods($model, $field['method'])) && !empty($data[$field['name']])) { 253 | $conditionsAdd = $model->{$field['method']}($data); 254 | $conditions = array_merge($conditions, (array)$conditionsAdd); 255 | } 256 | return $conditions; 257 | } 258 | 259 | /** 260 | * Add Conditions based expressions to search conditions. 261 | * 262 | * @param Object $model Instance of AppModel 263 | * @param array $conditions Existing conditions. 264 | * @param array $data Data for a field. 265 | * @param array $field Info for field. 266 | * @return array of conditions modified by this method. 267 | */ 268 | protected function _addCondExpression(Model $model, &$conditions, $data, $field) { 269 | $fieldName = $field['field']; 270 | if ((method_exists($model, $field['method']) || $this->__checkBehaviorMethods($model, $field['method'])) && !empty($data[$field['name']])) { 271 | $fieldValues = $model->{$field['method']}($data, $field); 272 | if (!empty($conditions[$fieldName]) && is_array($conditions[$fieldName])) { 273 | $conditions[$fieldName] = array_unique(array_merge(array($conditions[$fieldName]), array($fieldValues))); 274 | } else { 275 | $conditions[$fieldName] = $fieldValues; 276 | } 277 | } 278 | return $conditions; 279 | } 280 | 281 | /** 282 | * Add Conditions based subquery to search conditions. 283 | * 284 | * @param Object $model Instance of AppModel 285 | * @param array $conditions Existing conditions. 286 | * @param array $data Data for a field. 287 | * @param array $field Info for field. 288 | * @return array of conditions modified by this method. 289 | */ 290 | protected function _addCondSubquery(Model $model, &$conditions, $data, $field) { 291 | $fieldName = $field['field']; 292 | if ((method_exists($model, $field['method']) || $this->__checkBehaviorMethods($model, $field['method'])) && !empty($data[$field['name']])) { 293 | $subquery = $model->{$field['method']}($data); 294 | $conditions[] = array("$fieldName in ($subquery)"); 295 | } 296 | return $conditions; 297 | } 298 | 299 | /** 300 | * Helper method for getQuery. 301 | * extension of dbosource method. Create association query. 302 | * 303 | * @param AppModel $model 304 | * @param array $queryData 305 | * @param integer $recursive 306 | */ 307 | private function __queryGet(Model $model, $queryData = array(), $recursive = null) { 308 | $db =& ConnectionManager::getDataSource($model->useDbConfig); 309 | $db->__scrubQueryData($queryData); 310 | $null = null; 311 | $array = array(); 312 | $linkedModels = array(); 313 | $db->__bypass = false; 314 | $db->__booleans = array(); 315 | 316 | if ($recursive === null && isset($queryData['recursive'])) { 317 | $recursive = $queryData['recursive']; 318 | } 319 | 320 | if (!is_null($recursive)) { 321 | $_recursive = $model->recursive; 322 | $model->recursive = $recursive; 323 | } 324 | 325 | if (!empty($queryData['fields'])) { 326 | $db->__bypass = true; 327 | $queryData['fields'] = $db->fields($model, null, $queryData['fields']); 328 | } else { 329 | $queryData['fields'] = $db->fields($model); 330 | } 331 | 332 | foreach ($model->__associations as $type) { 333 | foreach ($model->{$type} as $assoc => $assocData) { 334 | if ($model->recursive > -1) { 335 | $linkModel =& $model->{$assoc}; 336 | 337 | $external = isset($assocData['external']); 338 | if ($model->alias == $linkModel->alias && $type != 'hasAndBelongsToMany' && $type != 'hasMany') { 339 | if (true === $db->generateSelfAssociationQuery($model, $linkModel, $type, $assoc, $assocData, $queryData, $external, $null)) { 340 | $linkedModels[] = $type . '/' . $assoc; 341 | } 342 | } else { 343 | if ($model->useDbConfig == $linkModel->useDbConfig) { 344 | if (true === $db->generateAssociationQuery($model, $linkModel, $type, $assoc, $assocData, $queryData, $external, $null)) { 345 | $linkedModels[] = $type . '/' . $assoc; 346 | } 347 | } 348 | } 349 | } 350 | } 351 | } 352 | return $db->generateAssociationQuery($model, $null, null, null, null, $queryData, false, $null); 353 | } 354 | 355 | /** 356 | * Check if model have some method in attached behaviors 357 | * 358 | * @param Model $Model 359 | * @param string $method 360 | * @return boolean, true if method exists in attached and enabled behaviors 361 | */ 362 | private function __checkBehaviorMethods(Model $Model, $method) { 363 | $behaviors = $Model->Behaviors->enabled(); 364 | $count = count($behaviors); 365 | $found = false; 366 | for ($i = 0; $i < $count; $i++) { 367 | $name = $behaviors[$i]; 368 | $methods = get_class_methods($Model->Behaviors->{$name}); 369 | $check = array_flip($methods); 370 | $found = isset($check[$method]); 371 | if ($found) { 372 | return true; 373 | } 374 | } 375 | return $found; 376 | } 377 | } 378 | --------------------------------------------------------------------------------