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