| Total Complexity | 161 |
| Total Lines | 773 |
| Duplicated Lines | 0 % |
| Changes | 3 | ||
| Bugs | 0 | Features | 0 |
Complex classes like Storage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Storage, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 45 | class Storage extends Storage\Base |
||
| 46 | { |
||
| 47 | /** |
||
| 48 | * Prefix used by the class |
||
| 49 | */ |
||
| 50 | const CF_PREFIX = '#'; |
||
| 51 | |||
| 52 | /** |
||
| 53 | * name of customefields table |
||
| 54 | * |
||
| 55 | * @var string |
||
| 56 | */ |
||
| 57 | var $extra_table; |
||
| 58 | |||
| 59 | /** |
||
| 60 | * name of id column, defaults to the regular tables auto id |
||
| 61 | * |
||
| 62 | * @var string |
||
| 63 | */ |
||
| 64 | var $extra_id = '_id'; |
||
| 65 | |||
| 66 | /** |
||
| 67 | * Name of key (cf name) column or just a postfix added to the table prefix |
||
| 68 | * |
||
| 69 | * @var string |
||
| 70 | */ |
||
| 71 | var $extra_key = '_name'; |
||
| 72 | |||
| 73 | /** |
||
| 74 | * Name of value column or just a postfix added to the table prefix |
||
| 75 | * |
||
| 76 | * @var string |
||
| 77 | */ |
||
| 78 | var $extra_value = '_value'; |
||
| 79 | |||
| 80 | var $extra_join; |
||
| 81 | var $extra_join_order; |
||
| 82 | var $extra_join_filter; |
||
| 83 | |||
| 84 | /** |
||
| 85 | * Does extra table has a unique index (over id and name) |
||
| 86 | * |
||
| 87 | * @var boolean |
||
| 88 | */ |
||
| 89 | var $extra_has_unique_index; |
||
| 90 | |||
| 91 | /** |
||
| 92 | * Custom fields of $app, read by the constructor |
||
| 93 | * |
||
| 94 | * @var array |
||
| 95 | */ |
||
| 96 | var $customfields; |
||
| 97 | |||
| 98 | /** |
||
| 99 | * Do we allow AND store multiple values for a cf (1:N) relations |
||
| 100 | * |
||
| 101 | * @var boolean |
||
| 102 | */ |
||
| 103 | var $allow_multiple_values = false; |
||
| 104 | |||
| 105 | /** |
||
| 106 | * constructor of the class |
||
| 107 | * |
||
| 108 | * Please note the different params compared to Storage\Base! |
||
| 109 | * |
||
| 110 | * @param string $app application name to load table schemas |
||
| 111 | * @param string $table name of the table to use |
||
| 112 | * @param string $extra_table name of the custom field table |
||
| 113 | * @param string $column_prefix ='' column prefix to automatic remove from the column-name, if the column name starts with it |
||
| 114 | * @param string $extra_key ='_name' column name for cf name column (will be prefixed with colum prefix, if starting with _) |
||
| 115 | * @param string $extra_value ='_value' column name for cf value column (will be prefixed with colum prefix, if starting with _) |
||
| 116 | * @param string $extra_id ='_id' column name for cf id column (will be prefixed with colum prefix, if starting with _) |
||
| 117 | * @param Db $db =null database object, if not the one in $GLOBALS['egw']->db should be used, eg. for an other database |
||
| 118 | * @param boolean $no_clone =true can we avoid to clone the db-object, default yes (different from Storage\Base!) |
||
| 119 | * new code using appnames and foreach(select(...,$app) can set it to avoid an extra instance of the db object |
||
| 120 | * @param boolean $allow_multiple_values =false should we allow AND store multiple values (1:N relations) |
||
| 121 | * @param string $timestamp_type =null default null=leave them as is, 'ts'|'integer' use integer unix timestamps, 'object' use DateTime objects |
||
| 122 | */ |
||
| 123 | function __construct($app,$table,$extra_table,$column_prefix='', |
||
| 124 | $extra_key='_name',$extra_value='_value',$extra_id='_id', |
||
| 125 | Db $db=null,$no_clone=true,$allow_multiple_values=false,$timestamp_type=null) |
||
| 126 | { |
||
| 127 | // calling the Storage\Base constructor |
||
| 128 | parent::__construct($app,$table,$db,$column_prefix,$no_clone,$timestamp_type); |
||
| 129 | |||
| 130 | $this->allow_multiple_values = $allow_multiple_values; |
||
| 131 | $this->extra_table = $extra_table; |
||
| 132 | if (!$this->extra_id) $this->extra_id = $this->autoinc_id; // default to auto id of regular table |
||
| 133 | |||
| 134 | // if names from columns of extra table are only postfixes (starting with _), prepend column prefix |
||
| 135 | if (!($prefix=$column_prefix)) |
||
| 136 | { |
||
| 137 | list($prefix) = explode('_',$this->autoinc_id); |
||
| 138 | } |
||
| 139 | elseif(substr($prefix,-1) == '_') |
||
| 140 | { |
||
| 141 | $prefix = substr($prefix,0,-1); // remove trailing underscore from column prefix parameter |
||
| 142 | } |
||
| 143 | foreach(array( |
||
| 144 | 'extra_id' => $extra_id, |
||
| 145 | 'extra_key' => $extra_key, |
||
| 146 | 'extra_value' => $extra_value |
||
| 147 | ) as $col => $val) |
||
| 148 | { |
||
| 149 | $this->$col = $col_name = $val; |
||
| 150 | if ($col_name[0] == '_') $this->$col = $prefix . $val; |
||
| 151 | } |
||
| 152 | // some sanity checks, maybe they should be active only for development |
||
| 153 | if (!($extra_defs = $this->db->get_table_definitions($app,$extra_table))) |
||
| 154 | { |
||
| 155 | throw new Exception\WrongParameter("extra table $extra_table is NOT defined!"); |
||
| 156 | } |
||
| 157 | foreach(array('extra_id','extra_key','extra_value') as $col) |
||
| 158 | { |
||
| 159 | if (!$this->$col || !isset($extra_defs['fd'][$this->$col])) |
||
| 160 | { |
||
| 161 | throw new Exception\WrongParameter("$col column $extra_table.{$this->$col} is NOT defined!"); |
||
| 162 | } |
||
| 163 | } |
||
| 164 | // check if our extra table has a unique index (if not we have to delete the old values, as replacing does not work!) |
||
| 165 | $this->extra_has_unique_index = $extra_defs['pk'] || $extra_defs['uc']; |
||
| 166 | |||
| 167 | // setting up our extra joins, now we know table and column names |
||
| 168 | $this->extra_join = " LEFT JOIN $extra_table ON $table.$this->autoinc_id=$extra_table.$this->extra_id"; |
||
| 169 | $this->extra_join_order = " LEFT JOIN $extra_table extra_order ON $table.$this->autoinc_id=extra_order.$this->extra_id"; |
||
| 170 | $this->extra_join_filter = " JOIN $extra_table extra_filter ON $table.$this->autoinc_id=extra_filter.$this->extra_id"; |
||
| 171 | |||
| 172 | $this->customfields = Storage\Customfields::get($app, false, null, $db); |
||
| 173 | } |
||
| 174 | |||
| 175 | /** |
||
| 176 | * Read all customfields of the given id's |
||
| 177 | * |
||
| 178 | * @param int|array $ids one ore more id's |
||
| 179 | * @param array $field_names =null custom fields to read, default all |
||
| 180 | * @return array id => $this->cf_field(name) => value |
||
| 181 | */ |
||
| 182 | function read_customfields($ids,$field_names=null) |
||
| 183 | { |
||
| 184 | if (is_null($field_names)) $field_names = array_keys($this->customfields); |
||
| 185 | |||
| 186 | foreach((array)$ids as $key => $id) |
||
| 187 | { |
||
| 188 | if (!(int)$id && is_array($ids)) unset($ids[$key]); |
||
| 189 | } |
||
| 190 | if (!$ids || !$field_names) return array(); // nothing to do |
||
|
|
|||
| 191 | |||
| 192 | $entries = array(); |
||
| 193 | foreach($this->db->select($this->extra_table,'*',array( |
||
| 194 | $this->extra_id => $ids, |
||
| 195 | $this->extra_key => $field_names, |
||
| 196 | ),__LINE__,__FILE__,false,'',$this->app) as $row) |
||
| 197 | { |
||
| 198 | $entry =& $entries[$row[$this->extra_id]]; |
||
| 199 | if (!is_array($entry)) $entry = array(); |
||
| 200 | $field = $this->get_cf_field($row[$this->extra_key]); |
||
| 201 | |||
| 202 | if ($this->allow_multiple_values && $this->is_multiple($row[$this->extra_key])) |
||
| 203 | { |
||
| 204 | $entry[$field][] = $row[$this->extra_value]; |
||
| 205 | } |
||
| 206 | else |
||
| 207 | { |
||
| 208 | $entry[$field] = $row[$this->extra_value]; |
||
| 209 | } |
||
| 210 | } |
||
| 211 | return $entries; |
||
| 212 | } |
||
| 213 | |||
| 214 | /** |
||
| 215 | * saves custom field data |
||
| 216 | * |
||
| 217 | * @param array $data data to save (cf's have to be prefixed with self::CF_PREFIX = #) |
||
| 218 | * @param array $extra_cols =array() extra-data to be saved |
||
| 219 | * @return bool false on success, errornumber on failure |
||
| 220 | */ |
||
| 221 | function save_customfields(&$data, array $extra_cols=array()) |
||
| 222 | { |
||
| 223 | $id = isset($data[$this->autoinc_id]) ? $data[$this->autoinc_id] : $data[$this->db_key_cols[$this->autoinc_id]]; |
||
| 224 | |||
| 225 | Customfields::handle_files($this->app, $id, $data, $this->customfields); |
||
| 226 | |||
| 227 | foreach (array_keys((array)$this->customfields) as $name) |
||
| 228 | { |
||
| 229 | if (!isset($data[$field = $this->get_cf_field($name)])) continue; |
||
| 230 | |||
| 231 | $where = array( |
||
| 232 | $this->extra_id => $id, |
||
| 233 | $this->extra_key => $name, |
||
| 234 | ); |
||
| 235 | $is_multiple = $this->is_multiple($name); |
||
| 236 | |||
| 237 | // we explicitly need to delete fields, if value is empty or field allows multiple values or we have no unique index |
||
| 238 | if(empty($data[$field]) || $is_multiple || !$this->extra_has_unique_index) |
||
| 239 | { |
||
| 240 | $this->db->delete($this->extra_table,$where,__LINE__,__FILE__,$this->app); |
||
| 241 | if (empty($data[$field])) continue; // nothing else to do for empty values |
||
| 242 | } |
||
| 243 | foreach($is_multiple && !is_array($data[$field]) ? explode(',',$data[$field]) : |
||
| 244 | // regular custom fields (!$is_multiple) eg. addressbook store multiple values comma-separated |
||
| 245 | (array)(!$is_multiple && is_array($data[$field]) ? implode(',', $data[$field]) : $data[$field]) as $value) |
||
| 246 | { |
||
| 247 | if (!$this->db->insert($this->extra_table,array($this->extra_value => $value)+$extra_cols,$where,__LINE__,__FILE__,$this->app)) |
||
| 248 | { |
||
| 249 | return $this->db->Errno; |
||
| 250 | } |
||
| 251 | } |
||
| 252 | } |
||
| 253 | return false; // no error |
||
| 254 | } |
||
| 255 | |||
| 256 | /** |
||
| 257 | * merges in new values from the given new data-array |
||
| 258 | * |
||
| 259 | * reimplemented to also merge the customfields |
||
| 260 | * |
||
| 261 | * @param $new array in form col => new_value with values to set |
||
| 262 | */ |
||
| 263 | function data_merge($new) |
||
| 264 | { |
||
| 265 | parent::data_merge($new); |
||
| 266 | |||
| 267 | if ($this->customfields) |
||
| 268 | { |
||
| 269 | foreach(array_keys($this->customfields) as $name) |
||
| 270 | { |
||
| 271 | if (isset($new[$field = $this->get_cf_field($name)])) |
||
| 272 | { |
||
| 273 | $this->data[$field] = $new[$field]; |
||
| 274 | } |
||
| 275 | } |
||
| 276 | } |
||
| 277 | } |
||
| 278 | |||
| 279 | /** |
||
| 280 | * reads row matched by key and puts all cols in the data array |
||
| 281 | * |
||
| 282 | * reimplented to also read the custom fields |
||
| 283 | * |
||
| 284 | * @param array $keys array with keys in form internalName => value, may be a scalar value if only one key |
||
| 285 | * @param string|array $extra_cols string or array of strings to be added to the SELECT, eg. "count(*) as num" |
||
| 286 | * @param string $join sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or |
||
| 287 | * @return array|boolean data if row could be retrived else False |
||
| 288 | */ |
||
| 289 | function read($keys,$extra_cols='',$join='') |
||
| 290 | { |
||
| 291 | if (!parent::read($keys,$extra_cols,$join)) |
||
| 292 | { |
||
| 293 | return false; |
||
| 294 | } |
||
| 295 | if (($id = (int)$this->data[$this->db_key_cols[$this->autoinc_id]]) && $this->customfields && |
||
| 296 | ($cfs = $this->read_customfields($id))) |
||
| 297 | { |
||
| 298 | $this->data = array_merge($this->data,$cfs[$id]); |
||
| 299 | } |
||
| 300 | return $this->data; |
||
| 301 | } |
||
| 302 | |||
| 303 | /** |
||
| 304 | * saves the content of data to the db |
||
| 305 | * |
||
| 306 | * reimplented to also save the custom fields |
||
| 307 | * |
||
| 308 | * @param array $keys if given $keys are copied to data before saveing => allows a save as |
||
| 309 | * @param string|array $extra_where =null extra where clause, eg. to check an etag, returns true if no affected rows! |
||
| 310 | * @return int|boolean 0 on success, or errno != 0 on error, or true if $extra_where is given and no rows affected |
||
| 311 | */ |
||
| 312 | function save($keys=null,$extra_where=null) |
||
| 313 | { |
||
| 314 | if (is_array($keys) && count($keys) && !isset($keys[0])) // allow to use an etag, eg array('etag=etag+1') |
||
| 315 | { |
||
| 316 | $this->data_merge($keys); |
||
| 317 | $keys = null; |
||
| 318 | } |
||
| 319 | $ret = parent::save($keys,$extra_where); |
||
| 320 | |||
| 321 | if ($ret == 0 && $this->customfields) |
||
| 322 | { |
||
| 323 | $this->save_customfields($this->data); |
||
| 324 | } |
||
| 325 | return $ret; |
||
| 326 | } |
||
| 327 | |||
| 328 | /** |
||
| 329 | * deletes row representing keys in internal data or the supplied $keys if != null |
||
| 330 | * |
||
| 331 | * reimplented to also delete the custom fields |
||
| 332 | * |
||
| 333 | * @param array|int $keys =null if given array with col => value pairs to characterise the rows to delete, or integer autoinc id |
||
| 334 | * @param boolean $only_return_ids =false return $ids of delete call to db object, but not run it (can be used by extending classes!) |
||
| 335 | * @return int|array affected rows, should be 1 if ok, 0 if an error or array with id's if $only_return_ids |
||
| 336 | */ |
||
| 337 | function delete($keys=null,$only_return_ids=false) |
||
| 338 | { |
||
| 339 | if ($this->customfields || $only_return_ids) |
||
| 340 | { |
||
| 341 | $query = parent::delete($keys,true); |
||
| 342 | // check if query contains more then the id's |
||
| 343 | if (!isset($query[$this->autoinc_id]) || count($query) != 1) |
||
| 344 | { |
||
| 345 | foreach($this->db->select($this->table_name,$this->autoinc_id,$query,__LINE__,__FILE__,false,'',$this->app) as $row) |
||
| 346 | { |
||
| 347 | $ids[] = $row[$this->autoinc_id]; |
||
| 348 | } |
||
| 349 | if (!$ids) return 0; // no rows affected |
||
| 350 | } |
||
| 351 | else |
||
| 352 | { |
||
| 353 | $ids = (array)$query[$this->autoinc_id]; |
||
| 354 | } |
||
| 355 | if ($only_return_ids) return $ids; |
||
| 356 | $this->db->delete($this->extra_table,array($this->extra_id => $ids),__LINE__,__FILE__); |
||
| 357 | } |
||
| 358 | return parent::delete($keys); |
||
| 359 | } |
||
| 360 | |||
| 361 | /** |
||
| 362 | * query rows for the nextmatch widget |
||
| 363 | * |
||
| 364 | * Reimplemented to also read the custom fields (if enabled via $query['selectcols']). |
||
| 365 | * |
||
| 366 | * Please note: the name of the nextmatch-customfields has to be 'customfields'! |
||
| 367 | * |
||
| 368 | * @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter' |
||
| 369 | * For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class. |
||
| 370 | * @param array &$rows returned rows/competitions |
||
| 371 | * @param array &$readonlys eg. to disable buttons based on acl, not use here, maybe in a derived class |
||
| 372 | * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or |
||
| 373 | * "LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join! |
||
| 374 | * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false |
||
| 375 | * @param mixed $only_keys =false, see search |
||
| 376 | * @param string|array $extra_cols =array() |
||
| 377 | * @return int total number of rows |
||
| 378 | */ |
||
| 379 | function get_rows($query,&$rows,&$readonlys,$join='',$need_full_no_count=false,$only_keys=false,$extra_cols=array()) |
||
| 409 | } |
||
| 410 | |||
| 411 | /** |
||
| 412 | * Return SQL fragment to search custom-fields for given $pattern |
||
| 413 | * |
||
| 414 | * To be or-ed to query for $_pattern in regular columns of main-table. |
||
| 415 | * |
||
| 416 | * @param string $_pattern search pattern incl. * or ? as wildcard, if no wildcards used we append and prepend one! |
||
| 417 | * @return string with SQL fragment running on main table: "id IN (SELECT id FROM extra-table WHERE extra_value like '$pattern')" |
||
| 418 | */ |
||
| 419 | public function cf_match($_pattern) |
||
| 420 | { |
||
| 421 | static $private_cfs=null; |
||
| 422 | |||
| 423 | if (!$this->customfields) return ''; // no custom-fields --> no search |
||
| 424 | |||
| 425 | $sql = ' OR '.$this->table_name.'.'.$this->autoinc_id.' IN (SELECT '.$this->extra_id. |
||
| 426 | ' FROM '.$this->extra_table.' WHERE '.$this->extra_value.' '. |
||
| 427 | $this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '. |
||
| 428 | $GLOBALS['egw']->db->quote($_pattern); |
||
| 429 | |||
| 430 | // check if there are private cfs not available to current user --> filter by available cfs |
||
| 431 | if (!isset($private_cfs)) |
||
| 432 | { |
||
| 433 | $private_cfs = array_diff_key( |
||
| 434 | Storage\Customfields::get($this->app, true, null, $this->db), // true: get private cfs too |
||
| 435 | $this->customfields); |
||
| 436 | //error_log(__METHOD__."() private_cfs=".array2string($private_cfs)); |
||
| 437 | } |
||
| 438 | if ($private_cfs) |
||
| 439 | { |
||
| 440 | $sql .= ' AND '.$this->db->expression($this->extra_table, array($this->extra_key => array_keys($this->customfields))); |
||
| 441 | } |
||
| 442 | return $sql.')'; |
||
| 443 | } |
||
| 444 | |||
| 445 | /** |
||
| 446 | * searches db for rows matching searchcriteria |
||
| 447 | * |
||
| 448 | * Reimplemented to search, order and filter by custom fields |
||
| 449 | * |
||
| 450 | * @param array|string $criteria array of key and data cols, OR string with search pattern (incl. * or ? as wildcards) |
||
| 451 | * @param boolean|string/array $only_keys =true True returns only keys, False returns all cols. or |
||
| 452 | * comma seperated list or array of columns to return |
||
| 453 | * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY) |
||
| 454 | * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num" |
||
| 455 | * @param string $wildcard ='' appended befor and after each criteria |
||
| 456 | * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row |
||
| 457 | * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together |
||
| 458 | * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num), or 'UNION' for a part of a union query |
||
| 459 | * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards |
||
| 460 | * @param string $join ='' sql to do a join, added as is after the table-name, eg. "JOIN table2 ON x=y" or |
||
| 461 | * "LEFT JOIN table2 ON (x=y AND z=o)", Note: there's no quoting done on $join, you are responsible for it!!! |
||
| 462 | * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false |
||
| 463 | * @return array|NULL array of matching rows (the row is an array of the cols) or NULL |
||
| 464 | */ |
||
| 465 | function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='',$need_full_no_count=false) |
||
| 466 | { |
||
| 467 | //error_log(__METHOD__.'('.array2string(array_combine(array_slice(array('criteria','only_keys','order_by','extra_cols','wildcard','empty','op','start','filter','join','need_full_no_count'), 0, count(func_get_args())), func_get_args())).')'); |
||
| 468 | $this->process_search($criteria, $only_keys, $order_by, $extra_cols, $wildcard, $op, $filter, $join); |
||
| 469 | |||
| 470 | return parent::search($criteria, $only_keys, $order_by, $extra_cols, $wildcard, $empty, $op, $start, $filter, $join, $need_full_no_count); |
||
| 471 | } |
||
| 472 | |||
| 473 | /** |
||
| 474 | * Full logic of search to be reused in custom search methods |
||
| 475 | * |
||
| 476 | * Works by modifying the parameters so search calls this method and then it's parent with the modified parameters. |
||
| 477 | * |
||
| 478 | * @param array|string $criteria array of key and data cols, OR string with search pattern (incl. * or ? as wildcards) |
||
| 479 | * @param boolean|string/array $only_keys =true True returns only keys, False returns all cols. or |
||
| 480 | * comma seperated list or array of columns to return |
||
| 481 | * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY) |
||
| 482 | * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num" |
||
| 483 | * @param string $wildcard ='' appended befor and after each criteria |
||
| 484 | * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together |
||
| 485 | * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards |
||
| 486 | * @param string $join ='' sql to do a join, added as is after the table-name, eg. "JOIN table2 ON x=y" or |
||
| 487 | * "LEFT JOIN table2 ON (x=y AND z=o)", Note: there's no quoting done on $join, you are responsible for it!!! |
||
| 488 | */ |
||
| 489 | protected function process_search(&$criteria, &$only_keys=True, &$order_by='', &$extra_cols='', &$wildcard='', &$op='AND', &$filter=null, &$join='') |
||
| 490 | { |
||
| 491 | if ($only_keys === false) |
||
| 492 | { |
||
| 493 | $only_keys = $this->table_name.'.*'; |
||
| 494 | } |
||
| 495 | $extra_join_added = $join && strpos($join, $this->extra_join) !== false; |
||
| 496 | if ($criteria && is_string($criteria)) |
||
| 497 | { |
||
| 498 | $extra_join_added = true; // we have NOT added the join, as we use a sub-query and therefore not need it |
||
| 499 | |||
| 500 | $criteria = $this->search2criteria($criteria, $wildcard, $op); |
||
| 501 | } |
||
| 502 | if ($criteria && is_array($criteria)) |
||
| 503 | { |
||
| 504 | // check if we search in the custom fields |
||
| 505 | if (isset($criteria[$this->extra_value])) |
||
| 506 | { |
||
| 507 | // we should check if the CF is (still) available, but that makes the slow search even slower :( |
||
| 508 | if (($negate = $criteria[$this->extra_value][0] === '!')) |
||
| 509 | { |
||
| 510 | $criteria[$this->extra_value] = substr($criteria[$this->extra_value],1); |
||
| 511 | } |
||
| 512 | $criteria[] = $this->extra_table.'.'.$this->extra_value . ' ' .($negate ? 'NOT ' : ''). |
||
| 513 | $this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE]. ' ' . |
||
| 514 | $this->db->quote($wildcard.$criteria[$this->extra_value].$wildcard); |
||
| 515 | unset($criteria[$this->extra_value]); |
||
| 516 | } |
||
| 517 | // replace ambiguous auto-id with (an exact match of) table_name.autoid |
||
| 518 | if (isset($criteria[$this->autoinc_id])) |
||
| 519 | { |
||
| 520 | if ($criteria[$this->autoinc_id]) |
||
| 521 | { |
||
| 522 | $criteria[] = $this->db->expression($this->table_name,$this->table_name.'.', |
||
| 523 | array($this->autoinc_id => $criteria[$this->autoinc_id])); |
||
| 524 | } |
||
| 525 | unset($criteria[$this->autoinc_id]); |
||
| 526 | } |
||
| 527 | // replace ambiguous column with (an exact match of) table_name.column |
||
| 528 | foreach($criteria as $name => $val) |
||
| 529 | { |
||
| 530 | // only add extra_join, if we really need it |
||
| 531 | if (!$extra_join_added && ( |
||
| 532 | is_int($name) && strpos($val, $this->extra_value) !== false || |
||
| 533 | is_string($name) && $this->is_cf($name) |
||
| 534 | )) |
||
| 535 | { |
||
| 536 | $join .= $this->extra_join; |
||
| 537 | $extra_join_added = true; |
||
| 538 | } |
||
| 539 | $extra_columns = $this->db->get_table_definitions($this->app, $this->extra_table); |
||
| 540 | if(is_string($name) && $extra_columns['fd'][array_search($name, $this->db_cols)]) |
||
| 541 | { |
||
| 542 | $criteria[] = $this->db->expression($this->table_name,$this->table_name.'.',array( |
||
| 543 | array_search($name, $this->db_cols) => $val, |
||
| 544 | )); |
||
| 545 | unset($criteria[$name]); |
||
| 546 | } |
||
| 547 | elseif (is_string($name) && $this->is_cf($name)) |
||
| 548 | { |
||
| 549 | if ($op != 'AND') |
||
| 550 | { |
||
| 551 | $name = substr($name, 1); |
||
| 552 | if (($negate = $criteria[$name][0] === '!')) |
||
| 553 | { |
||
| 554 | $val = substr($val,1); |
||
| 555 | } |
||
| 556 | $cfcriteria[] = '(' . $this->extra_table.'.'.$this->extra_value . ' ' .($negate ? 'NOT ' : ''). |
||
| 557 | $this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE]. ' ' . |
||
| 558 | $this->db->quote($wildcard.$val.$wildcard) . ' AND ' . |
||
| 559 | $this->extra_table.'.'.$this->extra_key . ' = ' . $this->db->quote($name) . |
||
| 560 | ')'; |
||
| 561 | unset($criteria[self::CF_PREFIX.$name]); |
||
| 562 | } |
||
| 563 | else |
||
| 564 | { |
||
| 565 | // criteria operator is AND we remap the criteria to be transformed to filters |
||
| 566 | $filter[$name] = $val; |
||
| 567 | unset($criteria[$name]); |
||
| 568 | } |
||
| 569 | } |
||
| 570 | } |
||
| 571 | if ($cfcriteria && $op =='OR') $criteria[] = implode(' OR ',$cfcriteria); |
||
| 572 | } |
||
| 573 | if($only_keys === true) |
||
| 574 | { |
||
| 575 | // Expand to keys here, so table_name can be prepended below |
||
| 576 | $only_keys = array_values($this->db_key_cols); |
||
| 577 | } |
||
| 578 | // replace ambiguous column with (an exact match of) table_name.column |
||
| 579 | if(is_array($only_keys)) |
||
| 580 | { |
||
| 581 | foreach($only_keys as $key => &$col) |
||
| 582 | { |
||
| 583 | if(is_numeric($key) && in_array($col, $this->db_cols, true)) |
||
| 584 | { |
||
| 585 | $col = $this->table_name .'.'.array_search($col, $this->db_cols).' AS '.$col; |
||
| 586 | } |
||
| 587 | // Check to make sure our order by doesn't have aliases that won't work |
||
| 588 | else if (stripos($col, 'AS') !== false && $order_by) |
||
| 589 | { |
||
| 590 | list($value, $alias) = explode(' AS ', $col); |
||
| 591 | if(stripos($order_by, $alias) !== FALSE && stripos($value, $this->table_name) === FALSE) |
||
| 592 | { |
||
| 593 | $order_by = str_replace($alias, $value, $order_by); |
||
| 594 | } |
||
| 595 | } |
||
| 596 | } |
||
| 597 | } |
||
| 598 | // check if we order by a custom field --> join cf table for given cf and order by it's value |
||
| 599 | if (strpos($order_by,self::CF_PREFIX) !== false) |
||
| 600 | { |
||
| 601 | // if $order_by contains more then order by columns (eg. group by) split it off before |
||
| 602 | if (($pos = stripos($order_by, 'order by')) !== false) |
||
| 603 | { |
||
| 604 | $group_by = substr($order_by, 0, $pos+9); |
||
| 605 | $order_by = substr($order_by, $pos+9); |
||
| 606 | } |
||
| 607 | // fields to order by, as cutomfields may have names with spaces, we examine each order by criteria |
||
| 608 | $fields2order = explode(',',$order_by); |
||
| 609 | foreach($fields2order as $v) |
||
| 610 | { |
||
| 611 | if (strpos($v,self::CF_PREFIX) !== false) |
||
| 612 | { |
||
| 613 | // we found a customfield, so we split that part by space char in order to get Sorting Direction and Fieldname |
||
| 614 | $buff = explode(' ',trim($v)); |
||
| 615 | $orderDir = array_pop($buff); |
||
| 616 | $key = substr(trim(implode(' ',$buff)), 1); |
||
| 617 | if (!isset($this->customfields[$key])) |
||
| 618 | { |
||
| 619 | $order_by = preg_replace('/'.preg_quote($v, '/').',?/', '', $order_by); |
||
| 620 | continue; // ignore unavaiable CF |
||
| 621 | } |
||
| 622 | switch($this->customfields[$key]['type']) |
||
| 623 | { |
||
| 624 | case 'int': |
||
| 625 | $order_by = str_replace($v, 'extra_order.'.$this->extra_value.' IS NULL,'. |
||
| 626 | $this->db->to_int('extra_order.'.$this->extra_value).' '.$orderDir, $order_by); |
||
| 627 | break; |
||
| 628 | case 'float': |
||
| 629 | $order_by = str_replace($v, 'extra_order.'.$this->extra_value.' IS NULL,'. |
||
| 630 | $this->db->to_double('extra_order.'.$this->extra_value).' '.$orderDir, $order_by); |
||
| 631 | break; |
||
| 632 | default: |
||
| 633 | $order_by = str_replace($v, 'extra_order.'.$this->extra_value.' IS NULL,extra_order.'. |
||
| 634 | $this->extra_value.' '.$orderDir, $order_by); |
||
| 635 | } |
||
| 636 | // postgres requires that expressions in order by appear in the columns of a distinct select |
||
| 637 | if ($this->db->Type != 'mysql') |
||
| 638 | { |
||
| 639 | if (!is_array($extra_cols)) |
||
| 640 | { |
||
| 641 | $extra_cols = $extra_cols ? explode(',', $extra_cols) : array(); |
||
| 642 | } |
||
| 643 | $extra_cols[] = 'extra_order.'.$this->extra_value; |
||
| 644 | $extra_cols[] = 'extra_order.'.$this->extra_value.' IS NULL'; |
||
| 645 | } |
||
| 646 | $join .= $this->extra_join_order.' AND extra_order.'.$this->extra_key.'='.$this->db->quote($key); |
||
| 647 | } |
||
| 648 | } |
||
| 649 | // add group by again |
||
| 650 | if (isset($group_by)) |
||
| 651 | { |
||
| 652 | $order_by = $group_by.$order_by; |
||
| 653 | } |
||
| 654 | } |
||
| 655 | // check if we filter by a custom field |
||
| 656 | if (is_array($filter)) |
||
| 657 | { |
||
| 658 | $_cfnames = array_keys($this->customfields); |
||
| 659 | $extra_filter = null; |
||
| 660 | foreach($filter as $name => $val) |
||
| 661 | { |
||
| 662 | // replace ambiguous auto-id with (an exact match of) table_name.autoid |
||
| 663 | if (is_string($name) && $name == $this->autoinc_id) |
||
| 664 | { |
||
| 665 | if ((int)$filter[$this->autoinc_id]) |
||
| 666 | { |
||
| 667 | $filter[] = $this->db->expression($this->table_name,$this->table_name.'.',array( |
||
| 668 | $this->autoinc_id => $filter[$this->autoinc_id], |
||
| 669 | )); |
||
| 670 | } |
||
| 671 | unset($filter[$this->autoinc_id]); |
||
| 672 | } |
||
| 673 | // replace ambiguous column with (an exact match of) table_name.column |
||
| 674 | elseif (is_string($name) && $val!=null && in_array($name, $this->db_cols)) |
||
| 675 | { |
||
| 676 | $extra_columns = $this->db->get_table_definitions($this->app, $this->extra_table); |
||
| 677 | if ($extra_columns['fd'][array_search($name, $this->db_cols)]) |
||
| 678 | { |
||
| 679 | $filter[] = $this->db->expression($this->table_name,$this->table_name.'.',array( |
||
| 680 | array_search($name, $this->db_cols) => $val, |
||
| 681 | )); |
||
| 682 | unset($filter[$name]); |
||
| 683 | } |
||
| 684 | } |
||
| 685 | elseif (is_string($name) && $this->is_cf($name)) |
||
| 686 | { |
||
| 687 | $cf_name = $this->get_cf_name($name); |
||
| 688 | if (!isset($this->customfields[$cf_name])) |
||
| 689 | { |
||
| 690 | unset($filter[$name]); |
||
| 691 | continue; // ignore unavailable CF |
||
| 692 | } |
||
| 693 | if (!empty($val)) // empty -> dont filter |
||
| 694 | { |
||
| 695 | if ($val[0] === '!') // negative filter |
||
| 696 | { |
||
| 697 | $sql_filter = 'extra_filter.'.$this->extra_value.'!='.$this->db->quote(substr($val,1)); |
||
| 698 | } |
||
| 699 | else // using Db::expression to allow to use array() with possible values or NULL |
||
| 700 | { |
||
| 701 | if($this->customfields[$cf_name]['type'] == 'select' && |
||
| 702 | $this->customfields[$cf_name]['rows'] > 1) |
||
| 703 | { |
||
| 704 | // Multi-select - any entry with the filter value selected matches |
||
| 705 | $sql_filter = str_replace($this->extra_value,'extra_filter.'. |
||
| 706 | $this->extra_value,$this->db->expression($this->extra_table,array( |
||
| 707 | $this->db->concat("','",$this->extra_value,"','").' '.$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote('%,'.$val.',%') |
||
| 708 | )) |
||
| 709 | ); |
||
| 710 | } |
||
| 711 | elseif ($this->customfields[$cf_name]['type'] == 'text') |
||
| 712 | { |
||
| 713 | $sql_filter = str_replace($this->extra_value,'extra_filter.'.$this->extra_value, |
||
| 714 | $this->db->expression($this->extra_table,array( |
||
| 715 | $this->extra_value.' '.$this->db->capabilities[Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($wildcard.$val.$wildcard) |
||
| 716 | )) |
||
| 717 | ); |
||
| 718 | } |
||
| 719 | else |
||
| 720 | { |
||
| 721 | $sql_filter = str_replace($this->extra_value,'extra_filter.'. |
||
| 722 | $this->extra_value,$this->db->expression($this->extra_table,array($this->extra_value => $val))); |
||
| 723 | } |
||
| 724 | } |
||
| 725 | // need to use a LEFT JOIN for negative search or to allow NULL values |
||
| 726 | $need_left_join = $val[0] === '!' || strpos($sql_filter,'IS NULL') !== false ? ' LEFT ' : ''; |
||
| 727 | $join .= str_replace('extra_filter','extra_filter'.$extra_filter,$need_left_join.$this->extra_join_filter. |
||
| 728 | ' AND extra_filter.'.$this->extra_key.'='.$this->db->quote($cf_name). |
||
| 729 | ' AND '.$sql_filter); |
||
| 730 | ++$extra_filter; |
||
| 731 | } |
||
| 732 | unset($filter[$name]); |
||
| 733 | } |
||
| 734 | elseif(is_int($name) && $this->is_cf($val)) // lettersearch: #cfname LIKE 's%' |
||
| 735 | { |
||
| 736 | $_cf = explode(' ',$val); |
||
| 737 | foreach($_cf as $cf_np) |
||
| 738 | { |
||
| 739 | // building cf_name by glueing parts together (, in case someone used whitespace in their custom field names) |
||
| 740 | $tcf_name = ($tcf_name?$tcf_name.' ':'').$cf_np; |
||
| 741 | // reacts on the first one found that matches an existing customfield, should be better then the old behavior of |
||
| 742 | // simply splitting by " " and using the first part |
||
| 743 | if ($this->is_cf($tcf_name) && ($cfn = $this->get_cf_name($tcf_name)) && array_search($cfn,(array)$_cfnames,true)!==false ) |
||
| 744 | { |
||
| 745 | $cf = $tcf_name; |
||
| 746 | break; |
||
| 747 | } |
||
| 748 | } |
||
| 749 | unset($filter[$name]); |
||
| 750 | $cf_name = $this->get_cf_name($cf); |
||
| 751 | if (!isset($this->customfields[$cf_name])) continue; // ignore unavailable CF |
||
| 752 | $join .= str_replace('extra_filter','extra_filter'.$extra_filter,$this->extra_join_filter. |
||
| 753 | ' AND extra_filter.'.$this->extra_key.'='.$this->db->quote($cf_name). |
||
| 754 | ' AND '.str_replace($cf,'extra_filter.'.$this->extra_value,$val)); |
||
| 755 | ++$extra_filter; |
||
| 756 | } |
||
| 757 | } |
||
| 758 | } |
||
| 759 | // add DISTINCT as by joining custom fields for search a row can be returned multiple times |
||
| 760 | if ($join && strpos($join, $this->extra_join) !== false) |
||
| 761 | { |
||
| 762 | if (is_array($only_keys)) |
||
| 763 | { |
||
| 764 | $only_keys = array_values($only_keys); |
||
| 765 | $only_keys[0] = 'DISTINCT '.($only_keys[0] != $this->autoinc_id ? $only_keys[0] : |
||
| 766 | $this->table_name.'.'.$this->autoinc_id.' AS '.$this->autoinc_id); |
||
| 767 | } |
||
| 768 | else |
||
| 769 | { |
||
| 770 | $only_keys = 'DISTINCT '.$only_keys; |
||
| 771 | } |
||
| 772 | } |
||
| 773 | } |
||
| 774 | |||
| 775 | /** |
||
| 776 | * Function to test if $field is a custom field: check for the prefix |
||
| 777 | * |
||
| 778 | * @param string $field |
||
| 779 | * @return boolean true if $name is a custom field, false otherwise |
||
| 780 | */ |
||
| 781 | function is_cf($field) |
||
| 782 | { |
||
| 783 | return $field[0] == self::CF_PREFIX; |
||
| 784 | } |
||
| 785 | |||
| 786 | /** |
||
| 787 | * Get name part from a custom field: remove the prefix |
||
| 788 | * |
||
| 789 | * @param string $field |
||
| 790 | * @return string name without prefix |
||
| 791 | */ |
||
| 792 | function get_cf_name($field) |
||
| 793 | { |
||
| 794 | return substr($field,1); |
||
| 795 | } |
||
| 796 | |||
| 797 | /** |
||
| 798 | * Get the field-name from the name of a custom field: prepend the prefix |
||
| 799 | * |
||
| 800 | * @param string $name |
||
| 801 | * @return string prefix-name |
||
| 802 | */ |
||
| 803 | function get_cf_field($name) |
||
| 804 | { |
||
| 805 | return self::CF_PREFIX.$name; |
||
| 806 | } |
||
| 807 | |||
| 808 | /** |
||
| 809 | * Check if cf is stored as 1:N relation in DB and array in memory |
||
| 810 | * |
||
| 811 | * @param string $name |
||
| 812 | * @return string |
||
| 813 | */ |
||
| 814 | function is_multiple($name) |
||
| 818 | } |
||
| 819 | } |
||
| 820 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)or! empty(...)instead.