pictcode / lib / Cake / Model / Datasource / DboSource.php @ 0b1b8047
履歴 | 表示 | アノテート | ダウンロード (102.335 KB)
1 | 635eef61 | spyder1211 | <?php
|
---|---|---|---|
2 | /**
|
||
3 | * Dbo Source
|
||
4 | *
|
||
5 | * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
||
6 | * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||
7 | *
|
||
8 | * Licensed under The MIT License
|
||
9 | * For full copyright and license information, please see the LICENSE.txt
|
||
10 | * Redistributions of files must retain the above copyright notice.
|
||
11 | *
|
||
12 | * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||
13 | * @link http://cakephp.org CakePHP(tm) Project
|
||
14 | * @package Cake.Model.Datasource
|
||
15 | * @since CakePHP(tm) v 0.10.0.1076
|
||
16 | * @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||
17 | */
|
||
18 | |||
19 | App::uses('DataSource', 'Model/Datasource'); |
||
20 | App::uses('CakeText', 'Utility'); |
||
21 | App::uses('View', 'View'); |
||
22 | |||
23 | /**
|
||
24 | * DboSource
|
||
25 | *
|
||
26 | * Creates DBO-descendant objects from a given db connection configuration
|
||
27 | *
|
||
28 | * @package Cake.Model.Datasource
|
||
29 | */
|
||
30 | class DboSource extends DataSource { |
||
31 | |||
32 | /**
|
||
33 | * Description string for this Database Data Source.
|
||
34 | *
|
||
35 | * @var string
|
||
36 | */
|
||
37 | public $description = "Database Data Source"; |
||
38 | |||
39 | /**
|
||
40 | * index definition, standard cake, primary, index, unique
|
||
41 | *
|
||
42 | * @var array
|
||
43 | */
|
||
44 | public $index = array('PRI' => 'primary', 'MUL' => 'index', 'UNI' => 'unique'); |
||
45 | |||
46 | /**
|
||
47 | * Database keyword used to assign aliases to identifiers.
|
||
48 | *
|
||
49 | * @var string
|
||
50 | */
|
||
51 | public $alias = 'AS '; |
||
52 | |||
53 | /**
|
||
54 | * Caches result from query parsing operations. Cached results for both DboSource::name() and
|
||
55 | * DboSource::conditions() will be stored here. Method caching uses `md5()`. If you have
|
||
56 | * problems with collisions, set DboSource::$cacheMethods to false.
|
||
57 | *
|
||
58 | * @var array
|
||
59 | */
|
||
60 | public static $methodCache = array(); |
||
61 | |||
62 | /**
|
||
63 | * Whether or not to cache the results of DboSource::name() and DboSource::conditions()
|
||
64 | * into the memory cache. Set to false to disable the use of the memory cache.
|
||
65 | *
|
||
66 | * @var bool
|
||
67 | */
|
||
68 | public $cacheMethods = true; |
||
69 | |||
70 | /**
|
||
71 | * Flag to support nested transactions. If it is set to false, you will be able to use
|
||
72 | * the transaction methods (begin/commit/rollback), but just the global transaction will
|
||
73 | * be executed.
|
||
74 | *
|
||
75 | * @var bool
|
||
76 | */
|
||
77 | public $useNestedTransactions = false; |
||
78 | |||
79 | /**
|
||
80 | * Print full query debug info?
|
||
81 | *
|
||
82 | * @var bool
|
||
83 | */
|
||
84 | public $fullDebug = false; |
||
85 | |||
86 | /**
|
||
87 | * String to hold how many rows were affected by the last SQL operation.
|
||
88 | *
|
||
89 | * @var string
|
||
90 | */
|
||
91 | public $affected = null; |
||
92 | |||
93 | /**
|
||
94 | * Number of rows in current resultset
|
||
95 | *
|
||
96 | * @var int
|
||
97 | */
|
||
98 | public $numRows = null; |
||
99 | |||
100 | /**
|
||
101 | * Time the last query took
|
||
102 | *
|
||
103 | * @var int
|
||
104 | */
|
||
105 | public $took = null; |
||
106 | |||
107 | /**
|
||
108 | * Result
|
||
109 | *
|
||
110 | * @var array
|
||
111 | */
|
||
112 | protected $_result = null; |
||
113 | |||
114 | /**
|
||
115 | * Queries count.
|
||
116 | *
|
||
117 | * @var int
|
||
118 | */
|
||
119 | protected $_queriesCnt = 0; |
||
120 | |||
121 | /**
|
||
122 | * Total duration of all queries.
|
||
123 | *
|
||
124 | * @var int
|
||
125 | */
|
||
126 | protected $_queriesTime = null; |
||
127 | |||
128 | /**
|
||
129 | * Log of queries executed by this DataSource
|
||
130 | *
|
||
131 | * @var array
|
||
132 | */
|
||
133 | protected $_queriesLog = array(); |
||
134 | |||
135 | /**
|
||
136 | * Maximum number of items in query log
|
||
137 | *
|
||
138 | * This is to prevent query log taking over too much memory.
|
||
139 | *
|
||
140 | * @var int
|
||
141 | */
|
||
142 | protected $_queriesLogMax = 200; |
||
143 | |||
144 | /**
|
||
145 | * Caches serialized results of executed queries
|
||
146 | *
|
||
147 | * @var array
|
||
148 | */
|
||
149 | protected $_queryCache = array(); |
||
150 | |||
151 | /**
|
||
152 | * A reference to the physical connection of this DataSource
|
||
153 | *
|
||
154 | * @var array
|
||
155 | */
|
||
156 | protected $_connection = null; |
||
157 | |||
158 | /**
|
||
159 | * The DataSource configuration key name
|
||
160 | *
|
||
161 | * @var string
|
||
162 | */
|
||
163 | public $configKeyName = null; |
||
164 | |||
165 | /**
|
||
166 | * The starting character that this DataSource uses for quoted identifiers.
|
||
167 | *
|
||
168 | * @var string
|
||
169 | */
|
||
170 | public $startQuote = null; |
||
171 | |||
172 | /**
|
||
173 | * The ending character that this DataSource uses for quoted identifiers.
|
||
174 | *
|
||
175 | * @var string
|
||
176 | */
|
||
177 | public $endQuote = null; |
||
178 | |||
179 | /**
|
||
180 | * The set of valid SQL operations usable in a WHERE statement
|
||
181 | *
|
||
182 | * @var array
|
||
183 | */
|
||
184 | protected $_sqlOps = array('like', 'ilike', 'rlike', 'or', 'not', 'in', 'between', 'regexp', 'similar to'); |
||
185 | |||
186 | /**
|
||
187 | * Indicates the level of nested transactions
|
||
188 | *
|
||
189 | * @var int
|
||
190 | */
|
||
191 | protected $_transactionNesting = 0; |
||
192 | |||
193 | /**
|
||
194 | * Default fields that are used by the DBO
|
||
195 | *
|
||
196 | * @var array
|
||
197 | */
|
||
198 | protected $_queryDefaults = array( |
||
199 | 'conditions' => array(), |
||
200 | 'fields' => null, |
||
201 | 'table' => null, |
||
202 | 'alias' => null, |
||
203 | 'order' => null, |
||
204 | 'limit' => null, |
||
205 | 'joins' => array(), |
||
206 | 'group' => null, |
||
207 | 'offset' => null |
||
208 | ); |
||
209 | |||
210 | /**
|
||
211 | * Separator string for virtualField composition
|
||
212 | *
|
||
213 | * @var string
|
||
214 | */
|
||
215 | public $virtualFieldSeparator = '__'; |
||
216 | |||
217 | /**
|
||
218 | * List of table engine specific parameters used on table creating
|
||
219 | *
|
||
220 | * @var array
|
||
221 | */
|
||
222 | public $tableParameters = array(); |
||
223 | |||
224 | /**
|
||
225 | * List of engine specific additional field parameters used on table creating
|
||
226 | *
|
||
227 | * @var array
|
||
228 | */
|
||
229 | public $fieldParameters = array(); |
||
230 | |||
231 | /**
|
||
232 | * Indicates whether there was a change on the cached results on the methods of this class
|
||
233 | * This will be used for storing in a more persistent cache
|
||
234 | *
|
||
235 | * @var bool
|
||
236 | */
|
||
237 | protected $_methodCacheChange = false; |
||
238 | |||
239 | /**
|
||
240 | * Constructor
|
||
241 | *
|
||
242 | * @param array $config Array of configuration information for the Datasource.
|
||
243 | * @param bool $autoConnect Whether or not the datasource should automatically connect.
|
||
244 | * @throws MissingConnectionException when a connection cannot be made.
|
||
245 | */
|
||
246 | public function __construct($config = null, $autoConnect = true) { |
||
247 | if (!isset($config['prefix'])) { |
||
248 | $config['prefix'] = ''; |
||
249 | } |
||
250 | parent::__construct($config); |
||
251 | $this->fullDebug = Configure::read('debug') > 1; |
||
252 | if (!$this->enabled()) { |
||
253 | throw new MissingConnectionException(array( |
||
254 | 'class' => get_class($this), |
||
255 | 'message' => __d('cake_dev', 'Selected driver is not enabled'), |
||
256 | 'enabled' => false |
||
257 | )); |
||
258 | } |
||
259 | if ($autoConnect) { |
||
260 | $this->connect();
|
||
261 | } |
||
262 | } |
||
263 | |||
264 | /**
|
||
265 | * Reconnects to database server with optional new settings
|
||
266 | *
|
||
267 | * @param array $config An array defining the new configuration settings
|
||
268 | * @return bool True on success, false on failure
|
||
269 | */
|
||
270 | public function reconnect($config = array()) { |
||
271 | $this->disconnect();
|
||
272 | $this->setConfig($config); |
||
273 | $this->_sources = null; |
||
274 | |||
275 | return $this->connect(); |
||
276 | } |
||
277 | |||
278 | /**
|
||
279 | * Disconnects from database.
|
||
280 | *
|
||
281 | * @return bool Always true
|
||
282 | */
|
||
283 | public function disconnect() { |
||
284 | if ($this->_result instanceof PDOStatement) { |
||
285 | $this->_result->closeCursor();
|
||
286 | } |
||
287 | $this->_connection = null; |
||
288 | $this->connected = false; |
||
289 | return true; |
||
290 | } |
||
291 | |||
292 | /**
|
||
293 | * Get the underlying connection object.
|
||
294 | *
|
||
295 | * @return PDO
|
||
296 | */
|
||
297 | public function getConnection() { |
||
298 | return $this->_connection; |
||
299 | } |
||
300 | |||
301 | /**
|
||
302 | * Gets the version string of the database server
|
||
303 | *
|
||
304 | * @return string The database version
|
||
305 | */
|
||
306 | public function getVersion() { |
||
307 | return $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION); |
||
308 | } |
||
309 | |||
310 | /**
|
||
311 | * Returns a quoted and escaped string of $data for use in an SQL statement.
|
||
312 | *
|
||
313 | * @param string $data String to be prepared for use in an SQL statement
|
||
314 | * @param string $column The column datatype into which this data will be inserted.
|
||
315 | * @param bool $null Column allows NULL values
|
||
316 | * @return string Quoted and escaped data
|
||
317 | */
|
||
318 | public function value($data, $column = null, $null = true) { |
||
319 | if (is_array($data) && !empty($data)) { |
||
320 | return array_map( |
||
321 | array(&$this, 'value'), |
||
322 | $data, array_fill(0, count($data), $column) |
||
323 | ); |
||
324 | } elseif (is_object($data) && isset($data->type, $data->value)) { |
||
325 | if ($data->type === 'identifier') { |
||
326 | return $this->name($data->value); |
||
327 | } elseif ($data->type === 'expression') { |
||
328 | return $data->value; |
||
329 | } |
||
330 | } elseif (in_array($data, array('{$__cakeID__$}', '{$__cakeForeignKey__$}'), true)) { |
||
331 | return $data; |
||
332 | } |
||
333 | |||
334 | if ($data === null || (is_array($data) && empty($data))) { |
||
335 | return 'NULL'; |
||
336 | } |
||
337 | |||
338 | if (empty($column)) { |
||
339 | $column = $this->introspectType($data); |
||
340 | } |
||
341 | |||
342 | switch ($column) { |
||
343 | case 'binary': |
||
344 | return $this->_connection->quote($data, PDO::PARAM_LOB); |
||
345 | case 'boolean': |
||
346 | return $this->_connection->quote($this->boolean($data, true), PDO::PARAM_BOOL); |
||
347 | case 'string': |
||
348 | case 'text': |
||
349 | return $this->_connection->quote($data, PDO::PARAM_STR); |
||
350 | default:
|
||
351 | if ($data === '') { |
||
352 | return $null ? 'NULL' : '""'; |
||
353 | } |
||
354 | if (is_float($data)) { |
||
355 | return str_replace(',', '.', strval($data)); |
||
356 | } |
||
357 | if ((is_int($data) || $data === '0') || ( |
||
358 | is_numeric($data) && |
||
359 | strpos($data, ',') === false && |
||
360 | $data[0] != '0' && |
||
361 | strpos($data, 'e') === false) |
||
362 | ) { |
||
363 | return $data; |
||
364 | } |
||
365 | return $this->_connection->quote($data); |
||
366 | } |
||
367 | } |
||
368 | |||
369 | /**
|
||
370 | * Returns an object to represent a database identifier in a query. Expression objects
|
||
371 | * are not sanitized or escaped.
|
||
372 | *
|
||
373 | * @param string $identifier A SQL expression to be used as an identifier
|
||
374 | * @return stdClass An object representing a database identifier to be used in a query
|
||
375 | */
|
||
376 | public function identifier($identifier) { |
||
377 | $obj = new stdClass(); |
||
378 | $obj->type = 'identifier'; |
||
379 | $obj->value = $identifier; |
||
380 | return $obj; |
||
381 | } |
||
382 | |||
383 | /**
|
||
384 | * Returns an object to represent a database expression in a query. Expression objects
|
||
385 | * are not sanitized or escaped.
|
||
386 | *
|
||
387 | * @param string $expression An arbitrary SQL expression to be inserted into a query.
|
||
388 | * @return stdClass An object representing a database expression to be used in a query
|
||
389 | */
|
||
390 | public function expression($expression) { |
||
391 | $obj = new stdClass(); |
||
392 | $obj->type = 'expression'; |
||
393 | $obj->value = $expression; |
||
394 | return $obj; |
||
395 | } |
||
396 | |||
397 | /**
|
||
398 | * Executes given SQL statement.
|
||
399 | *
|
||
400 | * @param string $sql SQL statement
|
||
401 | * @param array $params Additional options for the query.
|
||
402 | * @return mixed Resource or object representing the result set, or false on failure
|
||
403 | */
|
||
404 | public function rawQuery($sql, $params = array()) { |
||
405 | $this->took = $this->numRows = false; |
||
406 | return $this->execute($sql, array(), $params); |
||
407 | } |
||
408 | |||
409 | /**
|
||
410 | * Queries the database with given SQL statement, and obtains some metadata about the result
|
||
411 | * (rows affected, timing, any errors, number of rows in resultset). The query is also logged.
|
||
412 | * If Configure::read('debug') is set, the log is shown all the time, else it is only shown on errors.
|
||
413 | *
|
||
414 | * ### Options
|
||
415 | *
|
||
416 | * - log - Whether or not the query should be logged to the memory log.
|
||
417 | *
|
||
418 | * @param string $sql SQL statement
|
||
419 | * @param array $options The options for executing the query.
|
||
420 | * @param array $params values to be bound to the query.
|
||
421 | * @return mixed Resource or object representing the result set, or false on failure
|
||
422 | */
|
||
423 | public function execute($sql, $options = array(), $params = array()) { |
||
424 | $options += array('log' => $this->fullDebug); |
||
425 | |||
426 | $t = microtime(true); |
||
427 | $this->_result = $this->_execute($sql, $params); |
||
428 | |||
429 | if ($options['log']) { |
||
430 | $this->took = round((microtime(true) - $t) * 1000, 0); |
||
431 | $this->numRows = $this->affected = $this->lastAffected(); |
||
432 | $this->logQuery($sql, $params); |
||
433 | } |
||
434 | |||
435 | return $this->_result; |
||
436 | } |
||
437 | |||
438 | /**
|
||
439 | * Executes given SQL statement.
|
||
440 | *
|
||
441 | * @param string $sql SQL statement
|
||
442 | * @param array $params list of params to be bound to query
|
||
443 | * @param array $prepareOptions Options to be used in the prepare statement
|
||
444 | * @return mixed PDOStatement if query executes with no problem, true as the result of a successful, false on error
|
||
445 | * query returning no rows, such as a CREATE statement, false otherwise
|
||
446 | * @throws PDOException
|
||
447 | */
|
||
448 | protected function _execute($sql, $params = array(), $prepareOptions = array()) { |
||
449 | $sql = trim($sql); |
||
450 | if (preg_match('/^(?:CREATE|ALTER|DROP)\s+(?:TABLE|INDEX)/i', $sql)) { |
||
451 | $statements = array_filter(explode(';', $sql)); |
||
452 | if (count($statements) > 1) { |
||
453 | $result = array_map(array($this, '_execute'), $statements); |
||
454 | return array_search(false, $result) === false; |
||
455 | } |
||
456 | } |
||
457 | |||
458 | try {
|
||
459 | $query = $this->_connection->prepare($sql, $prepareOptions); |
||
460 | $query->setFetchMode(PDO::FETCH_LAZY); |
||
461 | if (!$query->execute($params)) { |
||
462 | $this->_results = $query; |
||
463 | $query->closeCursor();
|
||
464 | return false; |
||
465 | } |
||
466 | if (!$query->columnCount()) { |
||
467 | $query->closeCursor();
|
||
468 | if (!$query->rowCount()) { |
||
469 | return true; |
||
470 | } |
||
471 | } |
||
472 | return $query; |
||
473 | } catch (PDOException $e) { |
||
474 | if (isset($query->queryString)) { |
||
475 | $e->queryString = $query->queryString; |
||
476 | } else {
|
||
477 | $e->queryString = $sql; |
||
478 | } |
||
479 | throw $e; |
||
480 | } |
||
481 | } |
||
482 | |||
483 | /**
|
||
484 | * Returns a formatted error message from previous database operation.
|
||
485 | *
|
||
486 | * @param PDOStatement $query the query to extract the error from if any
|
||
487 | * @return string Error message with error number
|
||
488 | */
|
||
489 | public function lastError(PDOStatement $query = null) { |
||
490 | if ($query) { |
||
491 | $error = $query->errorInfo(); |
||
492 | } else {
|
||
493 | $error = $this->_connection->errorInfo(); |
||
494 | } |
||
495 | if (empty($error[2])) { |
||
496 | return null; |
||
497 | } |
||
498 | return $error[1] . ': ' . $error[2]; |
||
499 | } |
||
500 | |||
501 | /**
|
||
502 | * Returns number of affected rows in previous database operation. If no previous operation exists,
|
||
503 | * this returns false.
|
||
504 | *
|
||
505 | * @param mixed $source The source to check.
|
||
506 | * @return int Number of affected rows
|
||
507 | */
|
||
508 | public function lastAffected($source = null) { |
||
509 | if ($this->hasResult()) { |
||
510 | return $this->_result->rowCount(); |
||
511 | } |
||
512 | return 0; |
||
513 | } |
||
514 | |||
515 | /**
|
||
516 | * Returns number of rows in previous resultset. If no previous resultset exists,
|
||
517 | * this returns false.
|
||
518 | *
|
||
519 | * @param mixed $source Not used
|
||
520 | * @return int Number of rows in resultset
|
||
521 | */
|
||
522 | public function lastNumRows($source = null) { |
||
523 | return $this->lastAffected(); |
||
524 | } |
||
525 | |||
526 | /**
|
||
527 | * DataSource Query abstraction
|
||
528 | *
|
||
529 | * @return resource Result resource identifier.
|
||
530 | */
|
||
531 | public function query() { |
||
532 | $args = func_get_args(); |
||
533 | $fields = null; |
||
534 | $order = null; |
||
535 | $limit = null; |
||
536 | $page = null; |
||
537 | $recursive = null; |
||
538 | |||
539 | if (count($args) === 1) { |
||
540 | return $this->fetchAll($args[0]); |
||
541 | } elseif (count($args) > 1 && (strpos($args[0], 'findBy') === 0 || strpos($args[0], 'findAllBy') === 0)) { |
||
542 | $params = $args[1]; |
||
543 | |||
544 | if (substr($args[0], 0, 6) === 'findBy') { |
||
545 | $all = false; |
||
546 | $field = Inflector::underscore(substr($args[0], 6)); |
||
547 | } else {
|
||
548 | $all = true; |
||
549 | $field = Inflector::underscore(substr($args[0], 9)); |
||
550 | } |
||
551 | |||
552 | $or = (strpos($field, '_or_') !== false); |
||
553 | if ($or) { |
||
554 | $field = explode('_or_', $field); |
||
555 | } else {
|
||
556 | $field = explode('_and_', $field); |
||
557 | } |
||
558 | $off = count($field) - 1; |
||
559 | |||
560 | if (isset($params[1 + $off])) { |
||
561 | $fields = $params[1 + $off]; |
||
562 | } |
||
563 | |||
564 | if (isset($params[2 + $off])) { |
||
565 | $order = $params[2 + $off]; |
||
566 | } |
||
567 | |||
568 | if (!array_key_exists(0, $params)) { |
||
569 | return false; |
||
570 | } |
||
571 | |||
572 | $c = 0; |
||
573 | $conditions = array(); |
||
574 | |||
575 | foreach ($field as $f) { |
||
576 | $conditions[$args[2]->alias . '.' . $f] = $params[$c++]; |
||
577 | } |
||
578 | |||
579 | if ($or) { |
||
580 | $conditions = array('OR' => $conditions); |
||
581 | } |
||
582 | |||
583 | if ($all) { |
||
584 | if (isset($params[3 + $off])) { |
||
585 | $limit = $params[3 + $off]; |
||
586 | } |
||
587 | |||
588 | if (isset($params[4 + $off])) { |
||
589 | $page = $params[4 + $off]; |
||
590 | } |
||
591 | |||
592 | if (isset($params[5 + $off])) { |
||
593 | $recursive = $params[5 + $off]; |
||
594 | } |
||
595 | return $args[2]->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive')); |
||
596 | } |
||
597 | if (isset($params[3 + $off])) { |
||
598 | $recursive = $params[3 + $off]; |
||
599 | } |
||
600 | return $args[2]->find('first', compact('conditions', 'fields', 'order', 'recursive')); |
||
601 | } |
||
602 | if (isset($args[1]) && $args[1] === true) { |
||
603 | return $this->fetchAll($args[0], true); |
||
604 | } elseif (isset($args[1]) && !is_array($args[1])) { |
||
605 | return $this->fetchAll($args[0], false); |
||
606 | } elseif (isset($args[1]) && is_array($args[1])) { |
||
607 | if (isset($args[2])) { |
||
608 | $cache = $args[2]; |
||
609 | } else {
|
||
610 | $cache = true; |
||
611 | } |
||
612 | return $this->fetchAll($args[0], $args[1], array('cache' => $cache)); |
||
613 | } |
||
614 | } |
||
615 | |||
616 | /**
|
||
617 | * Returns a row from current resultset as an array
|
||
618 | *
|
||
619 | * @param string $sql Some SQL to be executed.
|
||
620 | * @return array The fetched row as an array
|
||
621 | */
|
||
622 | public function fetchRow($sql = null) { |
||
623 | if (is_string($sql) && strlen($sql) > 5 && !$this->execute($sql)) { |
||
624 | return null; |
||
625 | } |
||
626 | |||
627 | if ($this->hasResult()) { |
||
628 | $this->resultSet($this->_result); |
||
629 | $resultRow = $this->fetchResult(); |
||
630 | if (isset($resultRow[0])) { |
||
631 | $this->fetchVirtualField($resultRow); |
||
632 | } |
||
633 | return $resultRow; |
||
634 | } |
||
635 | return null; |
||
636 | } |
||
637 | |||
638 | /**
|
||
639 | * Returns an array of all result rows for a given SQL query.
|
||
640 | *
|
||
641 | * Returns false if no rows matched.
|
||
642 | *
|
||
643 | * ### Options
|
||
644 | *
|
||
645 | * - `cache` - Returns the cached version of the query, if exists and stores the result in cache.
|
||
646 | * This is a non-persistent cache, and only lasts for a single request. This option
|
||
647 | * defaults to true. If you are directly calling this method, you can disable caching
|
||
648 | * by setting $options to `false`
|
||
649 | *
|
||
650 | * @param string $sql SQL statement
|
||
651 | * @param array|bool $params Either parameters to be bound as values for the SQL statement,
|
||
652 | * or a boolean to control query caching.
|
||
653 | * @param array $options additional options for the query.
|
||
654 | * @return bool|array Array of resultset rows, or false if no rows matched
|
||
655 | */
|
||
656 | public function fetchAll($sql, $params = array(), $options = array()) { |
||
657 | if (is_string($options)) { |
||
658 | $options = array('modelName' => $options); |
||
659 | } |
||
660 | if (is_bool($params)) { |
||
661 | $options['cache'] = $params; |
||
662 | $params = array(); |
||
663 | } |
||
664 | $options += array('cache' => true); |
||
665 | $cache = $options['cache']; |
||
666 | if ($cache && ($cached = $this->getQueryCache($sql, $params)) !== false) { |
||
667 | return $cached; |
||
668 | } |
||
669 | $result = $this->execute($sql, array(), $params); |
||
670 | if ($result) { |
||
671 | $out = array(); |
||
672 | |||
673 | if ($this->hasResult()) { |
||
674 | $first = $this->fetchRow(); |
||
675 | if ($first) { |
||
676 | $out[] = $first; |
||
677 | } |
||
678 | while ($item = $this->fetchResult()) { |
||
679 | if (isset($item[0])) { |
||
680 | $this->fetchVirtualField($item); |
||
681 | } |
||
682 | $out[] = $item; |
||
683 | } |
||
684 | } |
||
685 | |||
686 | if (!is_bool($result) && $cache) { |
||
687 | $this->_writeQueryCache($sql, $out, $params); |
||
688 | } |
||
689 | |||
690 | if (empty($out) && is_bool($this->_result)) { |
||
691 | return $this->_result; |
||
692 | } |
||
693 | return $out; |
||
694 | } |
||
695 | return false; |
||
696 | } |
||
697 | |||
698 | /**
|
||
699 | * Fetches the next row from the current result set
|
||
700 | *
|
||
701 | * @return bool
|
||
702 | */
|
||
703 | public function fetchResult() { |
||
704 | return false; |
||
705 | } |
||
706 | |||
707 | /**
|
||
708 | * Modifies $result array to place virtual fields in model entry where they belongs to
|
||
709 | *
|
||
710 | * @param array &$result Reference to the fetched row
|
||
711 | * @return void
|
||
712 | */
|
||
713 | public function fetchVirtualField(&$result) { |
||
714 | if (isset($result[0]) && is_array($result[0])) { |
||
715 | foreach ($result[0] as $field => $value) { |
||
716 | if (strpos($field, $this->virtualFieldSeparator) === false) { |
||
717 | continue;
|
||
718 | } |
||
719 | |||
720 | list($alias, $virtual) = explode($this->virtualFieldSeparator, $field); |
||
721 | |||
722 | if (!ClassRegistry::isKeySet($alias)) { |
||
723 | return;
|
||
724 | } |
||
725 | |||
726 | $Model = ClassRegistry::getObject($alias); |
||
727 | |||
728 | if ($Model->isVirtualField($virtual)) { |
||
729 | $result[$alias][$virtual] = $value; |
||
730 | unset($result[0][$field]); |
||
731 | } |
||
732 | } |
||
733 | if (empty($result[0])) { |
||
734 | unset($result[0]); |
||
735 | } |
||
736 | } |
||
737 | } |
||
738 | |||
739 | /**
|
||
740 | * Returns a single field of the first of query results for a given SQL query, or false if empty.
|
||
741 | *
|
||
742 | * @param string $name The name of the field to get.
|
||
743 | * @param string $sql The SQL query.
|
||
744 | * @return mixed Value of field read, or false if not found.
|
||
745 | */
|
||
746 | public function field($name, $sql) { |
||
747 | $data = $this->fetchRow($sql); |
||
748 | if (empty($data[$name])) { |
||
749 | return false; |
||
750 | } |
||
751 | return $data[$name]; |
||
752 | } |
||
753 | |||
754 | /**
|
||
755 | * Empties the method caches.
|
||
756 | * These caches are used by DboSource::name() and DboSource::conditions()
|
||
757 | *
|
||
758 | * @return void
|
||
759 | */
|
||
760 | public function flushMethodCache() { |
||
761 | $this->_methodCacheChange = true; |
||
762 | static::$methodCache = array(); |
||
763 | } |
||
764 | |||
765 | /**
|
||
766 | * Cache a value into the methodCaches. Will respect the value of DboSource::$cacheMethods.
|
||
767 | * Will retrieve a value from the cache if $value is null.
|
||
768 | *
|
||
769 | * If caching is disabled and a write is attempted, the $value will be returned.
|
||
770 | * A read will either return the value or null.
|
||
771 | *
|
||
772 | * @param string $method Name of the method being cached.
|
||
773 | * @param string $key The key name for the cache operation.
|
||
774 | * @param mixed $value The value to cache into memory.
|
||
775 | * @return mixed Either null on failure, or the value if its set.
|
||
776 | */
|
||
777 | public function cacheMethod($method, $key, $value = null) { |
||
778 | if ($this->cacheMethods === false) { |
||
779 | return $value; |
||
780 | } |
||
781 | if (!$this->_methodCacheChange && empty(static::$methodCache)) { |
||
782 | static::$methodCache = (array)Cache::read('method_cache', '_cake_core_'); |
||
783 | } |
||
784 | if ($value === null) { |
||
785 | return (isset(static::$methodCache[$method][$key])) ? static::$methodCache[$method][$key] : null; |
||
786 | } |
||
787 | $this->_methodCacheChange = true; |
||
788 | return static::$methodCache[$method][$key] = $value; |
||
789 | } |
||
790 | |||
791 | /**
|
||
792 | * Returns a quoted name of $data for use in an SQL statement.
|
||
793 | * Strips fields out of SQL functions before quoting.
|
||
794 | *
|
||
795 | * Results of this method are stored in a memory cache. This improves performance, but
|
||
796 | * because the method uses a hashing algorithm it can have collisions.
|
||
797 | * Setting DboSource::$cacheMethods to false will disable the memory cache.
|
||
798 | *
|
||
799 | * @param mixed $data Either a string with a column to quote. An array of columns to quote or an
|
||
800 | * object from DboSource::expression() or DboSource::identifier()
|
||
801 | * @return string SQL field
|
||
802 | */
|
||
803 | public function name($data) { |
||
804 | if (is_object($data) && isset($data->type)) { |
||
805 | return $data->value; |
||
806 | } |
||
807 | if ($data === '*') { |
||
808 | return '*'; |
||
809 | } |
||
810 | if (is_array($data)) { |
||
811 | foreach ($data as $i => $dataItem) { |
||
812 | $data[$i] = $this->name($dataItem); |
||
813 | } |
||
814 | return $data; |
||
815 | } |
||
816 | $cacheKey = md5($this->startQuote . $data . $this->endQuote); |
||
817 | if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) { |
||
818 | return $return; |
||
819 | } |
||
820 | $data = trim($data); |
||
821 | if (preg_match('/^[\w-]+(?:\.[^ \*]*)*$/', $data)) { // string, string.string |
||
822 | if (strpos($data, '.') === false) { // string |
||
823 | return $this->cacheMethod(__FUNCTION__, $cacheKey, $this->startQuote . $data . $this->endQuote); |
||
824 | } |
||
825 | $items = explode('.', $data); |
||
826 | return $this->cacheMethod(__FUNCTION__, $cacheKey, |
||
827 | $this->startQuote . implode($this->endQuote . '.' . $this->startQuote, $items) . $this->endQuote |
||
828 | ); |
||
829 | } |
||
830 | if (preg_match('/^[\w-]+\.\*$/', $data)) { // string.* |
||
831 | return $this->cacheMethod(__FUNCTION__, $cacheKey, |
||
832 | $this->startQuote . str_replace('.*', $this->endQuote . '.*', $data) |
||
833 | ); |
||
834 | } |
||
835 | if (preg_match('/^([\w-]+)\((.*)\)$/', $data, $matches)) { // Functions |
||
836 | return $this->cacheMethod(__FUNCTION__, $cacheKey, |
||
837 | $matches[1] . '(' . $this->name($matches[2]) . ')' |
||
838 | ); |
||
839 | } |
||
840 | if (preg_match('/^([\w-]+(\.[\w-]+|\(.*\))*)\s+' . preg_quote($this->alias) . '\s*([\w-]+)$/i', $data, $matches)) { |
||
841 | return $this->cacheMethod( |
||
842 | __FUNCTION__, $cacheKey, |
||
843 | preg_replace(
|
||
844 | '/\s{2,}/', ' ', $this->name($matches[1]) . ' ' . $this->alias . ' ' . $this->name($matches[3]) |
||
845 | ) |
||
846 | ); |
||
847 | } |
||
848 | if (preg_match('/^[\w-_\s]*[\w-_]+/', $data)) { |
||
849 | return $this->cacheMethod(__FUNCTION__, $cacheKey, $this->startQuote . $data . $this->endQuote); |
||
850 | } |
||
851 | return $this->cacheMethod(__FUNCTION__, $cacheKey, $data); |
||
852 | } |
||
853 | |||
854 | /**
|
||
855 | * Checks if the source is connected to the database.
|
||
856 | *
|
||
857 | * @return bool True if the database is connected, else false
|
||
858 | */
|
||
859 | public function isConnected() { |
||
860 | if ($this->_connection === null) { |
||
861 | $connected = false; |
||
862 | } else {
|
||
863 | try {
|
||
864 | $connected = $this->_connection->query('SELECT 1'); |
||
865 | } catch (Exception $e) { |
||
866 | $connected = false; |
||
867 | } |
||
868 | } |
||
869 | $this->connected = !empty($connected); |
||
870 | return $this->connected; |
||
871 | } |
||
872 | |||
873 | /**
|
||
874 | * Checks if the result is valid
|
||
875 | *
|
||
876 | * @return bool True if the result is valid else false
|
||
877 | */
|
||
878 | public function hasResult() { |
||
879 | return $this->_result instanceof PDOStatement; |
||
880 | } |
||
881 | |||
882 | /**
|
||
883 | * Get the query log as an array.
|
||
884 | *
|
||
885 | * @param bool $sorted Get the queries sorted by time taken, defaults to false.
|
||
886 | * @param bool $clear If True the existing log will cleared.
|
||
887 | * @return array Array of queries run as an array
|
||
888 | */
|
||
889 | public function getLog($sorted = false, $clear = true) { |
||
890 | if ($sorted) { |
||
891 | $log = sortByKey($this->_queriesLog, 'took', 'desc', SORT_NUMERIC); |
||
892 | } else {
|
||
893 | $log = $this->_queriesLog; |
||
894 | } |
||
895 | if ($clear) { |
||
896 | $this->_queriesLog = array(); |
||
897 | } |
||
898 | return array('log' => $log, 'count' => $this->_queriesCnt, 'time' => $this->_queriesTime); |
||
899 | } |
||
900 | |||
901 | /**
|
||
902 | * Outputs the contents of the queries log. If in a non-CLI environment the sql_log element
|
||
903 | * will be rendered and output. If in a CLI environment, a plain text log is generated.
|
||
904 | *
|
||
905 | * @param bool $sorted Get the queries sorted by time taken, defaults to false.
|
||
906 | * @return void
|
||
907 | */
|
||
908 | public function showLog($sorted = false) { |
||
909 | $log = $this->getLog($sorted, false); |
||
910 | if (empty($log['log'])) { |
||
911 | return;
|
||
912 | } |
||
913 | if (PHP_SAPI !== 'cli') { |
||
914 | $controller = null; |
||
915 | $View = new View($controller, false); |
||
916 | $View->set('sqlLogs', array($this->configKeyName => $log)); |
||
917 | echo $View->element('sql_dump', array('_forced_from_dbo_' => true)); |
||
918 | } else {
|
||
919 | foreach ($log['log'] as $k => $i) { |
||
920 | print (($k + 1) . ". {$i['query']}\n"); |
||
921 | } |
||
922 | } |
||
923 | } |
||
924 | |||
925 | /**
|
||
926 | * Log given SQL query.
|
||
927 | *
|
||
928 | * @param string $sql SQL statement
|
||
929 | * @param array $params Values binded to the query (prepared statements)
|
||
930 | * @return void
|
||
931 | */
|
||
932 | public function logQuery($sql, $params = array()) { |
||
933 | $this->_queriesCnt++;
|
||
934 | $this->_queriesTime += $this->took; |
||
935 | $this->_queriesLog[] = array( |
||
936 | 'query' => $sql, |
||
937 | 'params' => $params, |
||
938 | 'affected' => $this->affected, |
||
939 | 'numRows' => $this->numRows, |
||
940 | 'took' => $this->took |
||
941 | ); |
||
942 | if (count($this->_queriesLog) > $this->_queriesLogMax) { |
||
943 | array_shift($this->_queriesLog); |
||
944 | } |
||
945 | } |
||
946 | |||
947 | /**
|
||
948 | * Gets full table name including prefix
|
||
949 | *
|
||
950 | * @param Model|string $model Either a Model object or a string table name.
|
||
951 | * @param bool $quote Whether you want the table name quoted.
|
||
952 | * @param bool $schema Whether you want the schema name included.
|
||
953 | * @return string Full quoted table name
|
||
954 | */
|
||
955 | public function fullTableName($model, $quote = true, $schema = true) { |
||
956 | if (is_object($model)) { |
||
957 | $schemaName = $model->schemaName; |
||
958 | $table = $model->tablePrefix . $model->table; |
||
959 | } elseif (!empty($this->config['prefix']) && strpos($model, $this->config['prefix']) !== 0) { |
||
960 | $table = $this->config['prefix'] . strval($model); |
||
961 | } else {
|
||
962 | $table = strval($model); |
||
963 | } |
||
964 | |||
965 | if ($schema && !isset($schemaName)) { |
||
966 | $schemaName = $this->getSchemaName(); |
||
967 | } |
||
968 | |||
969 | if ($quote) { |
||
970 | if ($schema && !empty($schemaName)) { |
||
971 | if (strstr($table, '.') === false) { |
||
972 | return $this->name($schemaName) . '.' . $this->name($table); |
||
973 | } |
||
974 | } |
||
975 | return $this->name($table); |
||
976 | } |
||
977 | |||
978 | if ($schema && !empty($schemaName)) { |
||
979 | if (strstr($table, '.') === false) { |
||
980 | return $schemaName . '.' . $table; |
||
981 | } |
||
982 | } |
||
983 | |||
984 | return $table; |
||
985 | } |
||
986 | |||
987 | /**
|
||
988 | * The "C" in CRUD
|
||
989 | *
|
||
990 | * Creates new records in the database.
|
||
991 | *
|
||
992 | * @param Model $Model Model object that the record is for.
|
||
993 | * @param array $fields An array of field names to insert. If null, $Model->data will be
|
||
994 | * used to generate field names.
|
||
995 | * @param array $values An array of values with keys matching the fields. If null, $Model->data will
|
||
996 | * be used to generate values.
|
||
997 | * @return bool Success
|
||
998 | */
|
||
999 | public function create(Model $Model, $fields = null, $values = null) { |
||
1000 | $id = null; |
||
1001 | |||
1002 | if (!$fields) { |
||
1003 | unset($fields, $values); |
||
1004 | $fields = array_keys($Model->data); |
||
1005 | $values = array_values($Model->data); |
||
1006 | } |
||
1007 | $count = count($fields); |
||
1008 | |||
1009 | for ($i = 0; $i < $count; $i++) { |
||
1010 | $schema = $Model->schema(); |
||
1011 | $valueInsert[] = $this->value($values[$i], $Model->getColumnType($fields[$i]), isset($schema[$fields[$i]]) ? $schema[$fields[$i]]['null'] : true); |
||
1012 | $fieldInsert[] = $this->name($fields[$i]); |
||
1013 | if ($fields[$i] === $Model->primaryKey) { |
||
1014 | $id = $values[$i]; |
||
1015 | } |
||
1016 | } |
||
1017 | |||
1018 | $query = array( |
||
1019 | 'table' => $this->fullTableName($Model), |
||
1020 | 'fields' => implode(', ', $fieldInsert), |
||
1021 | 'values' => implode(', ', $valueInsert) |
||
1022 | ); |
||
1023 | |||
1024 | if ($this->execute($this->renderStatement('create', $query))) { |
||
1025 | if (empty($id)) { |
||
1026 | $id = $this->lastInsertId($this->fullTableName($Model, false, false), $Model->primaryKey); |
||
1027 | } |
||
1028 | $Model->setInsertID($id); |
||
1029 | $Model->id = $id; |
||
1030 | return true; |
||
1031 | } |
||
1032 | |||
1033 | $Model->onError();
|
||
1034 | return false; |
||
1035 | } |
||
1036 | |||
1037 | /**
|
||
1038 | * The "R" in CRUD
|
||
1039 | *
|
||
1040 | * Reads record(s) from the database.
|
||
1041 | *
|
||
1042 | * @param Model $Model A Model object that the query is for.
|
||
1043 | * @param array $queryData An array of queryData information containing keys similar to Model::find().
|
||
1044 | * @param int $recursive Number of levels of association
|
||
1045 | * @return mixed boolean false on error/failure. An array of results on success.
|
||
1046 | */
|
||
1047 | public function read(Model $Model, $queryData = array(), $recursive = null) { |
||
1048 | $queryData = $this->_scrubQueryData($queryData); |
||
1049 | |||
1050 | $array = array('callbacks' => $queryData['callbacks']); |
||
1051 | |||
1052 | if ($recursive === null && isset($queryData['recursive'])) { |
||
1053 | $recursive = $queryData['recursive']; |
||
1054 | } |
||
1055 | |||
1056 | if ($recursive !== null) { |
||
1057 | $modelRecursive = $Model->recursive; |
||
1058 | $Model->recursive = $recursive; |
||
1059 | } |
||
1060 | |||
1061 | if (!empty($queryData['fields'])) { |
||
1062 | $noAssocFields = true; |
||
1063 | $queryData['fields'] = $this->fields($Model, null, $queryData['fields']); |
||
1064 | } else {
|
||
1065 | $noAssocFields = false; |
||
1066 | $queryData['fields'] = $this->fields($Model); |
||
1067 | } |
||
1068 | |||
1069 | if ($Model->recursive === -1) { |
||
1070 | // Primary model data only, no joins.
|
||
1071 | $associations = array(); |
||
1072 | |||
1073 | } else {
|
||
1074 | $associations = $Model->associations(); |
||
1075 | |||
1076 | if ($Model->recursive === 0) { |
||
1077 | // Primary model data and its domain.
|
||
1078 | unset($associations[2], $associations[3]); |
||
1079 | } |
||
1080 | } |
||
1081 | |||
1082 | $originalJoins = $queryData['joins']; |
||
1083 | $queryData['joins'] = array(); |
||
1084 | |||
1085 | // Generate hasOne and belongsTo associations inside $queryData
|
||
1086 | $linkedModels = array(); |
||
1087 | foreach ($associations as $type) { |
||
1088 | if ($type !== 'hasOne' && $type !== 'belongsTo') { |
||
1089 | continue;
|
||
1090 | } |
||
1091 | |||
1092 | foreach ($Model->{$type} as $assoc => $assocData) { |
||
1093 | $LinkModel = $Model->{$assoc}; |
||
1094 | |||
1095 | if ($Model->useDbConfig !== $LinkModel->useDbConfig) { |
||
1096 | continue;
|
||
1097 | } |
||
1098 | |||
1099 | if ($noAssocFields) { |
||
1100 | $assocData['fields'] = false; |
||
1101 | } |
||
1102 | |||
1103 | $external = isset($assocData['external']); |
||
1104 | |||
1105 | if ($this->generateAssociationQuery($Model, $LinkModel, $type, $assoc, $assocData, $queryData, $external) === true) { |
||
1106 | $linkedModels[$type . '/' . $assoc] = true; |
||
1107 | } |
||
1108 | } |
||
1109 | } |
||
1110 | |||
1111 | if (!empty($originalJoins)) { |
||
1112 | $queryData['joins'] = array_merge($queryData['joins'], $originalJoins); |
||
1113 | } |
||
1114 | |||
1115 | // Build SQL statement with the primary model, plus hasOne and belongsTo associations
|
||
1116 | $query = $this->buildAssociationQuery($Model, $queryData); |
||
1117 | |||
1118 | $resultSet = $this->fetchAll($query, $Model->cacheQueries); |
||
1119 | unset($query); |
||
1120 | |||
1121 | if ($resultSet === false) { |
||
1122 | $Model->onError();
|
||
1123 | return false; |
||
1124 | } |
||
1125 | |||
1126 | $filtered = array(); |
||
1127 | |||
1128 | // Deep associations
|
||
1129 | if ($Model->recursive > -1) { |
||
1130 | $joined = array(); |
||
1131 | if (isset($queryData['joins'][0]['alias'])) { |
||
1132 | $joined[$Model->alias] = (array)Hash::extract($queryData['joins'], '{n}.alias'); |
||
1133 | } |
||
1134 | |||
1135 | foreach ($associations as $type) { |
||
1136 | foreach ($Model->{$type} as $assoc => $assocData) { |
||
1137 | $LinkModel = $Model->{$assoc}; |
||
1138 | |||
1139 | if (!isset($linkedModels[$type . '/' . $assoc])) { |
||
1140 | $db = $Model->useDbConfig === $LinkModel->useDbConfig ? $this : $LinkModel->getDataSource(); |
||
1141 | } elseif ($Model->recursive > 1) { |
||
1142 | $db = $this; |
||
1143 | } |
||
1144 | |||
1145 | if (isset($db) && method_exists($db, 'queryAssociation')) { |
||
1146 | $stack = array($assoc); |
||
1147 | $stack['_joined'] = $joined; |
||
1148 | |||
1149 | $db->queryAssociation($Model, $LinkModel, $type, $assoc, $assocData, $array, true, $resultSet, $Model->recursive - 1, $stack); |
||
1150 | unset($db); |
||
1151 | |||
1152 | if ($type === 'hasMany' || $type === 'hasAndBelongsToMany') { |
||
1153 | $filtered[] = $assoc; |
||
1154 | } |
||
1155 | } |
||
1156 | } |
||
1157 | } |
||
1158 | } |
||
1159 | |||
1160 | if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { |
||
1161 | $this->_filterResults($resultSet, $Model, $filtered); |
||
1162 | } |
||
1163 | |||
1164 | if ($recursive !== null) { |
||
1165 | $Model->recursive = $modelRecursive; |
||
1166 | } |
||
1167 | |||
1168 | return $resultSet; |
||
1169 | } |
||
1170 | |||
1171 | /**
|
||
1172 | * Passes association results through afterFind filters of the corresponding model.
|
||
1173 | *
|
||
1174 | * The primary model is always excluded, because the filtering is later done by Model::_filterResults().
|
||
1175 | *
|
||
1176 | * @param array &$resultSet Reference of resultset to be filtered.
|
||
1177 | * @param Model $Model Instance of model to operate against.
|
||
1178 | * @param array $filtered List of classes already filtered, to be skipped.
|
||
1179 | * @return array Array of results that have been filtered through $Model->afterFind.
|
||
1180 | */
|
||
1181 | protected function _filterResults(&$resultSet, Model $Model, $filtered = array()) { |
||
1182 | if (!is_array($resultSet)) { |
||
1183 | return array(); |
||
1184 | } |
||
1185 | |||
1186 | $current = reset($resultSet); |
||
1187 | if (!is_array($current)) { |
||
1188 | return array(); |
||
1189 | } |
||
1190 | |||
1191 | $keys = array_diff(array_keys($current), $filtered, array($Model->alias)); |
||
1192 | $filtering = array(); |
||
1193 | |||
1194 | foreach ($keys as $className) { |
||
1195 | if (!isset($Model->{$className}) || !is_object($Model->{$className})) { |
||
1196 | continue;
|
||
1197 | } |
||
1198 | |||
1199 | $LinkedModel = $Model->{$className}; |
||
1200 | $filtering[] = $className; |
||
1201 | |||
1202 | foreach ($resultSet as $key => &$result) { |
||
1203 | $data = $LinkedModel->afterFind(array(array($className => $result[$className])), false); |
||
1204 | if (isset($data[0][$className])) { |
||
1205 | $result[$className] = $data[0][$className]; |
||
1206 | } else {
|
||
1207 | unset($resultSet[$key]); |
||
1208 | } |
||
1209 | } |
||
1210 | } |
||
1211 | |||
1212 | return $filtering; |
||
1213 | } |
||
1214 | |||
1215 | /**
|
||
1216 | * Passes association results through afterFind filters of the corresponding model.
|
||
1217 | *
|
||
1218 | * Similar to DboSource::_filterResults(), but this filters only specified models.
|
||
1219 | * The primary model can not be specified, because this call DboSource::_filterResults() internally.
|
||
1220 | *
|
||
1221 | * @param array &$resultSet Reference of resultset to be filtered.
|
||
1222 | * @param Model $Model Instance of model to operate against.
|
||
1223 | * @param array $toBeFiltered List of classes to be filtered.
|
||
1224 | * @return array Array of results that have been filtered through $Model->afterFind.
|
||
1225 | */
|
||
1226 | protected function _filterResultsInclusive(&$resultSet, Model $Model, $toBeFiltered = array()) { |
||
1227 | $exclude = array(); |
||
1228 | |||
1229 | if (is_array($resultSet)) { |
||
1230 | $current = reset($resultSet); |
||
1231 | if (is_array($current)) { |
||
1232 | $exclude = array_diff(array_keys($current), $toBeFiltered); |
||
1233 | } |
||
1234 | } |
||
1235 | |||
1236 | return $this->_filterResults($resultSet, $Model, $exclude); |
||
1237 | } |
||
1238 | |||
1239 | /**
|
||
1240 | * Queries associations.
|
||
1241 | *
|
||
1242 | * Used to fetch results on recursive models.
|
||
1243 | *
|
||
1244 | * - 'hasMany' associations with no limit set:
|
||
1245 | * Fetch, filter and merge is done recursively for every level.
|
||
1246 | *
|
||
1247 | * - 'hasAndBelongsToMany' associations:
|
||
1248 | * Fetch and filter is done unaffected by the (recursive) level set.
|
||
1249 | *
|
||
1250 | * @param Model $Model Primary Model object.
|
||
1251 | * @param Model $LinkModel Linked model object.
|
||
1252 | * @param string $type Association type, one of the model association types ie. hasMany.
|
||
1253 | * @param string $association Association name.
|
||
1254 | * @param array $assocData Association data.
|
||
1255 | * @param array &$queryData An array of queryData information containing keys similar to Model::find().
|
||
1256 | * @param bool $external Whether or not the association query is on an external datasource.
|
||
1257 | * @param array &$resultSet Existing results.
|
||
1258 | * @param int $recursive Number of levels of association.
|
||
1259 | * @param array $stack A list with joined models.
|
||
1260 | * @return mixed
|
||
1261 | * @throws CakeException when results cannot be created.
|
||
1262 | */
|
||
1263 | public function queryAssociation(Model $Model, Model $LinkModel, $type, $association, $assocData, &$queryData, $external, &$resultSet, $recursive, $stack) { |
||
1264 | if (isset($stack['_joined'])) { |
||
1265 | $joined = $stack['_joined']; |
||
1266 | unset($stack['_joined']); |
||
1267 | } |
||
1268 | |||
1269 | $queryTemplate = $this->generateAssociationQuery($Model, $LinkModel, $type, $association, $assocData, $queryData, $external); |
||
1270 | if (empty($queryTemplate)) { |
||
1271 | return null; |
||
1272 | } |
||
1273 | |||
1274 | if (!is_array($resultSet)) { |
||
1275 | throw new CakeException(__d('cake_dev', 'Error in Model %s', get_class($Model))); |
||
1276 | } |
||
1277 | |||
1278 | if ($type === 'hasMany' && empty($assocData['limit']) && !empty($assocData['foreignKey'])) { |
||
1279 | // 'hasMany' associations with no limit set.
|
||
1280 | |||
1281 | $assocIds = array(); |
||
1282 | foreach ($resultSet as $result) { |
||
1283 | $assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack); |
||
1284 | } |
||
1285 | $assocIds = array_filter($assocIds); |
||
1286 | |||
1287 | // Fetch
|
||
1288 | $assocResultSet = array(); |
||
1289 | if (!empty($assocIds)) { |
||
1290 | $assocResultSet = $this->_fetchHasMany($Model, $queryTemplate, $assocIds); |
||
1291 | } |
||
1292 | |||
1293 | // Recursively query associations
|
||
1294 | if ($recursive > 0 && !empty($assocResultSet) && is_array($assocResultSet)) { |
||
1295 | foreach ($LinkModel->associations() as $type1) { |
||
1296 | foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) { |
||
1297 | $DeepModel = $LinkModel->{$assoc1}; |
||
1298 | $tmpStack = $stack; |
||
1299 | $tmpStack[] = $assoc1; |
||
1300 | |||
1301 | $db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource(); |
||
1302 | |||
1303 | $db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack); |
||
1304 | } |
||
1305 | } |
||
1306 | } |
||
1307 | |||
1308 | // Filter
|
||
1309 | if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { |
||
1310 | $this->_filterResultsInclusive($assocResultSet, $Model, array($association)); |
||
1311 | } |
||
1312 | |||
1313 | // Merge
|
||
1314 | return $this->_mergeHasMany($resultSet, $assocResultSet, $association, $Model); |
||
1315 | |||
1316 | } elseif ($type === 'hasAndBelongsToMany') { |
||
1317 | // 'hasAndBelongsToMany' associations.
|
||
1318 | |||
1319 | $assocIds = array(); |
||
1320 | foreach ($resultSet as $result) { |
||
1321 | $assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack); |
||
1322 | } |
||
1323 | $assocIds = array_filter($assocIds); |
||
1324 | |||
1325 | // Fetch
|
||
1326 | $assocResultSet = array(); |
||
1327 | if (!empty($assocIds)) { |
||
1328 | $assocResultSet = $this->_fetchHasAndBelongsToMany($Model, $queryTemplate, $assocIds, $association); |
||
1329 | } |
||
1330 | |||
1331 | $habtmAssocData = $Model->hasAndBelongsToMany[$association]; |
||
1332 | $foreignKey = $habtmAssocData['foreignKey']; |
||
1333 | $joinKeys = array($foreignKey, $habtmAssocData['associationForeignKey']); |
||
1334 | list($with, $habtmFields) = $Model->joinModel($habtmAssocData['with'], $joinKeys); |
||
1335 | $habtmFieldsCount = count($habtmFields); |
||
1336 | |||
1337 | // Filter
|
||
1338 | if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { |
||
1339 | $this->_filterResultsInclusive($assocResultSet, $Model, array($association, $with)); |
||
1340 | } |
||
1341 | } |
||
1342 | |||
1343 | $modelAlias = $Model->alias; |
||
1344 | $primaryKey = $Model->primaryKey; |
||
1345 | $selfJoin = ($Model->name === $LinkModel->name); |
||
1346 | |||
1347 | foreach ($resultSet as &$row) { |
||
1348 | if ($type === 'hasOne' || $type === 'belongsTo' || $type === 'hasMany') { |
||
1349 | $assocResultSet = array(); |
||
1350 | $prefetched = false; |
||
1351 | |||
1352 | if (($type === 'hasOne' || $type === 'belongsTo') && |
||
1353 | isset($row[$LinkModel->alias], $joined[$Model->alias]) && |
||
1354 | in_array($LinkModel->alias, $joined[$Model->alias]) |
||
1355 | ) { |
||
1356 | $joinedData = Hash::filter($row[$LinkModel->alias]); |
||
1357 | if (!empty($joinedData)) { |
||
1358 | $assocResultSet[0] = array($LinkModel->alias => $row[$LinkModel->alias]); |
||
1359 | } |
||
1360 | $prefetched = true; |
||
1361 | } else {
|
||
1362 | $query = $this->insertQueryData($queryTemplate, $row, $association, $Model, $stack); |
||
1363 | if ($query !== false) { |
||
1364 | $assocResultSet = $this->fetchAll($query, $Model->cacheQueries); |
||
1365 | } |
||
1366 | } |
||
1367 | } |
||
1368 | |||
1369 | if (!empty($assocResultSet) && is_array($assocResultSet)) { |
||
1370 | if ($recursive > 0) { |
||
1371 | foreach ($LinkModel->associations() as $type1) { |
||
1372 | foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) { |
||
1373 | $DeepModel = $LinkModel->{$assoc1}; |
||
1374 | |||
1375 | if ($type1 === 'belongsTo' || |
||
1376 | ($type === 'belongsTo' && $DeepModel->alias === $modelAlias) || |
||
1377 | ($DeepModel->alias !== $modelAlias) |
||
1378 | ) { |
||
1379 | $tmpStack = $stack; |
||
1380 | $tmpStack[] = $assoc1; |
||
1381 | |||
1382 | $db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource(); |
||
1383 | |||
1384 | $db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack); |
||
1385 | } |
||
1386 | } |
||
1387 | } |
||
1388 | } |
||
1389 | |||
1390 | if ($type === 'hasAndBelongsToMany') { |
||
1391 | $merge = array(); |
||
1392 | foreach ($assocResultSet as $data) { |
||
1393 | if (isset($data[$with]) && $data[$with][$foreignKey] === $row[$modelAlias][$primaryKey]) { |
||
1394 | if ($habtmFieldsCount <= 2) { |
||
1395 | unset($data[$with]); |
||
1396 | } |
||
1397 | $merge[] = $data; |
||
1398 | } |
||
1399 | } |
||
1400 | |||
1401 | if (empty($merge) && !isset($row[$association])) { |
||
1402 | $row[$association] = $merge; |
||
1403 | } else {
|
||
1404 | $this->_mergeAssociation($row, $merge, $association, $type); |
||
1405 | } |
||
1406 | } else {
|
||
1407 | if (!$prefetched && $LinkModel->useConsistentAfterFind) { |
||
1408 | if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') { |
||
1409 | $this->_filterResultsInclusive($assocResultSet, $Model, array($association)); |
||
1410 | } |
||
1411 | } |
||
1412 | $this->_mergeAssociation($row, $assocResultSet, $association, $type, $selfJoin); |
||
1413 | } |
||
1414 | |||
1415 | if ($type !== 'hasAndBelongsToMany' && isset($row[$association]) && !$prefetched && !$LinkModel->useConsistentAfterFind) { |
||
1416 | $row[$association] = $LinkModel->afterFind($row[$association], false); |
||
1417 | } |
||
1418 | |||
1419 | } else {
|
||
1420 | $tempArray[0][$association] = false; |
||
1421 | $this->_mergeAssociation($row, $tempArray, $association, $type, $selfJoin); |
||
1422 | } |
||
1423 | } |
||
1424 | } |
||
1425 | |||
1426 | /**
|
||
1427 | * Fetch 'hasMany' associations.
|
||
1428 | *
|
||
1429 | * This is just a proxy to maintain BC.
|
||
1430 | *
|
||
1431 | * @param Model $Model Primary model object.
|
||
1432 | * @param string $query Association query template.
|
||
1433 | * @param array $ids Array of IDs of associated records.
|
||
1434 | * @return array Association results.
|
||
1435 | * @see DboSource::_fetchHasMany()
|
||
1436 | */
|
||
1437 | public function fetchAssociated(Model $Model, $query, $ids) { |
||
1438 | return $this->_fetchHasMany($Model, $query, $ids); |
||
1439 | } |
||
1440 | |||
1441 | /**
|
||
1442 | * Fetch 'hasMany' associations.
|
||
1443 | *
|
||
1444 | * @param Model $Model Primary model object.
|
||
1445 | * @param string $query Association query template.
|
||
1446 | * @param array $ids Array of IDs of associated records.
|
||
1447 | * @return array Association results.
|
||
1448 | */
|
||
1449 | protected function _fetchHasMany(Model $Model, $query, $ids) { |
||
1450 | $ids = array_unique($ids); |
||
1451 | |||
1452 | if (count($ids) > 1) { |
||
1453 | $query = str_replace('= ({$__cakeID__$}', 'IN ({$__cakeID__$}', $query); |
||
1454 | } |
||
1455 | $query = str_replace('{$__cakeID__$}', implode(', ', $ids), $query); |
||
1456 | return $this->fetchAll($query, $Model->cacheQueries); |
||
1457 | } |
||
1458 | |||
1459 | /**
|
||
1460 | * Fetch 'hasAndBelongsToMany' associations.
|
||
1461 | *
|
||
1462 | * @param Model $Model Primary model object.
|
||
1463 | * @param string $query Association query.
|
||
1464 | * @param array $ids Array of IDs of associated records.
|
||
1465 | * @param string $association Association name.
|
||
1466 | * @return array Association results.
|
||
1467 | */
|
||
1468 | protected function _fetchHasAndBelongsToMany(Model $Model, $query, $ids, $association) { |
||
1469 | $ids = array_unique($ids); |
||
1470 | |||
1471 | if (count($ids) > 1) { |
||
1472 | $query = str_replace('{$__cakeID__$}', '(' . implode(', ', $ids) . ')', $query); |
||
1473 | $query = str_replace('= (', 'IN (', $query); |
||
1474 | } else {
|
||
1475 | $query = str_replace('{$__cakeID__$}', $ids[0], $query); |
||
1476 | } |
||
1477 | $query = str_replace(' WHERE 1 = 1', '', $query); |
||
1478 | |||
1479 | return $this->fetchAll($query, $Model->cacheQueries); |
||
1480 | } |
||
1481 | |||
1482 | /**
|
||
1483 | * Merge the results of 'hasMany' associations.
|
||
1484 | *
|
||
1485 | * Note: this function also deals with the formatting of the data.
|
||
1486 | *
|
||
1487 | * @param array &$resultSet Data to merge into.
|
||
1488 | * @param array $assocResultSet Data to merge.
|
||
1489 | * @param string $association Name of Model being merged.
|
||
1490 | * @param Model $Model Model being merged onto.
|
||
1491 | * @return void
|
||
1492 | */
|
||
1493 | protected function _mergeHasMany(&$resultSet, $assocResultSet, $association, Model $Model) { |
||
1494 | $modelAlias = $Model->alias; |
||
1495 | $primaryKey = $Model->primaryKey; |
||
1496 | $foreignKey = $Model->hasMany[$association]['foreignKey']; |
||
1497 | |||
1498 | foreach ($resultSet as &$result) { |
||
1499 | if (!isset($result[$modelAlias])) { |
||
1500 | continue;
|
||
1501 | } |
||
1502 | |||
1503 | $resultPrimaryKey = $result[$modelAlias][$primaryKey]; |
||
1504 | |||
1505 | $merged = array(); |
||
1506 | foreach ($assocResultSet as $data) { |
||
1507 | if ($resultPrimaryKey !== $data[$association][$foreignKey]) { |
||
1508 | continue;
|
||
1509 | } |
||
1510 | |||
1511 | if (count($data) > 1) { |
||
1512 | $data = array_merge($data[$association], $data); |
||
1513 | unset($data[$association]); |
||
1514 | foreach ($data as $key => $name) { |
||
1515 | if (is_numeric($key)) { |
||
1516 | $data[$association][] = $name; |
||
1517 | unset($data[$key]); |
||
1518 | } |
||
1519 | } |
||
1520 | $merged[] = $data; |
||
1521 | } else {
|
||
1522 | $merged[] = $data[$association]; |
||
1523 | } |
||
1524 | } |
||
1525 | |||
1526 | $result = Hash::mergeDiff($result, array($association => $merged)); |
||
1527 | } |
||
1528 | } |
||
1529 | |||
1530 | /**
|
||
1531 | * Merge association of merge into data
|
||
1532 | *
|
||
1533 | * @param array &$data The data to merge.
|
||
1534 | * @param array &$merge The data to merge.
|
||
1535 | * @param string $association The association name to merge.
|
||
1536 | * @param string $type The type of association
|
||
1537 | * @param bool $selfJoin Whether or not this is a self join.
|
||
1538 | * @return void
|
||
1539 | */
|
||
1540 | protected function _mergeAssociation(&$data, &$merge, $association, $type, $selfJoin = false) { |
||
1541 | if (isset($merge[0]) && !isset($merge[0][$association])) { |
||
1542 | $association = Inflector::pluralize($association); |
||
1543 | } |
||
1544 | |||
1545 | $dataAssociation =& $data[$association]; |
||
1546 | |||
1547 | if ($type === 'belongsTo' || $type === 'hasOne') { |
||
1548 | if (isset($merge[$association])) { |
||
1549 | $dataAssociation = $merge[$association][0]; |
||
1550 | } else {
|
||
1551 | if (!empty($merge[0][$association])) { |
||
1552 | foreach ($merge[0] as $assoc => $data2) { |
||
1553 | if ($assoc !== $association) { |
||
1554 | $merge[0][$association][$assoc] = $data2; |
||
1555 | } |
||
1556 | } |
||
1557 | } |
||
1558 | if (!isset($dataAssociation)) { |
||
1559 | $dataAssociation = array(); |
||
1560 | if ($merge[0][$association]) { |
||
1561 | $dataAssociation = $merge[0][$association]; |
||
1562 | } |
||
1563 | } else {
|
||
1564 | if (is_array($merge[0][$association])) { |
||
1565 | $mergeAssocTmp = array(); |
||
1566 | foreach ($dataAssociation as $k => $v) { |
||
1567 | if (!is_array($v)) { |
||
1568 | $dataAssocTmp[$k] = $v; |
||
1569 | } |
||
1570 | } |
||
1571 | |||
1572 | foreach ($merge[0][$association] as $k => $v) { |
||
1573 | if (!is_array($v)) { |
||
1574 | $mergeAssocTmp[$k] = $v; |
||
1575 | } |
||
1576 | } |
||
1577 | $dataKeys = array_keys($data); |
||
1578 | $mergeKeys = array_keys($merge[0]); |
||
1579 | |||
1580 | if ($mergeKeys[0] === $dataKeys[0] || $mergeKeys === $dataKeys) { |
||
1581 | $dataAssociation[$association] = $merge[0][$association]; |
||
1582 | } else {
|
||
1583 | $diff = Hash::diff($dataAssocTmp, $mergeAssocTmp); |
||
1584 | $dataAssociation = array_merge($merge[0][$association], $diff); |
||
1585 | } |
||
1586 | } elseif ($selfJoin && array_key_exists($association, $merge[0])) { |
||
1587 | $dataAssociation = array_merge($dataAssociation, array($association => array())); |
||
1588 | } |
||
1589 | } |
||
1590 | } |
||
1591 | } else {
|
||
1592 | if (isset($merge[0][$association]) && $merge[0][$association] === false) { |
||
1593 | if (!isset($dataAssociation)) { |
||
1594 | $dataAssociation = array(); |
||
1595 | } |
||
1596 | } else {
|
||
1597 | foreach ($merge as $row) { |
||
1598 | $insert = array(); |
||
1599 | if (count($row) === 1) { |
||
1600 | $insert = $row[$association]; |
||
1601 | } elseif (isset($row[$association])) { |
||
1602 | $insert = array_merge($row[$association], $row); |
||
1603 | unset($insert[$association]); |
||
1604 | } |
||
1605 | |||
1606 | if (empty($dataAssociation) || (isset($dataAssociation) && !in_array($insert, $dataAssociation, true))) { |
||
1607 | $dataAssociation[] = $insert; |
||
1608 | } |
||
1609 | } |
||
1610 | } |
||
1611 | } |
||
1612 | } |
||
1613 | |||
1614 | /**
|
||
1615 | * Prepares fields required by an SQL statement.
|
||
1616 | *
|
||
1617 | * When no fields are set, all the $Model fields are returned.
|
||
1618 | *
|
||
1619 | * @param Model $Model The model to prepare.
|
||
1620 | * @param array $queryData An array of queryData information containing keys similar to Model::find().
|
||
1621 | * @return array Array containing SQL fields.
|
||
1622 | */
|
||
1623 | public function prepareFields(Model $Model, $queryData) { |
||
1624 | if (empty($queryData['fields'])) { |
||
1625 | $queryData['fields'] = $this->fields($Model); |
||
1626 | |||
1627 | } elseif (!empty($Model->hasMany) && $Model->recursive > -1) { |
||
1628 | // hasMany relationships need the $Model primary key.
|
||
1629 | $assocFields = $this->fields($Model, null, "{$Model->alias}.{$Model->primaryKey}"); |
||
1630 | $passedFields = $queryData['fields']; |
||
1631 | |||
1632 | if (count($passedFields) > 1 || |
||
1633 | (strpos($passedFields[0], $assocFields[0]) === false && !preg_match('/^[a-z]+\(/i', $passedFields[0])) |
||
1634 | ) { |
||
1635 | $queryData['fields'] = array_merge($passedFields, $assocFields); |
||
1636 | } |
||
1637 | } |
||
1638 | |||
1639 | return array_unique($queryData['fields']); |
||
1640 | } |
||
1641 | |||
1642 | /**
|
||
1643 | * Builds an SQL statement.
|
||
1644 | *
|
||
1645 | * This is merely a convenient wrapper to DboSource::buildStatement().
|
||
1646 | *
|
||
1647 | * @param Model $Model The model to build an association query for.
|
||
1648 | * @param array $queryData An array of queryData information containing keys similar to Model::find().
|
||
1649 | * @return string String containing an SQL statement.
|
||
1650 | * @see DboSource::buildStatement()
|
||
1651 | */
|
||
1652 | public function buildAssociationQuery(Model $Model, $queryData) { |
||
1653 | $queryData = $this->_scrubQueryData($queryData); |
||
1654 | |||
1655 | return $this->buildStatement( |
||
1656 | array(
|
||
1657 | 'fields' => $this->prepareFields($Model, $queryData), |
||
1658 | 'table' => $this->fullTableName($Model), |
||
1659 | 'alias' => $Model->alias, |
||
1660 | 'limit' => $queryData['limit'], |
||
1661 | 'offset' => $queryData['offset'], |
||
1662 | 'joins' => $queryData['joins'], |
||
1663 | 'conditions' => $queryData['conditions'], |
||
1664 | 'order' => $queryData['order'], |
||
1665 | 'group' => $queryData['group'] |
||
1666 | ), |
||
1667 | $Model
|
||
1668 | ); |
||
1669 | } |
||
1670 | |||
1671 | /**
|
||
1672 | * Generates a query or part of a query from a single model or two associated models.
|
||
1673 | *
|
||
1674 | * Builds a string containing an SQL statement template.
|
||
1675 | *
|
||
1676 | * @param Model $Model Primary Model object.
|
||
1677 | * @param Model|null $LinkModel Linked model object.
|
||
1678 | * @param string $type Association type, one of the model association types ie. hasMany.
|
||
1679 | * @param string $association Association name.
|
||
1680 | * @param array $assocData Association data.
|
||
1681 | * @param array &$queryData An array of queryData information containing keys similar to Model::find().
|
||
1682 | * @param bool $external Whether or not the association query is on an external datasource.
|
||
1683 | * @return mixed
|
||
1684 | * String representing a query.
|
||
1685 | * True, when $external is false and association $type is 'hasOne' or 'belongsTo'.
|
||
1686 | */
|
||
1687 | public function generateAssociationQuery(Model $Model, $LinkModel, $type, $association, $assocData, &$queryData, $external) { |
||
1688 | $assocData = $this->_scrubQueryData($assocData); |
||
1689 | $queryData = $this->_scrubQueryData($queryData); |
||
1690 | |||
1691 | if ($LinkModel === null) { |
||
1692 | return $this->buildStatement( |
||
1693 | array(
|
||
1694 | 'fields' => array_unique($queryData['fields']), |
||
1695 | 'table' => $this->fullTableName($Model), |
||
1696 | 'alias' => $Model->alias, |
||
1697 | 'limit' => $queryData['limit'], |
||
1698 | 'offset' => $queryData['offset'], |
||
1699 | 'joins' => $queryData['joins'], |
||
1700 | 'conditions' => $queryData['conditions'], |
||
1701 | 'order' => $queryData['order'], |
||
1702 | 'group' => $queryData['group'] |
||
1703 | ), |
||
1704 | $Model
|
||
1705 | ); |
||
1706 | } |
||
1707 | |||
1708 | if ($external && !empty($assocData['finderQuery'])) { |
||
1709 | return $assocData['finderQuery']; |
||
1710 | } |
||
1711 | |||
1712 | if ($type === 'hasMany' || $type === 'hasAndBelongsToMany') { |
||
1713 | if (empty($assocData['offset']) && !empty($assocData['page'])) { |
||
1714 | $assocData['offset'] = ($assocData['page'] - 1) * $assocData['limit']; |
||
1715 | } |
||
1716 | } |
||
1717 | |||
1718 | switch ($type) { |
||
1719 | case 'hasOne': |
||
1720 | case 'belongsTo': |
||
1721 | $conditions = $this->_mergeConditions( |
||
1722 | $assocData['conditions'], |
||
1723 | $this->getConstraint($type, $Model, $LinkModel, $association, array_merge($assocData, compact('external'))) |
||
1724 | ); |
||
1725 | |||
1726 | if ($external) { |
||
1727 | // Not self join
|
||
1728 | if ($Model->name !== $LinkModel->name) { |
||
1729 | $modelAlias = $Model->alias; |
||
1730 | foreach ($conditions as $key => $condition) { |
||
1731 | if (is_numeric($key) && strpos($condition, $modelAlias . '.') !== false) { |
||
1732 | unset($conditions[$key]); |
||
1733 | } |
||
1734 | } |
||
1735 | } |
||
1736 | |||
1737 | $query = array_merge($assocData, array( |
||
1738 | 'conditions' => $conditions, |
||
1739 | 'table' => $this->fullTableName($LinkModel), |
||
1740 | 'fields' => $this->fields($LinkModel, $association, $assocData['fields']), |
||
1741 | 'alias' => $association, |
||
1742 | 'group' => null |
||
1743 | )); |
||
1744 | } else {
|
||
1745 | $join = array( |
||
1746 | 'table' => $LinkModel, |
||
1747 | 'alias' => $association, |
||
1748 | 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT', |
||
1749 | 'conditions' => trim($this->conditions($conditions, true, false, $Model)) |
||
1750 | ); |
||
1751 | |||
1752 | $fields = array(); |
||
1753 | if ($assocData['fields'] !== false) { |
||
1754 | $fields = $this->fields($LinkModel, $association, $assocData['fields']); |
||
1755 | } |
||
1756 | |||
1757 | $queryData['fields'] = array_merge($this->prepareFields($Model, $queryData), $fields); |
||
1758 | |||
1759 | if (!empty($assocData['order'])) { |
||
1760 | $queryData['order'][] = $assocData['order']; |
||
1761 | } |
||
1762 | if (!in_array($join, $queryData['joins'], true)) { |
||
1763 | $queryData['joins'][] = $join; |
||
1764 | } |
||
1765 | |||
1766 | return true; |
||
1767 | } |
||
1768 | break;
|
||
1769 | case 'hasMany': |
||
1770 | $assocData['fields'] = $this->fields($LinkModel, $association, $assocData['fields']); |
||
1771 | if (!empty($assocData['foreignKey'])) { |
||
1772 | $assocData['fields'] = array_merge($assocData['fields'], $this->fields($LinkModel, $association, array("{$association}.{$assocData['foreignKey']}"))); |
||
1773 | } |
||
1774 | |||
1775 | $query = array( |
||
1776 | 'conditions' => $this->_mergeConditions($this->getConstraint('hasMany', $Model, $LinkModel, $association, $assocData), $assocData['conditions']), |
||
1777 | 'fields' => array_unique($assocData['fields']), |
||
1778 | 'table' => $this->fullTableName($LinkModel), |
||
1779 | 'alias' => $association, |
||
1780 | 'order' => $assocData['order'], |
||
1781 | 'limit' => $assocData['limit'], |
||
1782 | 'offset' => $assocData['offset'], |
||
1783 | 'group' => null |
||
1784 | ); |
||
1785 | break;
|
||
1786 | case 'hasAndBelongsToMany': |
||
1787 | $joinFields = array(); |
||
1788 | $joinAssoc = null; |
||
1789 | |||
1790 | if (isset($assocData['with']) && !empty($assocData['with'])) { |
||
1791 | $joinKeys = array($assocData['foreignKey'], $assocData['associationForeignKey']); |
||
1792 | list($with, $joinFields) = $Model->joinModel($assocData['with'], $joinKeys); |
||
1793 | |||
1794 | $joinTbl = $Model->{$with}; |
||
1795 | $joinAlias = $joinTbl; |
||
1796 | |||
1797 | if (is_array($joinFields) && !empty($joinFields)) { |
||
1798 | $joinAssoc = $joinAlias = $joinTbl->alias; |
||
1799 | $joinFields = $this->fields($joinTbl, $joinAlias, $joinFields); |
||
1800 | } else {
|
||
1801 | $joinFields = array(); |
||
1802 | } |
||
1803 | } else {
|
||
1804 | $joinTbl = $assocData['joinTable']; |
||
1805 | $joinAlias = $this->fullTableName($assocData['joinTable']); |
||
1806 | } |
||
1807 | |||
1808 | $query = array( |
||
1809 | 'conditions' => $assocData['conditions'], |
||
1810 | 'limit' => $assocData['limit'], |
||
1811 | 'offset' => $assocData['offset'], |
||
1812 | 'table' => $this->fullTableName($LinkModel), |
||
1813 | 'alias' => $association, |
||
1814 | 'fields' => array_merge($this->fields($LinkModel, $association, $assocData['fields']), $joinFields), |
||
1815 | 'order' => $assocData['order'], |
||
1816 | 'group' => null, |
||
1817 | 'joins' => array(array( |
||
1818 | 'table' => $joinTbl, |
||
1819 | 'alias' => $joinAssoc, |
||
1820 | 'conditions' => $this->getConstraint('hasAndBelongsToMany', $Model, $LinkModel, $joinAlias, $assocData, $association) |
||
1821 | )) |
||
1822 | ); |
||
1823 | break;
|
||
1824 | } |
||
1825 | |||
1826 | if (isset($query)) { |
||
1827 | return $this->buildStatement($query, $Model); |
||
1828 | } |
||
1829 | |||
1830 | return null; |
||
1831 | } |
||
1832 | |||
1833 | /**
|
||
1834 | * Returns a conditions array for the constraint between two models.
|
||
1835 | *
|
||
1836 | * @param string $type Association type.
|
||
1837 | * @param Model $Model Primary Model object.
|
||
1838 | * @param Model $LinkModel Linked model object.
|
||
1839 | * @param string $association Association name.
|
||
1840 | * @param array $assocData Association data.
|
||
1841 | * @param string $association2 HABTM association name.
|
||
1842 | * @return array Conditions array defining the constraint between $Model and $LinkModel.
|
||
1843 | */
|
||
1844 | public function getConstraint($type, Model $Model, Model $LinkModel, $association, $assocData, $association2 = null) { |
||
1845 | $assocData += array('external' => false); |
||
1846 | |||
1847 | if (empty($assocData['foreignKey'])) { |
||
1848 | return array(); |
||
1849 | } |
||
1850 | |||
1851 | switch ($type) { |
||
1852 | case 'hasOne': |
||
1853 | if ($assocData['external']) { |
||
1854 | return array( |
||
1855 | "{$association}.{$assocData['foreignKey']}" => '{$__cakeID__$}' |
||
1856 | ); |
||
1857 | } else {
|
||
1858 | return array( |
||
1859 | "{$association}.{$assocData['foreignKey']}" => $this->identifier("{$Model->alias}.{$Model->primaryKey}") |
||
1860 | ); |
||
1861 | } |
||
1862 | case 'belongsTo': |
||
1863 | if ($assocData['external']) { |
||
1864 | return array( |
||
1865 | "{$association}.{$LinkModel->primaryKey}" => '{$__cakeForeignKey__$}' |
||
1866 | ); |
||
1867 | } else {
|
||
1868 | return array( |
||
1869 | "{$Model->alias}.{$assocData['foreignKey']}" => $this->identifier("{$association}.{$LinkModel->primaryKey}") |
||
1870 | ); |
||
1871 | } |
||
1872 | case 'hasMany': |
||
1873 | return array("{$association}.{$assocData['foreignKey']}" => array('{$__cakeID__$}')); |
||
1874 | case 'hasAndBelongsToMany': |
||
1875 | return array( |
||
1876 | array(
|
||
1877 | "{$association}.{$assocData['foreignKey']}" => '{$__cakeID__$}' |
||
1878 | ), |
||
1879 | array(
|
||
1880 | "{$association}.{$assocData['associationForeignKey']}" => $this->identifier("{$association2}.{$LinkModel->primaryKey}") |
||
1881 | ) |
||
1882 | ); |
||
1883 | } |
||
1884 | |||
1885 | return array(); |
||
1886 | } |
||
1887 | |||
1888 | /**
|
||
1889 | * Builds and generates a JOIN condition from an array. Handles final clean-up before conversion.
|
||
1890 | *
|
||
1891 | * @param array $join An array defining a JOIN condition in a query.
|
||
1892 | * @return string An SQL JOIN condition to be used in a query.
|
||
1893 | * @see DboSource::renderJoinStatement()
|
||
1894 | * @see DboSource::buildStatement()
|
||
1895 | */
|
||
1896 | public function buildJoinStatement($join) { |
||
1897 | $data = array_merge(array( |
||
1898 | 'type' => null, |
||
1899 | 'alias' => null, |
||
1900 | 'table' => 'join_table', |
||
1901 | 'conditions' => '', |
||
1902 | ), $join);
|
||
1903 | |||
1904 | if (!empty($data['alias'])) { |
||
1905 | $data['alias'] = $this->alias . $this->name($data['alias']); |
||
1906 | } |
||
1907 | if (!empty($data['conditions'])) { |
||
1908 | $data['conditions'] = trim($this->conditions($data['conditions'], true, false)); |
||
1909 | } |
||
1910 | if (!empty($data['table']) && (!is_string($data['table']) || strpos($data['table'], '(') !== 0)) { |
||
1911 | $data['table'] = $this->fullTableName($data['table']); |
||
1912 | } |
||
1913 | return $this->renderJoinStatement($data); |
||
1914 | } |
||
1915 | |||
1916 | /**
|
||
1917 | * Builds and generates an SQL statement from an array. Handles final clean-up before conversion.
|
||
1918 | *
|
||
1919 | * @param array $query An array defining an SQL query.
|
||
1920 | * @param Model $Model The model object which initiated the query.
|
||
1921 | * @return string An executable SQL statement.
|
||
1922 | * @see DboSource::renderStatement()
|
||
1923 | */
|
||
1924 | public function buildStatement($query, Model $Model) { |
||
1925 | $query = array_merge($this->_queryDefaults, $query); |
||
1926 | |||
1927 | if (!empty($query['joins'])) { |
||
1928 | $count = count($query['joins']); |
||
1929 | for ($i = 0; $i < $count; $i++) { |
||
1930 | if (is_array($query['joins'][$i])) { |
||
1931 | $query['joins'][$i] = $this->buildJoinStatement($query['joins'][$i]); |
||
1932 | } |
||
1933 | } |
||
1934 | } |
||
1935 | |||
1936 | return $this->renderStatement('select', array( |
||
1937 | 'conditions' => $this->conditions($query['conditions'], true, true, $Model), |
||
1938 | 'fields' => implode(', ', $query['fields']), |
||
1939 | 'table' => $query['table'], |
||
1940 | 'alias' => $this->alias . $this->name($query['alias']), |
||
1941 | 'order' => $this->order($query['order'], 'ASC', $Model), |
||
1942 | 'limit' => $this->limit($query['limit'], $query['offset']), |
||
1943 | 'joins' => implode(' ', $query['joins']), |
||
1944 | 'group' => $this->group($query['group'], $Model) |
||
1945 | )); |
||
1946 | } |
||
1947 | |||
1948 | /**
|
||
1949 | * Renders a final SQL JOIN statement
|
||
1950 | *
|
||
1951 | * @param array $data The data to generate a join statement for.
|
||
1952 | * @return string
|
||
1953 | */
|
||
1954 | public function renderJoinStatement($data) { |
||
1955 | if (strtoupper($data['type']) === 'CROSS' || empty($data['conditions'])) { |
||
1956 | return "{$data['type']} JOIN {$data['table']} {$data['alias']}"; |
||
1957 | } |
||
1958 | return trim("{$data['type']} JOIN {$data['table']} {$data['alias']} ON ({$data['conditions']})"); |
||
1959 | } |
||
1960 | |||
1961 | /**
|
||
1962 | * Renders a final SQL statement by putting together the component parts in the correct order
|
||
1963 | *
|
||
1964 | * @param string $type type of query being run. e.g select, create, update, delete, schema, alter.
|
||
1965 | * @param array $data Array of data to insert into the query.
|
||
1966 | * @return string|null Rendered SQL expression to be run, otherwise null.
|
||
1967 | */
|
||
1968 | public function renderStatement($type, $data) { |
||
1969 | extract($data); |
||
1970 | $aliases = null; |
||
1971 | |||
1972 | switch (strtolower($type)) { |
||
1973 | case 'select': |
||
1974 | return trim("SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}"); |
||
1975 | case 'create': |
||
1976 | return "INSERT INTO {$table} ({$fields}) VALUES ({$values})"; |
||
1977 | case 'update': |
||
1978 | if (!empty($alias)) { |
||
1979 | $aliases = "{$this->alias}{$alias} {$joins} "; |
||
1980 | } |
||
1981 | return trim("UPDATE {$table} {$aliases}SET {$fields} {$conditions}"); |
||
1982 | case 'delete': |
||
1983 | if (!empty($alias)) { |
||
1984 | $aliases = "{$this->alias}{$alias} {$joins} "; |
||
1985 | } |
||
1986 | return trim("DELETE {$alias} FROM {$table} {$aliases}{$conditions}"); |
||
1987 | case 'schema': |
||
1988 | foreach (array('columns', 'indexes', 'tableParameters') as $var) { |
||
1989 | if (is_array(${$var})) { |
||
1990 | ${$var} = "\t" . implode(",\n\t", array_filter(${$var})); |
||
1991 | } else {
|
||
1992 | ${$var} = ''; |
||
1993 | } |
||
1994 | } |
||
1995 | if (trim($indexes) !== '') { |
||
1996 | $columns .= ','; |
||
1997 | } |
||
1998 | return "CREATE TABLE {$table} (\n{$columns}{$indexes}) {$tableParameters};"; |
||
1999 | case 'alter': |
||
2000 | return null; |
||
2001 | } |
||
2002 | } |
||
2003 | |||
2004 | /**
|
||
2005 | * Merges a mixed set of string/array conditions.
|
||
2006 | *
|
||
2007 | * @param mixed $query The query to merge conditions for.
|
||
2008 | * @param mixed $assoc The association names.
|
||
2009 | * @return array
|
||
2010 | */
|
||
2011 | protected function _mergeConditions($query, $assoc) { |
||
2012 | if (empty($assoc)) { |
||
2013 | return $query; |
||
2014 | } |
||
2015 | |||
2016 | if (is_array($query)) { |
||
2017 | return array_merge((array)$assoc, $query); |
||
2018 | } |
||
2019 | |||
2020 | if (!empty($query)) { |
||
2021 | $query = array($query); |
||
2022 | if (is_array($assoc)) { |
||
2023 | $query = array_merge($query, $assoc); |
||
2024 | } else {
|
||
2025 | $query[] = $assoc; |
||
2026 | } |
||
2027 | return $query; |
||
2028 | } |
||
2029 | |||
2030 | return $assoc; |
||
2031 | } |
||
2032 | |||
2033 | /**
|
||
2034 | * Generates and executes an SQL UPDATE statement for given model, fields, and values.
|
||
2035 | * For databases that do not support aliases in UPDATE queries.
|
||
2036 | *
|
||
2037 | * @param Model $Model The model to update.
|
||
2038 | * @param array $fields The fields to update
|
||
2039 | * @param array $values The values fo the fields.
|
||
2040 | * @param mixed $conditions The conditions for the update. When non-empty $values will not be quoted.
|
||
2041 | * @return bool Success
|
||
2042 | */
|
||
2043 | public function update(Model $Model, $fields = array(), $values = null, $conditions = null) { |
||
2044 | if (!$values) { |
||
2045 | $combined = $fields; |
||
2046 | } else {
|
||
2047 | $combined = array_combine($fields, $values); |
||
2048 | } |
||
2049 | |||
2050 | $fields = implode(', ', $this->_prepareUpdateFields($Model, $combined, empty($conditions))); |
||
2051 | |||
2052 | $alias = $joins = null; |
||
2053 | $table = $this->fullTableName($Model); |
||
2054 | $conditions = $this->_matchRecords($Model, $conditions); |
||
2055 | |||
2056 | if ($conditions === false) { |
||
2057 | return false; |
||
2058 | } |
||
2059 | $query = compact('table', 'alias', 'joins', 'fields', 'conditions'); |
||
2060 | |||
2061 | if (!$this->execute($this->renderStatement('update', $query))) { |
||
2062 | $Model->onError();
|
||
2063 | return false; |
||
2064 | } |
||
2065 | return true; |
||
2066 | } |
||
2067 | |||
2068 | /**
|
||
2069 | * Quotes and prepares fields and values for an SQL UPDATE statement
|
||
2070 | *
|
||
2071 | * @param Model $Model The model to prepare fields for.
|
||
2072 | * @param array $fields The fields to update.
|
||
2073 | * @param bool $quoteValues If values should be quoted, or treated as SQL snippets
|
||
2074 | * @param bool $alias Include the model alias in the field name
|
||
2075 | * @return array Fields and values, quoted and prepared
|
||
2076 | */
|
||
2077 | protected function _prepareUpdateFields(Model $Model, $fields, $quoteValues = true, $alias = false) { |
||
2078 | $quotedAlias = $this->startQuote . $Model->alias . $this->endQuote; |
||
2079 | $schema = $Model->schema(); |
||
2080 | |||
2081 | $updates = array(); |
||
2082 | foreach ($fields as $field => $value) { |
||
2083 | if ($alias && strpos($field, '.') === false) { |
||
2084 | $quoted = $Model->escapeField($field); |
||
2085 | } elseif (!$alias && strpos($field, '.') !== false) { |
||
2086 | $quoted = $this->name(str_replace($quotedAlias . '.', '', str_replace( |
||
2087 | $Model->alias . '.', '', $field |
||
2088 | ))); |
||
2089 | } else {
|
||
2090 | $quoted = $this->name($field); |
||
2091 | } |
||
2092 | |||
2093 | if ($value === null) { |
||
2094 | $updates[] = $quoted . ' = NULL'; |
||
2095 | continue;
|
||
2096 | } |
||
2097 | $update = $quoted . ' = '; |
||
2098 | |||
2099 | if ($quoteValues) { |
||
2100 | $update .= $this->value($value, $Model->getColumnType($field), isset($schema[$field]) ? $schema[$field]['null'] : true); |
||
2101 | } elseif ($Model->getColumnType($field) === 'boolean' && (is_int($value) || is_bool($value))) { |
||
2102 | $update .= $this->boolean($value, true); |
||
2103 | } elseif (!$alias) { |
||
2104 | $update .= str_replace($quotedAlias . '.', '', str_replace( |
||
2105 | $Model->alias . '.', '', $value |
||
2106 | )); |
||
2107 | } else {
|
||
2108 | $update .= $value; |
||
2109 | } |
||
2110 | $updates[] = $update; |
||
2111 | } |
||
2112 | return $updates; |
||
2113 | } |
||
2114 | |||
2115 | /**
|
||
2116 | * Generates and executes an SQL DELETE statement.
|
||
2117 | * For databases that do not support aliases in UPDATE queries.
|
||
2118 | *
|
||
2119 | * @param Model $Model The model to delete from
|
||
2120 | * @param mixed $conditions The conditions to use. If empty the model's primary key will be used.
|
||
2121 | * @return bool Success
|
||
2122 | */
|
||
2123 | public function delete(Model $Model, $conditions = null) { |
||
2124 | $alias = $joins = null; |
||
2125 | $table = $this->fullTableName($Model); |
||
2126 | $conditions = $this->_matchRecords($Model, $conditions); |
||
2127 | |||
2128 | if ($conditions === false) { |
||
2129 | return false; |
||
2130 | } |
||
2131 | |||
2132 | if ($this->execute($this->renderStatement('delete', compact('alias', 'table', 'joins', 'conditions'))) === false) { |
||
2133 | $Model->onError();
|
||
2134 | return false; |
||
2135 | } |
||
2136 | return true; |
||
2137 | } |
||
2138 | |||
2139 | /**
|
||
2140 | * Gets a list of record IDs for the given conditions. Used for multi-record updates and deletes
|
||
2141 | * in databases that do not support aliases in UPDATE/DELETE queries.
|
||
2142 | *
|
||
2143 | * @param Model $Model The model to find matching records for.
|
||
2144 | * @param mixed $conditions The conditions to match against.
|
||
2145 | * @return array List of record IDs
|
||
2146 | */
|
||
2147 | protected function _matchRecords(Model $Model, $conditions = null) { |
||
2148 | if ($conditions === true) { |
||
2149 | $conditions = $this->conditions(true); |
||
2150 | } elseif ($conditions === null) { |
||
2151 | $conditions = $this->conditions($this->defaultConditions($Model, $conditions, false), true, true, $Model); |
||
2152 | } else {
|
||
2153 | $noJoin = true; |
||
2154 | foreach ($conditions as $field => $value) { |
||
2155 | $originalField = $field; |
||
2156 | if (strpos($field, '.') !== false) { |
||
2157 | list(, $field) = explode('.', $field); |
||
2158 | $field = ltrim($field, $this->startQuote); |
||
2159 | $field = rtrim($field, $this->endQuote); |
||
2160 | } |
||
2161 | if (!$Model->hasField($field)) { |
||
2162 | $noJoin = false; |
||
2163 | break;
|
||
2164 | } |
||
2165 | if ($field !== $originalField) { |
||
2166 | $conditions[$field] = $value; |
||
2167 | unset($conditions[$originalField]); |
||
2168 | } |
||
2169 | } |
||
2170 | if ($noJoin === true) { |
||
2171 | return $this->conditions($conditions); |
||
2172 | } |
||
2173 | $idList = $Model->find('all', array( |
||
2174 | 'fields' => "{$Model->alias}.{$Model->primaryKey}", |
||
2175 | 'conditions' => $conditions |
||
2176 | )); |
||
2177 | |||
2178 | if (empty($idList)) { |
||
2179 | return false; |
||
2180 | } |
||
2181 | |||
2182 | $conditions = $this->conditions(array( |
||
2183 | $Model->primaryKey => Hash::extract($idList, "{n}.{$Model->alias}.{$Model->primaryKey}") |
||
2184 | )); |
||
2185 | } |
||
2186 | |||
2187 | return $conditions; |
||
2188 | } |
||
2189 | |||
2190 | /**
|
||
2191 | * Returns an array of SQL JOIN conditions from a model's associations.
|
||
2192 | *
|
||
2193 | * @param Model $Model The model to get joins for.2
|
||
2194 | * @return array
|
||
2195 | */
|
||
2196 | protected function _getJoins(Model $Model) { |
||
2197 | $join = array(); |
||
2198 | $joins = array_merge($Model->getAssociated('hasOne'), $Model->getAssociated('belongsTo')); |
||
2199 | |||
2200 | foreach ($joins as $assoc) { |
||
2201 | if (!isset($Model->{$assoc})) { |
||
2202 | continue;
|
||
2203 | } |
||
2204 | |||
2205 | $LinkModel = $Model->{$assoc}; |
||
2206 | |||
2207 | if ($Model->useDbConfig !== $LinkModel->useDbConfig) { |
||
2208 | continue;
|
||
2209 | } |
||
2210 | |||
2211 | $assocData = $Model->getAssociated($assoc); |
||
2212 | |||
2213 | $join[] = $this->buildJoinStatement(array( |
||
2214 | 'table' => $LinkModel, |
||
2215 | 'alias' => $assoc, |
||
2216 | 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT', |
||
2217 | 'conditions' => trim($this->conditions( |
||
2218 | $this->_mergeConditions($assocData['conditions'], $this->getConstraint($assocData['association'], $Model, $LinkModel, $assoc, $assocData)), |
||
2219 | true,
|
||
2220 | false,
|
||
2221 | $Model
|
||
2222 | )) |
||
2223 | )); |
||
2224 | } |
||
2225 | |||
2226 | return $join; |
||
2227 | } |
||
2228 | |||
2229 | /**
|
||
2230 | * Returns an SQL calculation, i.e. COUNT() or MAX()
|
||
2231 | *
|
||
2232 | * @param Model $Model The model to get a calculated field for.
|
||
2233 | * @param string $func Lowercase name of SQL function, i.e. 'count' or 'max'
|
||
2234 | * @param array $params Function parameters (any values must be quoted manually)
|
||
2235 | * @return string An SQL calculation function
|
||
2236 | */
|
||
2237 | public function calculate(Model $Model, $func, $params = array()) { |
||
2238 | $params = (array)$params; |
||
2239 | |||
2240 | switch (strtolower($func)) { |
||
2241 | case 'count': |
||
2242 | if (!isset($params[0])) { |
||
2243 | $params[0] = '*'; |
||
2244 | } |
||
2245 | if (!isset($params[1])) { |
||
2246 | $params[1] = 'count'; |
||
2247 | } |
||
2248 | if ($Model->isVirtualField($params[0])) { |
||
2249 | $arg = $this->_quoteFields($Model->getVirtualField($params[0])); |
||
2250 | } else {
|
||
2251 | $arg = $this->name($params[0]); |
||
2252 | } |
||
2253 | return 'COUNT(' . $arg . ') AS ' . $this->name($params[1]); |
||
2254 | case 'max': |
||
2255 | case 'min': |
||
2256 | if (!isset($params[1])) { |
||
2257 | $params[1] = $params[0]; |
||
2258 | } |
||
2259 | if ($Model->isVirtualField($params[0])) { |
||
2260 | $arg = $this->_quoteFields($Model->getVirtualField($params[0])); |
||
2261 | } else {
|
||
2262 | $arg = $this->name($params[0]); |
||
2263 | } |
||
2264 | return strtoupper($func) . '(' . $arg . ') AS ' . $this->name($params[1]); |
||
2265 | } |
||
2266 | } |
||
2267 | |||
2268 | /**
|
||
2269 | * Deletes all the records in a table and resets the count of the auto-incrementing
|
||
2270 | * primary key, where applicable.
|
||
2271 | *
|
||
2272 | * @param Model|string $table A string or model class representing the table to be truncated
|
||
2273 | * @return bool SQL TRUNCATE TABLE statement, false if not applicable.
|
||
2274 | */
|
||
2275 | public function truncate($table) { |
||
2276 | return $this->execute('TRUNCATE TABLE ' . $this->fullTableName($table)); |
||
2277 | } |
||
2278 | |||
2279 | /**
|
||
2280 | * Check if the server support nested transactions
|
||
2281 | *
|
||
2282 | * @return bool
|
||
2283 | */
|
||
2284 | public function nestedTransactionSupported() { |
||
2285 | return false; |
||
2286 | } |
||
2287 | |||
2288 | /**
|
||
2289 | * Begin a transaction
|
||
2290 | *
|
||
2291 | * @return bool True on success, false on fail
|
||
2292 | * (i.e. if the database/model does not support transactions,
|
||
2293 | * or a transaction has not started).
|
||
2294 | */
|
||
2295 | public function begin() { |
||
2296 | if ($this->_transactionStarted) { |
||
2297 | if ($this->nestedTransactionSupported()) { |
||
2298 | return $this->_beginNested(); |
||
2299 | } |
||
2300 | $this->_transactionNesting++;
|
||
2301 | return $this->_transactionStarted; |
||
2302 | } |
||
2303 | |||
2304 | $this->_transactionNesting = 0; |
||
2305 | if ($this->fullDebug) { |
||
2306 | $this->logQuery('BEGIN'); |
||
2307 | } |
||
2308 | return $this->_transactionStarted = $this->_connection->beginTransaction(); |
||
2309 | } |
||
2310 | |||
2311 | /**
|
||
2312 | * Begin a nested transaction
|
||
2313 | *
|
||
2314 | * @return bool
|
||
2315 | */
|
||
2316 | protected function _beginNested() { |
||
2317 | $query = 'SAVEPOINT LEVEL' . ++$this->_transactionNesting; |
||
2318 | if ($this->fullDebug) { |
||
2319 | $this->logQuery($query); |
||
2320 | } |
||
2321 | $this->_connection->exec($query); |
||
2322 | return true; |
||
2323 | } |
||
2324 | |||
2325 | /**
|
||
2326 | * Commit a transaction
|
||
2327 | *
|
||
2328 | * @return bool True on success, false on fail
|
||
2329 | * (i.e. if the database/model does not support transactions,
|
||
2330 | * or a transaction has not started).
|
||
2331 | */
|
||
2332 | public function commit() { |
||
2333 | if (!$this->_transactionStarted) { |
||
2334 | return false; |
||
2335 | } |
||
2336 | |||
2337 | if ($this->_transactionNesting === 0) { |
||
2338 | if ($this->fullDebug) { |
||
2339 | $this->logQuery('COMMIT'); |
||
2340 | } |
||
2341 | $this->_transactionStarted = false; |
||
2342 | return $this->_connection->commit(); |
||
2343 | } |
||
2344 | |||
2345 | if ($this->nestedTransactionSupported()) { |
||
2346 | return $this->_commitNested(); |
||
2347 | } |
||
2348 | |||
2349 | $this->_transactionNesting--;
|
||
2350 | return true; |
||
2351 | } |
||
2352 | |||
2353 | /**
|
||
2354 | * Commit a nested transaction
|
||
2355 | *
|
||
2356 | * @return bool
|
||
2357 | */
|
||
2358 | protected function _commitNested() { |
||
2359 | $query = 'RELEASE SAVEPOINT LEVEL' . $this->_transactionNesting--; |
||
2360 | if ($this->fullDebug) { |
||
2361 | $this->logQuery($query); |
||
2362 | } |
||
2363 | $this->_connection->exec($query); |
||
2364 | return true; |
||
2365 | } |
||
2366 | |||
2367 | /**
|
||
2368 | * Rollback a transaction
|
||
2369 | *
|
||
2370 | * @return bool True on success, false on fail
|
||
2371 | * (i.e. if the database/model does not support transactions,
|
||
2372 | * or a transaction has not started).
|
||
2373 | */
|
||
2374 | public function rollback() { |
||
2375 | if (!$this->_transactionStarted) { |
||
2376 | return false; |
||
2377 | } |
||
2378 | |||
2379 | if ($this->_transactionNesting === 0) { |
||
2380 | if ($this->fullDebug) { |
||
2381 | $this->logQuery('ROLLBACK'); |
||
2382 | } |
||
2383 | $this->_transactionStarted = false; |
||
2384 | return $this->_connection->rollBack(); |
||
2385 | } |
||
2386 | |||
2387 | if ($this->nestedTransactionSupported()) { |
||
2388 | return $this->_rollbackNested(); |
||
2389 | } |
||
2390 | |||
2391 | $this->_transactionNesting--;
|
||
2392 | return true; |
||
2393 | } |
||
2394 | |||
2395 | /**
|
||
2396 | * Rollback a nested transaction
|
||
2397 | *
|
||
2398 | * @return bool
|
||
2399 | */
|
||
2400 | protected function _rollbackNested() { |
||
2401 | $query = 'ROLLBACK TO SAVEPOINT LEVEL' . $this->_transactionNesting--; |
||
2402 | if ($this->fullDebug) { |
||
2403 | $this->logQuery($query); |
||
2404 | } |
||
2405 | $this->_connection->exec($query); |
||
2406 | return true; |
||
2407 | } |
||
2408 | |||
2409 | /**
|
||
2410 | * Returns the ID generated from the previous INSERT operation.
|
||
2411 | *
|
||
2412 | * @param mixed $source The source to get an id for.
|
||
2413 | * @return mixed
|
||
2414 | */
|
||
2415 | public function lastInsertId($source = null) { |
||
2416 | return $this->_connection->lastInsertId(); |
||
2417 | } |
||
2418 | |||
2419 | /**
|
||
2420 | * Creates a default set of conditions from the model if $conditions is null/empty.
|
||
2421 | * If conditions are supplied then they will be returned. If a model doesn't exist and no conditions
|
||
2422 | * were provided either null or false will be returned based on what was input.
|
||
2423 | *
|
||
2424 | * @param Model $Model The model to get conditions for.
|
||
2425 | * @param string|array|bool $conditions Array of conditions, conditions string, null or false. If an array of conditions,
|
||
2426 | * or string conditions those conditions will be returned. With other values the model's existence will be checked.
|
||
2427 | * If the model doesn't exist a null or false will be returned depending on the input value.
|
||
2428 | * @param bool $useAlias Use model aliases rather than table names when generating conditions
|
||
2429 | * @return mixed Either null, false, $conditions or an array of default conditions to use.
|
||
2430 | * @see DboSource::update()
|
||
2431 | * @see DboSource::conditions()
|
||
2432 | */
|
||
2433 | public function defaultConditions(Model $Model, $conditions, $useAlias = true) { |
||
2434 | if (!empty($conditions)) { |
||
2435 | return $conditions; |
||
2436 | } |
||
2437 | $exists = $Model->exists(); |
||
2438 | if (!$exists && ($conditions !== null || !empty($Model->__safeUpdateMode))) { |
||
2439 | return false; |
||
2440 | } elseif (!$exists) { |
||
2441 | return null; |
||
2442 | } |
||
2443 | $alias = $Model->alias; |
||
2444 | |||
2445 | if (!$useAlias) { |
||
2446 | $alias = $this->fullTableName($Model, false); |
||
2447 | } |
||
2448 | return array("{$alias}.{$Model->primaryKey}" => $Model->getID()); |
||
2449 | } |
||
2450 | |||
2451 | /**
|
||
2452 | * Returns a key formatted like a string Model.fieldname(i.e. Post.title, or Country.name)
|
||
2453 | *
|
||
2454 | * @param Model $Model The model to get a key for.
|
||
2455 | * @param string $key The key field.
|
||
2456 | * @param string $assoc The association name.
|
||
2457 | * @return string
|
||
2458 | */
|
||
2459 | public function resolveKey(Model $Model, $key, $assoc = null) { |
||
2460 | if (strpos('.', $key) !== false) { |
||
2461 | return $this->name($Model->alias) . '.' . $this->name($key); |
||
2462 | } |
||
2463 | return $key; |
||
2464 | } |
||
2465 | |||
2466 | /**
|
||
2467 | * Private helper method to remove query metadata in given data array.
|
||
2468 | *
|
||
2469 | * @param array $data The data to scrub.
|
||
2470 | * @return array
|
||
2471 | */
|
||
2472 | protected function _scrubQueryData($data) { |
||
2473 | static $base = null; |
||
2474 | if ($base === null) { |
||
2475 | $base = array_fill_keys(array('conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group'), array()); |
||
2476 | $base['callbacks'] = null; |
||
2477 | } |
||
2478 | return (array)$data + $base; |
||
2479 | } |
||
2480 | |||
2481 | /**
|
||
2482 | * Converts model virtual fields into sql expressions to be fetched later
|
||
2483 | *
|
||
2484 | * @param Model $Model The model to get virtual fields for.
|
||
2485 | * @param string $alias Alias table name
|
||
2486 | * @param array $fields virtual fields to be used on query
|
||
2487 | * @return array
|
||
2488 | */
|
||
2489 | protected function _constructVirtualFields(Model $Model, $alias, $fields) { |
||
2490 | $virtual = array(); |
||
2491 | foreach ($fields as $field) { |
||
2492 | $virtualField = $this->name($alias . $this->virtualFieldSeparator . $field); |
||
2493 | $expression = $this->_quoteFields($Model->getVirtualField($field)); |
||
2494 | $virtual[] = '(' . $expression . ") {$this->alias} {$virtualField}"; |
||
2495 | } |
||
2496 | return $virtual; |
||
2497 | } |
||
2498 | |||
2499 | /**
|
||
2500 | * Generates the fields list of an SQL query.
|
||
2501 | *
|
||
2502 | * @param Model $Model The model to get fields for.
|
||
2503 | * @param string $alias Alias table name
|
||
2504 | * @param mixed $fields The provided list of fields.
|
||
2505 | * @param bool $quote If false, returns fields array unquoted
|
||
2506 | * @return array
|
||
2507 | */
|
||
2508 | public function fields(Model $Model, $alias = null, $fields = array(), $quote = true) { |
||
2509 | if (empty($alias)) { |
||
2510 | $alias = $Model->alias; |
||
2511 | } |
||
2512 | $virtualFields = $Model->getVirtualField(); |
||
2513 | $cacheKey = array( |
||
2514 | $alias,
|
||
2515 | get_class($Model), |
||
2516 | $Model->alias,
|
||
2517 | $virtualFields,
|
||
2518 | $fields,
|
||
2519 | $quote,
|
||
2520 | ConnectionManager::getSourceName($this), |
||
2521 | $Model->schemaName,
|
||
2522 | $Model->table
|
||
2523 | ); |
||
2524 | $cacheKey = md5(serialize($cacheKey)); |
||
2525 | if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) { |
||
2526 | return $return; |
||
2527 | } |
||
2528 | $allFields = empty($fields); |
||
2529 | if ($allFields) { |
||
2530 | $fields = array_keys($Model->schema()); |
||
2531 | } elseif (!is_array($fields)) { |
||
2532 | $fields = CakeText::tokenize($fields); |
||
2533 | } |
||
2534 | $fields = array_values(array_filter($fields)); |
||
2535 | $allFields = $allFields || in_array('*', $fields) || in_array($Model->alias . '.*', $fields); |
||
2536 | |||
2537 | $virtual = array(); |
||
2538 | if (!empty($virtualFields)) { |
||
2539 | $virtualKeys = array_keys($virtualFields); |
||
2540 | foreach ($virtualKeys as $field) { |
||
2541 | $virtualKeys[] = $Model->alias . '.' . $field; |
||
2542 | } |
||
2543 | $virtual = ($allFields) ? $virtualKeys : array_intersect($virtualKeys, $fields); |
||
2544 | foreach ($virtual as $i => $field) { |
||
2545 | if (strpos($field, '.') !== false) { |
||
2546 | $virtual[$i] = str_replace($Model->alias . '.', '', $field); |
||
2547 | } |
||
2548 | $fields = array_diff($fields, array($field)); |
||
2549 | } |
||
2550 | $fields = array_values($fields); |
||
2551 | } |
||
2552 | if (!$quote) { |
||
2553 | if (!empty($virtual)) { |
||
2554 | $fields = array_merge($fields, $this->_constructVirtualFields($Model, $alias, $virtual)); |
||
2555 | } |
||
2556 | return $fields; |
||
2557 | } |
||
2558 | $count = count($fields); |
||
2559 | |||
2560 | if ($count >= 1 && !in_array($fields[0], array('*', 'COUNT(*)'))) { |
||
2561 | for ($i = 0; $i < $count; $i++) { |
||
2562 | if (is_string($fields[$i]) && in_array($fields[$i], $virtual)) { |
||
2563 | unset($fields[$i]); |
||
2564 | continue;
|
||
2565 | } |
||
2566 | if (is_object($fields[$i]) && isset($fields[$i]->type) && $fields[$i]->type === 'expression') { |
||
2567 | $fields[$i] = $fields[$i]->value; |
||
2568 | } elseif (preg_match('/^\(.*\)\s' . $this->alias . '.*/i', $fields[$i])) { |
||
2569 | continue;
|
||
2570 | } elseif (!preg_match('/^.+\\(.*\\)/', $fields[$i])) { |
||
2571 | $prepend = ''; |
||
2572 | |||
2573 | if (strpos($fields[$i], 'DISTINCT') !== false) { |
||
2574 | $prepend = 'DISTINCT '; |
||
2575 | $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i])); |
||
2576 | } |
||
2577 | $dot = strpos($fields[$i], '.'); |
||
2578 | |||
2579 | if ($dot === false) { |
||
2580 | $prefix = !(
|
||
2581 | strpos($fields[$i], ' ') !== false || |
||
2582 | strpos($fields[$i], '(') !== false |
||
2583 | ); |
||
2584 | $fields[$i] = $this->name(($prefix ? $alias . '.' : '') . $fields[$i]); |
||
2585 | } else {
|
||
2586 | if (strpos($fields[$i], ',') === false) { |
||
2587 | $build = explode('.', $fields[$i]); |
||
2588 | if (!Hash::numeric($build)) { |
||
2589 | $fields[$i] = $this->name(implode('.', $build)); |
||
2590 | } |
||
2591 | } |
||
2592 | } |
||
2593 | $fields[$i] = $prepend . $fields[$i]; |
||
2594 | } elseif (preg_match('/\(([\.\w]+)\)/', $fields[$i], $field)) { |
||
2595 | if (isset($field[1])) { |
||
2596 | if (strpos($field[1], '.') === false) { |
||
2597 | $field[1] = $this->name($alias . '.' . $field[1]); |
||
2598 | } else {
|
||
2599 | $field[0] = explode('.', $field[1]); |
||
2600 | if (!Hash::numeric($field[0])) { |
||
2601 | $field[0] = implode('.', array_map(array(&$this, 'name'), $field[0])); |
||
2602 | $fields[$i] = preg_replace('/\(' . $field[1] . '\)/', '(' . $field[0] . ')', $fields[$i], 1); |
||
2603 | } |
||
2604 | } |
||
2605 | } |
||
2606 | } |
||
2607 | } |
||
2608 | } |
||
2609 | if (!empty($virtual)) { |
||
2610 | $fields = array_merge($fields, $this->_constructVirtualFields($Model, $alias, $virtual)); |
||
2611 | } |
||
2612 | return $this->cacheMethod(__FUNCTION__, $cacheKey, array_unique($fields)); |
||
2613 | } |
||
2614 | |||
2615 | /**
|
||
2616 | * Creates a WHERE clause by parsing given conditions data. If an array or string
|
||
2617 | * conditions are provided those conditions will be parsed and quoted. If a boolean
|
||
2618 | * is given it will be integer cast as condition. Null will return 1 = 1.
|
||
2619 | *
|
||
2620 | * Results of this method are stored in a memory cache. This improves performance, but
|
||
2621 | * because the method uses a hashing algorithm it can have collisions.
|
||
2622 | * Setting DboSource::$cacheMethods to false will disable the memory cache.
|
||
2623 | *
|
||
2624 | * @param mixed $conditions Array or string of conditions, or any value.
|
||
2625 | * @param bool $quoteValues If true, values should be quoted
|
||
2626 | * @param bool $where If true, "WHERE " will be prepended to the return value
|
||
2627 | * @param Model $Model A reference to the Model instance making the query
|
||
2628 | * @return string SQL fragment
|
||
2629 | */
|
||
2630 | public function conditions($conditions, $quoteValues = true, $where = true, Model $Model = null) { |
||
2631 | $clause = $out = ''; |
||
2632 | |||
2633 | if ($where) { |
||
2634 | $clause = ' WHERE '; |
||
2635 | } |
||
2636 | |||
2637 | if (is_array($conditions) && !empty($conditions)) { |
||
2638 | $out = $this->conditionKeysToString($conditions, $quoteValues, $Model); |
||
2639 | |||
2640 | if (empty($out)) { |
||
2641 | return $clause . ' 1 = 1'; |
||
2642 | } |
||
2643 | return $clause . implode(' AND ', $out); |
||
2644 | } |
||
2645 | |||
2646 | if (is_bool($conditions)) { |
||
2647 | return $clause . (int)$conditions . ' = 1'; |
||
2648 | } |
||
2649 | |||
2650 | if (empty($conditions) || trim($conditions) === '') { |
||
2651 | return $clause . '1 = 1'; |
||
2652 | } |
||
2653 | |||
2654 | $clauses = '/^WHERE\\x20|^GROUP\\x20BY\\x20|^HAVING\\x20|^ORDER\\x20BY\\x20/i'; |
||
2655 | |||
2656 | if (preg_match($clauses, $conditions)) { |
||
2657 | $clause = ''; |
||
2658 | } |
||
2659 | |||
2660 | $conditions = $this->_quoteFields($conditions); |
||
2661 | |||
2662 | return $clause . $conditions; |
||
2663 | } |
||
2664 | |||
2665 | /**
|
||
2666 | * Creates a WHERE clause by parsing given conditions array. Used by DboSource::conditions().
|
||
2667 | *
|
||
2668 | * @param array $conditions Array or string of conditions
|
||
2669 | * @param bool $quoteValues If true, values should be quoted
|
||
2670 | * @param Model $Model A reference to the Model instance making the query
|
||
2671 | * @return string SQL fragment
|
||
2672 | */
|
||
2673 | public function conditionKeysToString($conditions, $quoteValues = true, Model $Model = null) { |
||
2674 | $out = array(); |
||
2675 | $data = $columnType = null; |
||
2676 | $bool = array('and', 'or', 'not', 'and not', 'or not', 'xor', '||', '&&'); |
||
2677 | |||
2678 | foreach ($conditions as $key => $value) { |
||
2679 | $join = ' AND '; |
||
2680 | $not = null; |
||
2681 | |||
2682 | if (is_array($value)) { |
||
2683 | $valueInsert = (
|
||
2684 | !empty($value) && |
||
2685 | (substr_count($key, '?') === count($value) || substr_count($key, ':') === count($value)) |
||
2686 | ); |
||
2687 | } |
||
2688 | |||
2689 | if (is_numeric($key) && empty($value)) { |
||
2690 | continue;
|
||
2691 | } elseif (is_numeric($key) && is_string($value)) { |
||
2692 | $out[] = $this->_quoteFields($value); |
||
2693 | } elseif ((is_numeric($key) && is_array($value)) || in_array(strtolower(trim($key)), $bool)) { |
||
2694 | if (in_array(strtolower(trim($key)), $bool)) { |
||
2695 | $join = ' ' . strtoupper($key) . ' '; |
||
2696 | } else {
|
||
2697 | $key = $join; |
||
2698 | } |
||
2699 | $value = $this->conditionKeysToString($value, $quoteValues, $Model); |
||
2700 | |||
2701 | if (strpos($join, 'NOT') !== false) { |
||
2702 | if (strtoupper(trim($key)) === 'NOT') { |
||
2703 | $key = 'AND ' . trim($key); |
||
2704 | } |
||
2705 | $not = 'NOT '; |
||
2706 | } |
||
2707 | |||
2708 | if (empty($value)) { |
||
2709 | continue;
|
||
2710 | } |
||
2711 | |||
2712 | if (empty($value[1])) { |
||
2713 | if ($not) { |
||
2714 | $out[] = $not . '(' . $value[0] . ')'; |
||
2715 | } else {
|
||
2716 | $out[] = $value[0]; |
||
2717 | } |
||
2718 | } else {
|
||
2719 | $out[] = '(' . $not . '(' . implode(') ' . strtoupper($key) . ' (', $value) . '))'; |
||
2720 | } |
||
2721 | } else {
|
||
2722 | if (is_object($value) && isset($value->type)) { |
||
2723 | if ($value->type === 'identifier') { |
||
2724 | $data .= $this->name($key) . ' = ' . $this->name($value->value); |
||
2725 | } elseif ($value->type === 'expression') { |
||
2726 | if (is_numeric($key)) { |
||
2727 | $data .= $value->value; |
||
2728 | } else {
|
||
2729 | $data .= $this->name($key) . ' = ' . $value->value; |
||
2730 | } |
||
2731 | } |
||
2732 | } elseif (is_array($value) && !empty($value) && !$valueInsert) { |
||
2733 | $keys = array_keys($value); |
||
2734 | if ($keys === array_values($keys)) { |
||
2735 | $count = count($value); |
||
2736 | if ($count === 1 && !preg_match('/\s+(?:NOT|\!=)$/', $key)) { |
||
2737 | $data = $this->_quoteFields($key) . ' = ('; |
||
2738 | if ($quoteValues) { |
||
2739 | if ($Model !== null) { |
||
2740 | $columnType = $Model->getColumnType($key); |
||
2741 | } |
||
2742 | $data .= implode(', ', $this->value($value, $columnType)); |
||
2743 | } |
||
2744 | $data .= ')'; |
||
2745 | } else {
|
||
2746 | $data = $this->_parseKey($key, $value, $Model); |
||
2747 | } |
||
2748 | } else {
|
||
2749 | $ret = $this->conditionKeysToString($value, $quoteValues, $Model); |
||
2750 | if (count($ret) > 1) { |
||
2751 | $data = '(' . implode(') AND (', $ret) . ')'; |
||
2752 | } elseif (isset($ret[0])) { |
||
2753 | $data = $ret[0]; |
||
2754 | } |
||
2755 | } |
||
2756 | } elseif (is_numeric($key) && !empty($value)) { |
||
2757 | $data = $this->_quoteFields($value); |
||
2758 | } else {
|
||
2759 | $data = $this->_parseKey(trim($key), $value, $Model); |
||
2760 | } |
||
2761 | |||
2762 | if ($data) { |
||
2763 | $out[] = $data; |
||
2764 | $data = null; |
||
2765 | } |
||
2766 | } |
||
2767 | } |
||
2768 | return $out; |
||
2769 | } |
||
2770 | |||
2771 | /**
|
||
2772 | * Extracts a Model.field identifier and an SQL condition operator from a string, formats
|
||
2773 | * and inserts values, and composes them into an SQL snippet.
|
||
2774 | *
|
||
2775 | * @param string $key An SQL key snippet containing a field and optional SQL operator
|
||
2776 | * @param mixed $value The value(s) to be inserted in the string
|
||
2777 | * @param Model $Model Model object initiating the query
|
||
2778 | * @return string
|
||
2779 | */
|
||
2780 | protected function _parseKey($key, $value, Model $Model = null) { |
||
2781 | $operatorMatch = '/^(((' . implode(')|(', $this->_sqlOps); |
||
2782 | $operatorMatch .= ')\\x20?)|<[>=]?(?![^>]+>)\\x20?|[>=!]{1,3}(?!<)\\x20?)/is'; |
||
2783 | $bound = (strpos($key, '?') !== false || (is_array($value) && strpos($key, ':') !== false)); |
||
2784 | |||
2785 | $key = trim($key); |
||
2786 | if (strpos($key, ' ') === false) { |
||
2787 | $operator = '='; |
||
2788 | } else {
|
||
2789 | list($key, $operator) = explode(' ', $key, 2); |
||
2790 | |||
2791 | if (!preg_match($operatorMatch, trim($operator)) && strpos($operator, ' ') !== false) { |
||
2792 | $key = $key . ' ' . $operator; |
||
2793 | $split = strrpos($key, ' '); |
||
2794 | $operator = substr($key, $split); |
||
2795 | $key = substr($key, 0, $split); |
||
2796 | } |
||
2797 | } |
||
2798 | |||
2799 | $virtual = false; |
||
2800 | $type = null; |
||
2801 | |||
2802 | if ($Model !== null) { |
||
2803 | if ($Model->isVirtualField($key)) { |
||
2804 | $key = $this->_quoteFields($Model->getVirtualField($key)); |
||
2805 | $virtual = true; |
||
2806 | } |
||
2807 | |||
2808 | $type = $Model->getColumnType($key); |
||
2809 | } |
||
2810 | |||
2811 | $null = $value === null || (is_array($value) && empty($value)); |
||
2812 | |||
2813 | if (strtolower($operator) === 'not') { |
||
2814 | $data = $this->conditionKeysToString( |
||
2815 | array($operator => array($key => $value)), true, $Model |
||
2816 | ); |
||
2817 | return $data[0]; |
||
2818 | } |
||
2819 | |||
2820 | $value = $this->value($value, $type); |
||
2821 | |||
2822 | if (!$virtual && $key !== '?') { |
||
2823 | $isKey = (
|
||
2824 | strpos($key, '(') !== false || |
||
2825 | strpos($key, ')') !== false || |
||
2826 | strpos($key, '|') !== false |
||
2827 | ); |
||
2828 | $key = $isKey ? $this->_quoteFields($key) : $this->name($key); |
||
2829 | } |
||
2830 | |||
2831 | if ($bound) { |
||
2832 | return CakeText::insert($key . ' ' . trim($operator), $value); |
||
2833 | } |
||
2834 | |||
2835 | if (!preg_match($operatorMatch, trim($operator))) { |
||
2836 | $operator .= is_array($value) ? ' IN' : ' ='; |
||
2837 | } |
||
2838 | $operator = trim($operator); |
||
2839 | |||
2840 | if (is_array($value)) { |
||
2841 | $value = implode(', ', $value); |
||
2842 | |||
2843 | switch ($operator) { |
||
2844 | case '=': |
||
2845 | $operator = 'IN'; |
||
2846 | break;
|
||
2847 | case '!=': |
||
2848 | case '<>': |
||
2849 | $operator = 'NOT IN'; |
||
2850 | break;
|
||
2851 | } |
||
2852 | $value = "({$value})"; |
||
2853 | } elseif ($null || $value === 'NULL') { |
||
2854 | switch ($operator) { |
||
2855 | case '=': |
||
2856 | $operator = 'IS'; |
||
2857 | break;
|
||
2858 | case '!=': |
||
2859 | case '<>': |
||
2860 | $operator = 'IS NOT'; |
||
2861 | break;
|
||
2862 | } |
||
2863 | } |
||
2864 | if ($virtual) { |
||
2865 | return "({$key}) {$operator} {$value}"; |
||
2866 | } |
||
2867 | return "{$key} {$operator} {$value}"; |
||
2868 | } |
||
2869 | |||
2870 | /**
|
||
2871 | * Quotes Model.fields
|
||
2872 | *
|
||
2873 | * @param string $conditions The conditions to quote.
|
||
2874 | * @return string or false if no match
|
||
2875 | */
|
||
2876 | protected function _quoteFields($conditions) { |
||
2877 | $start = $end = null; |
||
2878 | $original = $conditions; |
||
2879 | |||
2880 | if (!empty($this->startQuote)) { |
||
2881 | $start = preg_quote($this->startQuote); |
||
2882 | } |
||
2883 | if (!empty($this->endQuote)) { |
||
2884 | $end = preg_quote($this->endQuote); |
||
2885 | } |
||
2886 | // Remove quotes and requote all the Model.field names.
|
||
2887 | $conditions = str_replace(array($start, $end), '', $conditions); |
||
2888 | $conditions = preg_replace_callback( |
||
2889 | '/(?:[\'\"][^\'\"\\\]*(?:\\\.[^\'\"\\\]*)*[\'\"])|([a-z0-9_][a-z0-9\\-_]*\\.[a-z0-9_][a-z0-9_\\-]*)/i',
|
||
2890 | array(&$this, '_quoteMatchedField'), |
||
2891 | $conditions
|
||
2892 | ); |
||
2893 | // Quote `table_name AS Alias`
|
||
2894 | $conditions = preg_replace( |
||
2895 | '/(\s[a-z0-9\\-_.' . $start . $end . ']*' . $end . ')\s+AS\s+([a-z0-9\\-_]+)/i', |
||
2896 | '\1 AS ' . $this->startQuote . '\2' . $this->endQuote, |
||
2897 | $conditions
|
||
2898 | ); |
||
2899 | if ($conditions !== null) { |
||
2900 | return $conditions; |
||
2901 | } |
||
2902 | return $original; |
||
2903 | } |
||
2904 | |||
2905 | /**
|
||
2906 | * Auxiliary function to quote matches `Model.fields` from a preg_replace_callback call
|
||
2907 | *
|
||
2908 | * @param string $match matched string
|
||
2909 | * @return string quoted string
|
||
2910 | */
|
||
2911 | protected function _quoteMatchedField($match) { |
||
2912 | if (is_numeric($match[0])) { |
||
2913 | return $match[0]; |
||
2914 | } |
||
2915 | return $this->name($match[0]); |
||
2916 | } |
||
2917 | |||
2918 | /**
|
||
2919 | * Returns a limit statement in the correct format for the particular database.
|
||
2920 | *
|
||
2921 | * @param int $limit Limit of results returned
|
||
2922 | * @param int $offset Offset from which to start results
|
||
2923 | * @return string SQL limit/offset statement
|
||
2924 | */
|
||
2925 | public function limit($limit, $offset = null) { |
||
2926 | if ($limit) { |
||
2927 | $rt = ' LIMIT'; |
||
2928 | |||
2929 | if ($offset) { |
||
2930 | $rt .= sprintf(' %u,', $offset); |
||
2931 | } |
||
2932 | |||
2933 | $rt .= sprintf(' %u', $limit); |
||
2934 | return $rt; |
||
2935 | } |
||
2936 | return null; |
||
2937 | } |
||
2938 | |||
2939 | /**
|
||
2940 | * Returns an ORDER BY clause as a string.
|
||
2941 | *
|
||
2942 | * @param array|string $keys Field reference, as a key (i.e. Post.title)
|
||
2943 | * @param string $direction Direction (ASC or DESC)
|
||
2944 | * @param Model $Model Model reference (used to look for virtual field)
|
||
2945 | * @return string ORDER BY clause
|
||
2946 | */
|
||
2947 | public function order($keys, $direction = 'ASC', Model $Model = null) { |
||
2948 | if (!is_array($keys)) { |
||
2949 | $keys = array($keys); |
||
2950 | } |
||
2951 | |||
2952 | $keys = array_filter($keys); |
||
2953 | |||
2954 | $result = array(); |
||
2955 | while (!empty($keys)) { |
||
2956 | list($key, $dir) = each($keys); |
||
2957 | array_shift($keys); |
||
2958 | |||
2959 | if (is_numeric($key)) { |
||
2960 | $key = $dir; |
||
2961 | $dir = $direction; |
||
2962 | } |
||
2963 | |||
2964 | if (is_string($key) && strpos($key, ',') !== false && !preg_match('/\(.+\,.+\)/', $key)) { |
||
2965 | $key = array_map('trim', explode(',', $key)); |
||
2966 | } |
||
2967 | |||
2968 | if (is_array($key)) { |
||
2969 | //Flatten the array
|
||
2970 | $key = array_reverse($key, true); |
||
2971 | foreach ($key as $k => $v) { |
||
2972 | if (is_numeric($k)) { |
||
2973 | array_unshift($keys, $v); |
||
2974 | } else {
|
||
2975 | $keys = array($k => $v) + $keys; |
||
2976 | } |
||
2977 | } |
||
2978 | continue;
|
||
2979 | } elseif (is_object($key) && isset($key->type) && $key->type === 'expression') { |
||
2980 | $result[] = $key->value; |
||
2981 | continue;
|
||
2982 | } |
||
2983 | |||
2984 | if (preg_match('/\\x20(ASC|DESC).*/i', $key, $_dir)) { |
||
2985 | $dir = $_dir[0]; |
||
2986 | $key = preg_replace('/\\x20(ASC|DESC).*/i', '', $key); |
||
2987 | } |
||
2988 | |||
2989 | $key = trim($key); |
||
2990 | |||
2991 | if ($Model !== null) { |
||
2992 | if ($Model->isVirtualField($key)) { |
||
2993 | $key = '(' . $this->_quoteFields($Model->getVirtualField($key)) . ')'; |
||
2994 | } |
||
2995 | |||
2996 | list($alias) = pluginSplit($key); |
||
2997 | |||
2998 | if ($alias !== $Model->alias && is_object($Model->{$alias}) && $Model->{$alias}->isVirtualField($key)) { |
||
2999 | $key = '(' . $this->_quoteFields($Model->{$alias}->getVirtualField($key)) . ')'; |
||
3000 | } |
||
3001 | } |
||
3002 | |||
3003 | if (strpos($key, '.')) { |
||
3004 | $key = preg_replace_callback('/([a-zA-Z0-9_-]{1,})\\.([a-zA-Z0-9_-]{1,})/', array(&$this, '_quoteMatchedField'), $key); |
||
3005 | } |
||
3006 | |||
3007 | if (!preg_match('/\s/', $key) && strpos($key, '.') === false) { |
||
3008 | $key = $this->name($key); |
||
3009 | } |
||
3010 | |||
3011 | $key .= ' ' . trim($dir); |
||
3012 | |||
3013 | $result[] = $key; |
||
3014 | } |
||
3015 | |||
3016 | if (!empty($result)) { |
||
3017 | return ' ORDER BY ' . implode(', ', $result); |
||
3018 | } |
||
3019 | |||
3020 | return ''; |
||
3021 | } |
||
3022 | |||
3023 | /**
|
||
3024 | * Create a GROUP BY SQL clause.
|
||
3025 | *
|
||
3026 | * @param string|array $fields Group By fields
|
||
3027 | * @param Model $Model The model to get group by fields for.
|
||
3028 | * @return string Group By clause or null.
|
||
3029 | */
|
||
3030 | public function group($fields, Model $Model = null) { |
||
3031 | if (empty($fields)) { |
||
3032 | return null; |
||
3033 | } |
||
3034 | |||
3035 | if (!is_array($fields)) { |
||
3036 | $fields = array($fields); |
||
3037 | } |
||
3038 | |||
3039 | if ($Model !== null) { |
||
3040 | foreach ($fields as $index => $key) { |
||
3041 | if ($Model->isVirtualField($key)) { |
||
3042 | $fields[$index] = '(' . $Model->getVirtualField($key) . ')'; |
||
3043 | } |
||
3044 | } |
||
3045 | } |
||
3046 | |||
3047 | $fields = implode(', ', $fields); |
||
3048 | |||
3049 | return ' GROUP BY ' . $this->_quoteFields($fields); |
||
3050 | } |
||
3051 | |||
3052 | /**
|
||
3053 | * Disconnects database, kills the connection and says the connection is closed.
|
||
3054 | *
|
||
3055 | * @return void
|
||
3056 | */
|
||
3057 | public function close() { |
||
3058 | $this->disconnect();
|
||
3059 | } |
||
3060 | |||
3061 | /**
|
||
3062 | * Checks if the specified table contains any record matching specified SQL
|
||
3063 | *
|
||
3064 | * @param Model $Model Model to search
|
||
3065 | * @param string $sql SQL WHERE clause (condition only, not the "WHERE" part)
|
||
3066 | * @return bool True if the table has a matching record, else false
|
||
3067 | */
|
||
3068 | public function hasAny(Model $Model, $sql) { |
||
3069 | $sql = $this->conditions($sql); |
||
3070 | $table = $this->fullTableName($Model); |
||
3071 | $alias = $this->alias . $this->name($Model->alias); |
||
3072 | $where = $sql ? "{$sql}" : ' WHERE 1 = 1'; |
||
3073 | $id = $Model->escapeField(); |
||
3074 | |||
3075 | $out = $this->fetchRow("SELECT COUNT({$id}) {$this->alias}count FROM {$table} {$alias}{$where}"); |
||
3076 | |||
3077 | if (is_array($out)) { |
||
3078 | return $out[0]['count']; |
||
3079 | } |
||
3080 | return false; |
||
3081 | } |
||
3082 | |||
3083 | /**
|
||
3084 | * Gets the length of a database-native column description, or null if no length
|
||
3085 | *
|
||
3086 | * @param string $real Real database-layer column type (i.e. "varchar(255)")
|
||
3087 | * @return mixed An integer or string representing the length of the column, or null for unknown length.
|
||
3088 | */
|
||
3089 | public function length($real) { |
||
3090 | if (!preg_match_all('/([\w\s]+)(?:\((\d+)(?:,(\d+))?\))?(\sunsigned)?(\szerofill)?/', $real, $result)) { |
||
3091 | $col = str_replace(array(')', 'unsigned'), '', $real); |
||
3092 | $limit = null; |
||
3093 | |||
3094 | if (strpos($col, '(') !== false) { |
||
3095 | list($col, $limit) = explode('(', $col); |
||
3096 | } |
||
3097 | if ($limit !== null) { |
||
3098 | return (int)$limit; |
||
3099 | } |
||
3100 | return null; |
||
3101 | } |
||
3102 | |||
3103 | $types = array( |
||
3104 | 'int' => 1, 'tinyint' => 1, 'smallint' => 1, 'mediumint' => 1, 'integer' => 1, 'bigint' => 1 |
||
3105 | ); |
||
3106 | |||
3107 | list($real, $type, $length, $offset, $sign) = $result; |
||
3108 | $typeArr = $type; |
||
3109 | $type = $type[0]; |
||
3110 | $length = $length[0]; |
||
3111 | $offset = $offset[0]; |
||
3112 | |||
3113 | $isFloat = in_array($type, array('dec', 'decimal', 'float', 'numeric', 'double')); |
||
3114 | if ($isFloat && $offset) { |
||
3115 | return $length . ',' . $offset; |
||
3116 | } |
||
3117 | |||
3118 | if (($real[0] == $type) && (count($real) === 1)) { |
||
3119 | return null; |
||
3120 | } |
||
3121 | |||
3122 | if (isset($types[$type])) { |
||
3123 | $length += $types[$type]; |
||
3124 | if (!empty($sign)) { |
||
3125 | $length--;
|
||
3126 | } |
||
3127 | } elseif (in_array($type, array('enum', 'set'))) { |
||
3128 | $length = 0; |
||
3129 | foreach ($typeArr as $key => $enumValue) { |
||
3130 | if ($key === 0) { |
||
3131 | continue;
|
||
3132 | } |
||
3133 | $tmpLength = strlen($enumValue); |
||
3134 | if ($tmpLength > $length) { |
||
3135 | $length = $tmpLength; |
||
3136 | } |
||
3137 | } |
||
3138 | } |
||
3139 | return (int)$length; |
||
3140 | } |
||
3141 | |||
3142 | /**
|
||
3143 | * Translates between PHP boolean values and Database (faked) boolean values
|
||
3144 | *
|
||
3145 | * @param mixed $data Value to be translated
|
||
3146 | * @param bool $quote Whether or not the field should be cast to a string.
|
||
3147 | * @return string|bool Converted boolean value
|
||
3148 | */
|
||
3149 | public function boolean($data, $quote = false) { |
||
3150 | if ($quote) { |
||
3151 | return !empty($data) ? '1' : '0'; |
||
3152 | } |
||
3153 | return !empty($data); |
||
3154 | } |
||
3155 | |||
3156 | /**
|
||
3157 | * Inserts multiple values into a table
|
||
3158 | *
|
||
3159 | * @param string $table The table being inserted into.
|
||
3160 | * @param array $fields The array of field/column names being inserted.
|
||
3161 | * @param array $values The array of values to insert. The values should
|
||
3162 | * be an array of rows. Each row should have values keyed by the column name.
|
||
3163 | * Each row must have the values in the same order as $fields.
|
||
3164 | * @return bool
|
||
3165 | */
|
||
3166 | public function insertMulti($table, $fields, $values) { |
||
3167 | $table = $this->fullTableName($table); |
||
3168 | $holder = implode(',', array_fill(0, count($fields), '?')); |
||
3169 | $fields = implode(', ', array_map(array(&$this, 'name'), $fields)); |
||
3170 | |||
3171 | $pdoMap = array( |
||
3172 | 'integer' => PDO::PARAM_INT, |
||
3173 | 'float' => PDO::PARAM_STR, |
||
3174 | 'boolean' => PDO::PARAM_BOOL, |
||
3175 | 'string' => PDO::PARAM_STR, |
||
3176 | 'text' => PDO::PARAM_STR |
||
3177 | ); |
||
3178 | $columnMap = array(); |
||
3179 | |||
3180 | $sql = "INSERT INTO {$table} ({$fields}) VALUES ({$holder})"; |
||
3181 | $statement = $this->_connection->prepare($sql); |
||
3182 | $this->begin();
|
||
3183 | |||
3184 | foreach ($values[key($values)] as $key => $val) { |
||
3185 | $type = $this->introspectType($val); |
||
3186 | $columnMap[$key] = $pdoMap[$type]; |
||
3187 | } |
||
3188 | |||
3189 | foreach ($values as $value) { |
||
3190 | $i = 1; |
||
3191 | foreach ($value as $col => $val) { |
||
3192 | $statement->bindValue($i, $val, $columnMap[$col]); |
||
3193 | $i += 1; |
||
3194 | } |
||
3195 | $statement->execute();
|
||
3196 | $statement->closeCursor();
|
||
3197 | |||
3198 | if ($this->fullDebug) { |
||
3199 | $this->logQuery($sql, $value); |
||
3200 | } |
||
3201 | } |
||
3202 | return $this->commit(); |
||
3203 | } |
||
3204 | |||
3205 | /**
|
||
3206 | * Reset a sequence based on the MAX() value of $column. Useful
|
||
3207 | * for resetting sequences after using insertMulti().
|
||
3208 | *
|
||
3209 | * This method should be implemented by datasources that require sequences to be used.
|
||
3210 | *
|
||
3211 | * @param string $table The name of the table to update.
|
||
3212 | * @param string $column The column to use when resetting the sequence value.
|
||
3213 | * @return bool Success.
|
||
3214 | */
|
||
3215 | public function resetSequence($table, $column) { |
||
3216 | } |
||
3217 | |||
3218 | /**
|
||
3219 | * Returns an array of the indexes in given datasource name.
|
||
3220 | *
|
||
3221 | * @param string $model Name of model to inspect
|
||
3222 | * @return array Fields in table. Keys are column and unique
|
||
3223 | */
|
||
3224 | public function index($model) { |
||
3225 | return array(); |
||
3226 | } |
||
3227 | |||
3228 | /**
|
||
3229 | * Generate a database-native schema for the given Schema object
|
||
3230 | *
|
||
3231 | * @param CakeSchema $schema An instance of a subclass of CakeSchema
|
||
3232 | * @param string $tableName Optional. If specified only the table name given will be generated.
|
||
3233 | * Otherwise, all tables defined in the schema are generated.
|
||
3234 | * @return string
|
||
3235 | */
|
||
3236 | public function createSchema($schema, $tableName = null) { |
||
3237 | if (!$schema instanceof CakeSchema) { |
||
3238 | trigger_error(__d('cake_dev', 'Invalid schema object'), E_USER_WARNING); |
||
3239 | return null; |
||
3240 | } |
||
3241 | $out = ''; |
||
3242 | |||
3243 | foreach ($schema->tables as $curTable => $columns) { |
||
3244 | if (!$tableName || $tableName === $curTable) { |
||
3245 | $cols = $indexes = $tableParameters = array(); |
||
3246 | $primary = null; |
||
3247 | $table = $this->fullTableName($curTable); |
||
3248 | |||
3249 | $primaryCount = 0; |
||
3250 | foreach ($columns as $col) { |
||
3251 | if (isset($col['key']) && $col['key'] === 'primary') { |
||
3252 | $primaryCount++;
|
||
3253 | } |
||
3254 | } |
||
3255 | |||
3256 | foreach ($columns as $name => $col) { |
||
3257 | if (is_string($col)) { |
||
3258 | $col = array('type' => $col); |
||
3259 | } |
||
3260 | $isPrimary = isset($col['key']) && $col['key'] === 'primary'; |
||
3261 | // Multi-column primary keys are not supported.
|
||
3262 | if ($isPrimary && $primaryCount > 1) { |
||
3263 | unset($col['key']); |
||
3264 | $isPrimary = false; |
||
3265 | } |
||
3266 | if ($isPrimary) { |
||
3267 | $primary = $name; |
||
3268 | } |
||
3269 | if ($name !== 'indexes' && $name !== 'tableParameters') { |
||
3270 | $col['name'] = $name; |
||
3271 | if (!isset($col['type'])) { |
||
3272 | $col['type'] = 'string'; |
||
3273 | } |
||
3274 | $cols[] = $this->buildColumn($col); |
||
3275 | } elseif ($name === 'indexes') { |
||
3276 | $indexes = array_merge($indexes, $this->buildIndex($col, $table)); |
||
3277 | } elseif ($name === 'tableParameters') { |
||
3278 | $tableParameters = array_merge($tableParameters, $this->buildTableParameters($col, $table)); |
||
3279 | } |
||
3280 | } |
||
3281 | if (!isset($columns['indexes']['PRIMARY']) && !empty($primary)) { |
||
3282 | $col = array('PRIMARY' => array('column' => $primary, 'unique' => 1)); |
||
3283 | $indexes = array_merge($indexes, $this->buildIndex($col, $table)); |
||
3284 | } |
||
3285 | $columns = $cols; |
||
3286 | $out .= $this->renderStatement('schema', compact('table', 'columns', 'indexes', 'tableParameters')) . "\n\n"; |
||
3287 | } |
||
3288 | } |
||
3289 | return $out; |
||
3290 | } |
||
3291 | |||
3292 | /**
|
||
3293 | * Generate an alter syntax from CakeSchema::compare()
|
||
3294 | *
|
||
3295 | * @param mixed $compare The comparison data.
|
||
3296 | * @param string $table The table name.
|
||
3297 | * @return bool
|
||
3298 | */
|
||
3299 | public function alterSchema($compare, $table = null) { |
||
3300 | return false; |
||
3301 | } |
||
3302 | |||
3303 | /**
|
||
3304 | * Generate a "drop table" statement for the given Schema object
|
||
3305 | *
|
||
3306 | * @param CakeSchema $schema An instance of a subclass of CakeSchema
|
||
3307 | * @param string $table Optional. If specified only the table name given will be generated.
|
||
3308 | * Otherwise, all tables defined in the schema are generated.
|
||
3309 | * @return string
|
||
3310 | */
|
||
3311 | public function dropSchema(CakeSchema $schema, $table = null) { |
||
3312 | $out = ''; |
||
3313 | |||
3314 | if ($table && array_key_exists($table, $schema->tables)) { |
||
3315 | return $this->_dropTable($table) . "\n"; |
||
3316 | } elseif ($table) { |
||
3317 | return $out; |
||
3318 | } |
||
3319 | |||
3320 | foreach (array_keys($schema->tables) as $curTable) { |
||
3321 | $out .= $this->_dropTable($curTable) . "\n"; |
||
3322 | } |
||
3323 | return $out; |
||
3324 | } |
||
3325 | |||
3326 | /**
|
||
3327 | * Generate a "drop table" statement for a single table
|
||
3328 | *
|
||
3329 | * @param type $table Name of the table to drop
|
||
3330 | * @return string Drop table SQL statement
|
||
3331 | */
|
||
3332 | protected function _dropTable($table) { |
||
3333 | return 'DROP TABLE ' . $this->fullTableName($table) . ";"; |
||
3334 | } |
||
3335 | |||
3336 | /**
|
||
3337 | * Generate a database-native column schema string
|
||
3338 | *
|
||
3339 | * @param array $column An array structured like the following: array('name' => 'value', 'type' => 'value'[, options]),
|
||
3340 | * where options can be 'default', 'length', or 'key'.
|
||
3341 | * @return string
|
||
3342 | */
|
||
3343 | public function buildColumn($column) { |
||
3344 | $name = $type = null; |
||
3345 | extract(array_merge(array('null' => true), $column)); |
||
3346 | |||
3347 | if (empty($name) || empty($type)) { |
||
3348 | trigger_error(__d('cake_dev', 'Column name or type not defined in schema'), E_USER_WARNING); |
||
3349 | return null; |
||
3350 | } |
||
3351 | |||
3352 | if (!isset($this->columns[$type])) { |
||
3353 | trigger_error(__d('cake_dev', 'Column type %s does not exist', $type), E_USER_WARNING); |
||
3354 | return null; |
||
3355 | } |
||
3356 | |||
3357 | $real = $this->columns[$type]; |
||
3358 | $out = $this->name($name) . ' ' . $real['name']; |
||
3359 | |||
3360 | if (isset($column['length'])) { |
||
3361 | $length = $column['length']; |
||
3362 | } elseif (isset($column['limit'])) { |
||
3363 | $length = $column['limit']; |
||
3364 | } elseif (isset($real['length'])) { |
||
3365 | $length = $real['length']; |
||
3366 | } elseif (isset($real['limit'])) { |
||
3367 | $length = $real['limit']; |
||
3368 | } |
||
3369 | if (isset($length)) { |
||
3370 | $out .= '(' . $length . ')'; |
||
3371 | } |
||
3372 | |||
3373 | if (($column['type'] === 'integer' || $column['type'] === 'float') && isset($column['default']) && $column['default'] === '') { |
||
3374 | $column['default'] = null; |
||
3375 | } |
||
3376 | $out = $this->_buildFieldParameters($out, $column, 'beforeDefault'); |
||
3377 | |||
3378 | if (isset($column['key']) && $column['key'] === 'primary' && ($type === 'integer' || $type === 'biginteger')) { |
||
3379 | $out .= ' ' . $this->columns['primary_key']['name']; |
||
3380 | } elseif (isset($column['key']) && $column['key'] === 'primary') { |
||
3381 | $out .= ' NOT NULL'; |
||
3382 | } elseif (isset($column['default']) && isset($column['null']) && $column['null'] === false) { |
||
3383 | $out .= ' DEFAULT ' . $this->value($column['default'], $type) . ' NOT NULL'; |
||
3384 | } elseif (isset($column['default'])) { |
||
3385 | $out .= ' DEFAULT ' . $this->value($column['default'], $type); |
||
3386 | } elseif ($type !== 'timestamp' && !empty($column['null'])) { |
||
3387 | $out .= ' DEFAULT NULL'; |
||
3388 | } elseif ($type === 'timestamp' && !empty($column['null'])) { |
||
3389 | $out .= ' NULL'; |
||
3390 | } elseif (isset($column['null']) && $column['null'] === false) { |
||
3391 | $out .= ' NOT NULL'; |
||
3392 | } |
||
3393 | if ($type === 'timestamp' && isset($column['default']) && strtolower($column['default']) === 'current_timestamp') { |
||
3394 | $out = str_replace(array("'CURRENT_TIMESTAMP'", "'current_timestamp'"), 'CURRENT_TIMESTAMP', $out); |
||
3395 | } |
||
3396 | return $this->_buildFieldParameters($out, $column, 'afterDefault'); |
||
3397 | } |
||
3398 | |||
3399 | /**
|
||
3400 | * Build the field parameters, in a position
|
||
3401 | *
|
||
3402 | * @param string $columnString The partially built column string
|
||
3403 | * @param array $columnData The array of column data.
|
||
3404 | * @param string $position The position type to use. 'beforeDefault' or 'afterDefault' are common
|
||
3405 | * @return string a built column with the field parameters added.
|
||
3406 | */
|
||
3407 | protected function _buildFieldParameters($columnString, $columnData, $position) { |
||
3408 | foreach ($this->fieldParameters as $paramName => $value) { |
||
3409 | if (isset($columnData[$paramName]) && $value['position'] == $position) { |
||
3410 | if (isset($value['options']) && !in_array($columnData[$paramName], $value['options'], true)) { |
||
3411 | continue;
|
||
3412 | } |
||
3413 | if (isset($value['types']) && !in_array($columnData['type'], $value['types'], true)) { |
||
3414 | continue;
|
||
3415 | } |
||
3416 | $val = $columnData[$paramName]; |
||
3417 | if ($value['quote']) { |
||
3418 | $val = $this->value($val); |
||
3419 | } |
||
3420 | $columnString .= ' ' . $value['value'] . (empty($value['noVal']) ? $value['join'] . $val : ''); |
||
3421 | } |
||
3422 | } |
||
3423 | return $columnString; |
||
3424 | } |
||
3425 | |||
3426 | /**
|
||
3427 | * Format indexes for create table.
|
||
3428 | *
|
||
3429 | * @param array $indexes The indexes to build
|
||
3430 | * @param string $table The table name.
|
||
3431 | * @return array
|
||
3432 | */
|
||
3433 | public function buildIndex($indexes, $table = null) { |
||
3434 | $join = array(); |
||
3435 | foreach ($indexes as $name => $value) { |
||
3436 | $out = ''; |
||
3437 | if ($name === 'PRIMARY') { |
||
3438 | $out .= 'PRIMARY '; |
||
3439 | $name = null; |
||
3440 | } else {
|
||
3441 | if (!empty($value['unique'])) { |
||
3442 | $out .= 'UNIQUE '; |
||
3443 | } |
||
3444 | $name = $this->startQuote . $name . $this->endQuote; |
||
3445 | } |
||
3446 | if (is_array($value['column'])) { |
||
3447 | $out .= 'KEY ' . $name . ' (' . implode(', ', array_map(array(&$this, 'name'), $value['column'])) . ')'; |
||
3448 | } else {
|
||
3449 | $out .= 'KEY ' . $name . ' (' . $this->name($value['column']) . ')'; |
||
3450 | } |
||
3451 | $join[] = $out; |
||
3452 | } |
||
3453 | return $join; |
||
3454 | } |
||
3455 | |||
3456 | /**
|
||
3457 | * Read additional table parameters
|
||
3458 | *
|
||
3459 | * @param string $name The table name to read.
|
||
3460 | * @return array
|
||
3461 | */
|
||
3462 | public function readTableParameters($name) { |
||
3463 | $parameters = array(); |
||
3464 | if (method_exists($this, 'listDetailedSources')) { |
||
3465 | $currentTableDetails = $this->listDetailedSources($name); |
||
3466 | foreach ($this->tableParameters as $paramName => $parameter) { |
||
3467 | if (!empty($parameter['column']) && !empty($currentTableDetails[$parameter['column']])) { |
||
3468 | $parameters[$paramName] = $currentTableDetails[$parameter['column']]; |
||
3469 | } |
||
3470 | } |
||
3471 | } |
||
3472 | return $parameters; |
||
3473 | } |
||
3474 | |||
3475 | /**
|
||
3476 | * Format parameters for create table
|
||
3477 | *
|
||
3478 | * @param array $parameters The parameters to create SQL for.
|
||
3479 | * @param string $table The table name.
|
||
3480 | * @return array
|
||
3481 | */
|
||
3482 | public function buildTableParameters($parameters, $table = null) { |
||
3483 | $result = array(); |
||
3484 | foreach ($parameters as $name => $value) { |
||
3485 | if (isset($this->tableParameters[$name])) { |
||
3486 | if ($this->tableParameters[$name]['quote']) { |
||
3487 | $value = $this->value($value); |
||
3488 | } |
||
3489 | $result[] = $this->tableParameters[$name]['value'] . $this->tableParameters[$name]['join'] . $value; |
||
3490 | } |
||
3491 | } |
||
3492 | return $result; |
||
3493 | } |
||
3494 | |||
3495 | /**
|
||
3496 | * Guesses the data type of an array
|
||
3497 | *
|
||
3498 | * @param string $value The value to introspect for type data.
|
||
3499 | * @return string
|
||
3500 | */
|
||
3501 | public function introspectType($value) { |
||
3502 | if (!is_array($value)) { |
||
3503 | if (is_bool($value)) { |
||
3504 | return 'boolean'; |
||
3505 | } |
||
3506 | if (is_float($value) && (float)$value === $value) { |
||
3507 | return 'float'; |
||
3508 | } |
||
3509 | if (is_int($value) && (int)$value === $value) { |
||
3510 | return 'integer'; |
||
3511 | } |
||
3512 | if (is_string($value) && strlen($value) > 255) { |
||
3513 | return 'text'; |
||
3514 | } |
||
3515 | return 'string'; |
||
3516 | } |
||
3517 | |||
3518 | $isAllFloat = $isAllInt = true; |
||
3519 | $containsInt = $containsString = false; |
||
3520 | foreach ($value as $valElement) { |
||
3521 | $valElement = trim($valElement); |
||
3522 | if (!is_float($valElement) && !preg_match('/^[\d]+\.[\d]+$/', $valElement)) { |
||
3523 | $isAllFloat = false; |
||
3524 | } else {
|
||
3525 | continue;
|
||
3526 | } |
||
3527 | if (!is_int($valElement) && !preg_match('/^[\d]+$/', $valElement)) { |
||
3528 | $isAllInt = false; |
||
3529 | } else {
|
||
3530 | $containsInt = true; |
||
3531 | continue;
|
||
3532 | } |
||
3533 | $containsString = true; |
||
3534 | } |
||
3535 | |||
3536 | if ($isAllFloat) { |
||
3537 | return 'float'; |
||
3538 | } |
||
3539 | if ($isAllInt) { |
||
3540 | return 'integer'; |
||
3541 | } |
||
3542 | |||
3543 | if ($containsInt && !$containsString) { |
||
3544 | return 'integer'; |
||
3545 | } |
||
3546 | return 'string'; |
||
3547 | } |
||
3548 | |||
3549 | /**
|
||
3550 | * Writes a new key for the in memory sql query cache
|
||
3551 | *
|
||
3552 | * @param string $sql SQL query
|
||
3553 | * @param mixed $data result of $sql query
|
||
3554 | * @param array $params query params bound as values
|
||
3555 | * @return void
|
||
3556 | */
|
||
3557 | protected function _writeQueryCache($sql, $data, $params = array()) { |
||
3558 | if (preg_match('/^\s*select/i', $sql)) { |
||
3559 | $this->_queryCache[$sql][serialize($params)] = $data; |
||
3560 | } |
||
3561 | } |
||
3562 | |||
3563 | /**
|
||
3564 | * Returns the result for a sql query if it is already cached
|
||
3565 | *
|
||
3566 | * @param string $sql SQL query
|
||
3567 | * @param array $params query params bound as values
|
||
3568 | * @return mixed results for query if it is cached, false otherwise
|
||
3569 | */
|
||
3570 | public function getQueryCache($sql, $params = array()) { |
||
3571 | if (isset($this->_queryCache[$sql]) && preg_match('/^\s*select/i', $sql)) { |
||
3572 | $serialized = serialize($params); |
||
3573 | if (isset($this->_queryCache[$sql][$serialized])) { |
||
3574 | return $this->_queryCache[$sql][$serialized]; |
||
3575 | } |
||
3576 | } |
||
3577 | return false; |
||
3578 | } |
||
3579 | |||
3580 | /**
|
||
3581 | * Used for storing in cache the results of the in-memory methodCache
|
||
3582 | */
|
||
3583 | public function __destruct() { |
||
3584 | if ($this->_methodCacheChange) { |
||
3585 | Cache::write('method_cache', static::$methodCache, '_cake_core_'); |
||
3586 | } |
||
3587 | parent::__destruct();
|
||
3588 | } |
||
3589 | |||
3590 | } |