Blame | Last modification | View Log | RSS feed
<?php/*** Dbo Source** CakePHP(tm) : Rapid Development Framework (http://cakephp.org)* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)** Licensed under The MIT License* For full copyright and license information, please see the LICENSE.txt* Redistributions of files must retain the above copyright notice.** @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)* @link http://cakephp.org CakePHP(tm) Project* @package Cake.Model.Datasource* @since CakePHP(tm) v 0.10.0.1076* @license http://www.opensource.org/licenses/mit-license.php MIT License*/App::uses('DataSource', 'Model/Datasource');App::uses('CakeText', 'Utility');App::uses('View', 'View');/*** DboSource** Creates DBO-descendant objects from a given db connection configuration** @package Cake.Model.Datasource*/class DboSource extends DataSource {/*** Description string for this Database Data Source.** @var string*/public $description = "Database Data Source";/*** index definition, standard cake, primary, index, unique** @var array*/public $index = array('PRI' => 'primary', 'MUL' => 'index', 'UNI' => 'unique');/*** Database keyword used to assign aliases to identifiers.** @var string*/public $alias = 'AS ';/*** Caches result from query parsing operations. Cached results for both DboSource::name() and* DboSource::conditions() will be stored here. Method caching uses `md5()`. If you have* problems with collisions, set DboSource::$cacheMethods to false.** @var array*/public static $methodCache = array();/*** Whether or not to cache the results of DboSource::name() and DboSource::conditions()* into the memory cache. Set to false to disable the use of the memory cache.** @var bool*/public $cacheMethods = true;/*** Flag to support nested transactions. If it is set to false, you will be able to use* the transaction methods (begin/commit/rollback), but just the global transaction will* be executed.** @var bool*/public $useNestedTransactions = false;/*** Print full query debug info?** @var bool*/public $fullDebug = false;/*** String to hold how many rows were affected by the last SQL operation.** @var string*/public $affected = null;/*** Number of rows in current resultset** @var int*/public $numRows = null;/*** Time the last query took** @var int*/public $took = null;/*** Result** @var array*/protected $_result = null;/*** Queries count.** @var int*/protected $_queriesCnt = 0;/*** Total duration of all queries.** @var int*/protected $_queriesTime = null;/*** Log of queries executed by this DataSource** @var array*/protected $_queriesLog = array();/*** Maximum number of items in query log** This is to prevent query log taking over too much memory.** @var int*/protected $_queriesLogMax = 200;/*** Caches serialized results of executed queries** @var array*/protected $_queryCache = array();/*** A reference to the physical connection of this DataSource** @var array*/protected $_connection = null;/*** The DataSource configuration key name** @var string*/public $configKeyName = null;/*** The starting character that this DataSource uses for quoted identifiers.** @var string*/public $startQuote = null;/*** The ending character that this DataSource uses for quoted identifiers.** @var string*/public $endQuote = null;/*** The set of valid SQL operations usable in a WHERE statement** @var array*/protected $_sqlOps = array('like', 'ilike', 'rlike', 'or', 'not', 'in', 'between', 'regexp', 'similar to');/*** Indicates the level of nested transactions** @var int*/protected $_transactionNesting = 0;/*** Default fields that are used by the DBO** @var array*/protected $_queryDefaults = array('conditions' => array(),'fields' => null,'table' => null,'alias' => null,'order' => null,'limit' => null,'joins' => array(),'group' => null,'offset' => null);/*** Separator string for virtualField composition** @var string*/public $virtualFieldSeparator = '__';/*** List of table engine specific parameters used on table creating** @var array*/public $tableParameters = array();/*** List of engine specific additional field parameters used on table creating** @var array*/public $fieldParameters = array();/*** Indicates whether there was a change on the cached results on the methods of this class* This will be used for storing in a more persistent cache** @var bool*/protected $_methodCacheChange = false;/*** Constructor** @param array $config Array of configuration information for the Datasource.* @param bool $autoConnect Whether or not the datasource should automatically connect.* @throws MissingConnectionException when a connection cannot be made.*/public function __construct($config = null, $autoConnect = true) {if (!isset($config['prefix'])) {$config['prefix'] = '';}parent::__construct($config);$this->fullDebug = Configure::read('debug') > 1;if (!$this->enabled()) {throw new MissingConnectionException(array('class' => get_class($this),'message' => __d('cake_dev', 'Selected driver is not enabled'),'enabled' => false));}if ($autoConnect) {$this->connect();}}/*** Reconnects to database server with optional new settings** @param array $config An array defining the new configuration settings* @return bool True on success, false on failure*/public function reconnect($config = array()) {$this->disconnect();$this->setConfig($config);$this->_sources = null;return $this->connect();}/*** Disconnects from database.** @return bool Always true*/public function disconnect() {if ($this->_result instanceof PDOStatement) {$this->_result->closeCursor();}unset($this->_connection);$this->connected = false;return true;}/*** Get the underlying connection object.** @return PDO*/public function getConnection() {return $this->_connection;}/*** Gets the version string of the database server** @return string The database version*/public function getVersion() {return $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);}/*** Returns a quoted and escaped string of $data for use in an SQL statement.** @param string $data String to be prepared for use in an SQL statement* @param string $column The column datatype into which this data will be inserted.* @param bool $null Column allows NULL values* @return string Quoted and escaped data*/public function value($data, $column = null, $null = true) {if (is_array($data) && !empty($data)) {return array_map(array(&$this, 'value'),$data, array_fill(0, count($data), $column));} elseif (is_object($data) && isset($data->type, $data->value)) {if ($data->type === 'identifier') {return $this->name($data->value);} elseif ($data->type === 'expression') {return $data->value;}} elseif (in_array($data, array('{$__cakeID__$}', '{$__cakeForeignKey__$}'), true)) {return $data;}if ($data === null || (is_array($data) && empty($data))) {return 'NULL';}if (empty($column)) {$column = $this->introspectType($data);}switch ($column) {case 'binary':return $this->_connection->quote($data, PDO::PARAM_LOB);case 'boolean':return $this->_connection->quote($this->boolean($data, true), PDO::PARAM_BOOL);case 'string':case 'text':return $this->_connection->quote($data, PDO::PARAM_STR);default:if ($data === '') {return $null ? 'NULL' : '""';}if (is_float($data)) {return str_replace(',', '.', strval($data));}if ((is_int($data) || $data === '0') || (is_numeric($data) &&strpos($data, ',') === false &&$data[0] != '0' &&strpos($data, 'e') === false)) {return $data;}return $this->_connection->quote($data);}}/*** Returns an object to represent a database identifier in a query. Expression objects* are not sanitized or escaped.** @param string $identifier A SQL expression to be used as an identifier* @return stdClass An object representing a database identifier to be used in a query*/public function identifier($identifier) {$obj = new stdClass();$obj->type = 'identifier';$obj->value = $identifier;return $obj;}/*** Returns an object to represent a database expression in a query. Expression objects* are not sanitized or escaped.** @param string $expression An arbitrary SQL expression to be inserted into a query.* @return stdClass An object representing a database expression to be used in a query*/public function expression($expression) {$obj = new stdClass();$obj->type = 'expression';$obj->value = $expression;return $obj;}/*** Executes given SQL statement.** @param string $sql SQL statement* @param array $params Additional options for the query.* @return mixed Resource or object representing the result set, or false on failure*/public function rawQuery($sql, $params = array()) {$this->took = $this->numRows = false;return $this->execute($sql, array(), $params);}/*** Queries the database with given SQL statement, and obtains some metadata about the result* (rows affected, timing, any errors, number of rows in resultset). The query is also logged.* If Configure::read('debug') is set, the log is shown all the time, else it is only shown on errors.** ### Options** - log - Whether or not the query should be logged to the memory log.** @param string $sql SQL statement* @param array $options The options for executing the query.* @param array $params values to be bound to the query.* @return mixed Resource or object representing the result set, or false on failure*/public function execute($sql, $options = array(), $params = array()) {$options += array('log' => $this->fullDebug);$t = microtime(true);$this->_result = $this->_execute($sql, $params);if ($options['log']) {$this->took = round((microtime(true) - $t) * 1000, 0);$this->numRows = $this->affected = $this->lastAffected();$this->logQuery($sql, $params);}return $this->_result;}/*** Executes given SQL statement.** @param string $sql SQL statement* @param array $params list of params to be bound to query* @param array $prepareOptions Options to be used in the prepare statement* @return mixed PDOStatement if query executes with no problem, true as the result of a successful, false on error* query returning no rows, such as a CREATE statement, false otherwise* @throws PDOException*/protected function _execute($sql, $params = array(), $prepareOptions = array()) {$sql = trim($sql);if (preg_match('/^(?:CREATE|ALTER|DROP)\s+(?:TABLE|INDEX)/i', $sql)) {$statements = array_filter(explode(';', $sql));if (count($statements) > 1) {$result = array_map(array($this, '_execute'), $statements);return array_search(false, $result) === false;}}try {$query = $this->_connection->prepare($sql, $prepareOptions);$query->setFetchMode(PDO::FETCH_LAZY);if (!$query->execute($params)) {$this->_results = $query;$query->closeCursor();return false;}if (!$query->columnCount()) {$query->closeCursor();if (!$query->rowCount()) {return true;}}return $query;} catch (PDOException $e) {if (isset($query->queryString)) {$e->queryString = $query->queryString;} else {$e->queryString = $sql;}throw $e;}}/*** Returns a formatted error message from previous database operation.** @param PDOStatement $query the query to extract the error from if any* @return string Error message with error number*/public function lastError(PDOStatement $query = null) {if ($query) {$error = $query->errorInfo();} else {$error = $this->_connection->errorInfo();}if (empty($error[2])) {return null;}return $error[1] . ': ' . $error[2];}/*** Returns number of affected rows in previous database operation. If no previous operation exists,* this returns false.** @param mixed $source The source to check.* @return int Number of affected rows*/public function lastAffected($source = null) {if ($this->hasResult()) {return $this->_result->rowCount();}return 0;}/*** Returns number of rows in previous resultset. If no previous resultset exists,* this returns false.** @param mixed $source Not used* @return int Number of rows in resultset*/public function lastNumRows($source = null) {return $this->lastAffected();}/*** DataSource Query abstraction** @return resource Result resource identifier.*/public function query() {$args = func_get_args();$fields = null;$order = null;$limit = null;$page = null;$recursive = null;if (count($args) === 1) {return $this->fetchAll($args[0]);} elseif (count($args) > 1 && (strpos($args[0], 'findBy') === 0 || strpos($args[0], 'findAllBy') === 0)) {$params = $args[1];if (substr($args[0], 0, 6) === 'findBy') {$all = false;$field = Inflector::underscore(substr($args[0], 6));} else {$all = true;$field = Inflector::underscore(substr($args[0], 9));}$or = (strpos($field, '_or_') !== false);if ($or) {$field = explode('_or_', $field);} else {$field = explode('_and_', $field);}$off = count($field) - 1;if (isset($params[1 + $off])) {$fields = $params[1 + $off];}if (isset($params[2 + $off])) {$order = $params[2 + $off];}if (!array_key_exists(0, $params)) {return false;}$c = 0;$conditions = array();foreach ($field as $f) {$conditions[$args[2]->alias . '.' . $f] = $params[$c++];}if ($or) {$conditions = array('OR' => $conditions);}if ($all) {if (isset($params[3 + $off])) {$limit = $params[3 + $off];}if (isset($params[4 + $off])) {$page = $params[4 + $off];}if (isset($params[5 + $off])) {$recursive = $params[5 + $off];}return $args[2]->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive'));}if (isset($params[3 + $off])) {$recursive = $params[3 + $off];}return $args[2]->find('first', compact('conditions', 'fields', 'order', 'recursive'));}if (isset($args[1]) && $args[1] === true) {return $this->fetchAll($args[0], true);} elseif (isset($args[1]) && !is_array($args[1])) {return $this->fetchAll($args[0], false);} elseif (isset($args[1]) && is_array($args[1])) {if (isset($args[2])) {$cache = $args[2];} else {$cache = true;}return $this->fetchAll($args[0], $args[1], array('cache' => $cache));}}/*** Returns a row from current resultset as an array** @param string $sql Some SQL to be executed.* @return array The fetched row as an array*/public function fetchRow($sql = null) {if (is_string($sql) && strlen($sql) > 5 && !$this->execute($sql)) {return null;}if ($this->hasResult()) {$this->resultSet($this->_result);$resultRow = $this->fetchResult();if (isset($resultRow[0])) {$this->fetchVirtualField($resultRow);}return $resultRow;}return null;}/*** Returns an array of all result rows for a given SQL query.** Returns false if no rows matched.** ### Options** - `cache` - Returns the cached version of the query, if exists and stores the result in cache.* This is a non-persistent cache, and only lasts for a single request. This option* defaults to true. If you are directly calling this method, you can disable caching* by setting $options to `false`** @param string $sql SQL statement* @param array|bool $params Either parameters to be bound as values for the SQL statement,* or a boolean to control query caching.* @param array $options additional options for the query.* @return bool|array Array of resultset rows, or false if no rows matched*/public function fetchAll($sql, $params = array(), $options = array()) {if (is_string($options)) {$options = array('modelName' => $options);}if (is_bool($params)) {$options['cache'] = $params;$params = array();}$options += array('cache' => true);$cache = $options['cache'];if ($cache && ($cached = $this->getQueryCache($sql, $params)) !== false) {return $cached;}$result = $this->execute($sql, array(), $params);if ($result) {$out = array();if ($this->hasResult()) {$first = $this->fetchRow();if ($first) {$out[] = $first;}while ($item = $this->fetchResult()) {if (isset($item[0])) {$this->fetchVirtualField($item);}$out[] = $item;}}if (!is_bool($result) && $cache) {$this->_writeQueryCache($sql, $out, $params);}if (empty($out) && is_bool($this->_result)) {return $this->_result;}return $out;}return false;}/*** Fetches the next row from the current result set** @return bool*/public function fetchResult() {return false;}/*** Modifies $result array to place virtual fields in model entry where they belongs to** @param array &$result Reference to the fetched row* @return void*/public function fetchVirtualField(&$result) {if (isset($result[0]) && is_array($result[0])) {foreach ($result[0] as $field => $value) {if (strpos($field, $this->virtualFieldSeparator) === false) {continue;}list($alias, $virtual) = explode($this->virtualFieldSeparator, $field);if (!ClassRegistry::isKeySet($alias)) {return;}$Model = ClassRegistry::getObject($alias);if ($Model->isVirtualField($virtual)) {$result[$alias][$virtual] = $value;unset($result[0][$field]);}}if (empty($result[0])) {unset($result[0]);}}}/*** Returns a single field of the first of query results for a given SQL query, or false if empty.** @param string $name Name of the field* @param string $sql SQL query* @return mixed Value of field read.*/public function field($name, $sql) {$data = $this->fetchRow($sql);if (empty($data[$name])) {return false;}return $data[$name];}/*** Empties the method caches.* These caches are used by DboSource::name() and DboSource::conditions()** @return void*/public function flushMethodCache() {$this->_methodCacheChange = true;static::$methodCache = array();}/*** Cache a value into the methodCaches. Will respect the value of DboSource::$cacheMethods.* Will retrieve a value from the cache if $value is null.** If caching is disabled and a write is attempted, the $value will be returned.* A read will either return the value or null.** @param string $method Name of the method being cached.* @param string $key The key name for the cache operation.* @param mixed $value The value to cache into memory.* @return mixed Either null on failure, or the value if its set.*/public function cacheMethod($method, $key, $value = null) {if ($this->cacheMethods === false) {return $value;}if (!$this->_methodCacheChange && empty(static::$methodCache)) {static::$methodCache = (array)Cache::read('method_cache', '_cake_core_');}if ($value === null) {return (isset(static::$methodCache[$method][$key])) ? static::$methodCache[$method][$key] : null;}$this->_methodCacheChange = true;return static::$methodCache[$method][$key] = $value;}/*** Returns a quoted name of $data for use in an SQL statement.* Strips fields out of SQL functions before quoting.** Results of this method are stored in a memory cache. This improves performance, but* because the method uses a hashing algorithm it can have collisions.* Setting DboSource::$cacheMethods to false will disable the memory cache.** @param mixed $data Either a string with a column to quote. An array of columns to quote or an* object from DboSource::expression() or DboSource::identifier()* @return string SQL field*/public function name($data) {if (is_object($data) && isset($data->type)) {return $data->value;}if ($data === '*') {return '*';}if (is_array($data)) {foreach ($data as $i => $dataItem) {$data[$i] = $this->name($dataItem);}return $data;}$cacheKey = md5($this->startQuote . $data . $this->endQuote);if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) {return $return;}$data = trim($data);if (preg_match('/^[\w-]+(?:\.[^ \*]*)*$/', $data)) { // string, string.stringif (strpos($data, '.') === false) { // stringreturn $this->cacheMethod(__FUNCTION__, $cacheKey, $this->startQuote . $data . $this->endQuote);}$items = explode('.', $data);return $this->cacheMethod(__FUNCTION__, $cacheKey,$this->startQuote . implode($this->endQuote . '.' . $this->startQuote, $items) . $this->endQuote);}if (preg_match('/^[\w-]+\.\*$/', $data)) { // string.*return $this->cacheMethod(__FUNCTION__, $cacheKey,$this->startQuote . str_replace('.*', $this->endQuote . '.*', $data));}if (preg_match('/^([\w-]+)\((.*)\)$/', $data, $matches)) { // Functionsreturn $this->cacheMethod(__FUNCTION__, $cacheKey,$matches[1] . '(' . $this->name($matches[2]) . ')');}if (preg_match('/^([\w-]+(\.[\w-]+|\(.*\))*)\s+' . preg_quote($this->alias) . '\s*([\w-]+)$/i', $data, $matches)) {return $this->cacheMethod(__FUNCTION__, $cacheKey,preg_replace('/\s{2,}/', ' ', $this->name($matches[1]) . ' ' . $this->alias . ' ' . $this->name($matches[3])));}if (preg_match('/^[\w-_\s]*[\w-_]+/', $data)) {return $this->cacheMethod(__FUNCTION__, $cacheKey, $this->startQuote . $data . $this->endQuote);}return $this->cacheMethod(__FUNCTION__, $cacheKey, $data);}/*** Checks if the source is connected to the database.** @return bool True if the database is connected, else false*/public function isConnected() {try {$connected = $this->_connection->query('SELECT 1');} catch (Exception $e) {$connected = false;}$this->connected = ! empty($connected);return $this->connected;}/*** Checks if the result is valid** @return bool True if the result is valid else false*/public function hasResult() {return $this->_result instanceof PDOStatement;}/*** Get the query log as an array.** @param bool $sorted Get the queries sorted by time taken, defaults to false.* @param bool $clear If True the existing log will cleared.* @return array Array of queries run as an array*/public function getLog($sorted = false, $clear = true) {if ($sorted) {$log = sortByKey($this->_queriesLog, 'took', 'desc', SORT_NUMERIC);} else {$log = $this->_queriesLog;}if ($clear) {$this->_queriesLog = array();}return array('log' => $log, 'count' => $this->_queriesCnt, 'time' => $this->_queriesTime);}/*** Outputs the contents of the queries log. If in a non-CLI environment the sql_log element* will be rendered and output. If in a CLI environment, a plain text log is generated.** @param bool $sorted Get the queries sorted by time taken, defaults to false.* @return void*/public function showLog($sorted = false) {$log = $this->getLog($sorted, false);if (empty($log['log'])) {return;}if (PHP_SAPI !== 'cli') {$controller = null;$View = new View($controller, false);$View->set('sqlLogs', array($this->configKeyName => $log));echo $View->element('sql_dump', array('_forced_from_dbo_' => true));} else {foreach ($log['log'] as $k => $i) {print (($k + 1) . ". {$i['query']}\n");}}}/*** Log given SQL query.** @param string $sql SQL statement* @param array $params Values binded to the query (prepared statements)* @return void*/public function logQuery($sql, $params = array()) {$this->_queriesCnt++;$this->_queriesTime += $this->took;$this->_queriesLog[] = array('query' => $sql,'params' => $params,'affected' => $this->affected,'numRows' => $this->numRows,'took' => $this->took);if (count($this->_queriesLog) > $this->_queriesLogMax) {array_shift($this->_queriesLog);}}/*** Gets full table name including prefix** @param Model|string $model Either a Model object or a string table name.* @param bool $quote Whether you want the table name quoted.* @param bool $schema Whether you want the schema name included.* @return string Full quoted table name*/public function fullTableName($model, $quote = true, $schema = true) {if (is_object($model)) {$schemaName = $model->schemaName;$table = $model->tablePrefix . $model->table;} elseif (!empty($this->config['prefix']) && strpos($model, $this->config['prefix']) !== 0) {$table = $this->config['prefix'] . strval($model);} else {$table = strval($model);}if ($schema && !isset($schemaName)) {$schemaName = $this->getSchemaName();}if ($quote) {if ($schema && !empty($schemaName)) {if (strstr($table, '.') === false) {return $this->name($schemaName) . '.' . $this->name($table);}}return $this->name($table);}if ($schema && !empty($schemaName)) {if (strstr($table, '.') === false) {return $schemaName . '.' . $table;}}return $table;}/*** The "C" in CRUD** Creates new records in the database.** @param Model $Model Model object that the record is for.* @param array $fields An array of field names to insert. If null, $Model->data will be* used to generate field names.* @param array $values An array of values with keys matching the fields. If null, $Model->data will* be used to generate values.* @return bool Success*/public function create(Model $Model, $fields = null, $values = null) {$id = null;if (!$fields) {unset($fields, $values);$fields = array_keys($Model->data);$values = array_values($Model->data);}$count = count($fields);for ($i = 0; $i < $count; $i++) {$schema = $Model->schema();$valueInsert[] = $this->value($values[$i], $Model->getColumnType($fields[$i]), isset($schema[$fields[$i]]) ? $schema[$fields[$i]]['null'] : true);$fieldInsert[] = $this->name($fields[$i]);if ($fields[$i] === $Model->primaryKey) {$id = $values[$i];}}$query = array('table' => $this->fullTableName($Model),'fields' => implode(', ', $fieldInsert),'values' => implode(', ', $valueInsert));if ($this->execute($this->renderStatement('create', $query))) {if (empty($id)) {$id = $this->lastInsertId($this->fullTableName($Model, false, false), $Model->primaryKey);}$Model->setInsertID($id);$Model->id = $id;return true;}$Model->onError();return false;}/*** The "R" in CRUD** Reads record(s) from the database.** @param Model $Model A Model object that the query is for.* @param array $queryData An array of queryData information containing keys similar to Model::find().* @param int $recursive Number of levels of association* @return mixed boolean false on error/failure. An array of results on success.*/public function read(Model $Model, $queryData = array(), $recursive = null) {$queryData = $this->_scrubQueryData($queryData);$array = array('callbacks' => $queryData['callbacks']);if ($recursive === null && isset($queryData['recursive'])) {$recursive = $queryData['recursive'];}if ($recursive !== null) {$modelRecursive = $Model->recursive;$Model->recursive = $recursive;}if (!empty($queryData['fields'])) {$noAssocFields = true;$queryData['fields'] = $this->fields($Model, null, $queryData['fields']);} else {$noAssocFields = false;$queryData['fields'] = $this->fields($Model);}if ($Model->recursive === -1) {// Primary model data only, no joins.$associations = array();} else {$associations = $Model->associations();if ($Model->recursive === 0) {// Primary model data and its domain.unset($associations[2], $associations[3]);}}$originalJoins = $queryData['joins'];$queryData['joins'] = array();// Generate hasOne and belongsTo associations inside $queryData$linkedModels = array();foreach ($associations as $type) {if ($type !== 'hasOne' && $type !== 'belongsTo') {continue;}foreach ($Model->{$type} as $assoc => $assocData) {$LinkModel = $Model->{$assoc};if ($Model->useDbConfig !== $LinkModel->useDbConfig) {continue;}if ($noAssocFields) {$assocData['fields'] = false;}$external = isset($assocData['external']);if ($this->generateAssociationQuery($Model, $LinkModel, $type, $assoc, $assocData, $queryData, $external) === true) {$linkedModels[$type . '/' . $assoc] = true;}}}if (!empty($originalJoins)) {$queryData['joins'] = array_merge($queryData['joins'], $originalJoins);}// Build SQL statement with the primary model, plus hasOne and belongsTo associations$query = $this->buildAssociationQuery($Model, $queryData);$resultSet = $this->fetchAll($query, $Model->cacheQueries);unset($query);if ($resultSet === false) {$Model->onError();return false;}$filtered = array();// Deep associationsif ($Model->recursive > -1) {$joined = array();if (isset($queryData['joins'][0]['alias'])) {$joined[$Model->alias] = (array)Hash::extract($queryData['joins'], '{n}.alias');}foreach ($associations as $type) {foreach ($Model->{$type} as $assoc => $assocData) {$LinkModel = $Model->{$assoc};if (!isset($linkedModels[$type . '/' . $assoc])) {$db = $Model->useDbConfig === $LinkModel->useDbConfig ? $this : $LinkModel->getDataSource();} elseif ($Model->recursive > 1) {$db = $this;}if (isset($db) && method_exists($db, 'queryAssociation')) {$stack = array($assoc);$stack['_joined'] = $joined;$db->queryAssociation($Model, $LinkModel, $type, $assoc, $assocData, $array, true, $resultSet, $Model->recursive - 1, $stack);unset($db);if ($type === 'hasMany' || $type === 'hasAndBelongsToMany') {$filtered[] = $assoc;}}}}}if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {$this->_filterResults($resultSet, $Model, $filtered);}if ($recursive !== null) {$Model->recursive = $modelRecursive;}return $resultSet;}/*** Passes association results through afterFind filters of the corresponding model.** The primary model is always excluded, because the filtering is later done by Model::_filterResults().** @param array &$resultSet Reference of resultset to be filtered.* @param Model $Model Instance of model to operate against.* @param array $filtered List of classes already filtered, to be skipped.* @return array Array of results that have been filtered through $Model->afterFind.*/protected function _filterResults(&$resultSet, Model $Model, $filtered = array()) {if (!is_array($resultSet)) {return array();}$current = reset($resultSet);if (!is_array($current)) {return array();}$keys = array_diff(array_keys($current), $filtered, array($Model->alias));$filtering = array();foreach ($keys as $className) {if (!isset($Model->{$className}) || !is_object($Model->{$className})) {continue;}$LinkedModel = $Model->{$className};$filtering[] = $className;foreach ($resultSet as $key => &$result) {$data = $LinkedModel->afterFind(array(array($className => $result[$className])), false);if (isset($data[0][$className])) {$result[$className] = $data[0][$className];} else {unset($resultSet[$key]);}}}return $filtering;}/*** Passes association results through afterFind filters of the corresponding model.** Similar to DboSource::_filterResults(), but this filters only specified models.* The primary model can not be specified, because this call DboSource::_filterResults() internally.** @param array &$resultSet Reference of resultset to be filtered.* @param Model $Model Instance of model to operate against.* @param array $toBeFiltered List of classes to be filtered.* @return array Array of results that have been filtered through $Model->afterFind.*/protected function _filterResultsInclusive(&$resultSet, Model $Model, $toBeFiltered = array()) {$exclude = array();if (is_array($resultSet)) {$current = reset($resultSet);if (is_array($current)) {$exclude = array_diff(array_keys($current), $toBeFiltered);}}return $this->_filterResults($resultSet, $Model, $exclude);}/*** Queries associations.** Used to fetch results on recursive models.** - 'hasMany' associations with no limit set:* Fetch, filter and merge is done recursively for every level.** - 'hasAndBelongsToMany' associations:* Fetch and filter is done unaffected by the (recursive) level set.** @param Model $Model Primary Model object.* @param Model $LinkModel Linked model object.* @param string $type Association type, one of the model association types ie. hasMany.* @param string $association Association name.* @param array $assocData Association data.* @param array &$queryData An array of queryData information containing keys similar to Model::find().* @param bool $external Whether or not the association query is on an external datasource.* @param array &$resultSet Existing results.* @param int $recursive Number of levels of association.* @param array $stack A list with joined models.* @return mixed* @throws CakeException when results cannot be created.*/public function queryAssociation(Model $Model, Model $LinkModel, $type, $association, $assocData, &$queryData, $external, &$resultSet, $recursive, $stack) {if (isset($stack['_joined'])) {$joined = $stack['_joined'];unset($stack['_joined']);}$queryTemplate = $this->generateAssociationQuery($Model, $LinkModel, $type, $association, $assocData, $queryData, $external);if (empty($queryTemplate)) {return;}if (!is_array($resultSet)) {throw new CakeException(__d('cake_dev', 'Error in Model %s', get_class($Model)));}if ($type === 'hasMany' && empty($assocData['limit']) && !empty($assocData['foreignKey'])) {// 'hasMany' associations with no limit set.$assocIds = array();foreach ($resultSet as $result) {$assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack);}$assocIds = array_filter($assocIds);// Fetch$assocResultSet = array();if (!empty($assocIds)) {$assocResultSet = $this->_fetchHasMany($Model, $queryTemplate, $assocIds);}// Recursively query associationsif ($recursive > 0 && !empty($assocResultSet) && is_array($assocResultSet)) {foreach ($LinkModel->associations() as $type1) {foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) {$DeepModel = $LinkModel->{$assoc1};$tmpStack = $stack;$tmpStack[] = $assoc1;$db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource();$db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack);}}}// Filterif ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {$this->_filterResultsInclusive($assocResultSet, $Model, array($association));}// Mergereturn $this->_mergeHasMany($resultSet, $assocResultSet, $association, $Model);} elseif ($type === 'hasAndBelongsToMany') {// 'hasAndBelongsToMany' associations.$assocIds = array();foreach ($resultSet as $result) {$assocIds[] = $this->insertQueryData('{$__cakeID__$}', $result, $association, $Model, $stack);}$assocIds = array_filter($assocIds);// Fetch$assocResultSet = array();if (!empty($assocIds)) {$assocResultSet = $this->_fetchHasAndBelongsToMany($Model, $queryTemplate, $assocIds, $association);}$habtmAssocData = $Model->hasAndBelongsToMany[$association];$foreignKey = $habtmAssocData['foreignKey'];$joinKeys = array($foreignKey, $habtmAssocData['associationForeignKey']);list($with, $habtmFields) = $Model->joinModel($habtmAssocData['with'], $joinKeys);$habtmFieldsCount = count($habtmFields);// Filterif ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {$this->_filterResultsInclusive($assocResultSet, $Model, array($association, $with));}}$modelAlias = $Model->alias;$primaryKey = $Model->primaryKey;$selfJoin = ($Model->name === $LinkModel->name);foreach ($resultSet as &$row) {if ($type === 'hasOne' || $type === 'belongsTo' || $type === 'hasMany') {$assocResultSet = array();$prefetched = false;if (($type === 'hasOne' || $type === 'belongsTo') &&isset($row[$LinkModel->alias], $joined[$Model->alias]) &&in_array($LinkModel->alias, $joined[$Model->alias])) {$joinedData = Hash::filter($row[$LinkModel->alias]);if (!empty($joinedData)) {$assocResultSet[0] = array($LinkModel->alias => $row[$LinkModel->alias]);}$prefetched = true;} else {$query = $this->insertQueryData($queryTemplate, $row, $association, $Model, $stack);if ($query !== false) {$assocResultSet = $this->fetchAll($query, $Model->cacheQueries);}}}if (!empty($assocResultSet) && is_array($assocResultSet)) {if ($recursive > 0) {foreach ($LinkModel->associations() as $type1) {foreach ($LinkModel->{$type1} as $assoc1 => $assocData1) {$DeepModel = $LinkModel->{$assoc1};if ($type1 === 'belongsTo' ||($type === 'belongsTo' && $DeepModel->alias === $modelAlias) ||($DeepModel->alias !== $modelAlias)) {$tmpStack = $stack;$tmpStack[] = $assoc1;$db = $LinkModel->useDbConfig === $DeepModel->useDbConfig ? $this : $DeepModel->getDataSource();$db->queryAssociation($LinkModel, $DeepModel, $type1, $assoc1, $assocData1, $queryData, true, $assocResultSet, $recursive - 1, $tmpStack);}}}}if ($type === 'hasAndBelongsToMany') {$merge = array();foreach ($assocResultSet as $data) {if (isset($data[$with]) && $data[$with][$foreignKey] === $row[$modelAlias][$primaryKey]) {if ($habtmFieldsCount <= 2) {unset($data[$with]);}$merge[] = $data;}}if (empty($merge) && !isset($row[$association])) {$row[$association] = $merge;} else {$this->_mergeAssociation($row, $merge, $association, $type);}} else {if (!$prefetched && $LinkModel->useConsistentAfterFind) {if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {$this->_filterResultsInclusive($assocResultSet, $Model, array($association));}}$this->_mergeAssociation($row, $assocResultSet, $association, $type, $selfJoin);}if ($type !== 'hasAndBelongsToMany' && isset($row[$association]) && !$prefetched && !$LinkModel->useConsistentAfterFind) {$row[$association] = $LinkModel->afterFind($row[$association], false);}} else {$tempArray[0][$association] = false;$this->_mergeAssociation($row, $tempArray, $association, $type, $selfJoin);}}}/*** Fetch 'hasMany' associations.** This is just a proxy to maintain BC.** @param Model $Model Primary model object.* @param string $query Association query template.* @param array $ids Array of IDs of associated records.* @return array Association results.* @see DboSource::_fetchHasMany()*/public function fetchAssociated(Model $Model, $query, $ids) {return $this->_fetchHasMany($Model, $query, $ids);}/*** Fetch 'hasMany' associations.** @param Model $Model Primary model object.* @param string $query Association query template.* @param array $ids Array of IDs of associated records.* @return array Association results.*/protected function _fetchHasMany(Model $Model, $query, $ids) {$ids = array_unique($ids);if (count($ids) > 1) {$query = str_replace('= ({$__cakeID__$}', 'IN ({$__cakeID__$}', $query);}$query = str_replace('{$__cakeID__$}', implode(', ', $ids), $query);return $this->fetchAll($query, $Model->cacheQueries);}/*** Fetch 'hasAndBelongsToMany' associations.** @param Model $Model Primary model object.* @param string $query Association query.* @param array $ids Array of IDs of associated records.* @param string $association Association name.* @return array Association results.*/protected function _fetchHasAndBelongsToMany(Model $Model, $query, $ids, $association) {$ids = array_unique($ids);if (count($ids) > 1) {$query = str_replace('{$__cakeID__$}', '(' . implode(', ', $ids) . ')', $query);$query = str_replace('= (', 'IN (', $query);} else {$query = str_replace('{$__cakeID__$}', $ids[0], $query);}$query = str_replace(' WHERE 1 = 1', '', $query);return $this->fetchAll($query, $Model->cacheQueries);}/*** Merge the results of 'hasMany' associations.** Note: this function also deals with the formatting of the data.** @param array &$resultSet Data to merge into.* @param array $assocResultSet Data to merge.* @param string $association Name of Model being merged.* @param Model $Model Model being merged onto.* @return void*/protected function _mergeHasMany(&$resultSet, $assocResultSet, $association, Model $Model) {$modelAlias = $Model->alias;$primaryKey = $Model->primaryKey;$foreignKey = $Model->hasMany[$association]['foreignKey'];foreach ($resultSet as &$result) {if (!isset($result[$modelAlias])) {continue;}$resultPrimaryKey = $result[$modelAlias][$primaryKey];$merged = array();foreach ($assocResultSet as $data) {if ($resultPrimaryKey !== $data[$association][$foreignKey]) {continue;}if (count($data) > 1) {$data = array_merge($data[$association], $data);unset($data[$association]);foreach ($data as $key => $name) {if (is_numeric($key)) {$data[$association][] = $name;unset($data[$key]);}}$merged[] = $data;} else {$merged[] = $data[$association];}}$result = Hash::mergeDiff($result, array($association => $merged));}}/*** Merge association of merge into data** @param array &$data The data to merge.* @param array &$merge The data to merge.* @param string $association The association name to merge.* @param string $type The type of association* @param bool $selfJoin Whether or not this is a self join.* @return void*/protected function _mergeAssociation(&$data, &$merge, $association, $type, $selfJoin = false) {if (isset($merge[0]) && !isset($merge[0][$association])) {$association = Inflector::pluralize($association);}$dataAssociation =& $data[$association];if ($type === 'belongsTo' || $type === 'hasOne') {if (isset($merge[$association])) {$dataAssociation = $merge[$association][0];} else {if (!empty($merge[0][$association])) {foreach ($merge[0] as $assoc => $data2) {if ($assoc !== $association) {$merge[0][$association][$assoc] = $data2;}}}if (!isset($dataAssociation)) {$dataAssociation = array();if ($merge[0][$association]) {$dataAssociation = $merge[0][$association];}} else {if (is_array($merge[0][$association])) {foreach ($dataAssociation as $k => $v) {if (!is_array($v)) {$dataAssocTmp[$k] = $v;}}foreach ($merge[0][$association] as $k => $v) {if (!is_array($v)) {$mergeAssocTmp[$k] = $v;}}$dataKeys = array_keys($data);$mergeKeys = array_keys($merge[0]);if ($mergeKeys[0] === $dataKeys[0] || $mergeKeys === $dataKeys) {$dataAssociation[$association] = $merge[0][$association];} else {$diff = Hash::diff($dataAssocTmp, $mergeAssocTmp);$dataAssociation = array_merge($merge[0][$association], $diff);}} elseif ($selfJoin && array_key_exists($association, $merge[0])) {$dataAssociation = array_merge($dataAssociation, array($association => array()));}}}} else {if (isset($merge[0][$association]) && $merge[0][$association] === false) {if (!isset($dataAssociation)) {$dataAssociation = array();}} else {foreach ($merge as $row) {$insert = array();if (count($row) === 1) {$insert = $row[$association];} elseif (isset($row[$association])) {$insert = array_merge($row[$association], $row);unset($insert[$association]);}if (empty($dataAssociation) || (isset($dataAssociation) && !in_array($insert, $dataAssociation, true))) {$dataAssociation[] = $insert;}}}}}/*** Prepares fields required by an SQL statement.** When no fields are set, all the $Model fields are returned.** @param Model $Model The model to prepare.* @param array $queryData An array of queryData information containing keys similar to Model::find().* @return array Array containing SQL fields.*/public function prepareFields(Model $Model, $queryData) {if (empty($queryData['fields'])) {$queryData['fields'] = $this->fields($Model);} elseif (!empty($Model->hasMany) && $Model->recursive > -1) {// hasMany relationships need the $Model primary key.$assocFields = $this->fields($Model, null, "{$Model->alias}.{$Model->primaryKey}");$passedFields = $queryData['fields'];if (count($passedFields) > 1 ||(strpos($passedFields[0], $assocFields[0]) === false && !preg_match('/^[a-z]+\(/i', $passedFields[0]))) {$queryData['fields'] = array_merge($passedFields, $assocFields);}}return array_unique($queryData['fields']);}/*** Builds an SQL statement.** This is merely a convenient wrapper to DboSource::buildStatement().** @param Model $Model The model to build an association query for.* @param array $queryData An array of queryData information containing keys similar to Model::find().* @return string String containing an SQL statement.* @see DboSource::buildStatement()*/public function buildAssociationQuery(Model $Model, $queryData) {$queryData = $this->_scrubQueryData($queryData);return $this->buildStatement(array('fields' => $this->prepareFields($Model, $queryData),'table' => $this->fullTableName($Model),'alias' => $Model->alias,'limit' => $queryData['limit'],'offset' => $queryData['offset'],'joins' => $queryData['joins'],'conditions' => $queryData['conditions'],'order' => $queryData['order'],'group' => $queryData['group']),$Model);}/*** Generates a query or part of a query from a single model or two associated models.** Builds a string containing an SQL statement template.** @param Model $Model Primary Model object.* @param Model|null $LinkModel Linked model object.* @param string $type Association type, one of the model association types ie. hasMany.* @param string $association Association name.* @param array $assocData Association data.* @param array &$queryData An array of queryData information containing keys similar to Model::find().* @param bool $external Whether or not the association query is on an external datasource.* @return mixed* String representing a query.* True, when $external is false and association $type is 'hasOne' or 'belongsTo'.*/public function generateAssociationQuery(Model $Model, $LinkModel, $type, $association, $assocData, &$queryData, $external) {$assocData = $this->_scrubQueryData($assocData);$queryData = $this->_scrubQueryData($queryData);if ($LinkModel === null) {return $this->buildStatement(array('fields' => array_unique($queryData['fields']),'table' => $this->fullTableName($Model),'alias' => $Model->alias,'limit' => $queryData['limit'],'offset' => $queryData['offset'],'joins' => $queryData['joins'],'conditions' => $queryData['conditions'],'order' => $queryData['order'],'group' => $queryData['group']),$Model);}if ($external && !empty($assocData['finderQuery'])) {return $assocData['finderQuery'];}if ($type === 'hasMany' || $type === 'hasAndBelongsToMany') {if (empty($assocData['offset']) && !empty($assocData['page'])) {$assocData['offset'] = ($assocData['page'] - 1) * $assocData['limit'];}}switch ($type) {case 'hasOne':case 'belongsTo':$conditions = $this->_mergeConditions($assocData['conditions'],$this->getConstraint($type, $Model, $LinkModel, $association, array_merge($assocData, compact('external'))));if ($external) {// Not self joinif ($Model->name !== $LinkModel->name) {$modelAlias = $Model->alias;foreach ($conditions as $key => $condition) {if (is_numeric($key) && strpos($condition, $modelAlias . '.') !== false) {unset($conditions[$key]);}}}$query = array_merge($assocData, array('conditions' => $conditions,'table' => $this->fullTableName($LinkModel),'fields' => $this->fields($LinkModel, $association, $assocData['fields']),'alias' => $association,'group' => null));} else {$join = array('table' => $LinkModel,'alias' => $association,'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT','conditions' => trim($this->conditions($conditions, true, false, $Model)));$fields = array();if ($assocData['fields'] !== false) {$fields = $this->fields($LinkModel, $association, $assocData['fields']);}$queryData['fields'] = array_merge($this->prepareFields($Model, $queryData), $fields);if (!empty($assocData['order'])) {$queryData['order'][] = $assocData['order'];}if (!in_array($join, $queryData['joins'], true)) {$queryData['joins'][] = $join;}return true;}break;case 'hasMany':$assocData['fields'] = $this->fields($LinkModel, $association, $assocData['fields']);if (!empty($assocData['foreignKey'])) {$assocData['fields'] = array_merge($assocData['fields'], $this->fields($LinkModel, $association, array("{$association}.{$assocData['foreignKey']}")));}$query = array('conditions' => $this->_mergeConditions($this->getConstraint('hasMany', $Model, $LinkModel, $association, $assocData), $assocData['conditions']),'fields' => array_unique($assocData['fields']),'table' => $this->fullTableName($LinkModel),'alias' => $association,'order' => $assocData['order'],'limit' => $assocData['limit'],'offset' => $assocData['offset'],'group' => null);break;case 'hasAndBelongsToMany':$joinFields = array();$joinAssoc = null;if (isset($assocData['with']) && !empty($assocData['with'])) {$joinKeys = array($assocData['foreignKey'], $assocData['associationForeignKey']);list($with, $joinFields) = $Model->joinModel($assocData['with'], $joinKeys);$joinTbl = $Model->{$with};$joinAlias = $joinTbl;if (is_array($joinFields) && !empty($joinFields)) {$joinAssoc = $joinAlias = $joinTbl->alias;$joinFields = $this->fields($joinTbl, $joinAlias, $joinFields);} else {$joinFields = array();}} else {$joinTbl = $assocData['joinTable'];$joinAlias = $this->fullTableName($assocData['joinTable']);}$query = array('conditions' => $assocData['conditions'],'limit' => $assocData['limit'],'offset' => $assocData['offset'],'table' => $this->fullTableName($LinkModel),'alias' => $association,'fields' => array_merge($this->fields($LinkModel, $association, $assocData['fields']), $joinFields),'order' => $assocData['order'],'group' => null,'joins' => array(array('table' => $joinTbl,'alias' => $joinAssoc,'conditions' => $this->getConstraint('hasAndBelongsToMany', $Model, $LinkModel, $joinAlias, $assocData, $association))));break;}if (isset($query)) {return $this->buildStatement($query, $Model);}return null;}/*** Returns a conditions array for the constraint between two models.** @param string $type Association type.* @param Model $Model Primary Model object.* @param Model $LinkModel Linked model object.* @param string $association Association name.* @param array $assocData Association data.* @param string $association2 HABTM association name.* @return array Conditions array defining the constraint between $Model and $LinkModel.*/public function getConstraint($type, Model $Model, Model $LinkModel, $association, $assocData, $association2 = null) {$assocData += array('external' => false);if (empty($assocData['foreignKey'])) {return array();}switch ($type) {case 'hasOne':if ($assocData['external']) {return array("{$association}.{$assocData['foreignKey']}" => '{$__cakeID__$}');} else {return array("{$association}.{$assocData['foreignKey']}" => $this->identifier("{$Model->alias}.{$Model->primaryKey}"));}case 'belongsTo':if ($assocData['external']) {return array("{$association}.{$LinkModel->primaryKey}" => '{$__cakeForeignKey__$}');} else {return array("{$Model->alias}.{$assocData['foreignKey']}" => $this->identifier("{$association}.{$LinkModel->primaryKey}"));}case 'hasMany':return array("{$association}.{$assocData['foreignKey']}" => array('{$__cakeID__$}'));case 'hasAndBelongsToMany':return array(array("{$association}.{$assocData['foreignKey']}" => '{$__cakeID__$}'),array("{$association}.{$assocData['associationForeignKey']}" => $this->identifier("{$association2}.{$LinkModel->primaryKey}")));}return array();}/*** Builds and generates a JOIN condition from an array. Handles final clean-up before conversion.** @param array $join An array defining a JOIN condition in a query.* @return string An SQL JOIN condition to be used in a query.* @see DboSource::renderJoinStatement()* @see DboSource::buildStatement()*/public function buildJoinStatement($join) {$data = array_merge(array('type' => null,'alias' => null,'table' => 'join_table','conditions' => '',), $join);if (!empty($data['alias'])) {$data['alias'] = $this->alias . $this->name($data['alias']);}if (!empty($data['conditions'])) {$data['conditions'] = trim($this->conditions($data['conditions'], true, false));}if (!empty($data['table']) && (!is_string($data['table']) || strpos($data['table'], '(') !== 0)) {$data['table'] = $this->fullTableName($data['table']);}return $this->renderJoinStatement($data);}/*** Builds and generates an SQL statement from an array. Handles final clean-up before conversion.** @param array $query An array defining an SQL query.* @param Model $Model The model object which initiated the query.* @return string An executable SQL statement.* @see DboSource::renderStatement()*/public function buildStatement($query, Model $Model) {$query = array_merge($this->_queryDefaults, $query);if (!empty($query['joins'])) {$count = count($query['joins']);for ($i = 0; $i < $count; $i++) {if (is_array($query['joins'][$i])) {$query['joins'][$i] = $this->buildJoinStatement($query['joins'][$i]);}}}return $this->renderStatement('select', array('conditions' => $this->conditions($query['conditions'], true, true, $Model),'fields' => implode(', ', $query['fields']),'table' => $query['table'],'alias' => $this->alias . $this->name($query['alias']),'order' => $this->order($query['order'], 'ASC', $Model),'limit' => $this->limit($query['limit'], $query['offset']),'joins' => implode(' ', $query['joins']),'group' => $this->group($query['group'], $Model)));}/*** Renders a final SQL JOIN statement** @param array $data The data to generate a join statement for.* @return string*/public function renderJoinStatement($data) {if (strtoupper($data['type']) === 'CROSS' || empty($data['conditions'])) {return "{$data['type']} JOIN {$data['table']} {$data['alias']}";}return trim("{$data['type']} JOIN {$data['table']} {$data['alias']} ON ({$data['conditions']})");}/*** Renders a final SQL statement by putting together the component parts in the correct order** @param string $type type of query being run. e.g select, create, update, delete, schema, alter.* @param array $data Array of data to insert into the query.* @return string Rendered SQL expression to be run.*/public function renderStatement($type, $data) {extract($data);$aliases = null;switch (strtolower($type)) {case 'select':return trim("SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}");case 'create':return "INSERT INTO {$table} ({$fields}) VALUES ({$values})";case 'update':if (!empty($alias)) {$aliases = "{$this->alias}{$alias} {$joins} ";}return trim("UPDATE {$table} {$aliases}SET {$fields} {$conditions}");case 'delete':if (!empty($alias)) {$aliases = "{$this->alias}{$alias} {$joins} ";}return trim("DELETE {$alias} FROM {$table} {$aliases}{$conditions}");case 'schema':foreach (array('columns', 'indexes', 'tableParameters') as $var) {if (is_array(${$var})) {${$var} = "\t" . implode(",\n\t", array_filter(${$var}));} else {${$var} = '';}}if (trim($indexes) !== '') {$columns .= ',';}return "CREATE TABLE {$table} (\n{$columns}{$indexes}) {$tableParameters};";case 'alter':return;}}/*** Merges a mixed set of string/array conditions.** @param mixed $query The query to merge conditions for.* @param mixed $assoc The association names.* @return array*/protected function _mergeConditions($query, $assoc) {if (empty($assoc)) {return $query;}if (is_array($query)) {return array_merge((array)$assoc, $query);}if (!empty($query)) {$query = array($query);if (is_array($assoc)) {$query = array_merge($query, $assoc);} else {$query[] = $assoc;}return $query;}return $assoc;}/*** Generates and executes an SQL UPDATE statement for given model, fields, and values.* For databases that do not support aliases in UPDATE queries.** @param Model $Model The model to update.* @param array $fields The fields to update* @param array $values The values fo the fields.* @param mixed $conditions The conditions for the update. When non-empty $values will not be quoted.* @return bool Success*/public function update(Model $Model, $fields = array(), $values = null, $conditions = null) {if (!$values) {$combined = $fields;} else {$combined = array_combine($fields, $values);}$fields = implode(', ', $this->_prepareUpdateFields($Model, $combined, empty($conditions)));$alias = $joins = null;$table = $this->fullTableName($Model);$conditions = $this->_matchRecords($Model, $conditions);if ($conditions === false) {return false;}$query = compact('table', 'alias', 'joins', 'fields', 'conditions');if (!$this->execute($this->renderStatement('update', $query))) {$Model->onError();return false;}return true;}/*** Quotes and prepares fields and values for an SQL UPDATE statement** @param Model $Model The model to prepare fields for.* @param array $fields The fields to update.* @param bool $quoteValues If values should be quoted, or treated as SQL snippets* @param bool $alias Include the model alias in the field name* @return array Fields and values, quoted and prepared*/protected function _prepareUpdateFields(Model $Model, $fields, $quoteValues = true, $alias = false) {$quotedAlias = $this->startQuote . $Model->alias . $this->endQuote;$schema = $Model->schema();$updates = array();foreach ($fields as $field => $value) {if ($alias && strpos($field, '.') === false) {$quoted = $Model->escapeField($field);} elseif (!$alias && strpos($field, '.') !== false) {$quoted = $this->name(str_replace($quotedAlias . '.', '', str_replace($Model->alias . '.', '', $field)));} else {$quoted = $this->name($field);}if ($value === null) {$updates[] = $quoted . ' = NULL';continue;}$update = $quoted . ' = ';if ($quoteValues) {$update .= $this->value($value, $Model->getColumnType($field), isset($schema[$field]) ? $schema[$field]['null'] : true);} elseif ($Model->getColumnType($field) === 'boolean' && (is_int($value) || is_bool($value))) {$update .= $this->boolean($value, true);} elseif (!$alias) {$update .= str_replace($quotedAlias . '.', '', str_replace($Model->alias . '.', '', $value));} else {$update .= $value;}$updates[] = $update;}return $updates;}/*** Generates and executes an SQL DELETE statement.* For databases that do not support aliases in UPDATE queries.** @param Model $Model The model to delete from* @param mixed $conditions The conditions to use. If empty the model's primary key will be used.* @return bool Success*/public function delete(Model $Model, $conditions = null) {$alias = $joins = null;$table = $this->fullTableName($Model);$conditions = $this->_matchRecords($Model, $conditions);if ($conditions === false) {return false;}if ($this->execute($this->renderStatement('delete', compact('alias', 'table', 'joins', 'conditions'))) === false) {$Model->onError();return false;}return true;}/*** Gets a list of record IDs for the given conditions. Used for multi-record updates and deletes* in databases that do not support aliases in UPDATE/DELETE queries.** @param Model $Model The model to find matching records for.* @param mixed $conditions The conditions to match against.* @return array List of record IDs*/protected function _matchRecords(Model $Model, $conditions = null) {if ($conditions === true) {$conditions = $this->conditions(true);} elseif ($conditions === null) {$conditions = $this->conditions($this->defaultConditions($Model, $conditions, false), true, true, $Model);} else {$noJoin = true;foreach ($conditions as $field => $value) {$originalField = $field;if (strpos($field, '.') !== false) {list(, $field) = explode('.', $field);$field = ltrim($field, $this->startQuote);$field = rtrim($field, $this->endQuote);}if (!$Model->hasField($field)) {$noJoin = false;break;}if ($field !== $originalField) {$conditions[$field] = $value;unset($conditions[$originalField]);}}if ($noJoin === true) {return $this->conditions($conditions);}$idList = $Model->find('all', array('fields' => "{$Model->alias}.{$Model->primaryKey}",'conditions' => $conditions));if (empty($idList)) {return false;}$conditions = $this->conditions(array($Model->primaryKey => Hash::extract($idList, "{n}.{$Model->alias}.{$Model->primaryKey}")));}return $conditions;}/*** Returns an array of SQL JOIN conditions from a model's associations.** @param Model $Model The model to get joins for.2* @return array*/protected function _getJoins(Model $Model) {$join = array();$joins = array_merge($Model->getAssociated('hasOne'), $Model->getAssociated('belongsTo'));foreach ($joins as $assoc) {if (!isset($Model->{$assoc})) {continue;}$LinkModel = $Model->{$assoc};if ($Model->useDbConfig !== $LinkModel->useDbConfig) {continue;}$assocData = $Model->getAssociated($assoc);$join[] = $this->buildJoinStatement(array('table' => $LinkModel,'alias' => $assoc,'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT','conditions' => trim($this->conditions($this->_mergeConditions($assocData['conditions'], $this->getConstraint($assocData['association'], $Model, $LinkModel, $assoc, $assocData)),true,false,$Model))));}return $join;}/*** Returns an SQL calculation, i.e. COUNT() or MAX()** @param Model $Model The model to get a calculated field for.* @param string $func Lowercase name of SQL function, i.e. 'count' or 'max'* @param array $params Function parameters (any values must be quoted manually)* @return string An SQL calculation function*/public function calculate(Model $Model, $func, $params = array()) {$params = (array)$params;switch (strtolower($func)) {case 'count':if (!isset($params[0])) {$params[0] = '*';}if (!isset($params[1])) {$params[1] = 'count';}if ($Model->isVirtualField($params[0])) {$arg = $this->_quoteFields($Model->getVirtualField($params[0]));} else {$arg = $this->name($params[0]);}return 'COUNT(' . $arg . ') AS ' . $this->name($params[1]);case 'max':case 'min':if (!isset($params[1])) {$params[1] = $params[0];}if ($Model->isVirtualField($params[0])) {$arg = $this->_quoteFields($Model->getVirtualField($params[0]));} else {$arg = $this->name($params[0]);}return strtoupper($func) . '(' . $arg . ') AS ' . $this->name($params[1]);}}/*** Deletes all the records in a table and resets the count of the auto-incrementing* primary key, where applicable.** @param Model|string $table A string or model class representing the table to be truncated* @return bool SQL TRUNCATE TABLE statement, false if not applicable.*/public function truncate($table) {return $this->execute('TRUNCATE TABLE ' . $this->fullTableName($table));}/*** Check if the server support nested transactions** @return bool*/public function nestedTransactionSupported() {return false;}/*** Begin a transaction** @return bool True on success, false on fail* (i.e. if the database/model does not support transactions,* or a transaction has not started).*/public function begin() {if ($this->_transactionStarted) {if ($this->nestedTransactionSupported()) {return $this->_beginNested();}$this->_transactionNesting++;return $this->_transactionStarted;}$this->_transactionNesting = 0;if ($this->fullDebug) {$this->logQuery('BEGIN');}return $this->_transactionStarted = $this->_connection->beginTransaction();}/*** Begin a nested transaction** @return bool*/protected function _beginNested() {$query = 'SAVEPOINT LEVEL' . ++$this->_transactionNesting;if ($this->fullDebug) {$this->logQuery($query);}$this->_connection->exec($query);return true;}/*** Commit a transaction** @return bool True on success, false on fail* (i.e. if the database/model does not support transactions,* or a transaction has not started).*/public function commit() {if (!$this->_transactionStarted) {return false;}if ($this->_transactionNesting === 0) {if ($this->fullDebug) {$this->logQuery('COMMIT');}$this->_transactionStarted = false;return $this->_connection->commit();}if ($this->nestedTransactionSupported()) {return $this->_commitNested();}$this->_transactionNesting--;return true;}/*** Commit a nested transaction** @return bool*/protected function _commitNested() {$query = 'RELEASE SAVEPOINT LEVEL' . $this->_transactionNesting--;if ($this->fullDebug) {$this->logQuery($query);}$this->_connection->exec($query);return true;}/*** Rollback a transaction** @return bool True on success, false on fail* (i.e. if the database/model does not support transactions,* or a transaction has not started).*/public function rollback() {if (!$this->_transactionStarted) {return false;}if ($this->_transactionNesting === 0) {if ($this->fullDebug) {$this->logQuery('ROLLBACK');}$this->_transactionStarted = false;return $this->_connection->rollBack();}if ($this->nestedTransactionSupported()) {return $this->_rollbackNested();}$this->_transactionNesting--;return true;}/*** Rollback a nested transaction** @return bool*/protected function _rollbackNested() {$query = 'ROLLBACK TO SAVEPOINT LEVEL' . $this->_transactionNesting--;if ($this->fullDebug) {$this->logQuery($query);}$this->_connection->exec($query);return true;}/*** Returns the ID generated from the previous INSERT operation.** @param mixed $source The source to get an id for.* @return mixed*/public function lastInsertId($source = null) {return $this->_connection->lastInsertId();}/*** Creates a default set of conditions from the model if $conditions is null/empty.* If conditions are supplied then they will be returned. If a model doesn't exist and no conditions* were provided either null or false will be returned based on what was input.** @param Model $Model The model to get conditions for.* @param string|array|bool $conditions Array of conditions, conditions string, null or false. If an array of conditions,* or string conditions those conditions will be returned. With other values the model's existence will be checked.* If the model doesn't exist a null or false will be returned depending on the input value.* @param bool $useAlias Use model aliases rather than table names when generating conditions* @return mixed Either null, false, $conditions or an array of default conditions to use.* @see DboSource::update()* @see DboSource::conditions()*/public function defaultConditions(Model $Model, $conditions, $useAlias = true) {if (!empty($conditions)) {return $conditions;}$exists = $Model->exists();if (!$exists && ($conditions !== null || !empty($Model->__safeUpdateMode))) {return false;} elseif (!$exists) {return null;}$alias = $Model->alias;if (!$useAlias) {$alias = $this->fullTableName($Model, false);}return array("{$alias}.{$Model->primaryKey}" => $Model->getID());}/*** Returns a key formatted like a string Model.fieldname(i.e. Post.title, or Country.name)** @param Model $Model The model to get a key for.* @param string $key The key field.* @param string $assoc The association name.* @return string*/public function resolveKey(Model $Model, $key, $assoc = null) {if (strpos('.', $key) !== false) {return $this->name($Model->alias) . '.' . $this->name($key);}return $key;}/*** Private helper method to remove query metadata in given data array.** @param array $data The data to scrub.* @return array*/protected function _scrubQueryData($data) {static $base = null;if ($base === null) {$base = array_fill_keys(array('conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group'), array());$base['callbacks'] = null;}return (array)$data + $base;}/*** Converts model virtual fields into sql expressions to be fetched later** @param Model $Model The model to get virtual fields for.* @param string $alias Alias table name* @param array $fields virtual fields to be used on query* @return array*/protected function _constructVirtualFields(Model $Model, $alias, $fields) {$virtual = array();foreach ($fields as $field) {$virtualField = $this->name($alias . $this->virtualFieldSeparator . $field);$expression = $this->_quoteFields($Model->getVirtualField($field));$virtual[] = '(' . $expression . ") {$this->alias} {$virtualField}";}return $virtual;}/*** Generates the fields list of an SQL query.** @param Model $Model The model to get fields for.* @param string $alias Alias table name* @param mixed $fields The provided list of fields.* @param bool $quote If false, returns fields array unquoted* @return array*/public function fields(Model $Model, $alias = null, $fields = array(), $quote = true) {if (empty($alias)) {$alias = $Model->alias;}$virtualFields = $Model->getVirtualField();$cacheKey = array($alias,get_class($Model),$Model->alias,$virtualFields,$fields,$quote,ConnectionManager::getSourceName($this),$Model->schemaName,$Model->table);$cacheKey = md5(serialize($cacheKey));if ($return = $this->cacheMethod(__FUNCTION__, $cacheKey)) {return $return;}$allFields = empty($fields);if ($allFields) {$fields = array_keys($Model->schema());} elseif (!is_array($fields)) {$fields = CakeText::tokenize($fields);}$fields = array_values(array_filter($fields));$allFields = $allFields || in_array('*', $fields) || in_array($Model->alias . '.*', $fields);$virtual = array();if (!empty($virtualFields)) {$virtualKeys = array_keys($virtualFields);foreach ($virtualKeys as $field) {$virtualKeys[] = $Model->alias . '.' . $field;}$virtual = ($allFields) ? $virtualKeys : array_intersect($virtualKeys, $fields);foreach ($virtual as $i => $field) {if (strpos($field, '.') !== false) {$virtual[$i] = str_replace($Model->alias . '.', '', $field);}$fields = array_diff($fields, array($field));}$fields = array_values($fields);}if (!$quote) {if (!empty($virtual)) {$fields = array_merge($fields, $this->_constructVirtualFields($Model, $alias, $virtual));}return $fields;}$count = count($fields);if ($count >= 1 && !in_array($fields[0], array('*', 'COUNT(*)'))) {for ($i = 0; $i < $count; $i++) {if (is_string($fields[$i]) && in_array($fields[$i], $virtual)) {unset($fields[$i]);continue;}if (is_object($fields[$i]) && isset($fields[$i]->type) && $fields[$i]->type === 'expression') {$fields[$i] = $fields[$i]->value;} elseif (preg_match('/^\(.*\)\s' . $this->alias . '.*/i', $fields[$i])) {continue;} elseif (!preg_match('/^.+\\(.*\\)/', $fields[$i])) {$prepend = '';if (strpos($fields[$i], 'DISTINCT') !== false) {$prepend = 'DISTINCT ';$fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i]));}$dot = strpos($fields[$i], '.');if ($dot === false) {$prefix = !(strpos($fields[$i], ' ') !== false ||strpos($fields[$i], '(') !== false);$fields[$i] = $this->name(($prefix ? $alias . '.' : '') . $fields[$i]);} else {if (strpos($fields[$i], ',') === false) {$build = explode('.', $fields[$i]);if (!Hash::numeric($build)) {$fields[$i] = $this->name(implode('.', $build));}}}$fields[$i] = $prepend . $fields[$i];} elseif (preg_match('/\(([\.\w]+)\)/', $fields[$i], $field)) {if (isset($field[1])) {if (strpos($field[1], '.') === false) {$field[1] = $this->name($alias . '.' . $field[1]);} else {$field[0] = explode('.', $field[1]);if (!Hash::numeric($field[0])) {$field[0] = implode('.', array_map(array(&$this, 'name'), $field[0]));$fields[$i] = preg_replace('/\(' . $field[1] . '\)/', '(' . $field[0] . ')', $fields[$i], 1);}}}}}}if (!empty($virtual)) {$fields = array_merge($fields, $this->_constructVirtualFields($Model, $alias, $virtual));}return $this->cacheMethod(__FUNCTION__, $cacheKey, array_unique($fields));}/*** Creates a WHERE clause by parsing given conditions data. If an array or string* conditions are provided those conditions will be parsed and quoted. If a boolean* is given it will be integer cast as condition. Null will return 1 = 1.** Results of this method are stored in a memory cache. This improves performance, but* because the method uses a hashing algorithm it can have collisions.* Setting DboSource::$cacheMethods to false will disable the memory cache.** @param mixed $conditions Array or string of conditions, or any value.* @param bool $quoteValues If true, values should be quoted* @param bool $where If true, "WHERE " will be prepended to the return value* @param Model $Model A reference to the Model instance making the query* @return string SQL fragment*/public function conditions($conditions, $quoteValues = true, $where = true, Model $Model = null) {$clause = $out = '';if ($where) {$clause = ' WHERE ';}if (is_array($conditions) && !empty($conditions)) {$out = $this->conditionKeysToString($conditions, $quoteValues, $Model);if (empty($out)) {return $clause . ' 1 = 1';}return $clause . implode(' AND ', $out);}if (is_bool($conditions)) {return $clause . (int)$conditions . ' = 1';}if (empty($conditions) || trim($conditions) === '') {return $clause . '1 = 1';}$clauses = '/^WHERE\\x20|^GROUP\\x20BY\\x20|^HAVING\\x20|^ORDER\\x20BY\\x20/i';if (preg_match($clauses, $conditions)) {$clause = '';}$conditions = $this->_quoteFields($conditions);return $clause . $conditions;}/*** Creates a WHERE clause by parsing given conditions array. Used by DboSource::conditions().** @param array $conditions Array or string of conditions* @param bool $quoteValues If true, values should be quoted* @param Model $Model A reference to the Model instance making the query* @return string SQL fragment*/public function conditionKeysToString($conditions, $quoteValues = true, Model $Model = null) {$out = array();$data = $columnType = null;$bool = array('and', 'or', 'not', 'and not', 'or not', 'xor', '||', '&&');foreach ($conditions as $key => $value) {$join = ' AND ';$not = null;if (is_array($value)) {$valueInsert = (!empty($value) &&(substr_count($key, '?') === count($value) || substr_count($key, ':') === count($value)));}if (is_numeric($key) && empty($value)) {continue;} elseif (is_numeric($key) && is_string($value)) {$out[] = $this->_quoteFields($value);} elseif ((is_numeric($key) && is_array($value)) || in_array(strtolower(trim($key)), $bool)) {if (in_array(strtolower(trim($key)), $bool)) {$join = ' ' . strtoupper($key) . ' ';} else {$key = $join;}$value = $this->conditionKeysToString($value, $quoteValues, $Model);if (strpos($join, 'NOT') !== false) {if (strtoupper(trim($key)) === 'NOT') {$key = 'AND ' . trim($key);}$not = 'NOT ';}if (empty($value)) {continue;}if (empty($value[1])) {if ($not) {$out[] = $not . '(' . $value[0] . ')';} else {$out[] = $value[0];}} else {$out[] = '(' . $not . '(' . implode(') ' . strtoupper($key) . ' (', $value) . '))';}} else {if (is_object($value) && isset($value->type)) {if ($value->type === 'identifier') {$data .= $this->name($key) . ' = ' . $this->name($value->value);} elseif ($value->type === 'expression') {if (is_numeric($key)) {$data .= $value->value;} else {$data .= $this->name($key) . ' = ' . $value->value;}}} elseif (is_array($value) && !empty($value) && !$valueInsert) {$keys = array_keys($value);if ($keys === array_values($keys)) {$count = count($value);if ($count === 1 && !preg_match('/\s+(?:NOT|\!=)$/', $key)) {$data = $this->_quoteFields($key) . ' = (';if ($quoteValues) {if ($Model !== null) {$columnType = $Model->getColumnType($key);}$data .= implode(', ', $this->value($value, $columnType));}$data .= ')';} else {$data = $this->_parseKey($key, $value, $Model);}} else {$ret = $this->conditionKeysToString($value, $quoteValues, $Model);if (count($ret) > 1) {$data = '(' . implode(') AND (', $ret) . ')';} elseif (isset($ret[0])) {$data = $ret[0];}}} elseif (is_numeric($key) && !empty($value)) {$data = $this->_quoteFields($value);} else {$data = $this->_parseKey(trim($key), $value, $Model);}if ($data) {$out[] = $data;$data = null;}}}return $out;}/*** Extracts a Model.field identifier and an SQL condition operator from a string, formats* and inserts values, and composes them into an SQL snippet.** @param string $key An SQL key snippet containing a field and optional SQL operator* @param mixed $value The value(s) to be inserted in the string* @param Model $Model Model object initiating the query* @return string*/protected function _parseKey($key, $value, Model $Model = null) {$operatorMatch = '/^(((' . implode(')|(', $this->_sqlOps);$operatorMatch .= ')\\x20?)|<[>=]?(?![^>]+>)\\x20?|[>=!]{1,3}(?!<)\\x20?)/is';$bound = (strpos($key, '?') !== false || (is_array($value) && strpos($key, ':') !== false));$key = trim($key);if (strpos($key, ' ') === false) {$operator = '=';} else {list($key, $operator) = explode(' ', $key, 2);if (!preg_match($operatorMatch, trim($operator)) && strpos($operator, ' ') !== false) {$key = $key . ' ' . $operator;$split = strrpos($key, ' ');$operator = substr($key, $split);$key = substr($key, 0, $split);}}$virtual = false;$type = null;if ($Model !== null) {if ($Model->isVirtualField($key)) {$key = $this->_quoteFields($Model->getVirtualField($key));$virtual = true;}$type = $Model->getColumnType($key);}$null = $value === null || (is_array($value) && empty($value));if (strtolower($operator) === 'not') {$data = $this->conditionKeysToString(array($operator => array($key => $value)), true, $Model);return $data[0];}$value = $this->value($value, $type);if (!$virtual && $key !== '?') {$isKey = (strpos($key, '(') !== false ||strpos($key, ')') !== false ||strpos($key, '|') !== false);$key = $isKey ? $this->_quoteFields($key) : $this->name($key);}if ($bound) {return CakeText::insert($key . ' ' . trim($operator), $value);}if (!preg_match($operatorMatch, trim($operator))) {$operator .= is_array($value) ? ' IN' : ' =';}$operator = trim($operator);if (is_array($value)) {$value = implode(', ', $value);switch ($operator) {case '=':$operator = 'IN';break;case '!=':case '<>':$operator = 'NOT IN';break;}$value = "({$value})";} elseif ($null || $value === 'NULL') {switch ($operator) {case '=':$operator = 'IS';break;case '!=':case '<>':$operator = 'IS NOT';break;}}if ($virtual) {return "({$key}) {$operator} {$value}";}return "{$key} {$operator} {$value}";}/*** Quotes Model.fields** @param string $conditions The conditions to quote.* @return string or false if no match*/protected function _quoteFields($conditions) {$start = $end = null;$original = $conditions;if (!empty($this->startQuote)) {$start = preg_quote($this->startQuote);}if (!empty($this->endQuote)) {$end = preg_quote($this->endQuote);}// Remove quotes and requote all the Model.field names.$conditions = str_replace(array($start, $end), '', $conditions);$conditions = preg_replace_callback('/(?:[\'\"][^\'\"\\\]*(?:\\\.[^\'\"\\\]*)*[\'\"])|([a-z0-9_][a-z0-9\\-_]*\\.[a-z0-9_][a-z0-9_\\-]*)/i',array(&$this, '_quoteMatchedField'),$conditions);// Quote `table_name AS Alias`$conditions = preg_replace('/(\s[a-z0-9\\-_.' . $start . $end . ']*' . $end . ')\s+AS\s+([a-z0-9\\-_]+)/i','\1 AS ' . $this->startQuote . '\2' . $this->endQuote,$conditions);if ($conditions !== null) {return $conditions;}return $original;}/*** Auxiliary function to quote matches `Model.fields` from a preg_replace_callback call** @param string $match matched string* @return string quoted string*/protected function _quoteMatchedField($match) {if (is_numeric($match[0])) {return $match[0];}return $this->name($match[0]);}/*** Returns a limit statement in the correct format for the particular database.** @param int $limit Limit of results returned* @param int $offset Offset from which to start results* @return string SQL limit/offset statement*/public function limit($limit, $offset = null) {if ($limit) {$rt = ' LIMIT';if ($offset) {$rt .= sprintf(' %u,', $offset);}$rt .= sprintf(' %u', $limit);return $rt;}return null;}/*** Returns an ORDER BY clause as a string.** @param array|string $keys Field reference, as a key (i.e. Post.title)* @param string $direction Direction (ASC or DESC)* @param Model $Model Model reference (used to look for virtual field)* @return string ORDER BY clause*/public function order($keys, $direction = 'ASC', Model $Model = null) {if (!is_array($keys)) {$keys = array($keys);}$keys = array_filter($keys);$result = array();while (!empty($keys)) {list($key, $dir) = each($keys);array_shift($keys);if (is_numeric($key)) {$key = $dir;$dir = $direction;}if (is_string($key) && strpos($key, ',') !== false && !preg_match('/\(.+\,.+\)/', $key)) {$key = array_map('trim', explode(',', $key));}if (is_array($key)) {//Flatten the array$key = array_reverse($key, true);foreach ($key as $k => $v) {if (is_numeric($k)) {array_unshift($keys, $v);} else {$keys = array($k => $v) + $keys;}}continue;} elseif (is_object($key) && isset($key->type) && $key->type === 'expression') {$result[] = $key->value;continue;}if (preg_match('/\\x20(ASC|DESC).*/i', $key, $_dir)) {$dir = $_dir[0];$key = preg_replace('/\\x20(ASC|DESC).*/i', '', $key);}$key = trim($key);if ($Model !== null) {if ($Model->isVirtualField($key)) {$key = '(' . $this->_quoteFields($Model->getVirtualField($key)) . ')';}list($alias) = pluginSplit($key);if ($alias !== $Model->alias && is_object($Model->{$alias}) && $Model->{$alias}->isVirtualField($key)) {$key = '(' . $this->_quoteFields($Model->{$alias}->getVirtualField($key)) . ')';}}if (strpos($key, '.')) {$key = preg_replace_callback('/([a-zA-Z0-9_-]{1,})\\.([a-zA-Z0-9_-]{1,})/', array(&$this, '_quoteMatchedField'), $key);}if (!preg_match('/\s/', $key) && strpos($key, '.') === false) {$key = $this->name($key);}$key .= ' ' . trim($dir);$result[] = $key;}if (!empty($result)) {return ' ORDER BY ' . implode(', ', $result);}return '';}/*** Create a GROUP BY SQL clause.** @param string|array $fields Group By fields* @param Model $Model The model to get group by fields for.* @return string Group By clause or null.*/public function group($fields, Model $Model = null) {if (empty($fields)) {return null;}if (!is_array($fields)) {$fields = array($fields);}if ($Model !== null) {foreach ($fields as $index => $key) {if ($Model->isVirtualField($key)) {$fields[$index] = '(' . $Model->getVirtualField($key) . ')';}}}$fields = implode(', ', $fields);return ' GROUP BY ' . $this->_quoteFields($fields);}/*** Disconnects database, kills the connection and says the connection is closed.** @return void*/public function close() {$this->disconnect();}/*** Checks if the specified table contains any record matching specified SQL** @param Model $Model Model to search* @param string $sql SQL WHERE clause (condition only, not the "WHERE" part)* @return bool True if the table has a matching record, else false*/public function hasAny(Model $Model, $sql) {$sql = $this->conditions($sql);$table = $this->fullTableName($Model);$alias = $this->alias . $this->name($Model->alias);$where = $sql ? "{$sql}" : ' WHERE 1 = 1';$id = $Model->escapeField();$out = $this->fetchRow("SELECT COUNT({$id}) {$this->alias}count FROM {$table} {$alias}{$where}");if (is_array($out)) {return $out[0]['count'];}return false;}/*** Gets the length of a database-native column description, or null if no length** @param string $real Real database-layer column type (i.e. "varchar(255)")* @return mixed An integer or string representing the length of the column, or null for unknown length.*/public function length($real) {if (!preg_match_all('/([\w\s]+)(?:\((\d+)(?:,(\d+))?\))?(\sunsigned)?(\szerofill)?/', $real, $result)) {$col = str_replace(array(')', 'unsigned'), '', $real);$limit = null;if (strpos($col, '(') !== false) {list($col, $limit) = explode('(', $col);}if ($limit !== null) {return (int)$limit;}return null;}$types = array('int' => 1, 'tinyint' => 1, 'smallint' => 1, 'mediumint' => 1, 'integer' => 1, 'bigint' => 1);list($real, $type, $length, $offset, $sign) = $result;$typeArr = $type;$type = $type[0];$length = $length[0];$offset = $offset[0];$isFloat = in_array($type, array('dec', 'decimal', 'float', 'numeric', 'double'));if ($isFloat && $offset) {return $length . ',' . $offset;}if (($real[0] == $type) && (count($real) === 1)) {return null;}if (isset($types[$type])) {$length += $types[$type];if (!empty($sign)) {$length--;}} elseif (in_array($type, array('enum', 'set'))) {$length = 0;foreach ($typeArr as $key => $enumValue) {if ($key === 0) {continue;}$tmpLength = strlen($enumValue);if ($tmpLength > $length) {$length = $tmpLength;}}}return (int)$length;}/*** Translates between PHP boolean values and Database (faked) boolean values** @param mixed $data Value to be translated* @param bool $quote Whether or not the field should be cast to a string.* @return string|bool Converted boolean value*/public function boolean($data, $quote = false) {if ($quote) {return !empty($data) ? '1' : '0';}return !empty($data);}/*** Inserts multiple values into a table** @param string $table The table being inserted into.* @param array $fields The array of field/column names being inserted.* @param array $values The array of values to insert. The values should* be an array of rows. Each row should have values keyed by the column name.* Each row must have the values in the same order as $fields.* @return bool*/public function insertMulti($table, $fields, $values) {$table = $this->fullTableName($table);$holder = implode(',', array_fill(0, count($fields), '?'));$fields = implode(', ', array_map(array(&$this, 'name'), $fields));$pdoMap = array('integer' => PDO::PARAM_INT,'float' => PDO::PARAM_STR,'boolean' => PDO::PARAM_BOOL,'string' => PDO::PARAM_STR,'text' => PDO::PARAM_STR);$columnMap = array();$sql = "INSERT INTO {$table} ({$fields}) VALUES ({$holder})";$statement = $this->_connection->prepare($sql);$this->begin();foreach ($values[key($values)] as $key => $val) {$type = $this->introspectType($val);$columnMap[$key] = $pdoMap[$type];}foreach ($values as $value) {$i = 1;foreach ($value as $col => $val) {$statement->bindValue($i, $val, $columnMap[$col]);$i += 1;}$statement->execute();$statement->closeCursor();if ($this->fullDebug) {$this->logQuery($sql, $value);}}return $this->commit();}/*** Reset a sequence based on the MAX() value of $column. Useful* for resetting sequences after using insertMulti().** This method should be implemented by datasources that require sequences to be used.** @param string $table The name of the table to update.* @param string $column The column to use when resetting the sequence value.* @return bool|void success.*/public function resetSequence($table, $column) {}/*** Returns an array of the indexes in given datasource name.** @param string $model Name of model to inspect* @return array Fields in table. Keys are column and unique*/public function index($model) {return array();}/*** Generate a database-native schema for the given Schema object** @param CakeSchema $schema An instance of a subclass of CakeSchema* @param string $tableName Optional. If specified only the table name given will be generated.* Otherwise, all tables defined in the schema are generated.* @return string*/public function createSchema($schema, $tableName = null) {if (!$schema instanceof CakeSchema) {trigger_error(__d('cake_dev', 'Invalid schema object'), E_USER_WARNING);return null;}$out = '';foreach ($schema->tables as $curTable => $columns) {if (!$tableName || $tableName === $curTable) {$cols = $indexes = $tableParameters = array();$primary = null;$table = $this->fullTableName($curTable);$primaryCount = 0;foreach ($columns as $col) {if (isset($col['key']) && $col['key'] === 'primary') {$primaryCount++;}}foreach ($columns as $name => $col) {if (is_string($col)) {$col = array('type' => $col);}$isPrimary = isset($col['key']) && $col['key'] === 'primary';// Multi-column primary keys are not supported.if ($isPrimary && $primaryCount > 1) {unset($col['key']);$isPrimary = false;}if ($isPrimary) {$primary = $name;}if ($name !== 'indexes' && $name !== 'tableParameters') {$col['name'] = $name;if (!isset($col['type'])) {$col['type'] = 'string';}$cols[] = $this->buildColumn($col);} elseif ($name === 'indexes') {$indexes = array_merge($indexes, $this->buildIndex($col, $table));} elseif ($name === 'tableParameters') {$tableParameters = array_merge($tableParameters, $this->buildTableParameters($col, $table));}}if (!isset($columns['indexes']['PRIMARY']) && !empty($primary)) {$col = array('PRIMARY' => array('column' => $primary, 'unique' => 1));$indexes = array_merge($indexes, $this->buildIndex($col, $table));}$columns = $cols;$out .= $this->renderStatement('schema', compact('table', 'columns', 'indexes', 'tableParameters')) . "\n\n";}}return $out;}/*** Generate an alter syntax from CakeSchema::compare()** @param mixed $compare The comparison data.* @param string $table The table name.* @return bool*/public function alterSchema($compare, $table = null) {return false;}/*** Generate a "drop table" statement for the given Schema object** @param CakeSchema $schema An instance of a subclass of CakeSchema* @param string $table Optional. If specified only the table name given will be generated.* Otherwise, all tables defined in the schema are generated.* @return string*/public function dropSchema(CakeSchema $schema, $table = null) {$out = '';if ($table && array_key_exists($table, $schema->tables)) {return $this->_dropTable($table) . "\n";} elseif ($table) {return $out;}foreach (array_keys($schema->tables) as $curTable) {$out .= $this->_dropTable($curTable) . "\n";}return $out;}/*** Generate a "drop table" statement for a single table** @param type $table Name of the table to drop* @return string Drop table SQL statement*/protected function _dropTable($table) {return 'DROP TABLE ' . $this->fullTableName($table) . ";";}/*** Generate a database-native column schema string** @param array $column An array structured like the following: array('name' => 'value', 'type' => 'value'[, options]),* where options can be 'default', 'length', or 'key'.* @return string*/public function buildColumn($column) {$name = $type = null;extract(array_merge(array('null' => true), $column));if (empty($name) || empty($type)) {trigger_error(__d('cake_dev', 'Column name or type not defined in schema'), E_USER_WARNING);return null;}if (!isset($this->columns[$type])) {trigger_error(__d('cake_dev', 'Column type %s does not exist', $type), E_USER_WARNING);return null;}$real = $this->columns[$type];$out = $this->name($name) . ' ' . $real['name'];if (isset($column['length'])) {$length = $column['length'];} elseif (isset($column['limit'])) {$length = $column['limit'];} elseif (isset($real['length'])) {$length = $real['length'];} elseif (isset($real['limit'])) {$length = $real['limit'];}if (isset($length)) {$out .= '(' . $length . ')';}if (($column['type'] === 'integer' || $column['type'] === 'float') && isset($column['default']) && $column['default'] === '') {$column['default'] = null;}$out = $this->_buildFieldParameters($out, $column, 'beforeDefault');if (isset($column['key']) && $column['key'] === 'primary' && ($type === 'integer' || $type === 'biginteger')) {$out .= ' ' . $this->columns['primary_key']['name'];} elseif (isset($column['key']) && $column['key'] === 'primary') {$out .= ' NOT NULL';} elseif (isset($column['default']) && isset($column['null']) && $column['null'] === false) {$out .= ' DEFAULT ' . $this->value($column['default'], $type) . ' NOT NULL';} elseif (isset($column['default'])) {$out .= ' DEFAULT ' . $this->value($column['default'], $type);} elseif ($type !== 'timestamp' && !empty($column['null'])) {$out .= ' DEFAULT NULL';} elseif ($type === 'timestamp' && !empty($column['null'])) {$out .= ' NULL';} elseif (isset($column['null']) && $column['null'] === false) {$out .= ' NOT NULL';}if ($type === 'timestamp' && isset($column['default']) && strtolower($column['default']) === 'current_timestamp') {$out = str_replace(array("'CURRENT_TIMESTAMP'", "'current_timestamp'"), 'CURRENT_TIMESTAMP', $out);}return $this->_buildFieldParameters($out, $column, 'afterDefault');}/*** Build the field parameters, in a position** @param string $columnString The partially built column string* @param array $columnData The array of column data.* @param string $position The position type to use. 'beforeDefault' or 'afterDefault' are common* @return string a built column with the field parameters added.*/protected function _buildFieldParameters($columnString, $columnData, $position) {foreach ($this->fieldParameters as $paramName => $value) {if (isset($columnData[$paramName]) && $value['position'] == $position) {if (isset($value['options']) && !in_array($columnData[$paramName], $value['options'], true)) {continue;}if (isset($value['types']) && !in_array($columnData['type'], $value['types'], true)) {continue;}$val = $columnData[$paramName];if ($value['quote']) {$val = $this->value($val);}$columnString .= ' ' . $value['value'] . (empty($value['noVal']) ? $value['join'] . $val : '');}}return $columnString;}/*** Format indexes for create table.** @param array $indexes The indexes to build* @param string $table The table name.* @return array*/public function buildIndex($indexes, $table = null) {$join = array();foreach ($indexes as $name => $value) {$out = '';if ($name === 'PRIMARY') {$out .= 'PRIMARY ';$name = null;} else {if (!empty($value['unique'])) {$out .= 'UNIQUE ';}$name = $this->startQuote . $name . $this->endQuote;}if (is_array($value['column'])) {$out .= 'KEY ' . $name . ' (' . implode(', ', array_map(array(&$this, 'name'), $value['column'])) . ')';} else {$out .= 'KEY ' . $name . ' (' . $this->name($value['column']) . ')';}$join[] = $out;}return $join;}/*** Read additional table parameters** @param string $name The table name to read.* @return array*/public function readTableParameters($name) {$parameters = array();if (method_exists($this, 'listDetailedSources')) {$currentTableDetails = $this->listDetailedSources($name);foreach ($this->tableParameters as $paramName => $parameter) {if (!empty($parameter['column']) && !empty($currentTableDetails[$parameter['column']])) {$parameters[$paramName] = $currentTableDetails[$parameter['column']];}}}return $parameters;}/*** Format parameters for create table** @param array $parameters The parameters to create SQL for.* @param string $table The table name.* @return array*/public function buildTableParameters($parameters, $table = null) {$result = array();foreach ($parameters as $name => $value) {if (isset($this->tableParameters[$name])) {if ($this->tableParameters[$name]['quote']) {$value = $this->value($value);}$result[] = $this->tableParameters[$name]['value'] . $this->tableParameters[$name]['join'] . $value;}}return $result;}/*** Guesses the data type of an array** @param string $value The value to introspect for type data.* @return string*/public function introspectType($value) {if (!is_array($value)) {if (is_bool($value)) {return 'boolean';}if (is_float($value) && (float)$value === $value) {return 'float';}if (is_int($value) && (int)$value === $value) {return 'integer';}if (is_string($value) && strlen($value) > 255) {return 'text';}return 'string';}$isAllFloat = $isAllInt = true;$containsInt = $containsString = false;foreach ($value as $valElement) {$valElement = trim($valElement);if (!is_float($valElement) && !preg_match('/^[\d]+\.[\d]+$/', $valElement)) {$isAllFloat = false;} else {continue;}if (!is_int($valElement) && !preg_match('/^[\d]+$/', $valElement)) {$isAllInt = false;} else {$containsInt = true;continue;}$containsString = true;}if ($isAllFloat) {return 'float';}if ($isAllInt) {return 'integer';}if ($containsInt && !$containsString) {return 'integer';}return 'string';}/*** Writes a new key for the in memory sql query cache** @param string $sql SQL query* @param mixed $data result of $sql query* @param array $params query params bound as values* @return void*/protected function _writeQueryCache($sql, $data, $params = array()) {if (preg_match('/^\s*select/i', $sql)) {$this->_queryCache[$sql][serialize($params)] = $data;}}/*** Returns the result for a sql query if it is already cached** @param string $sql SQL query* @param array $params query params bound as values* @return mixed results for query if it is cached, false otherwise*/public function getQueryCache($sql, $params = array()) {if (isset($this->_queryCache[$sql]) && preg_match('/^\s*select/i', $sql)) {$serialized = serialize($params);if (isset($this->_queryCache[$sql][$serialized])) {return $this->_queryCache[$sql][$serialized];}}return false;}/*** Used for storing in cache the results of the in-memory methodCache*/public function __destruct() {if ($this->_methodCacheChange) {Cache::write('method_cache', static::$methodCache, '_cake_core_');}parent::__destruct();}}