summaryrefslogtreecommitdiff
path: root/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model
diff options
context:
space:
mode:
Diffstat (limited to 'poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model')
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AclNode.php182
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aco.php41
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AcoAction.php41
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aro.php41
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/AclBehavior.php142
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/ContainableBehavior.php428
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TranslateBehavior.php627
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TreeBehavior.php1007
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/BehaviorCollection.php296
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/CakeSchema.php710
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ConnectionManager.php266
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/CakeSession.php688
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DataSource.php442
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Mysql.php688
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Postgres.php907
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlite.php571
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlserver.php783
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DboSource.php3268
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CacheSession.php90
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CakeSessionHandlerInterface.php72
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/DatabaseSession.php144
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/I18nModel.php44
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Model.php3402
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelBehavior.php236
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelValidator.php599
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Permission.php257
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationRule.php333
-rw-r--r--poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationSet.php350
28 files changed, 16655 insertions, 0 deletions
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AclNode.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AclNode.php
new file mode 100644
index 0000000..ca3357c
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AclNode.php
@@ -0,0 +1,182 @@
+<?php
+/**
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 0.2.9
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('Model', 'Model');
+
+/**
+ * ACL Node
+ *
+ * @package Cake.Model
+ */
+class AclNode extends Model {
+
+/**
+ * Explicitly disable in-memory query caching for ACL models
+ *
+ * @var boolean
+ */
+ public $cacheQueries = false;
+
+/**
+ * ACL models use the Tree behavior
+ *
+ * @var array
+ */
+ public $actsAs = array('Tree' => array('type' => 'nested'));
+
+/**
+ * Constructor
+ *
+ */
+ public function __construct() {
+ $config = Configure::read('Acl.database');
+ if (isset($config)) {
+ $this->useDbConfig = $config;
+ }
+ parent::__construct();
+ }
+
+/**
+ * Retrieves the Aro/Aco node for this model
+ *
+ * @param string|array|Model $ref Array with 'model' and 'foreign_key', model object, or string value
+ * @return array Node found in database
+ * @throws CakeException when binding to a model that doesn't exist.
+ */
+ public function node($ref = null) {
+ $db = $this->getDataSource();
+ $type = $this->alias;
+ $result = null;
+
+ if (!empty($this->useTable)) {
+ $table = $this->useTable;
+ } else {
+ $table = Inflector::pluralize(Inflector::underscore($type));
+ }
+
+ if (empty($ref)) {
+ return null;
+ } elseif (is_string($ref)) {
+ $path = explode('/', $ref);
+ $start = $path[0];
+ unset($path[0]);
+
+ $queryData = array(
+ 'conditions' => array(
+ $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}0.lft"),
+ $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}0.rght")),
+ 'fields' => array('id', 'parent_id', 'model', 'foreign_key', 'alias'),
+ 'joins' => array(array(
+ 'table' => $table,
+ 'alias' => "{$type}0",
+ 'type' => 'LEFT',
+ 'conditions' => array("{$type}0.alias" => $start)
+ )),
+ 'order' => $db->name("{$type}.lft") . ' DESC'
+ );
+
+ foreach ($path as $i => $alias) {
+ $j = $i - 1;
+
+ $queryData['joins'][] = array(
+ 'table' => $table,
+ 'alias' => "{$type}{$i}",
+ 'type' => 'LEFT',
+ 'conditions' => array(
+ $db->name("{$type}{$i}.lft") . ' > ' . $db->name("{$type}{$j}.lft"),
+ $db->name("{$type}{$i}.rght") . ' < ' . $db->name("{$type}{$j}.rght"),
+ $db->name("{$type}{$i}.alias") . ' = ' . $db->value($alias, 'string'),
+ $db->name("{$type}{$j}.id") . ' = ' . $db->name("{$type}{$i}.parent_id")
+ )
+ );
+
+ $queryData['conditions'] = array('or' => array(
+ $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}0.lft") . ' AND ' . $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}0.rght"),
+ $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}{$i}.lft") . ' AND ' . $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}{$i}.rght"))
+ );
+ }
+ $result = $db->read($this, $queryData, -1);
+ $path = array_values($path);
+
+ if (
+ !isset($result[0][$type]) ||
+ (!empty($path) && $result[0][$type]['alias'] != $path[count($path) - 1]) ||
+ (empty($path) && $result[0][$type]['alias'] != $start)
+ ) {
+ return false;
+ }
+ } elseif (is_object($ref) && is_a($ref, 'Model')) {
+ $ref = array('model' => $ref->name, 'foreign_key' => $ref->id);
+ } elseif (is_array($ref) && !(isset($ref['model']) && isset($ref['foreign_key']))) {
+ $name = key($ref);
+ list($plugin, $alias) = pluginSplit($name);
+
+ $model = ClassRegistry::init(array('class' => $name, 'alias' => $alias));
+
+ if (empty($model)) {
+ throw new CakeException('cake_dev', "Model class '%s' not found in AclNode::node() when trying to bind %s object", $type, $this->alias);
+ }
+
+ $tmpRef = null;
+ if (method_exists($model, 'bindNode')) {
+ $tmpRef = $model->bindNode($ref);
+ }
+ if (empty($tmpRef)) {
+ $ref = array('model' => $alias, 'foreign_key' => $ref[$name][$model->primaryKey]);
+ } else {
+ if (is_string($tmpRef)) {
+ return $this->node($tmpRef);
+ }
+ $ref = $tmpRef;
+ }
+ }
+ if (is_array($ref)) {
+ if (is_array(current($ref)) && is_string(key($ref))) {
+ $name = key($ref);
+ $ref = current($ref);
+ }
+ foreach ($ref as $key => $val) {
+ if (strpos($key, $type) !== 0 && strpos($key, '.') === false) {
+ unset($ref[$key]);
+ $ref["{$type}0.{$key}"] = $val;
+ }
+ }
+ $queryData = array(
+ 'conditions' => $ref,
+ 'fields' => array('id', 'parent_id', 'model', 'foreign_key', 'alias'),
+ 'joins' => array(array(
+ 'table' => $table,
+ 'alias' => "{$type}0",
+ 'type' => 'LEFT',
+ 'conditions' => array(
+ $db->name("{$type}.lft") . ' <= ' . $db->name("{$type}0.lft"),
+ $db->name("{$type}.rght") . ' >= ' . $db->name("{$type}0.rght")
+ )
+ )),
+ 'order' => $db->name("{$type}.lft") . ' DESC'
+ );
+ $result = $db->read($this, $queryData, -1);
+
+ if (!$result) {
+ throw new CakeException(__d('cake_dev', "AclNode::node() - Couldn't find %s node identified by \"%s\"", $type, print_r($ref, true)));
+ }
+ }
+ return $result;
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aco.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aco.php
new file mode 100644
index 0000000..0ee696e
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aco.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 0.2.9
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('AclNode', 'Model');
+
+/**
+ * Access Control Object
+ *
+ * @package Cake.Model
+ */
+class Aco extends AclNode {
+
+/**
+ * Model name
+ *
+ * @var string
+ */
+ public $name = 'Aco';
+
+/**
+ * Binds to ARO nodes through permissions settings
+ *
+ * @var array
+ */
+ public $hasAndBelongsToMany = array('Aro' => array('with' => 'Permission'));
+} \ No newline at end of file
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AcoAction.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AcoAction.php
new file mode 100644
index 0000000..2783600
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/AcoAction.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 0.2.9
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('AppModel', 'Model');
+
+/**
+ * Action for Access Control Object
+ *
+ * @package Cake.Model
+ */
+class AcoAction extends AppModel {
+
+/**
+ * Model name
+ *
+ * @var string
+ */
+ public $name = 'AcoAction';
+
+/**
+ * ACO Actions belong to ACOs
+ *
+ * @var array
+ */
+ public $belongsTo = array('Aco');
+} \ No newline at end of file
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aro.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aro.php
new file mode 100644
index 0000000..6c03f46
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Aro.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 0.2.9
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('AclNode', 'Model');
+
+/**
+ * Access Request Object
+ *
+ * @package Cake.Model
+ */
+class Aro extends AclNode {
+
+/**
+ * Model name
+ *
+ * @var string
+ */
+ public $name = 'Aro';
+
+/**
+ * AROs are linked to ACOs by means of Permission
+ *
+ * @var array
+ */
+ public $hasAndBelongsToMany = array('Aco' => array('with' => 'Permission'));
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/AclBehavior.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/AclBehavior.php
new file mode 100644
index 0000000..d0f0a4c
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/AclBehavior.php
@@ -0,0 +1,142 @@
+<?php
+/**
+ * ACL behavior class.
+ *
+ * Enables objects to easily tie into an ACL system
+ *
+ * PHP 5
+ *
+ * CakePHP : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc.
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP Project
+ * @package Cake.Model.Behavior
+ * @since CakePHP v 1.2.0.4487
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+App::uses('AclNode', 'Model');
+App::uses('Hash', 'Utility');
+
+/**
+ * ACL behavior
+ *
+ * Enables objects to easily tie into an ACL system
+ *
+ * @package Cake.Model.Behavior
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/acl.html
+ */
+class AclBehavior extends ModelBehavior {
+
+/**
+ * Maps ACL type options to ACL models
+ *
+ * @var array
+ */
+ protected $_typeMaps = array('requester' => 'Aro', 'controlled' => 'Aco', 'both' => array('Aro', 'Aco'));
+
+/**
+ * Sets up the configuration for the model, and loads ACL models if they haven't been already
+ *
+ * @param Model $model
+ * @param array $config
+ * @return void
+ */
+ public function setup(Model $model, $config = array()) {
+ if (isset($config[0])) {
+ $config['type'] = $config[0];
+ unset($config[0]);
+ }
+ $this->settings[$model->name] = array_merge(array('type' => 'controlled'), $config);
+ $this->settings[$model->name]['type'] = strtolower($this->settings[$model->name]['type']);
+
+ $types = $this->_typeMaps[$this->settings[$model->name]['type']];
+
+ if (!is_array($types)) {
+ $types = array($types);
+ }
+ foreach ($types as $type) {
+ $model->{$type} = ClassRegistry::init($type);
+ }
+ if (!method_exists($model, 'parentNode')) {
+ trigger_error(__d('cake_dev', 'Callback parentNode() not defined in %s', $model->alias), E_USER_WARNING);
+ }
+ }
+
+/**
+ * Retrieves the Aro/Aco node for this model
+ *
+ * @param Model $model
+ * @param string|array|Model $ref Array with 'model' and 'foreign_key', model object, or string value
+ * @param string $type Only needed when Acl is set up as 'both', specify 'Aro' or 'Aco' to get the correct node
+ * @return array
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/acl.html#node
+ */
+ public function node(Model $model, $ref = null, $type = null) {
+ if (empty($type)) {
+ $type = $this->_typeMaps[$this->settings[$model->name]['type']];
+ if (is_array($type)) {
+ trigger_error(__d('cake_dev', 'AclBehavior is setup with more then one type, please specify type parameter for node()'), E_USER_WARNING);
+ return null;
+ }
+ }
+ if (empty($ref)) {
+ $ref = array('model' => $model->name, 'foreign_key' => $model->id);
+ }
+ return $model->{$type}->node($ref);
+ }
+
+/**
+ * Creates a new ARO/ACO node bound to this record
+ *
+ * @param Model $model
+ * @param boolean $created True if this is a new record
+ * @return void
+ */
+ public function afterSave(Model $model, $created) {
+ $types = $this->_typeMaps[$this->settings[$model->name]['type']];
+ if (!is_array($types)) {
+ $types = array($types);
+ }
+ foreach ($types as $type) {
+ $parent = $model->parentNode();
+ if (!empty($parent)) {
+ $parent = $this->node($model, $parent, $type);
+ }
+ $data = array(
+ 'parent_id' => isset($parent[0][$type]['id']) ? $parent[0][$type]['id'] : null,
+ 'model' => $model->name,
+ 'foreign_key' => $model->id
+ );
+ if (!$created) {
+ $node = $this->node($model, null, $type);
+ $data['id'] = isset($node[0][$type]['id']) ? $node[0][$type]['id'] : null;
+ }
+ $model->{$type}->create();
+ $model->{$type}->save($data);
+ }
+ }
+
+/**
+ * Destroys the ARO/ACO node bound to the deleted record
+ *
+ * @param Model $model
+ * @return void
+ */
+ public function afterDelete(Model $model) {
+ $types = $this->_typeMaps[$this->settings[$model->name]['type']];
+ if (!is_array($types)) {
+ $types = array($types);
+ }
+ foreach ($types as $type) {
+ $node = Hash::extract($this->node($model, null, $type), "0.{$type}.id");
+ if (!empty($node)) {
+ $model->{$type}->delete($node);
+ }
+ }
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/ContainableBehavior.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/ContainableBehavior.php
new file mode 100644
index 0000000..bb33eaf
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/ContainableBehavior.php
@@ -0,0 +1,428 @@
+<?php
+/**
+ * Behavior for binding management.
+ *
+ * Behavior to simplify manipulating a model's bindings when doing a find operation
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Behavior
+ * @since CakePHP(tm) v 1.2.0.5669
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+/**
+ * Behavior to allow for dynamic and atomic manipulation of a Model's associations
+ * used for a find call. Most useful for limiting the amount of associations and
+ * data returned.
+ *
+ * @package Cake.Model.Behavior
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html
+ */
+class ContainableBehavior extends ModelBehavior {
+
+/**
+ * Types of relationships available for models
+ *
+ * @var array
+ */
+ public $types = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
+
+/**
+ * Runtime configuration for this behavior
+ *
+ * @var array
+ */
+ public $runtime = array();
+
+/**
+ * Initiate behavior for the model using specified settings.
+ *
+ * Available settings:
+ *
+ * - recursive: (boolean, optional) set to true to allow containable to automatically
+ * determine the recursiveness level needed to fetch specified models,
+ * and set the model recursiveness to this level. setting it to false
+ * disables this feature. DEFAULTS TO: true
+ * - notices: (boolean, optional) issues E_NOTICES for bindings referenced in a
+ * containable call that are not valid. DEFAULTS TO: true
+ * - autoFields: (boolean, optional) auto-add needed fields to fetch requested
+ * bindings. DEFAULTS TO: true
+ *
+ * @param Model $Model Model using the behavior
+ * @param array $settings Settings to override for model.
+ * @return void
+ */
+ public function setup(Model $Model, $settings = array()) {
+ if (!isset($this->settings[$Model->alias])) {
+ $this->settings[$Model->alias] = array('recursive' => true, 'notices' => true, 'autoFields' => true);
+ }
+ $this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], $settings);
+ }
+
+/**
+ * Runs before a find() operation. Used to allow 'contain' setting
+ * as part of the find call, like this:
+ *
+ * `Model->find('all', array('contain' => array('Model1', 'Model2')));`
+ *
+ * {{{
+ * Model->find('all', array('contain' => array(
+ * 'Model1' => array('Model11', 'Model12'),
+ * 'Model2',
+ * 'Model3' => array(
+ * 'Model31' => 'Model311',
+ * 'Model32',
+ * 'Model33' => array('Model331', 'Model332')
+ * )));
+ * }}}
+ *
+ * @param Model $Model Model using the behavior
+ * @param array $query Query parameters as set by cake
+ * @return array
+ */
+ public function beforeFind(Model $Model, $query) {
+ $reset = (isset($query['reset']) ? $query['reset'] : true);
+ $noContain = (
+ (isset($this->runtime[$Model->alias]['contain']) && empty($this->runtime[$Model->alias]['contain'])) ||
+ (isset($query['contain']) && empty($query['contain']))
+ );
+ $contain = array();
+ if (isset($this->runtime[$Model->alias]['contain'])) {
+ $contain = $this->runtime[$Model->alias]['contain'];
+ unset($this->runtime[$Model->alias]['contain']);
+ }
+ if (isset($query['contain'])) {
+ $contain = array_merge($contain, (array)$query['contain']);
+ }
+ if (
+ $noContain || !$contain || in_array($contain, array(null, false), true) ||
+ (isset($contain[0]) && $contain[0] === null)
+ ) {
+ if ($noContain) {
+ $query['recursive'] = -1;
+ }
+ return $query;
+ }
+ if ((isset($contain[0]) && is_bool($contain[0])) || is_bool(end($contain))) {
+ $reset = is_bool(end($contain))
+ ? array_pop($contain)
+ : array_shift($contain);
+ }
+ $containments = $this->containments($Model, $contain);
+ $map = $this->containmentsMap($containments);
+
+ $mandatory = array();
+ foreach ($containments['models'] as $name => $model) {
+ $instance = $model['instance'];
+ $needed = $this->fieldDependencies($instance, $map, false);
+ if (!empty($needed)) {
+ $mandatory = array_merge($mandatory, $needed);
+ }
+ if ($contain) {
+ $backupBindings = array();
+ foreach ($this->types as $relation) {
+ if (!empty($instance->__backAssociation[$relation])) {
+ $backupBindings[$relation] = $instance->__backAssociation[$relation];
+ } else {
+ $backupBindings[$relation] = $instance->{$relation};
+ }
+ }
+ foreach ($this->types as $type) {
+ $unbind = array();
+ foreach ($instance->{$type} as $assoc => $options) {
+ if (!isset($model['keep'][$assoc])) {
+ $unbind[] = $assoc;
+ }
+ }
+ if (!empty($unbind)) {
+ if (!$reset && empty($instance->__backOriginalAssociation)) {
+ $instance->__backOriginalAssociation = $backupBindings;
+ }
+ $instance->unbindModel(array($type => $unbind), $reset);
+ }
+ foreach ($instance->{$type} as $assoc => $options) {
+ if (isset($model['keep'][$assoc]) && !empty($model['keep'][$assoc])) {
+ if (isset($model['keep'][$assoc]['fields'])) {
+ $model['keep'][$assoc]['fields'] = $this->fieldDependencies($containments['models'][$assoc]['instance'], $map, $model['keep'][$assoc]['fields']);
+ }
+ if (!$reset && empty($instance->__backOriginalAssociation)) {
+ $instance->__backOriginalAssociation = $backupBindings;
+ } elseif ($reset) {
+ $instance->__backAssociation[$type] = $backupBindings[$type];
+ }
+ $instance->{$type}[$assoc] = array_merge($instance->{$type}[$assoc], $model['keep'][$assoc]);
+ }
+ if (!$reset) {
+ $instance->__backInnerAssociation[] = $assoc;
+ }
+ }
+ }
+ }
+ }
+
+ if ($this->settings[$Model->alias]['recursive']) {
+ $query['recursive'] = (isset($query['recursive'])) ? $query['recursive'] : $containments['depth'];
+ }
+
+ $autoFields = ($this->settings[$Model->alias]['autoFields']
+ && !in_array($Model->findQueryType, array('list', 'count'))
+ && !empty($query['fields']));
+
+ if (!$autoFields) {
+ return $query;
+ }
+
+ $query['fields'] = (array)$query['fields'];
+ foreach (array('hasOne', 'belongsTo') as $type) {
+ if (!empty($Model->{$type})) {
+ foreach ($Model->{$type} as $assoc => $data) {
+ if ($Model->useDbConfig == $Model->{$assoc}->useDbConfig && !empty($data['fields'])) {
+ foreach ((array)$data['fields'] as $field) {
+ $query['fields'][] = (strpos($field, '.') === false ? $assoc . '.' : '') . $field;
+ }
+ }
+ }
+ }
+ }
+
+ if (!empty($mandatory[$Model->alias])) {
+ foreach ($mandatory[$Model->alias] as $field) {
+ if ($field == '--primaryKey--') {
+ $field = $Model->primaryKey;
+ } elseif (preg_match('/^.+\.\-\-[^-]+\-\-$/', $field)) {
+ list($modelName, $field) = explode('.', $field);
+ if ($Model->useDbConfig == $Model->{$modelName}->useDbConfig) {
+ $field = $modelName . '.' . (
+ ($field === '--primaryKey--') ? $Model->$modelName->primaryKey : $field
+ );
+ } else {
+ $field = null;
+ }
+ }
+ if ($field !== null) {
+ $query['fields'][] = $field;
+ }
+ }
+ }
+ $query['fields'] = array_unique($query['fields']);
+ return $query;
+ }
+
+/**
+ * Unbinds all relations from a model except the specified ones. Calling this function without
+ * parameters unbinds all related models.
+ *
+ * @param Model $Model Model on which binding restriction is being applied
+ * @return void
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html#using-containable
+ */
+ public function contain(Model $Model) {
+ $args = func_get_args();
+ $contain = call_user_func_array('am', array_slice($args, 1));
+ $this->runtime[$Model->alias]['contain'] = $contain;
+ }
+
+/**
+ * Permanently restore the original binding settings of given model, useful
+ * for restoring the bindings after using 'reset' => false as part of the
+ * contain call.
+ *
+ * @param Model $Model Model on which to reset bindings
+ * @return void
+ */
+ public function resetBindings(Model $Model) {
+ if (!empty($Model->__backOriginalAssociation)) {
+ $Model->__backAssociation = $Model->__backOriginalAssociation;
+ unset($Model->__backOriginalAssociation);
+ }
+ $Model->resetAssociations();
+ if (!empty($Model->__backInnerAssociation)) {
+ $assocs = $Model->__backInnerAssociation;
+ $Model->__backInnerAssociation = array();
+ foreach ($assocs as $currentModel) {
+ $this->resetBindings($Model->$currentModel);
+ }
+ }
+ }
+
+/**
+ * Process containments for model.
+ *
+ * @param Model $Model Model on which binding restriction is being applied
+ * @param array $contain Parameters to use for restricting this model
+ * @param array $containments Current set of containments
+ * @param boolean $throwErrors Whether non-existent bindings show throw errors
+ * @return array Containments
+ */
+ public function containments(Model $Model, $contain, $containments = array(), $throwErrors = null) {
+ $options = array('className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery', 'deleteQuery', 'insertQuery');
+ $keep = array();
+ if ($throwErrors === null) {
+ $throwErrors = (empty($this->settings[$Model->alias]) ? true : $this->settings[$Model->alias]['notices']);
+ }
+ foreach ((array)$contain as $name => $children) {
+ if (is_numeric($name)) {
+ $name = $children;
+ $children = array();
+ }
+ if (preg_match('/(?<!\.)\(/', $name)) {
+ $name = str_replace('(', '.(', $name);
+ }
+ if (strpos($name, '.') !== false) {
+ $chain = explode('.', $name);
+ $name = array_shift($chain);
+ $children = array(implode('.', $chain) => $children);
+ }
+
+ $children = (array)$children;
+ foreach ($children as $key => $val) {
+ if (is_string($key) && is_string($val) && !in_array($key, $options, true)) {
+ $children[$key] = (array)$val;
+ }
+ }
+
+ $keys = array_keys($children);
+ if ($keys && isset($children[0])) {
+ $keys = array_merge(array_values($children), $keys);
+ }
+
+ foreach ($keys as $i => $key) {
+ if (is_array($key)) {
+ continue;
+ }
+ $optionKey = in_array($key, $options, true);
+ if (!$optionKey && is_string($key) && preg_match('/^[a-z(]/', $key) && (!isset($Model->{$key}) || !is_object($Model->{$key}))) {
+ $option = 'fields';
+ $val = array($key);
+ if ($key{0} == '(') {
+ $val = preg_split('/\s*,\s*/', substr($key, 1, -1));
+ } elseif (preg_match('/ASC|DESC$/', $key)) {
+ $option = 'order';
+ $val = $Model->{$name}->alias . '.' . $key;
+ } elseif (preg_match('/[ =!]/', $key)) {
+ $option = 'conditions';
+ $val = $Model->{$name}->alias . '.' . $key;
+ }
+ $children[$option] = is_array($val) ? $val : array($val);
+ $newChildren = null;
+ if (!empty($name) && !empty($children[$key])) {
+ $newChildren = $children[$key];
+ }
+ unset($children[$key], $children[$i]);
+ $key = $option;
+ $optionKey = true;
+ if (!empty($newChildren)) {
+ $children = Hash::merge($children, $newChildren);
+ }
+ }
+ if ($optionKey && isset($children[$key])) {
+ if (!empty($keep[$name][$key]) && is_array($keep[$name][$key])) {
+ $keep[$name][$key] = array_merge((isset($keep[$name][$key]) ? $keep[$name][$key] : array()), (array)$children[$key]);
+ } else {
+ $keep[$name][$key] = $children[$key];
+ }
+ unset($children[$key]);
+ }
+ }
+
+ if (!isset($Model->{$name}) || !is_object($Model->{$name})) {
+ if ($throwErrors) {
+ trigger_error(__d('cake_dev', 'Model "%s" is not associated with model "%s"', $Model->alias, $name), E_USER_WARNING);
+ }
+ continue;
+ }
+
+ $containments = $this->containments($Model->{$name}, $children, $containments);
+ $depths[] = $containments['depth'] + 1;
+ if (!isset($keep[$name])) {
+ $keep[$name] = array();
+ }
+ }
+
+ if (!isset($containments['models'][$Model->alias])) {
+ $containments['models'][$Model->alias] = array('keep' => array(), 'instance' => &$Model);
+ }
+
+ $containments['models'][$Model->alias]['keep'] = array_merge($containments['models'][$Model->alias]['keep'], $keep);
+ $containments['depth'] = empty($depths) ? 0 : max($depths);
+ return $containments;
+ }
+
+/**
+ * Calculate needed fields to fetch the required bindings for the given model.
+ *
+ * @param Model $Model Model
+ * @param array $map Map of relations for given model
+ * @param array|boolean $fields If array, fields to initially load, if false use $Model as primary model
+ * @return array Fields
+ */
+ public function fieldDependencies(Model $Model, $map, $fields = array()) {
+ if ($fields === false) {
+ foreach ($map as $parent => $children) {
+ foreach ($children as $type => $bindings) {
+ foreach ($bindings as $dependency) {
+ if ($type == 'hasAndBelongsToMany') {
+ $fields[$parent][] = '--primaryKey--';
+ } elseif ($type == 'belongsTo') {
+ $fields[$parent][] = $dependency . '.--primaryKey--';
+ }
+ }
+ }
+ }
+ return $fields;
+ }
+ if (empty($map[$Model->alias])) {
+ return $fields;
+ }
+ foreach ($map[$Model->alias] as $type => $bindings) {
+ foreach ($bindings as $dependency) {
+ $innerFields = array();
+ switch ($type) {
+ case 'belongsTo':
+ $fields[] = $Model->{$type}[$dependency]['foreignKey'];
+ break;
+ case 'hasOne':
+ case 'hasMany':
+ $innerFields[] = $Model->$dependency->primaryKey;
+ $fields[] = $Model->primaryKey;
+ break;
+ }
+ if (!empty($innerFields) && !empty($Model->{$type}[$dependency]['fields'])) {
+ $Model->{$type}[$dependency]['fields'] = array_unique(array_merge($Model->{$type}[$dependency]['fields'], $innerFields));
+ }
+ }
+ }
+ return array_unique($fields);
+ }
+
+/**
+ * Build the map of containments
+ *
+ * @param array $containments Containments
+ * @return array Built containments
+ */
+ public function containmentsMap($containments) {
+ $map = array();
+ foreach ($containments['models'] as $name => $model) {
+ $instance = $model['instance'];
+ foreach ($this->types as $type) {
+ foreach ($instance->{$type} as $assoc => $options) {
+ if (isset($model['keep'][$assoc])) {
+ $map[$name][$type] = isset($map[$name][$type]) ? array_merge($map[$name][$type], (array)$assoc) : (array)$assoc;
+ }
+ }
+ }
+ }
+ return $map;
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TranslateBehavior.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TranslateBehavior.php
new file mode 100644
index 0000000..387a37f
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TranslateBehavior.php
@@ -0,0 +1,627 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Behavior
+ * @since CakePHP(tm) v 1.2.0.4525
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('I18n', 'I18n');
+App::uses('I18nModel', 'Model');
+
+/**
+ * Translate behavior
+ *
+ * @package Cake.Model.Behavior
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/translate.html
+ */
+class TranslateBehavior extends ModelBehavior {
+
+/**
+ * Used for runtime configuration of model
+ *
+ * @var array
+ */
+ public $runtime = array();
+
+/**
+ * Stores the joinTable object for generating joins.
+ *
+ * @var object
+ */
+ protected $_joinTable;
+
+/**
+ * Stores the runtime model for generating joins.
+ *
+ * @var Model
+ */
+ protected $_runtimeModel;
+
+/**
+ * Callback
+ *
+ * $config for TranslateBehavior should be
+ * array('fields' => array('field_one',
+ * 'field_two' => 'FieldAssoc', 'field_three'))
+ *
+ * With above example only one permanent hasMany will be joined (for field_two
+ * as FieldAssoc)
+ *
+ * $config could be empty - and translations configured dynamically by
+ * bindTranslation() method
+ *
+ * @param Model $model Model the behavior is being attached to.
+ * @param array $config Array of configuration information.
+ * @return mixed
+ */
+ public function setup(Model $model, $config = array()) {
+ $db = ConnectionManager::getDataSource($model->useDbConfig);
+ if (!$db->connected) {
+ trigger_error(
+ __d('cake_dev', 'Datasource %s for TranslateBehavior of model %s is not connected', $model->useDbConfig, $model->alias),
+ E_USER_ERROR
+ );
+ return false;
+ }
+
+ $this->settings[$model->alias] = array();
+ $this->runtime[$model->alias] = array('fields' => array());
+ $this->translateModel($model);
+ return $this->bindTranslation($model, $config, false);
+ }
+
+/**
+ * Cleanup Callback unbinds bound translations and deletes setting information.
+ *
+ * @param Model $model Model being detached.
+ * @return void
+ */
+ public function cleanup(Model $model) {
+ $this->unbindTranslation($model);
+ unset($this->settings[$model->alias]);
+ unset($this->runtime[$model->alias]);
+ }
+
+/**
+ * beforeFind Callback
+ *
+ * @param Model $model Model find is being run on.
+ * @param array $query Array of Query parameters.
+ * @return array Modified query
+ */
+ public function beforeFind(Model $model, $query) {
+ $this->runtime[$model->alias]['virtualFields'] = $model->virtualFields;
+ $locale = $this->_getLocale($model);
+ if (empty($locale)) {
+ return $query;
+ }
+ $db = $model->getDataSource();
+ $RuntimeModel = $this->translateModel($model);
+
+ if (!empty($RuntimeModel->tablePrefix)) {
+ $tablePrefix = $RuntimeModel->tablePrefix;
+ } else {
+ $tablePrefix = $db->config['prefix'];
+ }
+ $joinTable = new StdClass();
+ $joinTable->tablePrefix = $tablePrefix;
+ $joinTable->table = $RuntimeModel->table;
+ $joinTable->schemaName = $RuntimeModel->getDataSource()->getSchemaName();
+
+ $this->_joinTable = $joinTable;
+ $this->_runtimeModel = $RuntimeModel;
+
+ if (is_string($query['fields']) && 'COUNT(*) AS ' . $db->name('count') == $query['fields']) {
+ $query['fields'] = 'COUNT(DISTINCT(' . $db->name($model->alias . '.' . $model->primaryKey) . ')) ' . $db->alias . 'count';
+ $query['joins'][] = array(
+ 'type' => 'INNER',
+ 'alias' => $RuntimeModel->alias,
+ 'table' => $joinTable,
+ 'conditions' => array(
+ $model->alias . '.' . $model->primaryKey => $db->identifier($RuntimeModel->alias . '.foreign_key'),
+ $RuntimeModel->alias . '.model' => $model->name,
+ $RuntimeModel->alias . '.locale' => $locale
+ )
+ );
+ $conditionFields = $this->_checkConditions($model, $query);
+ foreach ($conditionFields as $field) {
+ $query = $this->_addJoin($model, $query, $field, $field, $locale);
+ }
+ unset($this->_joinTable, $this->_runtimeModel);
+ return $query;
+ }
+
+ $fields = array_merge($this->settings[$model->alias], $this->runtime[$model->alias]['fields']);
+ $addFields = array();
+ if (empty($query['fields'])) {
+ $addFields = $fields;
+ } elseif (is_array($query['fields'])) {
+ foreach ($fields as $key => $value) {
+ $field = (is_numeric($key)) ? $value : $key;
+
+ if (in_array($model->alias . '.*', $query['fields']) || in_array($model->alias . '.' . $field, $query['fields']) || in_array($field, $query['fields'])) {
+ $addFields[] = $field;
+ }
+ }
+ }
+
+ $this->runtime[$model->alias]['virtualFields'] = $model->virtualFields;
+ if ($addFields) {
+ foreach ($addFields as $_f => $field) {
+ $aliasField = is_numeric($_f) ? $field : $_f;
+
+ foreach (array($aliasField, $model->alias . '.' . $aliasField) as $_field) {
+ $key = array_search($_field, (array)$query['fields']);
+
+ if ($key !== false) {
+ unset($query['fields'][$key]);
+ }
+ }
+ $query = $this->_addJoin($model, $query, $field, $aliasField, $locale);
+ }
+ }
+ $this->runtime[$model->alias]['beforeFind'] = $addFields;
+ unset($this->_joinTable, $this->_runtimeModel);
+ return $query;
+ }
+
+/**
+ * Check a query's conditions for translated fields.
+ * Return an array of translated fields found in the conditions.
+ *
+ * @param Model $model The model being read.
+ * @param array $query The query array.
+ * @return array The list of translated fields that are in the conditions.
+ */
+ protected function _checkConditions(Model $model, $query) {
+ $conditionFields = array();
+ if (empty($query['conditions']) || (!empty($query['conditions']) && !is_array($query['conditions'])) ) {
+ return $conditionFields;
+ }
+ foreach ($query['conditions'] as $col => $val) {
+ foreach ($this->settings[$model->alias] as $field => $assoc) {
+ if (is_numeric($field)) {
+ $field = $assoc;
+ }
+ if (strpos($col, $field) !== false) {
+ $conditionFields[] = $field;
+ }
+ }
+ }
+ return $conditionFields;
+ }
+
+/**
+ * Appends a join for translated fields and possibly a field.
+ *
+ * @param Model $model The model being worked on.
+ * @param object $joinTable The jointable object.
+ * @param array $query The query array to append a join to.
+ * @param string $field The field name being joined.
+ * @param string $aliasField The aliased field name being joined.
+ * @param string|array $locale The locale(s) having joins added.
+ * @param boolean $addField Whether or not to add a field.
+ * @return array The modfied query
+ */
+ protected function _addJoin(Model $model, $query, $field, $aliasField, $locale, $addField = false) {
+ $db = ConnectionManager::getDataSource($model->useDbConfig);
+
+ $RuntimeModel = $this->_runtimeModel;
+ $joinTable = $this->_joinTable;
+
+ if (is_array($locale)) {
+ foreach ($locale as $_locale) {
+ $model->virtualFields['i18n_' . $field . '_' . $_locale] = 'I18n__' . $field . '__' . $_locale . '.content';
+ if (!empty($query['fields']) && is_array($query['fields'])) {
+ $query['fields'][] = 'i18n_' . $field . '_' . $_locale;
+ }
+ $query['joins'][] = array(
+ 'type' => 'LEFT',
+ 'alias' => 'I18n__' . $field . '__' . $_locale,
+ 'table' => $joinTable,
+ 'conditions' => array(
+ $model->alias . '.' . $model->primaryKey => $db->identifier("I18n__{$field}__{$_locale}.foreign_key"),
+ 'I18n__' . $field . '__' . $_locale . '.model' => $model->name,
+ 'I18n__' . $field . '__' . $_locale . '.' . $RuntimeModel->displayField => $aliasField,
+ 'I18n__' . $field . '__' . $_locale . '.locale' => $_locale
+ )
+ );
+ }
+ } else {
+ $model->virtualFields['i18n_' . $field] = 'I18n__' . $field . '.content';
+ if (!empty($query['fields']) && is_array($query['fields'])) {
+ $query['fields'][] = 'i18n_' . $field;
+ }
+ $query['joins'][] = array(
+ 'type' => 'INNER',
+ 'alias' => 'I18n__' . $field,
+ 'table' => $joinTable,
+ 'conditions' => array(
+ $model->alias . '.' . $model->primaryKey => $db->identifier("I18n__{$field}.foreign_key"),
+ 'I18n__' . $field . '.model' => $model->name,
+ 'I18n__' . $field . '.' . $RuntimeModel->displayField => $aliasField,
+ 'I18n__' . $field . '.locale' => $locale
+ )
+ );
+ }
+ return $query;
+ }
+
+/**
+ * afterFind Callback
+ *
+ * @param Model $model Model find was run on
+ * @param array $results Array of model results.
+ * @param boolean $primary Did the find originate on $model.
+ * @return array Modified results
+ */
+ public function afterFind(Model $model, $results, $primary) {
+ $model->virtualFields = $this->runtime[$model->alias]['virtualFields'];
+ $this->runtime[$model->alias]['virtualFields'] = $this->runtime[$model->alias]['fields'] = array();
+ $locale = $this->_getLocale($model);
+
+ if (empty($locale) || empty($results) || empty($this->runtime[$model->alias]['beforeFind'])) {
+ return $results;
+ }
+ $beforeFind = $this->runtime[$model->alias]['beforeFind'];
+
+ foreach ($results as $key => &$row) {
+ $results[$key][$model->alias]['locale'] = (is_array($locale)) ? current($locale) : $locale;
+ foreach ($beforeFind as $_f => $field) {
+ $aliasField = is_numeric($_f) ? $field : $_f;
+
+ if (is_array($locale)) {
+ foreach ($locale as $_locale) {
+ if (!isset($row[$model->alias][$aliasField]) && !empty($row[$model->alias]['i18n_' . $field . '_' . $_locale])) {
+ $row[$model->alias][$aliasField] = $row[$model->alias]['i18n_' . $field . '_' . $_locale];
+ $row[$model->alias]['locale'] = $_locale;
+ }
+ unset($row[$model->alias]['i18n_' . $field . '_' . $_locale]);
+ }
+
+ if (!isset($row[$model->alias][$aliasField])) {
+ $row[$model->alias][$aliasField] = '';
+ }
+ } else {
+ $value = '';
+ if (!empty($row[$model->alias]['i18n_' . $field])) {
+ $value = $row[$model->alias]['i18n_' . $field];
+ }
+ $row[$model->alias][$aliasField] = $value;
+ unset($row[$model->alias]['i18n_' . $field]);
+ }
+ }
+ }
+ return $results;
+ }
+
+/**
+ * beforeValidate Callback
+ *
+ * @param Model $model Model invalidFields was called on.
+ * @return boolean
+ */
+ public function beforeValidate(Model $model) {
+ unset($this->runtime[$model->alias]['beforeSave']);
+ $this->_setRuntimeData($model);
+ return true;
+ }
+
+/**
+ * beforeSave callback.
+ *
+ * Copies data into the runtime property when `$options['validate']` is
+ * disabled. Or the runtime data hasn't been set yet.
+ *
+ * @param Model $model Model save was called on.
+ * @return boolean true.
+ */
+ public function beforeSave(Model $model, $options = array()) {
+ if (isset($options['validate']) && $options['validate'] == false) {
+ unset($this->runtime[$model->alias]['beforeSave']);
+ }
+ if (isset($this->runtime[$model->alias]['beforeSave'])) {
+ return true;
+ }
+ $this->_setRuntimeData($model);
+ return true;
+ }
+
+/**
+ * Sets the runtime data.
+ *
+ * Used from beforeValidate() and beforeSave() for compatibility issues,
+ * and to allow translations to be persisted even when validation
+ * is disabled.
+ *
+ * @param Model $model
+ * @return void
+ */
+ protected function _setRuntimeData(Model $model) {
+ $locale = $this->_getLocale($model);
+ if (empty($locale)) {
+ return true;
+ }
+ $fields = array_merge($this->settings[$model->alias], $this->runtime[$model->alias]['fields']);
+ $tempData = array();
+
+ foreach ($fields as $key => $value) {
+ $field = (is_numeric($key)) ? $value : $key;
+
+ if (isset($model->data[$model->alias][$field])) {
+ $tempData[$field] = $model->data[$model->alias][$field];
+ if (is_array($model->data[$model->alias][$field])) {
+ if (is_string($locale) && !empty($model->data[$model->alias][$field][$locale])) {
+ $model->data[$model->alias][$field] = $model->data[$model->alias][$field][$locale];
+ } else {
+ $values = array_values($model->data[$model->alias][$field]);
+ $model->data[$model->alias][$field] = $values[0];
+ }
+ }
+ }
+ }
+ $this->runtime[$model->alias]['beforeSave'] = $tempData;
+ }
+
+/**
+ * afterSave Callback
+ *
+ * @param Model $model Model the callback is called on
+ * @param boolean $created Whether or not the save created a record.
+ * @return void
+ */
+ public function afterSave(Model $model, $created) {
+ if (!isset($this->runtime[$model->alias]['beforeValidate']) && !isset($this->runtime[$model->alias]['beforeSave'])) {
+ return true;
+ }
+ $locale = $this->_getLocale($model);
+ if (isset($this->runtime[$model->alias]['beforeValidate'])) {
+ $tempData = $this->runtime[$model->alias]['beforeValidate'];
+ } else {
+ $tempData = $this->runtime[$model->alias]['beforeSave'];
+ }
+
+ unset($this->runtime[$model->alias]['beforeValidate'], $this->runtime[$model->alias]['beforeSave']);
+ $conditions = array('model' => $model->alias, 'foreign_key' => $model->id);
+ $RuntimeModel = $this->translateModel($model);
+
+ $fields = array_merge($this->settings[$model->alias], $this->runtime[$model->alias]['fields']);
+ if ($created) {
+ foreach ($fields as $field) {
+ if (!isset($tempData[$field])) {
+ //set the field value to an empty string
+ $tempData[$field] = '';
+ }
+ }
+ }
+
+ foreach ($tempData as $field => $value) {
+ unset($conditions['content']);
+ $conditions['field'] = $field;
+ if (is_array($value)) {
+ $conditions['locale'] = array_keys($value);
+ } else {
+ $conditions['locale'] = $locale;
+ if (is_array($locale)) {
+ $value = array($locale[0] => $value);
+ } else {
+ $value = array($locale => $value);
+ }
+ }
+ $translations = $RuntimeModel->find('list', array('conditions' => $conditions, 'fields' => array($RuntimeModel->alias . '.locale', $RuntimeModel->alias . '.id')));
+ foreach ($value as $_locale => $_value) {
+ $RuntimeModel->create();
+ $conditions['locale'] = $_locale;
+ $conditions['content'] = $_value;
+ if (array_key_exists($_locale, $translations)) {
+ $RuntimeModel->save(array($RuntimeModel->alias => array_merge($conditions, array('id' => $translations[$_locale]))));
+ } else {
+ $RuntimeModel->save(array($RuntimeModel->alias => $conditions));
+ }
+ }
+ }
+ }
+
+/**
+ * afterDelete Callback
+ *
+ * @param Model $model Model the callback was run on.
+ * @return void
+ */
+ public function afterDelete(Model $model) {
+ $RuntimeModel = $this->translateModel($model);
+ $conditions = array('model' => $model->alias, 'foreign_key' => $model->id);
+ $RuntimeModel->deleteAll($conditions);
+ }
+
+/**
+ * Get selected locale for model
+ *
+ * @param Model $model Model the locale needs to be set/get on.
+ * @return mixed string or false
+ */
+ protected function _getLocale(Model $model) {
+ if (!isset($model->locale) || is_null($model->locale)) {
+ $I18n = I18n::getInstance();
+ $I18n->l10n->get(Configure::read('Config.language'));
+ $model->locale = $I18n->l10n->locale;
+ }
+
+ return $model->locale;
+ }
+
+/**
+ * Get instance of model for translations.
+ *
+ * If the model has a translateModel property set, this will be used as the class
+ * name to find/use. If no translateModel property is found 'I18nModel' will be used.
+ *
+ * @param Model $model Model to get a translatemodel for.
+ * @return Model
+ */
+ public function translateModel(Model $model) {
+ if (!isset($this->runtime[$model->alias]['model'])) {
+ if (!isset($model->translateModel) || empty($model->translateModel)) {
+ $className = 'I18nModel';
+ } else {
+ $className = $model->translateModel;
+ }
+
+ $this->runtime[$model->alias]['model'] = ClassRegistry::init($className, 'Model');
+ }
+ if (!empty($model->translateTable) && $model->translateTable !== $this->runtime[$model->alias]['model']->useTable) {
+ $this->runtime[$model->alias]['model']->setSource($model->translateTable);
+ } elseif (empty($model->translateTable) && empty($model->translateModel)) {
+ $this->runtime[$model->alias]['model']->setSource('i18n');
+ }
+ return $this->runtime[$model->alias]['model'];
+ }
+
+/**
+ * Bind translation for fields, optionally with hasMany association for
+ * fake field.
+ *
+ * *Note* You should avoid binding translations that overlap existing model properties.
+ * This can cause un-expected and un-desirable behavior.
+ *
+ * @param Model $model instance of model
+ * @param string|array $fields string with field or array(field1, field2=>AssocName, field3)
+ * @param boolean $reset Leave true to have the fields only modified for the next operation.
+ * if false the field will be added for all future queries.
+ * @return boolean
+ * @throws CakeException when attempting to bind a translating called name. This is not allowed
+ * as it shadows Model::$name.
+ */
+ public function bindTranslation(Model $model, $fields, $reset = true) {
+ if (is_string($fields)) {
+ $fields = array($fields);
+ }
+ $associations = array();
+ $RuntimeModel = $this->translateModel($model);
+ $default = array('className' => $RuntimeModel->alias, 'foreignKey' => 'foreign_key');
+
+ foreach ($fields as $key => $value) {
+ if (is_numeric($key)) {
+ $field = $value;
+ $association = null;
+ } else {
+ $field = $key;
+ $association = $value;
+ }
+ if ($association === 'name') {
+ throw new CakeException(
+ __d('cake_dev', 'You cannot bind a translation named "name".')
+ );
+ }
+
+ $this->_removeField($model, $field);
+
+ if (is_null($association)) {
+ if ($reset) {
+ $this->runtime[$model->alias]['fields'][] = $field;
+ } else {
+ $this->settings[$model->alias][] = $field;
+ }
+ } else {
+ if ($reset) {
+ $this->runtime[$model->alias]['fields'][$field] = $association;
+ } else {
+ $this->settings[$model->alias][$field] = $association;
+ }
+
+ foreach (array('hasOne', 'hasMany', 'belongsTo', 'hasAndBelongsToMany') as $type) {
+ if (isset($model->{$type}[$association]) || isset($model->__backAssociation[$type][$association])) {
+ trigger_error(
+ __d('cake_dev', 'Association %s is already bound to model %s', $association, $model->alias),
+ E_USER_ERROR
+ );
+ return false;
+ }
+ }
+ $associations[$association] = array_merge($default, array('conditions' => array(
+ 'model' => $model->alias,
+ $RuntimeModel->displayField => $field
+ )));
+ }
+ }
+
+ if (!empty($associations)) {
+ $model->bindModel(array('hasMany' => $associations), $reset);
+ }
+ return true;
+ }
+
+/**
+ * Update runtime setting for a given field.
+ *
+ * @param string $field The field to update.
+ */
+ protected function _removeField(Model $model, $field) {
+ if (array_key_exists($field, $this->settings[$model->alias])) {
+ unset($this->settings[$model->alias][$field]);
+ } elseif (in_array($field, $this->settings[$model->alias])) {
+ $this->settings[$model->alias] = array_merge(array_diff($this->settings[$model->alias], array($field)));
+ }
+
+ if (array_key_exists($field, $this->runtime[$model->alias]['fields'])) {
+ unset($this->runtime[$model->alias]['fields'][$field]);
+ } elseif (in_array($field, $this->runtime[$model->alias]['fields'])) {
+ $this->runtime[$model->alias]['fields'] = array_merge(array_diff($this->runtime[$model->alias]['fields'], array($field)));
+ }
+ }
+
+/**
+ * Unbind translation for fields, optionally unbinds hasMany association for
+ * fake field
+ *
+ * @param Model $model instance of model
+ * @param string|array $fields string with field, or array(field1, field2=>AssocName, field3), or null for
+ * unbind all original translations
+ * @return boolean
+ */
+ public function unbindTranslation(Model $model, $fields = null) {
+ if (empty($fields) && empty($this->settings[$model->alias])) {
+ return false;
+ }
+ if (empty($fields)) {
+ return $this->unbindTranslation($model, $this->settings[$model->alias]);
+ }
+
+ if (is_string($fields)) {
+ $fields = array($fields);
+ }
+ $RuntimeModel = $this->translateModel($model);
+ $associations = array();
+
+ foreach ($fields as $key => $value) {
+ if (is_numeric($key)) {
+ $field = $value;
+ $association = null;
+ } else {
+ $field = $key;
+ $association = $value;
+ }
+
+ $this->_removeField($model, $field);
+
+ if (!is_null($association) && (isset($model->hasMany[$association]) || isset($model->__backAssociation['hasMany'][$association]))) {
+ $associations[] = $association;
+ }
+ }
+
+ if (!empty($associations)) {
+ $model->unbindModel(array('hasMany' => $associations), false);
+ }
+ return true;
+ }
+
+}
+
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TreeBehavior.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TreeBehavior.php
new file mode 100644
index 0000000..a92c367
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Behavior/TreeBehavior.php
@@ -0,0 +1,1007 @@
+<?php
+/**
+ * Tree behavior class.
+ *
+ * Enables a model object to act as a node-based tree.
+ *
+ * PHP 5
+ *
+ * CakePHP : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc.
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP Project
+ * @package Cake.Model.Behavior
+ * @since CakePHP v 1.2.0.4487
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+/**
+ * Tree Behavior.
+ *
+ * Enables a model object to act as a node-based tree. Using Modified Preorder Tree Traversal
+ *
+ * @see http://en.wikipedia.org/wiki/Tree_traversal
+ * @package Cake.Model.Behavior
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html
+ */
+class TreeBehavior extends ModelBehavior {
+
+/**
+ * Errors
+ *
+ * @var array
+ */
+ public $errors = array();
+
+/**
+ * Defaults
+ *
+ * @var array
+ */
+ protected $_defaults = array(
+ 'parent' => 'parent_id', 'left' => 'lft', 'right' => 'rght',
+ 'scope' => '1 = 1', 'type' => 'nested', '__parentChange' => false, 'recursive' => -1
+ );
+
+/**
+ * Used to preserve state between delete callbacks.
+ *
+ * @var array
+ */
+ protected $_deletedRow = null;
+
+/**
+ * Initiate Tree behavior
+ *
+ * @param Model $Model instance of model
+ * @param array $config array of configuration settings.
+ * @return void
+ */
+ public function setup(Model $Model, $config = array()) {
+ if (isset($config[0])) {
+ $config['type'] = $config[0];
+ unset($config[0]);
+ }
+ $settings = array_merge($this->_defaults, $config);
+
+ if (in_array($settings['scope'], $Model->getAssociated('belongsTo'))) {
+ $data = $Model->getAssociated($settings['scope']);
+ $parent = $Model->{$settings['scope']};
+ $settings['scope'] = $Model->alias . '.' . $data['foreignKey'] . ' = ' . $parent->alias . '.' . $parent->primaryKey;
+ $settings['recursive'] = 0;
+ }
+ $this->settings[$Model->alias] = $settings;
+ }
+
+/**
+ * After save method. Called after all saves
+ *
+ * Overridden to transparently manage setting the lft and rght fields if and only if the parent field is included in the
+ * parameters to be saved.
+ *
+ * @param Model $Model Model instance.
+ * @param boolean $created indicates whether the node just saved was created or updated
+ * @return boolean true on success, false on failure
+ */
+ public function afterSave(Model $Model, $created) {
+ extract($this->settings[$Model->alias]);
+ if ($created) {
+ if ((isset($Model->data[$Model->alias][$parent])) && $Model->data[$Model->alias][$parent]) {
+ return $this->_setParent($Model, $Model->data[$Model->alias][$parent], $created);
+ }
+ } elseif ($this->settings[$Model->alias]['__parentChange']) {
+ $this->settings[$Model->alias]['__parentChange'] = false;
+ return $this->_setParent($Model, $Model->data[$Model->alias][$parent]);
+ }
+ }
+
+/**
+ * Runs before a find() operation
+ *
+ * @param Model $Model Model using the behavior
+ * @param array $query Query parameters as set by cake
+ * @return array
+ */
+ public function beforeFind(Model $Model, $query) {
+ if ($Model->findQueryType == 'threaded' && !isset($query['parent'])) {
+ $query['parent'] = $this->settings[$Model->alias]['parent'];
+ }
+ return $query;
+ }
+
+/**
+ * Stores the record about to be deleted.
+ *
+ * This is used to delete child nodes in the afterDelete.
+ *
+ * @param Model $Model Model instance
+ * @param boolean $cascade
+ * @return boolean
+ */
+ public function beforeDelete(Model $Model, $cascade = true) {
+ extract($this->settings[$Model->alias]);
+ $data = $Model->find('first', array(
+ 'conditions' => array($Model->alias . '.' . $Model->primaryKey => $Model->id),
+ 'fields' => array($Model->alias . '.' . $left, $Model->alias . '.' . $right),
+ 'recursive' => -1));
+ if ($data) {
+ $this->_deletedRow = current($data);
+ }
+ return true;
+ }
+
+/**
+ * After delete method.
+ *
+ * Will delete the current node and all children using the deleteAll method and sync the table
+ *
+ * @param Model $Model Model instance
+ * @return boolean true to continue, false to abort the delete
+ */
+ public function afterDelete(Model $Model) {
+ extract($this->settings[$Model->alias]);
+ $data = $this->_deletedRow;
+ $this->_deletedRow = null;
+
+ if (!$data[$right] || !$data[$left]) {
+ return true;
+ }
+ $diff = $data[$right] - $data[$left] + 1;
+
+ if ($diff > 2) {
+ if (is_string($scope)) {
+ $scope = array($scope);
+ }
+ $scope[]["{$Model->alias}.{$left} BETWEEN ? AND ?"] = array($data[$left] + 1, $data[$right] - 1);
+ $Model->deleteAll($scope);
+ }
+ $this->_sync($Model, $diff, '-', '> ' . $data[$right]);
+ return true;
+ }
+
+/**
+ * Before save method. Called before all saves
+ *
+ * Overridden to transparently manage setting the lft and rght fields if and only if the parent field is included in the
+ * parameters to be saved. For newly created nodes with NO parent the left and right field values are set directly by
+ * this method bypassing the setParent logic.
+ *
+ * @since 1.2
+ * @param Model $Model Model instance
+ * @return boolean true to continue, false to abort the save
+ */
+ public function beforeSave(Model $Model) {
+ extract($this->settings[$Model->alias]);
+
+ $this->_addToWhitelist($Model, array($left, $right));
+ if (!$Model->id) {
+ if (array_key_exists($parent, $Model->data[$Model->alias]) && $Model->data[$Model->alias][$parent]) {
+ $parentNode = $Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $Model->data[$Model->alias][$parent]),
+ 'fields' => array($Model->primaryKey, $right), 'recursive' => $recursive
+ ));
+ if (!$parentNode) {
+ return false;
+ }
+ list($parentNode) = array_values($parentNode);
+ $Model->data[$Model->alias][$left] = 0;
+ $Model->data[$Model->alias][$right] = 0;
+ } else {
+ $edge = $this->_getMax($Model, $scope, $right, $recursive);
+ $Model->data[$Model->alias][$left] = $edge + 1;
+ $Model->data[$Model->alias][$right] = $edge + 2;
+ }
+ } elseif (array_key_exists($parent, $Model->data[$Model->alias])) {
+ if ($Model->data[$Model->alias][$parent] != $Model->field($parent)) {
+ $this->settings[$Model->alias]['__parentChange'] = true;
+ }
+ if (!$Model->data[$Model->alias][$parent]) {
+ $Model->data[$Model->alias][$parent] = null;
+ $this->_addToWhitelist($Model, $parent);
+ } else {
+ $values = $Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $Model->id),
+ 'fields' => array($Model->primaryKey, $parent, $left, $right), 'recursive' => $recursive)
+ );
+
+ if ($values === false) {
+ return false;
+ }
+ list($node) = array_values($values);
+
+ $parentNode = $Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $Model->data[$Model->alias][$parent]),
+ 'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive
+ ));
+ if (!$parentNode) {
+ return false;
+ }
+ list($parentNode) = array_values($parentNode);
+
+ if (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) {
+ return false;
+ } elseif ($node[$Model->primaryKey] == $parentNode[$Model->primaryKey]) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+/**
+ * Get the number of child nodes
+ *
+ * If the direct parameter is set to true, only the direct children are counted (based upon the parent_id field)
+ * If false is passed for the id parameter, all top level nodes are counted, or all nodes are counted.
+ *
+ * @param Model $Model Model instance
+ * @param integer|string|boolean $id The ID of the record to read or false to read all top level nodes
+ * @param boolean $direct whether to count direct, or all, children
+ * @return integer number of child nodes
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::childCount
+ */
+ public function childCount(Model $Model, $id = null, $direct = false) {
+ if (is_array($id)) {
+ extract(array_merge(array('id' => null), $id));
+ }
+ if ($id === null && $Model->id) {
+ $id = $Model->id;
+ } elseif (!$id) {
+ $id = null;
+ }
+ extract($this->settings[$Model->alias]);
+
+ if ($direct) {
+ return $Model->find('count', array('conditions' => array($scope, $Model->escapeField($parent) => $id)));
+ }
+
+ if ($id === null) {
+ return $Model->find('count', array('conditions' => $scope));
+ } elseif ($Model->id === $id && isset($Model->data[$Model->alias][$left]) && isset($Model->data[$Model->alias][$right])) {
+ $data = $Model->data[$Model->alias];
+ } else {
+ $data = $Model->find('first', array('conditions' => array($scope, $Model->escapeField() => $id), 'recursive' => $recursive));
+ if (!$data) {
+ return 0;
+ }
+ $data = $data[$Model->alias];
+ }
+ return ($data[$right] - $data[$left] - 1) / 2;
+ }
+
+/**
+ * Get the child nodes of the current model
+ *
+ * If the direct parameter is set to true, only the direct children are returned (based upon the parent_id field)
+ * If false is passed for the id parameter, top level, or all (depending on direct parameter appropriate) are counted.
+ *
+ * @param Model $Model Model instance
+ * @param integer|string $id The ID of the record to read
+ * @param boolean $direct whether to return only the direct, or all, children
+ * @param string|array $fields Either a single string of a field name, or an array of field names
+ * @param string $order SQL ORDER BY conditions (e.g. "price DESC" or "name ASC") defaults to the tree order
+ * @param integer $limit SQL LIMIT clause, for calculating items per page.
+ * @param integer $page Page number, for accessing paged data
+ * @param integer $recursive The number of levels deep to fetch associated records
+ * @return array Array of child nodes
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::children
+ */
+ public function children(Model $Model, $id = null, $direct = false, $fields = null, $order = null, $limit = null, $page = 1, $recursive = null) {
+ if (is_array($id)) {
+ extract(array_merge(array('id' => null), $id));
+ }
+ $overrideRecursive = $recursive;
+
+ if ($id === null && $Model->id) {
+ $id = $Model->id;
+ } elseif (!$id) {
+ $id = null;
+ }
+
+ extract($this->settings[$Model->alias]);
+
+ if (!is_null($overrideRecursive)) {
+ $recursive = $overrideRecursive;
+ }
+ if (!$order) {
+ $order = $Model->alias . '.' . $left . ' asc';
+ }
+ if ($direct) {
+ $conditions = array($scope, $Model->escapeField($parent) => $id);
+ return $Model->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive'));
+ }
+
+ if (!$id) {
+ $conditions = $scope;
+ } else {
+ $result = array_values((array)$Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $id),
+ 'fields' => array($left, $right),
+ 'recursive' => $recursive
+ )));
+
+ if (empty($result) || !isset($result[0])) {
+ return array();
+ }
+ $conditions = array($scope,
+ $Model->escapeField($right) . ' <' => $result[0][$right],
+ $Model->escapeField($left) . ' >' => $result[0][$left]
+ );
+ }
+ return $Model->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive'));
+ }
+
+/**
+ * A convenience method for returning a hierarchical array used for HTML select boxes
+ *
+ * @param Model $Model Model instance
+ * @param string|array $conditions SQL conditions as a string or as an array('field' =>'value',...)
+ * @param string $keyPath A string path to the key, i.e. "{n}.Post.id"
+ * @param string $valuePath A string path to the value, i.e. "{n}.Post.title"
+ * @param string $spacer The character or characters which will be repeated
+ * @param integer $recursive The number of levels deep to fetch associated records
+ * @return array An associative array of records, where the id is the key, and the display field is the value
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::generateTreeList
+ */
+ public function generateTreeList(Model $Model, $conditions = null, $keyPath = null, $valuePath = null, $spacer = '_', $recursive = null) {
+ $overrideRecursive = $recursive;
+ extract($this->settings[$Model->alias]);
+ if (!is_null($overrideRecursive)) {
+ $recursive = $overrideRecursive;
+ }
+
+ if ($keyPath == null && $valuePath == null && $Model->hasField($Model->displayField)) {
+ $fields = array($Model->primaryKey, $Model->displayField, $left, $right);
+ } else {
+ $fields = null;
+ }
+
+ if ($keyPath == null) {
+ $keyPath = '{n}.' . $Model->alias . '.' . $Model->primaryKey;
+ }
+
+ if ($valuePath == null) {
+ $valuePath = array('%s%s', '{n}.tree_prefix', '{n}.' . $Model->alias . '.' . $Model->displayField);
+
+ } elseif (is_string($valuePath)) {
+ $valuePath = array('%s%s', '{n}.tree_prefix', $valuePath);
+
+ } else {
+ $valuePath[0] = '{' . (count($valuePath) - 1) . '}' . $valuePath[0];
+ $valuePath[] = '{n}.tree_prefix';
+ }
+ $order = $Model->alias . '.' . $left . ' asc';
+ $results = $Model->find('all', compact('conditions', 'fields', 'order', 'recursive'));
+ $stack = array();
+
+ foreach ($results as $i => $result) {
+ $count = count($stack);
+ while ($stack && ($stack[$count - 1] < $result[$Model->alias][$right])) {
+ array_pop($stack);
+ $count--;
+ }
+ $results[$i]['tree_prefix'] = str_repeat($spacer, $count);
+ $stack[] = $result[$Model->alias][$right];
+ }
+ if (empty($results)) {
+ return array();
+ }
+ return Hash::combine($results, $keyPath, $valuePath);
+ }
+
+/**
+ * Get the parent node
+ *
+ * reads the parent id and returns this node
+ *
+ * @param Model $Model Model instance
+ * @param integer|string $id The ID of the record to read
+ * @param string|array $fields
+ * @param integer $recursive The number of levels deep to fetch associated records
+ * @return array|boolean Array of data for the parent node
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getParentNode
+ */
+ public function getParentNode(Model $Model, $id = null, $fields = null, $recursive = null) {
+ if (is_array($id)) {
+ extract(array_merge(array('id' => null), $id));
+ }
+ $overrideRecursive = $recursive;
+ if (empty ($id)) {
+ $id = $Model->id;
+ }
+ extract($this->settings[$Model->alias]);
+ if (!is_null($overrideRecursive)) {
+ $recursive = $overrideRecursive;
+ }
+ $parentId = $Model->find('first', array('conditions' => array($Model->primaryKey => $id), 'fields' => array($parent), 'recursive' => -1));
+
+ if ($parentId) {
+ $parentId = $parentId[$Model->alias][$parent];
+ $parent = $Model->find('first', array('conditions' => array($Model->escapeField() => $parentId), 'fields' => $fields, 'recursive' => $recursive));
+
+ return $parent;
+ }
+ return false;
+ }
+
+/**
+ * Get the path to the given node
+ *
+ * @param Model $Model Model instance
+ * @param integer|string $id The ID of the record to read
+ * @param string|array $fields Either a single string of a field name, or an array of field names
+ * @param integer $recursive The number of levels deep to fetch associated records
+ * @return array Array of nodes from top most parent to current node
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::getPath
+ */
+ public function getPath(Model $Model, $id = null, $fields = null, $recursive = null) {
+ if (is_array($id)) {
+ extract(array_merge(array('id' => null), $id));
+ }
+ $overrideRecursive = $recursive;
+ if (empty ($id)) {
+ $id = $Model->id;
+ }
+ extract($this->settings[$Model->alias]);
+ if (!is_null($overrideRecursive)) {
+ $recursive = $overrideRecursive;
+ }
+ $result = $Model->find('first', array('conditions' => array($Model->escapeField() => $id), 'fields' => array($left, $right), 'recursive' => $recursive));
+ if ($result) {
+ $result = array_values($result);
+ } else {
+ return null;
+ }
+ $item = $result[0];
+ $results = $Model->find('all', array(
+ 'conditions' => array($scope, $Model->escapeField($left) . ' <=' => $item[$left], $Model->escapeField($right) . ' >=' => $item[$right]),
+ 'fields' => $fields, 'order' => array($Model->escapeField($left) => 'asc'), 'recursive' => $recursive
+ ));
+ return $results;
+ }
+
+/**
+ * Reorder the node without changing the parent.
+ *
+ * If the node is the last child, or is a top level node with no subsequent node this method will return false
+ *
+ * @param Model $Model Model instance
+ * @param integer|string $id The ID of the record to move
+ * @param integer|boolean $number how many places to move the node or true to move to last position
+ * @return boolean true on success, false on failure
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::moveDown
+ */
+ public function moveDown(Model $Model, $id = null, $number = 1) {
+ if (is_array($id)) {
+ extract(array_merge(array('id' => null), $id));
+ }
+ if (!$number) {
+ return false;
+ }
+ if (empty ($id)) {
+ $id = $Model->id;
+ }
+ extract($this->settings[$Model->alias]);
+ list($node) = array_values($Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $id),
+ 'fields' => array($Model->primaryKey, $left, $right, $parent), 'recursive' => $recursive
+ )));
+ if ($node[$parent]) {
+ list($parentNode) = array_values($Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $node[$parent]),
+ 'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive
+ )));
+ if (($node[$right] + 1) == $parentNode[$right]) {
+ return false;
+ }
+ }
+ $nextNode = $Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField($left) => ($node[$right] + 1)),
+ 'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive)
+ );
+ if ($nextNode) {
+ list($nextNode) = array_values($nextNode);
+ } else {
+ return false;
+ }
+ $edge = $this->_getMax($Model, $scope, $right, $recursive);
+ $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]);
+ $this->_sync($Model, $nextNode[$left] - $node[$left], '-', 'BETWEEN ' . $nextNode[$left] . ' AND ' . $nextNode[$right]);
+ $this->_sync($Model, $edge - $node[$left] - ($nextNode[$right] - $nextNode[$left]), '-', '> ' . $edge);
+
+ if (is_int($number)) {
+ $number--;
+ }
+ if ($number) {
+ $this->moveDown($Model, $id, $number);
+ }
+ return true;
+ }
+
+/**
+ * Reorder the node without changing the parent.
+ *
+ * If the node is the first child, or is a top level node with no previous node this method will return false
+ *
+ * @param Model $Model Model instance
+ * @param integer|string $id The ID of the record to move
+ * @param integer|boolean $number how many places to move the node, or true to move to first position
+ * @return boolean true on success, false on failure
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::moveUp
+ */
+ public function moveUp(Model $Model, $id = null, $number = 1) {
+ if (is_array($id)) {
+ extract(array_merge(array('id' => null), $id));
+ }
+ if (!$number) {
+ return false;
+ }
+ if (empty ($id)) {
+ $id = $Model->id;
+ }
+ extract($this->settings[$Model->alias]);
+ list($node) = array_values($Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $id),
+ 'fields' => array($Model->primaryKey, $left, $right, $parent), 'recursive' => $recursive
+ )));
+ if ($node[$parent]) {
+ list($parentNode) = array_values($Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $node[$parent]),
+ 'fields' => array($Model->primaryKey, $left, $right), 'recursive' => $recursive
+ )));
+ if (($node[$left] - 1) == $parentNode[$left]) {
+ return false;
+ }
+ }
+ $previousNode = $Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField($right) => ($node[$left] - 1)),
+ 'fields' => array($Model->primaryKey, $left, $right),
+ 'recursive' => $recursive
+ ));
+
+ if ($previousNode) {
+ list($previousNode) = array_values($previousNode);
+ } else {
+ return false;
+ }
+ $edge = $this->_getMax($Model, $scope, $right, $recursive);
+ $this->_sync($Model, $edge - $previousNode[$left] + 1, '+', 'BETWEEN ' . $previousNode[$left] . ' AND ' . $previousNode[$right]);
+ $this->_sync($Model, $node[$left] - $previousNode[$left], '-', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right]);
+ $this->_sync($Model, $edge - $previousNode[$left] - ($node[$right] - $node[$left]), '-', '> ' . $edge);
+ if (is_int($number)) {
+ $number--;
+ }
+ if ($number) {
+ $this->moveUp($Model, $id, $number);
+ }
+ return true;
+ }
+
+/**
+ * Recover a corrupted tree
+ *
+ * The mode parameter is used to specify the source of info that is valid/correct. The opposite source of data
+ * will be populated based upon that source of info. E.g. if the MPTT fields are corrupt or empty, with the $mode
+ * 'parent' the values of the parent_id field will be used to populate the left and right fields. The missingParentAction
+ * parameter only applies to "parent" mode and determines what to do if the parent field contains an id that is not present.
+ *
+ * @todo Could be written to be faster, *maybe*. Ideally using a subquery and putting all the logic burden on the DB.
+ * @param Model $Model Model instance
+ * @param string $mode parent or tree
+ * @param string|integer $missingParentAction 'return' to do nothing and return, 'delete' to
+ * delete, or the id of the parent to set as the parent_id
+ * @return boolean true on success, false on failure
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::recover
+ */
+ public function recover(Model $Model, $mode = 'parent', $missingParentAction = null) {
+ if (is_array($mode)) {
+ extract(array_merge(array('mode' => 'parent'), $mode));
+ }
+ extract($this->settings[$Model->alias]);
+ $Model->recursive = $recursive;
+ if ($mode == 'parent') {
+ $Model->bindModel(array('belongsTo' => array('VerifyParent' => array(
+ 'className' => $Model->name,
+ 'foreignKey' => $parent,
+ 'fields' => array($Model->primaryKey, $left, $right, $parent),
+ ))));
+ $missingParents = $Model->find('list', array(
+ 'recursive' => 0,
+ 'conditions' => array($scope, array(
+ 'NOT' => array($Model->escapeField($parent) => null), $Model->VerifyParent->escapeField() => null
+ ))
+ ));
+ $Model->unbindModel(array('belongsTo' => array('VerifyParent')));
+ if ($missingParents) {
+ if ($missingParentAction == 'return') {
+ foreach ($missingParents as $id => $display) {
+ $this->errors[] = 'cannot find the parent for ' . $Model->alias . ' with id ' . $id . '(' . $display . ')';
+ }
+ return false;
+ } elseif ($missingParentAction == 'delete') {
+ $Model->deleteAll(array($Model->primaryKey => array_flip($missingParents)));
+ } else {
+ $Model->updateAll(array($parent => $missingParentAction), array($Model->escapeField($Model->primaryKey) => array_flip($missingParents)));
+ }
+ }
+ $count = 1;
+ foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey), 'order' => $left)) as $array) {
+ $lft = $count++;
+ $rght = $count++;
+ $Model->create(false);
+ $Model->id = $array[$Model->alias][$Model->primaryKey];
+ $Model->save(array($left => $lft, $right => $rght), array('callbacks' => false, 'validate' => false));
+ }
+ foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey, $parent), 'order' => $left)) as $array) {
+ $Model->create(false);
+ $Model->id = $array[$Model->alias][$Model->primaryKey];
+ $this->_setParent($Model, $array[$Model->alias][$parent]);
+ }
+ } else {
+ $db = ConnectionManager::getDataSource($Model->useDbConfig);
+ foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey, $parent), 'order' => $left)) as $array) {
+ $path = $this->getPath($Model, $array[$Model->alias][$Model->primaryKey]);
+ if ($path == null || count($path) < 2) {
+ $parentId = null;
+ } else {
+ $parentId = $path[count($path) - 2][$Model->alias][$Model->primaryKey];
+ }
+ $Model->updateAll(array($parent => $db->value($parentId, $parent)), array($Model->escapeField() => $array[$Model->alias][$Model->primaryKey]));
+ }
+ }
+ return true;
+ }
+
+/**
+ * Reorder method.
+ *
+ * Reorders the nodes (and child nodes) of the tree according to the field and direction specified in the parameters.
+ * This method does not change the parent of any node.
+ *
+ * Requires a valid tree, by default it verifies the tree before beginning.
+ *
+ * Options:
+ *
+ * - 'id' id of record to use as top node for reordering
+ * - 'field' Which field to use in reordering defaults to displayField
+ * - 'order' Direction to order either DESC or ASC (defaults to ASC)
+ * - 'verify' Whether or not to verify the tree before reorder. defaults to true.
+ *
+ * @param Model $Model Model instance
+ * @param array $options array of options to use in reordering.
+ * @return boolean true on success, false on failure
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::reorder
+ */
+ public function reorder(Model $Model, $options = array()) {
+ $options = array_merge(array('id' => null, 'field' => $Model->displayField, 'order' => 'ASC', 'verify' => true), $options);
+ extract($options);
+ if ($verify && !$this->verify($Model)) {
+ return false;
+ }
+ $verify = false;
+ extract($this->settings[$Model->alias]);
+ $fields = array($Model->primaryKey, $field, $left, $right);
+ $sort = $field . ' ' . $order;
+ $nodes = $this->children($Model, $id, true, $fields, $sort, null, null, $recursive);
+
+ $cacheQueries = $Model->cacheQueries;
+ $Model->cacheQueries = false;
+ if ($nodes) {
+ foreach ($nodes as $node) {
+ $id = $node[$Model->alias][$Model->primaryKey];
+ $this->moveDown($Model, $id, true);
+ if ($node[$Model->alias][$left] != $node[$Model->alias][$right] - 1) {
+ $this->reorder($Model, compact('id', 'field', 'order', 'verify'));
+ }
+ }
+ }
+ $Model->cacheQueries = $cacheQueries;
+ return true;
+ }
+
+/**
+ * Remove the current node from the tree, and reparent all children up one level.
+ *
+ * If the parameter delete is false, the node will become a new top level node. Otherwise the node will be deleted
+ * after the children are reparented.
+ *
+ * @param Model $Model Model instance
+ * @param integer|string $id The ID of the record to remove
+ * @param boolean $delete whether to delete the node after reparenting children (if any)
+ * @return boolean true on success, false on failure
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::removeFromTree
+ */
+ public function removeFromTree(Model $Model, $id = null, $delete = false) {
+ if (is_array($id)) {
+ extract(array_merge(array('id' => null), $id));
+ }
+ extract($this->settings[$Model->alias]);
+
+ list($node) = array_values($Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $id),
+ 'fields' => array($Model->primaryKey, $left, $right, $parent),
+ 'recursive' => $recursive
+ )));
+
+ if ($node[$right] == $node[$left] + 1) {
+ if ($delete) {
+ return $Model->delete($id);
+ } else {
+ $Model->id = $id;
+ return $Model->saveField($parent, null);
+ }
+ } elseif ($node[$parent]) {
+ list($parentNode) = array_values($Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $node[$parent]),
+ 'fields' => array($Model->primaryKey, $left, $right),
+ 'recursive' => $recursive
+ )));
+ } else {
+ $parentNode[$right] = $node[$right] + 1;
+ }
+
+ $db = ConnectionManager::getDataSource($Model->useDbConfig);
+ $Model->updateAll(
+ array($parent => $db->value($node[$parent], $parent)),
+ array($Model->escapeField($parent) => $node[$Model->primaryKey])
+ );
+ $this->_sync($Model, 1, '-', 'BETWEEN ' . ($node[$left] + 1) . ' AND ' . ($node[$right] - 1));
+ $this->_sync($Model, 2, '-', '> ' . ($node[$right]));
+ $Model->id = $id;
+
+ if ($delete) {
+ $Model->updateAll(
+ array(
+ $Model->escapeField($left) => 0,
+ $Model->escapeField($right) => 0,
+ $Model->escapeField($parent) => null
+ ),
+ array($Model->escapeField() => $id)
+ );
+ return $Model->delete($id);
+ } else {
+ $edge = $this->_getMax($Model, $scope, $right, $recursive);
+ if ($node[$right] == $edge) {
+ $edge = $edge - 2;
+ }
+ $Model->id = $id;
+ return $Model->save(
+ array($left => $edge + 1, $right => $edge + 2, $parent => null),
+ array('callbacks' => false, 'validate' => false)
+ );
+ }
+ }
+
+/**
+ * Check if the current tree is valid.
+ *
+ * Returns true if the tree is valid otherwise an array of (type, incorrect left/right index, message)
+ *
+ * @param Model $Model Model instance
+ * @return mixed true if the tree is valid or empty, otherwise an array of (error type [index, node],
+ * [incorrect left/right index,node id], message)
+ * @link http://book.cakephp.org/2.0/en/core-libraries/behaviors/tree.html#TreeBehavior::verify
+ */
+ public function verify(Model $Model) {
+ extract($this->settings[$Model->alias]);
+ if (!$Model->find('count', array('conditions' => $scope))) {
+ return true;
+ }
+ $min = $this->_getMin($Model, $scope, $left, $recursive);
+ $edge = $this->_getMax($Model, $scope, $right, $recursive);
+ $errors = array();
+
+ for ($i = $min; $i <= $edge; $i++) {
+ $count = $Model->find('count', array('conditions' => array(
+ $scope, 'OR' => array($Model->escapeField($left) => $i, $Model->escapeField($right) => $i)
+ )));
+ if ($count != 1) {
+ if ($count == 0) {
+ $errors[] = array('index', $i, 'missing');
+ } else {
+ $errors[] = array('index', $i, 'duplicate');
+ }
+ }
+ }
+ $node = $Model->find('first', array('conditions' => array($scope, $Model->escapeField($right) . '< ' . $Model->escapeField($left)), 'recursive' => 0));
+ if ($node) {
+ $errors[] = array('node', $node[$Model->alias][$Model->primaryKey], 'left greater than right.');
+ }
+
+ $Model->bindModel(array('belongsTo' => array('VerifyParent' => array(
+ 'className' => $Model->name,
+ 'foreignKey' => $parent,
+ 'fields' => array($Model->primaryKey, $left, $right, $parent)
+ ))));
+
+ foreach ($Model->find('all', array('conditions' => $scope, 'recursive' => 0)) as $instance) {
+ if (is_null($instance[$Model->alias][$left]) || is_null($instance[$Model->alias][$right])) {
+ $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey],
+ 'has invalid left or right values');
+ } elseif ($instance[$Model->alias][$left] == $instance[$Model->alias][$right]) {
+ $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey],
+ 'left and right values identical');
+ } elseif ($instance[$Model->alias][$parent]) {
+ if (!$instance['VerifyParent'][$Model->primaryKey]) {
+ $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey],
+ 'The parent node ' . $instance[$Model->alias][$parent] . ' doesn\'t exist');
+ } elseif ($instance[$Model->alias][$left] < $instance['VerifyParent'][$left]) {
+ $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey],
+ 'left less than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').');
+ } elseif ($instance[$Model->alias][$right] > $instance['VerifyParent'][$right]) {
+ $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey],
+ 'right greater than parent (node ' . $instance['VerifyParent'][$Model->primaryKey] . ').');
+ }
+ } elseif ($Model->find('count', array('conditions' => array($scope, $Model->escapeField($left) . ' <' => $instance[$Model->alias][$left], $Model->escapeField($right) . ' >' => $instance[$Model->alias][$right]), 'recursive' => 0))) {
+ $errors[] = array('node', $instance[$Model->alias][$Model->primaryKey], 'The parent field is blank, but has a parent');
+ }
+ }
+ if ($errors) {
+ return $errors;
+ }
+ return true;
+ }
+
+/**
+ * Sets the parent of the given node
+ *
+ * The force parameter is used to override the "don't change the parent to the current parent" logic in the event
+ * of recovering a corrupted table, or creating new nodes. Otherwise it should always be false. In reality this
+ * method could be private, since calling save with parent_id set also calls setParent
+ *
+ * @param Model $Model Model instance
+ * @param integer|string $parentId
+ * @param boolean $created
+ * @return boolean true on success, false on failure
+ */
+ protected function _setParent(Model $Model, $parentId = null, $created = false) {
+ extract($this->settings[$Model->alias]);
+ list($node) = array_values($Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $Model->id),
+ 'fields' => array($Model->primaryKey, $parent, $left, $right),
+ 'recursive' => $recursive
+ )));
+ $edge = $this->_getMax($Model, $scope, $right, $recursive, $created);
+
+ if (empty ($parentId)) {
+ $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created);
+ $this->_sync($Model, $node[$right] - $node[$left] + 1, '-', '> ' . $node[$left], $created);
+ } else {
+ $values = $Model->find('first', array(
+ 'conditions' => array($scope, $Model->escapeField() => $parentId),
+ 'fields' => array($Model->primaryKey, $left, $right),
+ 'recursive' => $recursive
+ ));
+
+ if ($values === false) {
+ return false;
+ }
+ $parentNode = array_values($values);
+
+ if (empty($parentNode) || empty($parentNode[0])) {
+ return false;
+ }
+ $parentNode = $parentNode[0];
+
+ if (($Model->id == $parentId)) {
+ return false;
+ } elseif (($node[$left] < $parentNode[$left]) && ($parentNode[$right] < $node[$right])) {
+ return false;
+ }
+ if (empty($node[$left]) && empty($node[$right])) {
+ $this->_sync($Model, 2, '+', '>= ' . $parentNode[$right], $created);
+ $result = $Model->save(
+ array($left => $parentNode[$right], $right => $parentNode[$right] + 1, $parent => $parentId),
+ array('validate' => false, 'callbacks' => false)
+ );
+ $Model->data = $result;
+ } else {
+ $this->_sync($Model, $edge - $node[$left] + 1, '+', 'BETWEEN ' . $node[$left] . ' AND ' . $node[$right], $created);
+ $diff = $node[$right] - $node[$left] + 1;
+
+ if ($node[$left] > $parentNode[$left]) {
+ if ($node[$right] < $parentNode[$right]) {
+ $this->_sync($Model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created);
+ $this->_sync($Model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created);
+ } else {
+ $this->_sync($Model, $diff, '+', 'BETWEEN ' . $parentNode[$right] . ' AND ' . $node[$right], $created);
+ $this->_sync($Model, $edge - $parentNode[$right] + 1, '-', '> ' . $edge, $created);
+ }
+ } else {
+ $this->_sync($Model, $diff, '-', 'BETWEEN ' . $node[$right] . ' AND ' . ($parentNode[$right] - 1), $created);
+ $this->_sync($Model, $edge - $parentNode[$right] + $diff + 1, '-', '> ' . $edge, $created);
+ }
+ }
+ }
+ return true;
+ }
+
+/**
+ * get the maximum index value in the table.
+ *
+ * @param Model $Model
+ * @param string $scope
+ * @param string $right
+ * @param integer $recursive
+ * @param boolean $created
+ * @return integer
+ */
+ protected function _getMax(Model $Model, $scope, $right, $recursive = -1, $created = false) {
+ $db = ConnectionManager::getDataSource($Model->useDbConfig);
+ if ($created) {
+ if (is_string($scope)) {
+ $scope .= " AND {$Model->alias}.{$Model->primaryKey} <> ";
+ $scope .= $db->value($Model->id, $Model->getColumnType($Model->primaryKey));
+ } else {
+ $scope['NOT'][$Model->alias . '.' . $Model->primaryKey] = $Model->id;
+ }
+ }
+ $name = $Model->alias . '.' . $right;
+ list($edge) = array_values($Model->find('first', array(
+ 'conditions' => $scope,
+ 'fields' => $db->calculate($Model, 'max', array($name, $right)),
+ 'recursive' => $recursive
+ )));
+ return (empty($edge[$right])) ? 0 : $edge[$right];
+ }
+
+/**
+ * get the minimum index value in the table.
+ *
+ * @param Model $Model
+ * @param string $scope
+ * @param string $left
+ * @param integer $recursive
+ * @return integer
+ */
+ protected function _getMin(Model $Model, $scope, $left, $recursive = -1) {
+ $db = ConnectionManager::getDataSource($Model->useDbConfig);
+ $name = $Model->alias . '.' . $left;
+ list($edge) = array_values($Model->find('first', array(
+ 'conditions' => $scope,
+ 'fields' => $db->calculate($Model, 'min', array($name, $left)),
+ 'recursive' => $recursive
+ )));
+ return (empty($edge[$left])) ? 0 : $edge[$left];
+ }
+
+/**
+ * Table sync method.
+ *
+ * Handles table sync operations, Taking account of the behavior scope.
+ *
+ * @param Model $Model
+ * @param integer $shift
+ * @param string $dir
+ * @param array $conditions
+ * @param boolean $created
+ * @param string $field
+ * @return void
+ */
+ protected function _sync(Model $Model, $shift, $dir = '+', $conditions = array(), $created = false, $field = 'both') {
+ $ModelRecursive = $Model->recursive;
+ extract($this->settings[$Model->alias]);
+ $Model->recursive = $recursive;
+
+ if ($field == 'both') {
+ $this->_sync($Model, $shift, $dir, $conditions, $created, $left);
+ $field = $right;
+ }
+ if (is_string($conditions)) {
+ $conditions = array("{$Model->alias}.{$field} {$conditions}");
+ }
+ if (($scope != '1 = 1' && $scope !== true) && $scope) {
+ $conditions[] = $scope;
+ }
+ if ($created) {
+ $conditions['NOT'][$Model->alias . '.' . $Model->primaryKey] = $Model->id;
+ }
+ $Model->updateAll(array($Model->alias . '.' . $field => $Model->escapeField($field) . ' ' . $dir . ' ' . $shift), $conditions);
+ $Model->recursive = $ModelRecursive;
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/BehaviorCollection.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/BehaviorCollection.php
new file mode 100644
index 0000000..fff3e7d
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/BehaviorCollection.php
@@ -0,0 +1,296 @@
+<?php
+/**
+ * BehaviorCollection
+ *
+ * Provides management and interface for interacting with collections of behaviors.
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 1.2.0.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('ObjectCollection', 'Utility');
+App::uses('CakeEventListener', 'Event');
+
+/**
+ * Model behavior collection class.
+ *
+ * Defines the Behavior interface, and contains common model interaction functionality.
+ *
+ * @package Cake.Model
+ */
+class BehaviorCollection extends ObjectCollection implements CakeEventListener {
+
+/**
+ * Stores a reference to the attached name
+ *
+ * @var string
+ */
+ public $modelName = null;
+
+/**
+ * Keeps a list of all methods of attached behaviors
+ *
+ * @var array
+ */
+ protected $_methods = array();
+
+/**
+ * Keeps a list of all methods which have been mapped with regular expressions
+ *
+ * @var array
+ */
+ protected $_mappedMethods = array();
+
+/**
+ * Attaches a model object and loads a list of behaviors
+ *
+ * @todo Make this method a constructor instead..
+ * @param string $modelName
+ * @param array $behaviors
+ * @return void
+ */
+ public function init($modelName, $behaviors = array()) {
+ $this->modelName = $modelName;
+
+ if (!empty($behaviors)) {
+ foreach (BehaviorCollection::normalizeObjectArray($behaviors) as $behavior => $config) {
+ $this->load($config['class'], $config['settings']);
+ }
+ }
+ }
+
+/**
+ * Backwards compatible alias for load()
+ *
+ * @param string $behavior
+ * @param array $config
+ * @return void
+ * @deprecated Replaced with load()
+ */
+ public function attach($behavior, $config = array()) {
+ return $this->load($behavior, $config);
+ }
+
+/**
+ * Loads a behavior into the collection. You can use use `$config['enabled'] = false`
+ * to load a behavior with callbacks disabled. By default callbacks are enabled. Disable behaviors
+ * can still be used as normal.
+ *
+ * You can alias your behavior as an existing behavior by setting the 'className' key, i.e.,
+ * {{{
+ * public $actsAs = array(
+ * 'Tree' => array(
+ * 'className' => 'AliasedTree'
+ * );
+ * );
+ * }}}
+ * All calls to the `Tree` behavior would use `AliasedTree` instead.
+ *
+ * @param string $behavior CamelCased name of the behavior to load
+ * @param array $config Behavior configuration parameters
+ * @return boolean True on success, false on failure
+ * @throws MissingBehaviorException when a behavior could not be found.
+ */
+ public function load($behavior, $config = array()) {
+ if (is_array($config) && isset($config['className'])) {
+ $alias = $behavior;
+ $behavior = $config['className'];
+ }
+ $configDisabled = isset($config['enabled']) && $config['enabled'] === false;
+ unset($config['enabled'], $config['className']);
+
+ list($plugin, $name) = pluginSplit($behavior, true);
+ if (!isset($alias)) {
+ $alias = $name;
+ }
+
+ $class = $name . 'Behavior';
+
+ App::uses($class, $plugin . 'Model/Behavior');
+ if (!class_exists($class)) {
+ throw new MissingBehaviorException(array(
+ 'class' => $class,
+ 'plugin' => substr($plugin, 0, -1)
+ ));
+ }
+
+ if (!isset($this->{$alias})) {
+ if (ClassRegistry::isKeySet($class)) {
+ $this->_loaded[$alias] = ClassRegistry::getObject($class);
+ } else {
+ $this->_loaded[$alias] = new $class();
+ ClassRegistry::addObject($class, $this->_loaded[$alias]);
+ if (!empty($plugin)) {
+ ClassRegistry::addObject($plugin . '.' . $class, $this->_loaded[$alias]);
+ }
+ }
+ } elseif (isset($this->_loaded[$alias]->settings) && isset($this->_loaded[$alias]->settings[$this->modelName])) {
+ if ($config !== null && $config !== false) {
+ $config = array_merge($this->_loaded[$alias]->settings[$this->modelName], $config);
+ } else {
+ $config = array();
+ }
+ }
+ if (empty($config)) {
+ $config = array();
+ }
+ $this->_loaded[$alias]->setup(ClassRegistry::getObject($this->modelName), $config);
+
+ foreach ($this->_loaded[$alias]->mapMethods as $method => $methodAlias) {
+ $this->_mappedMethods[$method] = array($alias, $methodAlias);
+ }
+ $methods = get_class_methods($this->_loaded[$alias]);
+ $parentMethods = array_flip(get_class_methods('ModelBehavior'));
+ $callbacks = array(
+ 'setup', 'cleanup', 'beforeFind', 'afterFind', 'beforeSave', 'afterSave',
+ 'beforeDelete', 'afterDelete', 'onError'
+ );
+
+ foreach ($methods as $m) {
+ if (!isset($parentMethods[$m])) {
+ $methodAllowed = (
+ $m[0] != '_' && !array_key_exists($m, $this->_methods) &&
+ !in_array($m, $callbacks)
+ );
+ if ($methodAllowed) {
+ $this->_methods[$m] = array($alias, $m);
+ }
+ }
+ }
+
+ if (!in_array($alias, $this->_enabled) && !$configDisabled) {
+ $this->enable($alias);
+ } else {
+ $this->disable($alias);
+ }
+ return true;
+ }
+
+/**
+ * Detaches a behavior from a model
+ *
+ * @param string $name CamelCased name of the behavior to unload
+ * @return void
+ */
+ public function unload($name) {
+ list($plugin, $name) = pluginSplit($name);
+ if (isset($this->_loaded[$name])) {
+ $this->_loaded[$name]->cleanup(ClassRegistry::getObject($this->modelName));
+ parent::unload($name);
+ }
+ foreach ($this->_methods as $m => $callback) {
+ if (is_array($callback) && $callback[0] == $name) {
+ unset($this->_methods[$m]);
+ }
+ }
+ }
+
+/**
+ * Backwards compatible alias for unload()
+ *
+ * @param string $name Name of behavior
+ * @return void
+ * @deprecated Use unload instead.
+ */
+ public function detach($name) {
+ return $this->unload($name);
+ }
+
+/**
+ * Dispatches a behavior method. Will call either normal methods or mapped methods.
+ *
+ * If a method is not handled by the BehaviorCollection, and $strict is false, a
+ * special return of `array('unhandled')` will be returned to signal the method was not found.
+ *
+ * @param Model $model The model the method was originally called on.
+ * @param string $method The method called.
+ * @param array $params Parameters for the called method.
+ * @param boolean $strict If methods are not found, trigger an error.
+ * @return array All methods for all behaviors attached to this object
+ */
+ public function dispatchMethod($model, $method, $params = array(), $strict = false) {
+ $method = $this->hasMethod($method, true);
+
+ if ($strict && empty($method)) {
+ trigger_error(__d('cake_dev', "BehaviorCollection::dispatchMethod() - Method %s not found in any attached behavior", $method), E_USER_WARNING);
+ return null;
+ }
+ if (empty($method)) {
+ return array('unhandled');
+ }
+ if (count($method) === 3) {
+ array_unshift($params, $method[2]);
+ unset($method[2]);
+ }
+ return call_user_func_array(
+ array($this->_loaded[$method[0]], $method[1]),
+ array_merge(array(&$model), $params)
+ );
+ }
+
+/**
+ * Gets the method list for attached behaviors, i.e. all public, non-callback methods.
+ * This does not include mappedMethods.
+ *
+ * @return array All public methods for all behaviors attached to this collection
+ */
+ public function methods() {
+ return $this->_methods;
+ }
+
+/**
+ * Check to see if a behavior in this collection implements the provided method. Will
+ * also check mappedMethods.
+ *
+ * @param string $method The method to find.
+ * @param boolean $callback Return the callback for the method.
+ * @return mixed If $callback is false, a boolean will be returned, if its true, an array
+ * containing callback information will be returned. For mapped methods the array will have 3 elements.
+ */
+ public function hasMethod($method, $callback = false) {
+ if (isset($this->_methods[$method])) {
+ return $callback ? $this->_methods[$method] : true;
+ }
+ foreach ($this->_mappedMethods as $pattern => $target) {
+ if (preg_match($pattern . 'i', $method)) {
+ if ($callback) {
+ $target[] = $method;
+ return $target;
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+/**
+ * Returns the implemented events that will get routed to the trigger function
+ * in order to dispatch them separately on each behavior
+ *
+ * @return array
+ */
+ public function implementedEvents() {
+ return array(
+ 'Model.beforeFind' => 'trigger',
+ 'Model.afterFind' => 'trigger',
+ 'Model.beforeValidate' => 'trigger',
+ 'Model.afterValidate' => 'trigger',
+ 'Model.beforeSave' => 'trigger',
+ 'Model.afterSave' => 'trigger',
+ 'Model.beforeDelete' => 'trigger',
+ 'Model.afterDelete' => 'trigger'
+ );
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/CakeSchema.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/CakeSchema.php
new file mode 100644
index 0000000..60c3b76
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/CakeSchema.php
@@ -0,0 +1,710 @@
+<?php
+/**
+ * Schema database management for CakePHP.
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 1.2.0.5550
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('Model', 'Model');
+App::uses('AppModel', 'Model');
+App::uses('ConnectionManager', 'Model');
+App::uses('File', 'Utility');
+
+/**
+ * Base Class for Schema management
+ *
+ * @package Cake.Model
+ */
+class CakeSchema extends Object {
+
+/**
+ * Name of the schema
+ *
+ * @var string
+ */
+ public $name = null;
+
+/**
+ * Path to write location
+ *
+ * @var string
+ */
+ public $path = null;
+
+/**
+ * File to write
+ *
+ * @var string
+ */
+ public $file = 'schema.php';
+
+/**
+ * Connection used for read
+ *
+ * @var string
+ */
+ public $connection = 'default';
+
+/**
+ * plugin name.
+ *
+ * @var string
+ */
+ public $plugin = null;
+
+/**
+ * Set of tables
+ *
+ * @var array
+ */
+ public $tables = array();
+
+/**
+ * Constructor
+ *
+ * @param array $options optional load object properties
+ */
+ public function __construct($options = array()) {
+ parent::__construct();
+
+ if (empty($options['name'])) {
+ $this->name = preg_replace('/schema$/i', '', get_class($this));
+ }
+ if (!empty($options['plugin'])) {
+ $this->plugin = $options['plugin'];
+ }
+
+ if (strtolower($this->name) === 'cake') {
+ $this->name = Inflector::camelize(Inflector::slug(Configure::read('App.dir')));
+ }
+
+ if (empty($options['path'])) {
+ $this->path = APP . 'Config' . DS . 'Schema';
+ }
+
+ $options = array_merge(get_object_vars($this), $options);
+ $this->build($options);
+ }
+
+/**
+ * Builds schema object properties
+ *
+ * @param array $data loaded object properties
+ * @return void
+ */
+ public function build($data) {
+ $file = null;
+ foreach ($data as $key => $val) {
+ if (!empty($val)) {
+ if (!in_array($key, array('plugin', 'name', 'path', 'file', 'connection', 'tables', '_log'))) {
+ if ($key[0] === '_') {
+ continue;
+ }
+ $this->tables[$key] = $val;
+ unset($this->{$key});
+ } elseif ($key !== 'tables') {
+ if ($key === 'name' && $val !== $this->name && !isset($data['file'])) {
+ $file = Inflector::underscore($val) . '.php';
+ }
+ $this->{$key} = $val;
+ }
+ }
+ }
+ if (file_exists($this->path . DS . $file) && is_file($this->path . DS . $file)) {
+ $this->file = $file;
+ } elseif (!empty($this->plugin)) {
+ $this->path = CakePlugin::path($this->plugin) . 'Config' . DS . 'Schema';
+ }
+ }
+
+/**
+ * Before callback to be implemented in subclasses
+ *
+ * @param array $event schema object properties
+ * @return boolean Should process continue
+ */
+ public function before($event = array()) {
+ return true;
+ }
+
+/**
+ * After callback to be implemented in subclasses
+ *
+ * @param array $event schema object properties
+ * @return void
+ */
+ public function after($event = array()) {
+ }
+
+/**
+ * Reads database and creates schema tables
+ *
+ * @param array $options schema object properties
+ * @return array Set of name and tables
+ */
+ public function load($options = array()) {
+ if (is_string($options)) {
+ $options = array('path' => $options);
+ }
+
+ $this->build($options);
+ extract(get_object_vars($this));
+
+ $class = $name . 'Schema';
+
+ if (!class_exists($class)) {
+ if (file_exists($path . DS . $file) && is_file($path . DS . $file)) {
+ require_once $path . DS . $file;
+ } elseif (file_exists($path . DS . 'schema.php') && is_file($path . DS . 'schema.php')) {
+ require_once $path . DS . 'schema.php';
+ }
+ }
+
+ if (class_exists($class)) {
+ $Schema = new $class($options);
+ return $Schema;
+ }
+ return false;
+ }
+
+/**
+ * Reads database and creates schema tables
+ *
+ * Options
+ *
+ * - 'connection' - the db connection to use
+ * - 'name' - name of the schema
+ * - 'models' - a list of models to use, or false to ignore models
+ *
+ * @param array $options schema object properties
+ * @return array Array indexed by name and tables
+ */
+ public function read($options = array()) {
+ extract(array_merge(
+ array(
+ 'connection' => $this->connection,
+ 'name' => $this->name,
+ 'models' => true,
+ ),
+ $options
+ ));
+ $db = ConnectionManager::getDataSource($connection);
+
+ if (isset($this->plugin)) {
+ App::uses($this->plugin . 'AppModel', $this->plugin . '.Model');
+ }
+
+ $tables = array();
+ $currentTables = (array)$db->listSources();
+
+ $prefix = null;
+ if (isset($db->config['prefix'])) {
+ $prefix = $db->config['prefix'];
+ }
+
+ if (!is_array($models) && $models !== false) {
+ if (isset($this->plugin)) {
+ $models = App::objects($this->plugin . '.Model', null, false);
+ } else {
+ $models = App::objects('Model');
+ }
+ }
+
+ if (is_array($models)) {
+ foreach ($models as $model) {
+ $importModel = $model;
+ $plugin = null;
+ if ($model == 'AppModel') {
+ continue;
+ }
+
+ if (isset($this->plugin)) {
+ if ($model == $this->plugin . 'AppModel') {
+ continue;
+ }
+ $importModel = $model;
+ $plugin = $this->plugin . '.';
+ }
+
+ App::uses($importModel, $plugin . 'Model');
+ if (!class_exists($importModel)) {
+ continue;
+ }
+
+ $vars = get_class_vars($model);
+ if (empty($vars['useDbConfig']) || $vars['useDbConfig'] != $connection) {
+ continue;
+ }
+
+ try {
+ $Object = ClassRegistry::init(array('class' => $model, 'ds' => $connection));
+ } catch (CakeException $e) {
+ continue;
+ }
+
+ $db = $Object->getDataSource();
+ if (is_object($Object) && $Object->useTable !== false) {
+ $fulltable = $table = $db->fullTableName($Object, false, false);
+ if ($prefix && strpos($table, $prefix) !== 0) {
+ continue;
+ }
+ $table = $this->_noPrefixTable($prefix, $table);
+
+ if (in_array($fulltable, $currentTables)) {
+ $key = array_search($fulltable, $currentTables);
+ if (empty($tables[$table])) {
+ $tables[$table] = $this->_columns($Object);
+ $tables[$table]['indexes'] = $db->index($Object);
+ $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable);
+ unset($currentTables[$key]);
+ }
+ if (!empty($Object->hasAndBelongsToMany)) {
+ foreach ($Object->hasAndBelongsToMany as $Assoc => $assocData) {
+ if (isset($assocData['with'])) {
+ $class = $assocData['with'];
+ }
+ if (is_object($Object->$class)) {
+ $withTable = $db->fullTableName($Object->$class, false, false);
+ if ($prefix && strpos($withTable, $prefix) !== 0) {
+ continue;
+ }
+ if (in_array($withTable, $currentTables)) {
+ $key = array_search($withTable, $currentTables);
+ $noPrefixWith = $this->_noPrefixTable($prefix, $withTable);
+
+ $tables[$noPrefixWith] = $this->_columns($Object->$class);
+ $tables[$noPrefixWith]['indexes'] = $db->index($Object->$class);
+ $tables[$noPrefixWith]['tableParameters'] = $db->readTableParameters($withTable);
+ unset($currentTables[$key]);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!empty($currentTables)) {
+ foreach ($currentTables as $table) {
+ if ($prefix) {
+ if (strpos($table, $prefix) !== 0) {
+ continue;
+ }
+ $table = $this->_noPrefixTable($prefix, $table);
+ }
+ $Object = new AppModel(array(
+ 'name' => Inflector::classify($table), 'table' => $table, 'ds' => $connection
+ ));
+
+ $systemTables = array(
+ 'aros', 'acos', 'aros_acos', Configure::read('Session.table'), 'i18n'
+ );
+
+ $fulltable = $db->fullTableName($Object, false, false);
+
+ if (in_array($table, $systemTables)) {
+ $tables[$Object->table] = $this->_columns($Object);
+ $tables[$Object->table]['indexes'] = $db->index($Object);
+ $tables[$Object->table]['tableParameters'] = $db->readTableParameters($fulltable);
+ } elseif ($models === false) {
+ $tables[$table] = $this->_columns($Object);
+ $tables[$table]['indexes'] = $db->index($Object);
+ $tables[$table]['tableParameters'] = $db->readTableParameters($fulltable);
+ } else {
+ $tables['missing'][$table] = $this->_columns($Object);
+ $tables['missing'][$table]['indexes'] = $db->index($Object);
+ $tables['missing'][$table]['tableParameters'] = $db->readTableParameters($fulltable);
+ }
+ }
+ }
+
+ ksort($tables);
+ return compact('name', 'tables');
+ }
+
+/**
+ * Writes schema file from object or options
+ *
+ * @param array|object $object schema object or options array
+ * @param array $options schema object properties to override object
+ * @return mixed false or string written to file
+ */
+ public function write($object, $options = array()) {
+ if (is_object($object)) {
+ $object = get_object_vars($object);
+ $this->build($object);
+ }
+
+ if (is_array($object)) {
+ $options = $object;
+ unset($object);
+ }
+
+ extract(array_merge(
+ get_object_vars($this), $options
+ ));
+
+ $out = "class {$name}Schema extends CakeSchema {\n\n";
+
+ if ($path !== $this->path) {
+ $out .= "\tpublic \$path = '{$path}';\n\n";
+ }
+
+ if ($file !== $this->file) {
+ $out .= "\tpublic \$file = '{$file}';\n\n";
+ }
+
+ if ($connection !== 'default') {
+ $out .= "\tpublic \$connection = '{$connection}';\n\n";
+ }
+
+ $out .= "\tpublic function before(\$event = array()) {\n\t\treturn true;\n\t}\n\n\tpublic function after(\$event = array()) {\n\t}\n\n";
+
+ if (empty($tables)) {
+ $this->read();
+ }
+
+ foreach ($tables as $table => $fields) {
+ if (!is_numeric($table) && $table !== 'missing') {
+ $out .= $this->generateTable($table, $fields);
+ }
+ }
+ $out .= "}\n";
+
+ $file = new File($path . DS . $file, true);
+ $content = "<?php \n{$out}";
+ if ($file->write($content)) {
+ return $content;
+ }
+ return false;
+ }
+
+/**
+ * Generate the code for a table. Takes a table name and $fields array
+ * Returns a completed variable declaration to be used in schema classes
+ *
+ * @param string $table Table name you want returned.
+ * @param array $fields Array of field information to generate the table with.
+ * @return string Variable declaration for a schema class
+ */
+ public function generateTable($table, $fields) {
+ $out = "\tpublic \${$table} = array(\n";
+ if (is_array($fields)) {
+ $cols = array();
+ foreach ($fields as $field => $value) {
+ if ($field != 'indexes' && $field != 'tableParameters') {
+ if (is_string($value)) {
+ $type = $value;
+ $value = array('type' => $type);
+ }
+ $col = "\t\t'{$field}' => array('type' => '" . $value['type'] . "', ";
+ unset($value['type']);
+ $col .= join(', ', $this->_values($value));
+ } elseif ($field == 'indexes') {
+ $col = "\t\t'indexes' => array(\n\t\t\t";
+ $props = array();
+ foreach ((array)$value as $key => $index) {
+ $props[] = "'{$key}' => array(" . join(', ', $this->_values($index)) . ")";
+ }
+ $col .= join(",\n\t\t\t", $props) . "\n\t\t";
+ } elseif ($field == 'tableParameters') {
+ $col = "\t\t'tableParameters' => array(";
+ $props = array();
+ foreach ((array)$value as $key => $param) {
+ $props[] = "'{$key}' => '$param'";
+ }
+ $col .= join(', ', $props);
+ }
+ $col .= ")";
+ $cols[] = $col;
+ }
+ $out .= join(",\n", $cols);
+ }
+ $out .= "\n\t);\n";
+ return $out;
+ }
+
+/**
+ * Compares two sets of schemas
+ *
+ * @param array|object $old Schema object or array
+ * @param array|object $new Schema object or array
+ * @return array Tables (that are added, dropped, or changed)
+ */
+ public function compare($old, $new = null) {
+ if (empty($new)) {
+ $new = $this;
+ }
+ if (is_array($new)) {
+ if (isset($new['tables'])) {
+ $new = $new['tables'];
+ }
+ } else {
+ $new = $new->tables;
+ }
+
+ if (is_array($old)) {
+ if (isset($old['tables'])) {
+ $old = $old['tables'];
+ }
+ } else {
+ $old = $old->tables;
+ }
+ $tables = array();
+ foreach ($new as $table => $fields) {
+ if ($table == 'missing') {
+ continue;
+ }
+ if (!array_key_exists($table, $old)) {
+ $tables[$table]['add'] = $fields;
+ } else {
+ $diff = $this->_arrayDiffAssoc($fields, $old[$table]);
+ if (!empty($diff)) {
+ $tables[$table]['add'] = $diff;
+ }
+ $diff = $this->_arrayDiffAssoc($old[$table], $fields);
+ if (!empty($diff)) {
+ $tables[$table]['drop'] = $diff;
+ }
+ }
+
+ foreach ($fields as $field => $value) {
+ if (!empty($old[$table][$field])) {
+ $diff = $this->_arrayDiffAssoc($value, $old[$table][$field]);
+ if (!empty($diff) && $field !== 'indexes' && $field !== 'tableParameters') {
+ $tables[$table]['change'][$field] = $value;
+ }
+ }
+
+ if (isset($tables[$table]['add'][$field]) && $field !== 'indexes' && $field !== 'tableParameters') {
+ $wrapper = array_keys($fields);
+ if ($column = array_search($field, $wrapper)) {
+ if (isset($wrapper[$column - 1])) {
+ $tables[$table]['add'][$field]['after'] = $wrapper[$column - 1];
+ }
+ }
+ }
+ }
+
+ if (isset($old[$table]['indexes']) && isset($new[$table]['indexes'])) {
+ $diff = $this->_compareIndexes($new[$table]['indexes'], $old[$table]['indexes']);
+ if ($diff) {
+ if (!isset($tables[$table])) {
+ $tables[$table] = array();
+ }
+ if (isset($diff['drop'])) {
+ $tables[$table]['drop']['indexes'] = $diff['drop'];
+ }
+ if ($diff && isset($diff['add'])) {
+ $tables[$table]['add']['indexes'] = $diff['add'];
+ }
+ }
+ }
+ if (isset($old[$table]['tableParameters']) && isset($new[$table]['tableParameters'])) {
+ $diff = $this->_compareTableParameters($new[$table]['tableParameters'], $old[$table]['tableParameters']);
+ if ($diff) {
+ $tables[$table]['change']['tableParameters'] = $diff;
+ }
+ }
+ }
+ return $tables;
+ }
+
+/**
+ * Extended array_diff_assoc noticing change from/to NULL values
+ *
+ * It behaves almost the same way as array_diff_assoc except for NULL values: if
+ * one of the values is not NULL - change is detected. It is useful in situation
+ * where one value is strval('') ant other is strval(null) - in string comparing
+ * methods this results as EQUAL, while it is not.
+ *
+ * @param array $array1 Base array
+ * @param array $array2 Corresponding array checked for equality
+ * @return array Difference as array with array(keys => values) from input array
+ * where match was not found.
+ */
+ protected function _arrayDiffAssoc($array1, $array2) {
+ $difference = array();
+ foreach ($array1 as $key => $value) {
+ if (!array_key_exists($key, $array2)) {
+ $difference[$key] = $value;
+ continue;
+ }
+ $correspondingValue = $array2[$key];
+ if (is_null($value) !== is_null($correspondingValue)) {
+ $difference[$key] = $value;
+ continue;
+ }
+ if (is_bool($value) !== is_bool($correspondingValue)) {
+ $difference[$key] = $value;
+ continue;
+ }
+ if (is_array($value) && is_array($correspondingValue)) {
+ continue;
+ }
+ if ($value === $correspondingValue) {
+ continue;
+ }
+ $difference[$key] = $value;
+ }
+ return $difference;
+ }
+
+/**
+ * Formats Schema columns from Model Object
+ *
+ * @param array $values options keys(type, null, default, key, length, extra)
+ * @return array Formatted values
+ */
+ protected function _values($values) {
+ $vals = array();
+ if (is_array($values)) {
+ foreach ($values as $key => $val) {
+ if (is_array($val)) {
+ $vals[] = "'{$key}' => array('" . implode("', '", $val) . "')";
+ } elseif (!is_numeric($key)) {
+ $val = var_export($val, true);
+ if ($val === 'NULL') {
+ $val = 'null';
+ }
+ $vals[] = "'{$key}' => {$val}";
+ }
+ }
+ }
+ return $vals;
+ }
+
+/**
+ * Formats Schema columns from Model Object
+ *
+ * @param array $Obj model object
+ * @return array Formatted columns
+ */
+ protected function _columns(&$Obj) {
+ $db = $Obj->getDataSource();
+ $fields = $Obj->schema(true);
+
+ $columns = $props = array();
+ foreach ($fields as $name => $value) {
+ if ($Obj->primaryKey == $name) {
+ $value['key'] = 'primary';
+ }
+ if (!isset($db->columns[$value['type']])) {
+ trigger_error(__d('cake_dev', 'Schema generation error: invalid column type %s for %s.%s does not exist in DBO', $value['type'], $Obj->name, $name), E_USER_NOTICE);
+ continue;
+ } else {
+ $defaultCol = $db->columns[$value['type']];
+ if (isset($defaultCol['limit']) && $defaultCol['limit'] == $value['length']) {
+ unset($value['length']);
+ } elseif (isset($defaultCol['length']) && $defaultCol['length'] == $value['length']) {
+ unset($value['length']);
+ }
+ unset($value['limit']);
+ }
+
+ if (isset($value['default']) && ($value['default'] === '' || $value['default'] === false)) {
+ unset($value['default']);
+ }
+ if (empty($value['length'])) {
+ unset($value['length']);
+ }
+ if (empty($value['key'])) {
+ unset($value['key']);
+ }
+ $columns[$name] = $value;
+ }
+
+ return $columns;
+ }
+
+/**
+ * Compare two schema files table Parameters
+ *
+ * @param array $new New indexes
+ * @param array $old Old indexes
+ * @return mixed False on failure, or an array of parameters to add & drop.
+ */
+ protected function _compareTableParameters($new, $old) {
+ if (!is_array($new) || !is_array($old)) {
+ return false;
+ }
+ $change = $this->_arrayDiffAssoc($new, $old);
+ return $change;
+ }
+
+/**
+ * Compare two schema indexes
+ *
+ * @param array $new New indexes
+ * @param array $old Old indexes
+ * @return mixed false on failure or array of indexes to add and drop
+ */
+ protected function _compareIndexes($new, $old) {
+ if (!is_array($new) || !is_array($old)) {
+ return false;
+ }
+
+ $add = $drop = array();
+
+ $diff = $this->_arrayDiffAssoc($new, $old);
+ if (!empty($diff)) {
+ $add = $diff;
+ }
+
+ $diff = $this->_arrayDiffAssoc($old, $new);
+ if (!empty($diff)) {
+ $drop = $diff;
+ }
+
+ foreach ($new as $name => $value) {
+ if (isset($old[$name])) {
+ $newUnique = isset($value['unique']) ? $value['unique'] : 0;
+ $oldUnique = isset($old[$name]['unique']) ? $old[$name]['unique'] : 0;
+ $newColumn = $value['column'];
+ $oldColumn = $old[$name]['column'];
+
+ $diff = false;
+
+ if ($newUnique != $oldUnique) {
+ $diff = true;
+ } elseif (is_array($newColumn) && is_array($oldColumn)) {
+ $diff = ($newColumn !== $oldColumn);
+ } elseif (is_string($newColumn) && is_string($oldColumn)) {
+ $diff = ($newColumn != $oldColumn);
+ } else {
+ $diff = true;
+ }
+ if ($diff) {
+ $drop[$name] = null;
+ $add[$name] = $value;
+ }
+ }
+ }
+ return array_filter(compact('add', 'drop'));
+ }
+
+/**
+ * Trim the table prefix from the full table name, and return the prefix-less table
+ *
+ * @param string $prefix Table prefix
+ * @param string $table Full table name
+ * @return string Prefix-less table name
+ */
+ protected function _noPrefixTable($prefix, $table) {
+ return preg_replace('/^' . preg_quote($prefix) . '/', '', $table);
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ConnectionManager.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ConnectionManager.php
new file mode 100644
index 0000000..edbcfa5
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ConnectionManager.php
@@ -0,0 +1,266 @@
+<?php
+/**
+ * Datasource connection manager
+ *
+ * Provides an interface for loading and enumerating connections defined in app/Config/database.php
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 0.10.x.1402
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('DataSource', 'Model/Datasource');
+
+/**
+ * Manages loaded instances of DataSource objects
+ *
+ * Provides an interface for loading and enumerating connections defined in
+ * app/Config/database.php
+ *
+ * @package Cake.Model
+ */
+class ConnectionManager {
+
+/**
+ * Holds a loaded instance of the Connections object
+ *
+ * @var DATABASE_CONFIG
+ */
+ public static $config = null;
+
+/**
+ * Holds instances DataSource objects
+ *
+ * @var array
+ */
+ protected static $_dataSources = array();
+
+/**
+ * Contains a list of all file and class names used in Connection settings
+ *
+ * @var array
+ */
+ protected static $_connectionsEnum = array();
+
+/**
+ * Indicates if the init code for this class has already been executed
+ *
+ * @var boolean
+ */
+ protected static $_init = false;
+
+/**
+ * Loads connections configuration.
+ *
+ * @return void
+ */
+ protected static function _init() {
+ include_once APP . 'Config' . DS . 'database.php';
+ if (class_exists('DATABASE_CONFIG')) {
+ self::$config = new DATABASE_CONFIG();
+ }
+ self::$_init = true;
+ }
+
+/**
+ * Gets a reference to a DataSource object
+ *
+ * @param string $name The name of the DataSource, as defined in app/Config/database.php
+ * @return DataSource Instance
+ * @throws MissingDatasourceConfigException
+ * @throws MissingDatasourceException
+ */
+ public static function getDataSource($name) {
+ if (empty(self::$_init)) {
+ self::_init();
+ }
+
+ if (!empty(self::$_dataSources[$name])) {
+ $return = self::$_dataSources[$name];
+ return $return;
+ }
+
+ if (empty(self::$_connectionsEnum[$name])) {
+ self::_getConnectionObject($name);
+ }
+
+ self::loadDataSource($name);
+ $conn = self::$_connectionsEnum[$name];
+ $class = $conn['classname'];
+
+ self::$_dataSources[$name] = new $class(self::$config->{$name});
+ self::$_dataSources[$name]->configKeyName = $name;
+
+ return self::$_dataSources[$name];
+ }
+
+/**
+ * Gets the list of available DataSource connections
+ * This will only return the datasources instantiated by this manager
+ * It differs from enumConnectionObjects, since the latter will return all configured connections
+ *
+ * @return array List of available connections
+ */
+ public static function sourceList() {
+ if (empty(self::$_init)) {
+ self::_init();
+ }
+ return array_keys(self::$_dataSources);
+ }
+
+/**
+ * Gets a DataSource name from an object reference.
+ *
+ * @param DataSource $source DataSource object
+ * @return string Datasource name, or null if source is not present
+ * in the ConnectionManager.
+ */
+ public static function getSourceName($source) {
+ if (empty(self::$_init)) {
+ self::_init();
+ }
+ foreach (self::$_dataSources as $name => $ds) {
+ if ($ds === $source) {
+ return $name;
+ }
+ }
+ return null;
+ }
+
+/**
+ * Loads the DataSource class for the given connection name
+ *
+ * @param string|array $connName A string name of the connection, as defined in app/Config/database.php,
+ * or an array containing the filename (without extension) and class name of the object,
+ * to be found in app/Model/Datasource/ or lib/Cake/Model/Datasource/.
+ * @return boolean True on success, null on failure or false if the class is already loaded
+ * @throws MissingDatasourceException
+ */
+ public static function loadDataSource($connName) {
+ if (empty(self::$_init)) {
+ self::_init();
+ }
+
+ if (is_array($connName)) {
+ $conn = $connName;
+ } else {
+ $conn = self::$_connectionsEnum[$connName];
+ }
+
+ if (class_exists($conn['classname'], false)) {
+ return false;
+ }
+
+ $plugin = $package = null;
+ if (!empty($conn['plugin'])) {
+ $plugin = $conn['plugin'] . '.';
+ }
+ if (!empty($conn['package'])) {
+ $package = '/' . $conn['package'];
+ }
+
+ App::uses($conn['classname'], $plugin . 'Model/Datasource' . $package);
+ if (!class_exists($conn['classname'])) {
+ throw new MissingDatasourceException(array(
+ 'class' => $conn['classname'],
+ 'plugin' => substr($plugin, 0, -1)
+ ));
+ }
+ return true;
+ }
+
+/**
+ * Return a list of connections
+ *
+ * @return array An associative array of elements where the key is the connection name
+ * (as defined in Connections), and the value is an array with keys 'filename' and 'classname'.
+ */
+ public static function enumConnectionObjects() {
+ if (empty(self::$_init)) {
+ self::_init();
+ }
+ return (array)self::$config;
+ }
+
+/**
+ * Dynamically creates a DataSource object at runtime, with the given name and settings
+ *
+ * @param string $name The DataSource name
+ * @param array $config The DataSource configuration settings
+ * @return DataSource A reference to the DataSource object, or null if creation failed
+ */
+ public static function create($name = '', $config = array()) {
+ if (empty(self::$_init)) {
+ self::_init();
+ }
+
+ if (empty($name) || empty($config) || array_key_exists($name, self::$_connectionsEnum)) {
+ return null;
+ }
+ self::$config->{$name} = $config;
+ self::$_connectionsEnum[$name] = self::_connectionData($config);
+ $return = self::getDataSource($name);
+ return $return;
+ }
+
+/**
+ * Removes a connection configuration at runtime given its name
+ *
+ * @param string $name the connection name as it was created
+ * @return boolean success if connection was removed, false if it does not exist
+ */
+ public static function drop($name) {
+ if (empty(self::$_init)) {
+ self::_init();
+ }
+
+ if (!isset(self::$config->{$name})) {
+ return false;
+ }
+ unset(self::$_connectionsEnum[$name], self::$_dataSources[$name], self::$config->{$name});
+ return true;
+ }
+
+/**
+ * Gets a list of class and file names associated with the user-defined DataSource connections
+ *
+ * @param string $name Connection name
+ * @return void
+ * @throws MissingDatasourceConfigException
+ */
+ protected static function _getConnectionObject($name) {
+ if (!empty(self::$config->{$name})) {
+ self::$_connectionsEnum[$name] = self::_connectionData(self::$config->{$name});
+ } else {
+ throw new MissingDatasourceConfigException(array('config' => $name));
+ }
+ }
+
+/**
+ * Returns the file, class name, and parent for the given driver.
+ *
+ * @param array $config Array with connection configuration. Key 'datasource' is required
+ * @return array An indexed array with: filename, classname, plugin and parent
+ */
+ protected static function _connectionData($config) {
+ $package = $classname = $plugin = null;
+
+ list($plugin, $classname) = pluginSplit($config['datasource']);
+ if (strpos($classname, '/') !== false) {
+ $package = dirname($classname);
+ $classname = basename($classname);
+ }
+ return compact('package', 'classname', 'plugin');
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/CakeSession.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/CakeSession.php
new file mode 100644
index 0000000..6baab1d
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/CakeSession.php
@@ -0,0 +1,688 @@
+<?php
+/**
+ * Session class for Cake.
+ *
+ * Cake abstracts the handling of sessions.
+ * There are several convenient methods to access session information.
+ * This class is the implementation of those methods.
+ * They are mostly used by the Session Component.
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, 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.1222
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('Hash', 'Utility');
+App::uses('Security', 'Utility');
+
+/**
+ * Session class for Cake.
+ *
+ * Cake abstracts the handling of sessions. There are several convenient methods to access session information.
+ * This class is the implementation of those methods. They are mostly used by the Session Component.
+ *
+ * @package Cake.Model.Datasource
+ */
+class CakeSession {
+
+/**
+ * True if the Session is still valid
+ *
+ * @var boolean
+ */
+ public static $valid = false;
+
+/**
+ * Error messages for this session
+ *
+ * @var array
+ */
+ public static $error = false;
+
+/**
+ * User agent string
+ *
+ * @var string
+ */
+ protected static $_userAgent = '';
+
+/**
+ * Path to where the session is active.
+ *
+ * @var string
+ */
+ public static $path = '/';
+
+/**
+ * Error number of last occurred error
+ *
+ * @var integer
+ */
+ public static $lastError = null;
+
+/**
+ * Start time for this session.
+ *
+ * @var integer
+ */
+ public static $time = false;
+
+/**
+ * Cookie lifetime
+ *
+ * @var integer
+ */
+ public static $cookieLifeTime;
+
+/**
+ * Time when this session becomes invalid.
+ *
+ * @var integer
+ */
+ public static $sessionTime = false;
+
+/**
+ * Current Session id
+ *
+ * @var string
+ */
+ public static $id = null;
+
+/**
+ * Hostname
+ *
+ * @var string
+ */
+ public static $host = null;
+
+/**
+ * Session timeout multiplier factor
+ *
+ * @var integer
+ */
+ public static $timeout = null;
+
+/**
+ * Number of requests that can occur during a session time without the session being renewed.
+ * This feature is only used when config value `Session.autoRegenerate` is set to true.
+ *
+ * @var integer
+ * @see CakeSession::_checkValid()
+ */
+ public static $requestCountdown = 10;
+
+/**
+ * Pseudo constructor.
+ *
+ * @param string $base The base path for the Session
+ * @return void
+ */
+ public static function init($base = null) {
+ self::$time = time();
+
+ $checkAgent = Configure::read('Session.checkAgent');
+ if (($checkAgent === true || $checkAgent === null) && env('HTTP_USER_AGENT') != null) {
+ self::$_userAgent = md5(env('HTTP_USER_AGENT') . Configure::read('Security.salt'));
+ }
+ self::_setPath($base);
+ self::_setHost(env('HTTP_HOST'));
+
+ register_shutdown_function('session_write_close');
+ }
+
+/**
+ * Setup the Path variable
+ *
+ * @param string $base base path
+ * @return void
+ */
+ protected static function _setPath($base = null) {
+ if (empty($base)) {
+ self::$path = '/';
+ return;
+ }
+ if (strpos($base, 'index.php') !== false) {
+ $base = str_replace('index.php', '', $base);
+ }
+ if (strpos($base, '?') !== false) {
+ $base = str_replace('?', '', $base);
+ }
+ self::$path = $base;
+ }
+
+/**
+ * Set the host name
+ *
+ * @param string $host Hostname
+ * @return void
+ */
+ protected static function _setHost($host) {
+ self::$host = $host;
+ if (strpos(self::$host, ':') !== false) {
+ self::$host = substr(self::$host, 0, strpos(self::$host, ':'));
+ }
+ }
+
+/**
+ * Starts the Session.
+ *
+ * @return boolean True if session was started
+ */
+ public static function start() {
+ if (self::started()) {
+ return true;
+ }
+ self::init();
+ $id = self::id();
+ session_write_close();
+ self::_configureSession();
+ self::_startSession();
+
+ if (!$id && self::started()) {
+ self::_checkValid();
+ }
+
+ self::$error = false;
+ return self::started();
+ }
+
+/**
+ * Determine if Session has been started.
+ *
+ * @return boolean True if session has been started.
+ */
+ public static function started() {
+ return isset($_SESSION) && session_id();
+ }
+
+/**
+ * Returns true if given variable is set in session.
+ *
+ * @param string $name Variable name to check for
+ * @return boolean True if variable is there
+ */
+ public static function check($name = null) {
+ if (!self::started() && !self::start()) {
+ return false;
+ }
+ if (empty($name)) {
+ return false;
+ }
+ $result = Hash::get($_SESSION, $name);
+ return isset($result);
+ }
+
+/**
+ * Returns the Session id
+ *
+ * @param string $id
+ * @return string Session id
+ */
+ public static function id($id = null) {
+ if ($id) {
+ self::$id = $id;
+ session_id(self::$id);
+ }
+ if (self::started()) {
+ return session_id();
+ }
+ return self::$id;
+ }
+
+/**
+ * Removes a variable from session.
+ *
+ * @param string $name Session variable to remove
+ * @return boolean Success
+ */
+ public static function delete($name) {
+ if (self::check($name)) {
+ self::_overwrite($_SESSION, Hash::remove($_SESSION, $name));
+ return (self::check($name) == false);
+ }
+ self::_setError(2, __d('cake_dev', "%s doesn't exist", $name));
+ return false;
+ }
+
+/**
+ * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself
+ *
+ * @param array $old Set of old variables => values
+ * @param array $new New set of variable => value
+ * @return void
+ */
+ protected static function _overwrite(&$old, $new) {
+ if (!empty($old)) {
+ foreach ($old as $key => $var) {
+ if (!isset($new[$key])) {
+ unset($old[$key]);
+ }
+ }
+ }
+ foreach ($new as $key => $var) {
+ $old[$key] = $var;
+ }
+ }
+
+/**
+ * Return error description for given error number.
+ *
+ * @param integer $errorNumber Error to set
+ * @return string Error as string
+ */
+ protected static function _error($errorNumber) {
+ if (!is_array(self::$error) || !array_key_exists($errorNumber, self::$error)) {
+ return false;
+ } else {
+ return self::$error[$errorNumber];
+ }
+ }
+
+/**
+ * Returns last occurred error as a string, if any.
+ *
+ * @return mixed Error description as a string, or false.
+ */
+ public static function error() {
+ if (self::$lastError) {
+ return self::_error(self::$lastError);
+ }
+ return false;
+ }
+
+/**
+ * Returns true if session is valid.
+ *
+ * @return boolean Success
+ */
+ public static function valid() {
+ if (self::read('Config')) {
+ if (self::_validAgentAndTime() && self::$error === false) {
+ self::$valid = true;
+ } else {
+ self::$valid = false;
+ self::_setError(1, 'Session Highjacking Attempted !!!');
+ }
+ }
+ return self::$valid;
+ }
+
+/**
+ * Tests that the user agent is valid and that the session hasn't 'timed out'.
+ * Since timeouts are implemented in CakeSession it checks the current self::$time
+ * against the time the session is set to expire. The User agent is only checked
+ * if Session.checkAgent == true.
+ *
+ * @return boolean
+ */
+ protected static function _validAgentAndTime() {
+ $config = self::read('Config');
+ $validAgent = (
+ Configure::read('Session.checkAgent') === false ||
+ self::$_userAgent == $config['userAgent']
+ );
+ return ($validAgent && self::$time <= $config['time']);
+ }
+
+/**
+ * Get / Set the userAgent
+ *
+ * @param string $userAgent Set the userAgent
+ * @return void
+ */
+ public static function userAgent($userAgent = null) {
+ if ($userAgent) {
+ self::$_userAgent = $userAgent;
+ }
+ if (empty(self::$_userAgent)) {
+ CakeSession::init(self::$path);
+ }
+ return self::$_userAgent;
+ }
+
+/**
+ * Returns given session variable, or all of them, if no parameters given.
+ *
+ * @param string|array $name The name of the session variable (or a path as sent to Set.extract)
+ * @return mixed The value of the session variable
+ */
+ public static function read($name = null) {
+ if (!self::started() && !self::start()) {
+ return false;
+ }
+ if (is_null($name)) {
+ return self::_returnSessionVars();
+ }
+ if (empty($name)) {
+ return false;
+ }
+ $result = Hash::get($_SESSION, $name);
+
+ if (isset($result)) {
+ return $result;
+ }
+ self::_setError(2, "$name doesn't exist");
+ return null;
+ }
+
+/**
+ * Returns all session variables.
+ *
+ * @return mixed Full $_SESSION array, or false on error.
+ */
+ protected static function _returnSessionVars() {
+ if (!empty($_SESSION)) {
+ return $_SESSION;
+ }
+ self::_setError(2, 'No Session vars set');
+ return false;
+ }
+
+/**
+ * Writes value to given session variable name.
+ *
+ * @param string|array $name Name of variable
+ * @param string $value Value to write
+ * @return boolean True if the write was successful, false if the write failed
+ */
+ public static function write($name, $value = null) {
+ if (!self::started() && !self::start()) {
+ return false;
+ }
+ if (empty($name)) {
+ return false;
+ }
+ $write = $name;
+ if (!is_array($name)) {
+ $write = array($name => $value);
+ }
+ foreach ($write as $key => $val) {
+ self::_overwrite($_SESSION, Hash::insert($_SESSION, $key, $val));
+ if (Hash::get($_SESSION, $key) !== $val) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+/**
+ * Helper method to destroy invalid sessions.
+ *
+ * @return void
+ */
+ public static function destroy() {
+ if (self::started()) {
+ session_destroy();
+ }
+ self::clear();
+ }
+
+/**
+ * Clears the session, the session id, and renew's the session.
+ *
+ * @return void
+ */
+ public static function clear() {
+ $_SESSION = null;
+ self::$id = null;
+ self::start();
+ self::renew();
+ }
+
+/**
+ * Helper method to initialize a session, based on Cake core settings.
+ *
+ * Sessions can be configured with a few shortcut names as well as have any number of ini settings declared.
+ *
+ * @return void
+ * @throws CakeSessionException Throws exceptions when ini_set() fails.
+ */
+ protected static function _configureSession() {
+ $sessionConfig = Configure::read('Session');
+ $iniSet = function_exists('ini_set');
+
+ if (isset($sessionConfig['defaults'])) {
+ $defaults = self::_defaultConfig($sessionConfig['defaults']);
+ if ($defaults) {
+ $sessionConfig = Hash::merge($defaults, $sessionConfig);
+ }
+ }
+ if (!isset($sessionConfig['ini']['session.cookie_secure']) && env('HTTPS')) {
+ $sessionConfig['ini']['session.cookie_secure'] = 1;
+ }
+ if (isset($sessionConfig['timeout']) && !isset($sessionConfig['cookieTimeout'])) {
+ $sessionConfig['cookieTimeout'] = $sessionConfig['timeout'];
+ }
+ if (!isset($sessionConfig['ini']['session.cookie_lifetime'])) {
+ $sessionConfig['ini']['session.cookie_lifetime'] = $sessionConfig['cookieTimeout'] * 60;
+ }
+ if (!isset($sessionConfig['ini']['session.name'])) {
+ $sessionConfig['ini']['session.name'] = $sessionConfig['cookie'];
+ }
+ if (!empty($sessionConfig['handler'])) {
+ $sessionConfig['ini']['session.save_handler'] = 'user';
+ }
+ if (!isset($sessionConfig['ini']['session.gc_maxlifetime'])) {
+ $sessionConfig['ini']['session.gc_maxlifetime'] = $sessionConfig['timeout'] * 60;
+ }
+ if (!isset($sessionConfig['ini']['session.cookie_httponly'])) {
+ $sessionConfig['ini']['session.cookie_httponly'] = 1;
+ }
+
+ if (empty($_SESSION)) {
+ if (!empty($sessionConfig['ini']) && is_array($sessionConfig['ini'])) {
+ foreach ($sessionConfig['ini'] as $setting => $value) {
+ if (ini_set($setting, $value) === false) {
+ throw new CakeSessionException(sprintf(
+ __d('cake_dev', 'Unable to configure the session, setting %s failed.'),
+ $setting
+ ));
+ }
+ }
+ }
+ }
+ if (!empty($sessionConfig['handler']) && !isset($sessionConfig['handler']['engine'])) {
+ call_user_func_array('session_set_save_handler', $sessionConfig['handler']);
+ }
+ if (!empty($sessionConfig['handler']['engine'])) {
+ $handler = self::_getHandler($sessionConfig['handler']['engine']);
+ session_set_save_handler(
+ array($handler, 'open'),
+ array($handler, 'close'),
+ array($handler, 'read'),
+ array($handler, 'write'),
+ array($handler, 'destroy'),
+ array($handler, 'gc')
+ );
+ }
+ Configure::write('Session', $sessionConfig);
+ self::$sessionTime = self::$time + ($sessionConfig['timeout'] * 60);
+ }
+
+/**
+ * Find the handler class and make sure it implements the correct interface.
+ *
+ * @param string $handler
+ * @return void
+ * @throws CakeSessionException
+ */
+ protected static function _getHandler($handler) {
+ list($plugin, $class) = pluginSplit($handler, true);
+ App::uses($class, $plugin . 'Model/Datasource/Session');
+ if (!class_exists($class)) {
+ throw new CakeSessionException(__d('cake_dev', 'Could not load %s to handle the session.', $class));
+ }
+ $handler = new $class();
+ if ($handler instanceof CakeSessionHandlerInterface) {
+ return $handler;
+ }
+ throw new CakeSessionException(__d('cake_dev', 'Chosen SessionHandler does not implement CakeSessionHandlerInterface it cannot be used with an engine key.'));
+ }
+
+/**
+ * Get one of the prebaked default session configurations.
+ *
+ * @param string $name
+ * @return boolean|array
+ */
+ protected static function _defaultConfig($name) {
+ $defaults = array(
+ 'php' => array(
+ 'cookie' => 'CAKEPHP',
+ 'timeout' => 240,
+ 'ini' => array(
+ 'session.use_trans_sid' => 0,
+ 'session.cookie_path' => self::$path
+ )
+ ),
+ 'cake' => array(
+ 'cookie' => 'CAKEPHP',
+ 'timeout' => 240,
+ 'ini' => array(
+ 'session.use_trans_sid' => 0,
+ 'url_rewriter.tags' => '',
+ 'session.serialize_handler' => 'php',
+ 'session.use_cookies' => 1,
+ 'session.cookie_path' => self::$path,
+ 'session.auto_start' => 0,
+ 'session.save_path' => TMP . 'sessions',
+ 'session.save_handler' => 'files'
+ )
+ ),
+ 'cache' => array(
+ 'cookie' => 'CAKEPHP',
+ 'timeout' => 240,
+ 'ini' => array(
+ 'session.use_trans_sid' => 0,
+ 'url_rewriter.tags' => '',
+ 'session.auto_start' => 0,
+ 'session.use_cookies' => 1,
+ 'session.cookie_path' => self::$path,
+ 'session.save_handler' => 'user',
+ ),
+ 'handler' => array(
+ 'engine' => 'CacheSession',
+ 'config' => 'default'
+ )
+ ),
+ 'database' => array(
+ 'cookie' => 'CAKEPHP',
+ 'timeout' => 240,
+ 'ini' => array(
+ 'session.use_trans_sid' => 0,
+ 'url_rewriter.tags' => '',
+ 'session.auto_start' => 0,
+ 'session.use_cookies' => 1,
+ 'session.cookie_path' => self::$path,
+ 'session.save_handler' => 'user',
+ 'session.serialize_handler' => 'php',
+ ),
+ 'handler' => array(
+ 'engine' => 'DatabaseSession',
+ 'model' => 'Session'
+ )
+ )
+ );
+ if (isset($defaults[$name])) {
+ return $defaults[$name];
+ }
+ return false;
+ }
+
+/**
+ * Helper method to start a session
+ *
+ * @return boolean Success
+ */
+ protected static function _startSession() {
+ if (headers_sent()) {
+ if (empty($_SESSION)) {
+ $_SESSION = array();
+ }
+ } else {
+ // For IE<=8
+ session_cache_limiter("must-revalidate");
+ session_start();
+ }
+ return true;
+ }
+
+/**
+ * Helper method to create a new session.
+ *
+ * @return void
+ */
+ protected static function _checkValid() {
+ if (!self::started() && !self::start()) {
+ self::$valid = false;
+ return false;
+ }
+ if ($config = self::read('Config')) {
+ $sessionConfig = Configure::read('Session');
+
+ if (self::_validAgentAndTime()) {
+ self::write('Config.time', self::$sessionTime);
+ if (isset($sessionConfig['autoRegenerate']) && $sessionConfig['autoRegenerate'] === true) {
+ $check = $config['countdown'];
+ $check -= 1;
+ self::write('Config.countdown', $check);
+
+ if ($check < 1) {
+ self::renew();
+ self::write('Config.countdown', self::$requestCountdown);
+ }
+ }
+ self::$valid = true;
+ } else {
+ self::destroy();
+ self::$valid = false;
+ self::_setError(1, 'Session Highjacking Attempted !!!');
+ }
+ } else {
+ self::write('Config.userAgent', self::$_userAgent);
+ self::write('Config.time', self::$sessionTime);
+ self::write('Config.countdown', self::$requestCountdown);
+ self::$valid = true;
+ }
+ }
+
+/**
+ * Restarts this session.
+ *
+ * @return void
+ */
+ public static function renew() {
+ if (session_id()) {
+ if (session_id() != '' || isset($_COOKIE[session_name()])) {
+ setcookie(Configure::read('Session.cookie'), '', time() - 42000, self::$path);
+ }
+ session_regenerate_id(true);
+ }
+ }
+
+/**
+ * Helper method to set an internal error message.
+ *
+ * @param integer $errorNumber Number of the error
+ * @param string $errorMessage Description of the error
+ * @return void
+ */
+ protected static function _setError($errorNumber, $errorMessage) {
+ if (self::$error === false) {
+ self::$error = array();
+ }
+ self::$error[$errorNumber] = $errorMessage;
+ self::$lastError = $errorNumber;
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DataSource.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DataSource.php
new file mode 100644
index 0000000..e5e665f
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DataSource.php
@@ -0,0 +1,442 @@
+<?php
+/**
+ * DataSource base class
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Datasource
+ * @since CakePHP(tm) v 0.10.5.1790
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+/**
+ * DataSource base class
+ *
+ * @package Cake.Model.Datasource
+ */
+class DataSource extends Object {
+
+/**
+ * Are we connected to the DataSource?
+ *
+ * @var boolean
+ */
+ public $connected = false;
+
+/**
+ * The default configuration of a specific DataSource
+ *
+ * @var array
+ */
+ protected $_baseConfig = array();
+
+/**
+ * Holds references to descriptions loaded by the DataSource
+ *
+ * @var array
+ */
+ protected $_descriptions = array();
+
+/**
+ * Holds a list of sources (tables) contained in the DataSource
+ *
+ * @var array
+ */
+ protected $_sources = null;
+
+/**
+ * The DataSource configuration
+ *
+ * @var array
+ */
+ public $config = array();
+
+/**
+ * Whether or not this DataSource is in the middle of a transaction
+ *
+ * @var boolean
+ */
+ protected $_transactionStarted = false;
+
+/**
+ * Whether or not source data like available tables and schema descriptions
+ * should be cached
+ *
+ * @var boolean
+ */
+ public $cacheSources = true;
+
+/**
+ * Constructor.
+ *
+ * @param array $config Array of configuration information for the datasource.
+ */
+ public function __construct($config = array()) {
+ parent::__construct();
+ $this->setConfig($config);
+ }
+
+/**
+ * Caches/returns cached results for child instances
+ *
+ * @param mixed $data
+ * @return array Array of sources available in this datasource.
+ */
+ public function listSources($data = null) {
+ if ($this->cacheSources === false) {
+ return null;
+ }
+
+ if ($this->_sources !== null) {
+ return $this->_sources;
+ }
+
+ $key = ConnectionManager::getSourceName($this) . '_' . $this->config['database'] . '_list';
+ $key = preg_replace('/[^A-Za-z0-9_\-.+]/', '_', $key);
+ $sources = Cache::read($key, '_cake_model_');
+
+ if (empty($sources)) {
+ $sources = $data;
+ Cache::write($key, $data, '_cake_model_');
+ }
+
+ return $this->_sources = $sources;
+ }
+
+/**
+ * Returns a Model description (metadata) or null if none found.
+ *
+ * @param Model|string $model
+ * @return array Array of Metadata for the $model
+ */
+ public function describe($model) {
+ if ($this->cacheSources === false) {
+ return null;
+ }
+ if (is_string($model)) {
+ $table = $model;
+ } else {
+ $table = $model->tablePrefix . $model->table;
+ }
+
+ if (isset($this->_descriptions[$table])) {
+ return $this->_descriptions[$table];
+ }
+ $cache = $this->_cacheDescription($table);
+
+ if ($cache !== null) {
+ $this->_descriptions[$table] =& $cache;
+ return $cache;
+ }
+ return null;
+ }
+
+/**
+ * Begin a transaction
+ *
+ * @return boolean Returns true if a transaction is not in progress
+ */
+ public function begin() {
+ return !$this->_transactionStarted;
+ }
+
+/**
+ * Commit a transaction
+ *
+ * @return boolean Returns true if a transaction is in progress
+ */
+ public function commit() {
+ return $this->_transactionStarted;
+ }
+
+/**
+ * Rollback a transaction
+ *
+ * @return boolean Returns true if a transaction is in progress
+ */
+ public function rollback() {
+ return $this->_transactionStarted;
+ }
+
+/**
+ * Converts column types to basic types
+ *
+ * @param string $real Real column type (i.e. "varchar(255)")
+ * @return string Abstract column type (i.e. "string")
+ */
+ public function column($real) {
+ return false;
+ }
+
+/**
+ * Used to create new records. The "C" CRUD.
+ *
+ * To-be-overridden in subclasses.
+ *
+ * @param Model $model The Model to be created.
+ * @param array $fields An Array of fields to be saved.
+ * @param array $values An Array of values to save.
+ * @return boolean success
+ */
+ public function create(Model $model, $fields = null, $values = null) {
+ return false;
+ }
+
+/**
+ * Used to read records from the Datasource. The "R" in CRUD
+ *
+ * To-be-overridden in subclasses.
+ *
+ * @param Model $model The model being read.
+ * @param array $queryData An array of query data used to find the data you want
+ * @return mixed
+ */
+ public function read(Model $model, $queryData = array()) {
+ return false;
+ }
+
+/**
+ * Update a record(s) in the datasource.
+ *
+ * To-be-overridden in subclasses.
+ *
+ * @param Model $model Instance of the model class being updated
+ * @param array $fields Array of fields to be updated
+ * @param array $values Array of values to be update $fields to.
+ * @return boolean Success
+ */
+ public function update(Model $model, $fields = null, $values = null) {
+ return false;
+ }
+
+/**
+ * Delete a record(s) in the datasource.
+ *
+ * To-be-overridden in subclasses.
+ *
+ * @param Model $model The model class having record(s) deleted
+ * @param mixed $conditions The conditions to use for deleting.
+ * @return void
+ */
+ public function delete(Model $model, $id = null) {
+ return false;
+ }
+
+/**
+ * Returns the ID generated from the previous INSERT operation.
+ *
+ * @param mixed $source
+ * @return mixed Last ID key generated in previous INSERT
+ */
+ public function lastInsertId($source = null) {
+ return false;
+ }
+
+/**
+ * Returns the number of rows returned by last operation.
+ *
+ * @param mixed $source
+ * @return integer Number of rows returned by last operation
+ */
+ public function lastNumRows($source = null) {
+ return false;
+ }
+
+/**
+ * Returns the number of rows affected by last query.
+ *
+ * @param mixed $source
+ * @return integer Number of rows affected by last query.
+ */
+ public function lastAffected($source = null) {
+ return false;
+ }
+
+/**
+ * Check whether the conditions for the Datasource being available
+ * are satisfied. Often used from connect() to check for support
+ * before establishing a connection.
+ *
+ * @return boolean Whether or not the Datasources conditions for use are met.
+ */
+ public function enabled() {
+ return true;
+ }
+
+/**
+ * Sets the configuration for the DataSource.
+ * Merges the $config information with the _baseConfig and the existing $config property.
+ *
+ * @param array $config The configuration array
+ * @return void
+ */
+ public function setConfig($config = array()) {
+ $this->config = array_merge($this->_baseConfig, $this->config, $config);
+ }
+
+/**
+ * Cache the DataSource description
+ *
+ * @param string $object The name of the object (model) to cache
+ * @param mixed $data The description of the model, usually a string or array
+ * @return mixed
+ */
+ protected function _cacheDescription($object, $data = null) {
+ if ($this->cacheSources === false) {
+ return null;
+ }
+
+ if ($data !== null) {
+ $this->_descriptions[$object] =& $data;
+ }
+
+ $key = ConnectionManager::getSourceName($this) . '_' . $object;
+ $cache = Cache::read($key, '_cake_model_');
+
+ if (empty($cache)) {
+ $cache = $data;
+ Cache::write($key, $cache, '_cake_model_');
+ }
+
+ return $cache;
+ }
+
+/**
+ * Replaces `{$__cakeID__$}` and `{$__cakeForeignKey__$}` placeholders in query data.
+ *
+ * @param string $query Query string needing replacements done.
+ * @param array $data Array of data with values that will be inserted in placeholders.
+ * @param string $association Name of association model being replaced
+ * @param array $assocData
+ * @param Model $model Instance of the model to replace $__cakeID__$
+ * @param Model $linkModel Instance of model to replace $__cakeForeignKey__$
+ * @param array $stack
+ * @return string String of query data with placeholders replaced.
+ * @todo Remove and refactor $assocData, ensure uses of the method have the param removed too.
+ */
+ public function insertQueryData($query, $data, $association, $assocData, Model $model, Model $linkModel, $stack) {
+ $keys = array('{$__cakeID__$}', '{$__cakeForeignKey__$}');
+
+ foreach ($keys as $key) {
+ $val = null;
+ $type = null;
+
+ if (strpos($query, $key) !== false) {
+ switch ($key) {
+ case '{$__cakeID__$}':
+ if (isset($data[$model->alias]) || isset($data[$association])) {
+ if (isset($data[$model->alias][$model->primaryKey])) {
+ $val = $data[$model->alias][$model->primaryKey];
+ } elseif (isset($data[$association][$model->primaryKey])) {
+ $val = $data[$association][$model->primaryKey];
+ }
+ } else {
+ $found = false;
+ foreach (array_reverse($stack) as $assoc) {
+ if (isset($data[$assoc]) && isset($data[$assoc][$model->primaryKey])) {
+ $val = $data[$assoc][$model->primaryKey];
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ $val = '';
+ }
+ }
+ $type = $model->getColumnType($model->primaryKey);
+ break;
+ case '{$__cakeForeignKey__$}':
+ foreach ($model->associations() as $id => $name) {
+ foreach ($model->$name as $assocName => $assoc) {
+ if ($assocName === $association) {
+ if (isset($assoc['foreignKey'])) {
+ $foreignKey = $assoc['foreignKey'];
+ $assocModel = $model->$assocName;
+ $type = $assocModel->getColumnType($assocModel->primaryKey);
+
+ if (isset($data[$model->alias][$foreignKey])) {
+ $val = $data[$model->alias][$foreignKey];
+ } elseif (isset($data[$association][$foreignKey])) {
+ $val = $data[$association][$foreignKey];
+ } else {
+ $found = false;
+ foreach (array_reverse($stack) as $assoc) {
+ if (isset($data[$assoc]) && isset($data[$assoc][$foreignKey])) {
+ $val = $data[$assoc][$foreignKey];
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ $val = '';
+ }
+ }
+ }
+ break 3;
+ }
+ }
+ }
+ break;
+ }
+ if (empty($val) && $val !== '0') {
+ return false;
+ }
+ $query = str_replace($key, $this->value($val, $type), $query);
+ }
+ }
+ return $query;
+ }
+
+/**
+ * To-be-overridden in subclasses.
+ *
+ * @param Model $model Model instance
+ * @param string $key Key name to make
+ * @return string Key name for model.
+ */
+ public function resolveKey(Model $model, $key) {
+ return $model->alias . $key;
+ }
+
+/**
+ * Returns the schema name. Override this in subclasses.
+ *
+ * @return string schema name
+ * @access public
+ */
+ public function getSchemaName() {
+ return null;
+ }
+
+/**
+ * Closes a connection. Override in subclasses
+ *
+ * @return boolean
+ * @access public
+ */
+ public function close() {
+ return $this->connected = false;
+ }
+
+/**
+ * Closes the current datasource.
+ *
+ */
+ public function __destruct() {
+ if ($this->_transactionStarted) {
+ $this->rollback();
+ }
+ if ($this->connected) {
+ $this->close();
+ }
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Mysql.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Mysql.php
new file mode 100644
index 0000000..9506a8a
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Mysql.php
@@ -0,0 +1,688 @@
+<?php
+/**
+ * MySQL layer for DBO
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Datasource.Database
+ * @since CakePHP(tm) v 0.10.5.1790
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('DboSource', 'Model/Datasource');
+
+/**
+ * MySQL DBO driver object
+ *
+ * Provides connection and SQL generation for MySQL RDMS
+ *
+ * @package Cake.Model.Datasource.Database
+ */
+class Mysql extends DboSource {
+
+/**
+ * Datasource description
+ *
+ * @var string
+ */
+ public $description = "MySQL DBO Driver";
+
+/**
+ * Base configuration settings for MySQL driver
+ *
+ * @var array
+ */
+ protected $_baseConfig = array(
+ 'persistent' => true,
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'database' => 'cake',
+ 'port' => '3306'
+ );
+
+/**
+ * Reference to the PDO object connection
+ *
+ * @var PDO $_connection
+ */
+ protected $_connection = null;
+
+/**
+ * Start quote
+ *
+ * @var string
+ */
+ public $startQuote = "`";
+
+/**
+ * End quote
+ *
+ * @var string
+ */
+ public $endQuote = "`";
+
+/**
+ * use alias for update and delete. Set to true if version >= 4.1
+ *
+ * @var boolean
+ */
+ protected $_useAlias = true;
+
+/**
+ * List of engine specific additional field parameters used on table creating
+ *
+ * @var array
+ */
+ public $fieldParameters = array(
+ 'charset' => array('value' => 'CHARACTER SET', 'quote' => false, 'join' => ' ', 'column' => false, 'position' => 'beforeDefault'),
+ 'collate' => array('value' => 'COLLATE', 'quote' => false, 'join' => ' ', 'column' => 'Collation', 'position' => 'beforeDefault'),
+ 'comment' => array('value' => 'COMMENT', 'quote' => true, 'join' => ' ', 'column' => 'Comment', 'position' => 'afterDefault')
+ );
+
+/**
+ * List of table engine specific parameters used on table creating
+ *
+ * @var array
+ */
+ public $tableParameters = array(
+ 'charset' => array('value' => 'DEFAULT CHARSET', 'quote' => false, 'join' => '=', 'column' => 'charset'),
+ 'collate' => array('value' => 'COLLATE', 'quote' => false, 'join' => '=', 'column' => 'Collation'),
+ 'engine' => array('value' => 'ENGINE', 'quote' => false, 'join' => '=', 'column' => 'Engine')
+ );
+
+/**
+ * MySQL column definition
+ *
+ * @var array
+ */
+ public $columns = array(
+ 'primary_key' => array('name' => 'NOT NULL AUTO_INCREMENT'),
+ 'string' => array('name' => 'varchar', 'limit' => '255'),
+ 'text' => array('name' => 'text'),
+ 'integer' => array('name' => 'int', 'limit' => '11', 'formatter' => 'intval'),
+ 'float' => array('name' => 'float', 'formatter' => 'floatval'),
+ 'datetime' => array('name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
+ 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
+ 'time' => array('name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'),
+ 'date' => array('name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'),
+ 'binary' => array('name' => 'blob'),
+ 'boolean' => array('name' => 'tinyint', 'limit' => '1')
+ );
+
+/**
+ * Connects to the database using options in the given configuration array.
+ *
+ * @return boolean True if the database could be connected, else false
+ * @throws MissingConnectionException
+ */
+ public function connect() {
+ $config = $this->config;
+ $this->connected = false;
+ try {
+ $flags = array(
+ PDO::ATTR_PERSISTENT => $config['persistent'],
+ PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+ );
+ if (!empty($config['encoding'])) {
+ $flags[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $config['encoding'];
+ }
+ if (empty($config['unix_socket'])) {
+ $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
+ } else {
+ $dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
+ }
+ $this->_connection = new PDO(
+ $dsn,
+ $config['login'],
+ $config['password'],
+ $flags
+ );
+ $this->connected = true;
+ } catch (PDOException $e) {
+ throw new MissingConnectionException(array('class' => $e->getMessage()));
+ }
+
+ $this->_useAlias = (bool)version_compare($this->getVersion(), "4.1", ">=");
+
+ return $this->connected;
+ }
+
+/**
+ * Check whether the MySQL extension is installed/loaded
+ *
+ * @return boolean
+ */
+ public function enabled() {
+ return in_array('mysql', PDO::getAvailableDrivers());
+ }
+
+/**
+ * Returns an array of sources (tables) in the database.
+ *
+ * @param mixed $data
+ * @return array Array of table names in the database
+ */
+ public function listSources($data = null) {
+ $cache = parent::listSources();
+ if ($cache != null) {
+ return $cache;
+ }
+ $result = $this->_execute('SHOW TABLES FROM ' . $this->name($this->config['database']));
+
+ if (!$result) {
+ $result->closeCursor();
+ return array();
+ } else {
+ $tables = array();
+
+ while ($line = $result->fetch(PDO::FETCH_NUM)) {
+ $tables[] = $line[0];
+ }
+
+ $result->closeCursor();
+ parent::listSources($tables);
+ return $tables;
+ }
+ }
+
+/**
+ * Builds a map of the columns contained in a result
+ *
+ * @param PDOStatement $results
+ * @return void
+ */
+ public function resultSet($results) {
+ $this->map = array();
+ $numFields = $results->columnCount();
+ $index = 0;
+
+ while ($numFields-- > 0) {
+ $column = $results->getColumnMeta($index);
+ if (empty($column['native_type'])) {
+ $type = ($column['len'] == 1) ? 'boolean' : 'string';
+ } else {
+ $type = $column['native_type'];
+ }
+ if (!empty($column['table']) && strpos($column['name'], $this->virtualFieldSeparator) === false) {
+ $this->map[$index++] = array($column['table'], $column['name'], $type);
+ } else {
+ $this->map[$index++] = array(0, $column['name'], $type);
+ }
+ }
+ }
+
+/**
+ * Fetches the next row from the current result set
+ *
+ * @return mixed array with results fetched and mapped to column names or false if there is no results left to fetch
+ */
+ public function fetchResult() {
+ if ($row = $this->_result->fetch(PDO::FETCH_NUM)) {
+ $resultRow = array();
+ foreach ($this->map as $col => $meta) {
+ list($table, $column, $type) = $meta;
+ $resultRow[$table][$column] = $row[$col];
+ if ($type === 'boolean' && $row[$col] !== null) {
+ $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]);
+ }
+ }
+ return $resultRow;
+ }
+ $this->_result->closeCursor();
+ return false;
+ }
+
+/**
+ * Gets the database encoding
+ *
+ * @return string The database encoding
+ */
+ public function getEncoding() {
+ return $this->_execute('SHOW VARIABLES LIKE ?', array('character_set_client'))->fetchObject()->Value;
+ }
+
+/**
+ * Query charset by collation
+ *
+ * @param string $name Collation name
+ * @return string Character set name
+ */
+ public function getCharsetName($name) {
+ if ((bool)version_compare($this->getVersion(), "5", ">=")) {
+ $r = $this->_execute('SELECT CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.COLLATIONS WHERE COLLATION_NAME = ?', array($name));
+ $cols = $r->fetch(PDO::FETCH_ASSOC);
+
+ if (isset($cols['CHARACTER_SET_NAME'])) {
+ return $cols['CHARACTER_SET_NAME'];
+ }
+ }
+ return false;
+ }
+
+/**
+ * Returns an array of the fields in given table name.
+ *
+ * @param Model|string $model Name of database table to inspect or model instance
+ * @return array Fields in table. Keys are name and type
+ * @throws CakeException
+ */
+ public function describe($model) {
+ $key = $this->fullTableName($model, false);
+ $cache = parent::describe($key);
+ if ($cache != null) {
+ return $cache;
+ }
+ $table = $this->fullTableName($model);
+
+ $fields = false;
+ $cols = $this->_execute('SHOW FULL COLUMNS FROM ' . $table);
+ if (!$cols) {
+ throw new CakeException(__d('cake_dev', 'Could not describe table for %s', $table));
+ }
+
+ while ($column = $cols->fetch(PDO::FETCH_OBJ)) {
+ $fields[$column->Field] = array(
+ 'type' => $this->column($column->Type),
+ 'null' => ($column->Null === 'YES' ? true : false),
+ 'default' => $column->Default,
+ 'length' => $this->length($column->Type),
+ );
+ if (!empty($column->Key) && isset($this->index[$column->Key])) {
+ $fields[$column->Field]['key'] = $this->index[$column->Key];
+ }
+ foreach ($this->fieldParameters as $name => $value) {
+ if (!empty($column->{$value['column']})) {
+ $fields[$column->Field][$name] = $column->{$value['column']};
+ }
+ }
+ if (isset($fields[$column->Field]['collate'])) {
+ $charset = $this->getCharsetName($fields[$column->Field]['collate']);
+ if ($charset) {
+ $fields[$column->Field]['charset'] = $charset;
+ }
+ }
+ }
+ $this->_cacheDescription($key, $fields);
+ $cols->closeCursor();
+ return $fields;
+ }
+
+/**
+ * Generates and executes an SQL UPDATE statement for given model, fields, and values.
+ *
+ * @param Model $model
+ * @param array $fields
+ * @param array $values
+ * @param mixed $conditions
+ * @return array
+ */
+ public function update(Model $model, $fields = array(), $values = null, $conditions = null) {
+ if (!$this->_useAlias) {
+ return parent::update($model, $fields, $values, $conditions);
+ }
+
+ if ($values == null) {
+ $combined = $fields;
+ } else {
+ $combined = array_combine($fields, $values);
+ }
+
+ $alias = $joins = false;
+ $fields = $this->_prepareUpdateFields($model, $combined, empty($conditions), !empty($conditions));
+ $fields = implode(', ', $fields);
+ $table = $this->fullTableName($model);
+
+ if (!empty($conditions)) {
+ $alias = $this->name($model->alias);
+ if ($model->name == $model->alias) {
+ $joins = implode(' ', $this->_getJoins($model));
+ }
+ }
+ $conditions = $this->conditions($this->defaultConditions($model, $conditions, $alias), true, true, $model);
+
+ if ($conditions === false) {
+ return false;
+ }
+
+ if (!$this->execute($this->renderStatement('update', compact('table', 'alias', 'joins', 'fields', 'conditions')))) {
+ $model->onError();
+ return false;
+ }
+ return true;
+ }
+
+/**
+ * Generates and executes an SQL DELETE statement for given id/conditions on given model.
+ *
+ * @param Model $model
+ * @param mixed $conditions
+ * @return boolean Success
+ */
+ public function delete(Model $model, $conditions = null) {
+ if (!$this->_useAlias) {
+ return parent::delete($model, $conditions);
+ }
+ $alias = $this->name($model->alias);
+ $table = $this->fullTableName($model);
+ $joins = implode(' ', $this->_getJoins($model));
+
+ if (empty($conditions)) {
+ $alias = $joins = false;
+ }
+ $complexConditions = false;
+ foreach ((array)$conditions as $key => $value) {
+ if (strpos($key, $model->alias) === false) {
+ $complexConditions = true;
+ break;
+ }
+ }
+ if (!$complexConditions) {
+ $joins = false;
+ }
+
+ $conditions = $this->conditions($this->defaultConditions($model, $conditions, $alias), true, true, $model);
+ if ($conditions === false) {
+ return false;
+ }
+ if ($this->execute($this->renderStatement('delete', compact('alias', 'table', 'joins', 'conditions'))) === false) {
+ $model->onError();
+ return false;
+ }
+ return true;
+ }
+
+/**
+ * Sets the database encoding
+ *
+ * @param string $enc Database encoding
+ * @return boolean
+ */
+ public function setEncoding($enc) {
+ return $this->_execute('SET NAMES ' . $enc) !== false;
+ }
+
+/**
+ * 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) {
+ $index = array();
+ $table = $this->fullTableName($model);
+ $old = version_compare($this->getVersion(), '4.1', '<=');
+ if ($table) {
+ $indices = $this->_execute('SHOW INDEX FROM ' . $table);
+ // @codingStandardsIgnoreStart
+ // MySQL columns don't match the cakephp conventions.
+ while ($idx = $indices->fetch(PDO::FETCH_OBJ)) {
+ if ($old) {
+ $idx = (object)current((array)$idx);
+ }
+ if (!isset($index[$idx->Key_name]['column'])) {
+ $col = array();
+ $index[$idx->Key_name]['column'] = $idx->Column_name;
+ $index[$idx->Key_name]['unique'] = intval($idx->Non_unique == 0);
+ } else {
+ if (!empty($index[$idx->Key_name]['column']) && !is_array($index[$idx->Key_name]['column'])) {
+ $col[] = $index[$idx->Key_name]['column'];
+ }
+ $col[] = $idx->Column_name;
+ $index[$idx->Key_name]['column'] = $col;
+ }
+ }
+ // @codingStandardsIgnoreEnd
+ $indices->closeCursor();
+ }
+ return $index;
+ }
+
+/**
+ * Generate a MySQL Alter Table syntax for the given Schema comparison
+ *
+ * @param array $compare Result of a CakeSchema::compare()
+ * @param string $table
+ * @return array Array of alter statements to make.
+ */
+ public function alterSchema($compare, $table = null) {
+ if (!is_array($compare)) {
+ return false;
+ }
+ $out = '';
+ $colList = array();
+ foreach ($compare as $curTable => $types) {
+ $indexes = $tableParameters = $colList = array();
+ if (!$table || $table == $curTable) {
+ $out .= 'ALTER TABLE ' . $this->fullTableName($curTable) . " \n";
+ foreach ($types as $type => $column) {
+ if (isset($column['indexes'])) {
+ $indexes[$type] = $column['indexes'];
+ unset($column['indexes']);
+ }
+ if (isset($column['tableParameters'])) {
+ $tableParameters[$type] = $column['tableParameters'];
+ unset($column['tableParameters']);
+ }
+ switch ($type) {
+ case 'add':
+ foreach ($column as $field => $col) {
+ $col['name'] = $field;
+ $alter = 'ADD ' . $this->buildColumn($col);
+ if (isset($col['after'])) {
+ $alter .= ' AFTER ' . $this->name($col['after']);
+ }
+ $colList[] = $alter;
+ }
+ break;
+ case 'drop':
+ foreach ($column as $field => $col) {
+ $col['name'] = $field;
+ $colList[] = 'DROP ' . $this->name($field);
+ }
+ break;
+ case 'change':
+ foreach ($column as $field => $col) {
+ if (!isset($col['name'])) {
+ $col['name'] = $field;
+ }
+ $colList[] = 'CHANGE ' . $this->name($field) . ' ' . $this->buildColumn($col);
+ }
+ break;
+ }
+ }
+ $colList = array_merge($colList, $this->_alterIndexes($curTable, $indexes));
+ $colList = array_merge($colList, $this->_alterTableParameters($curTable, $tableParameters));
+ $out .= "\t" . implode(",\n\t", $colList) . ";\n\n";
+ }
+ }
+ return $out;
+ }
+
+/**
+ * Generate a MySQL "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 = '';
+ foreach ($schema->tables as $curTable => $columns) {
+ if (!$table || $table === $curTable) {
+ $out .= 'DROP TABLE IF EXISTS ' . $this->fullTableName($curTable) . ";\n";
+ }
+ }
+ return $out;
+ }
+
+/**
+ * Generate MySQL table parameter alteration statements for a table.
+ *
+ * @param string $table Table to alter parameters for.
+ * @param array $parameters Parameters to add & drop.
+ * @return array Array of table property alteration statements.
+ * @todo Implement this method.
+ */
+ protected function _alterTableParameters($table, $parameters) {
+ if (isset($parameters['change'])) {
+ return $this->buildTableParameters($parameters['change']);
+ }
+ return array();
+ }
+
+/**
+ * Generate MySQL index alteration statements for a table.
+ *
+ * @param string $table Table to alter indexes for
+ * @param array $indexes Indexes to add and drop
+ * @return array Index alteration statements
+ */
+ protected function _alterIndexes($table, $indexes) {
+ $alter = array();
+ if (isset($indexes['drop'])) {
+ foreach ($indexes['drop'] as $name => $value) {
+ $out = 'DROP ';
+ if ($name == 'PRIMARY') {
+ $out .= 'PRIMARY KEY';
+ } else {
+ $out .= 'KEY ' . $name;
+ }
+ $alter[] = $out;
+ }
+ }
+ if (isset($indexes['add'])) {
+ foreach ($indexes['add'] as $name => $value) {
+ $out = 'ADD ';
+ if ($name == 'PRIMARY') {
+ $out .= 'PRIMARY ';
+ $name = null;
+ } else {
+ if (!empty($value['unique'])) {
+ $out .= 'UNIQUE ';
+ }
+ }
+ if (is_array($value['column'])) {
+ $out .= 'KEY ' . $name . ' (' . implode(', ', array_map(array(&$this, 'name'), $value['column'])) . ')';
+ } else {
+ $out .= 'KEY ' . $name . ' (' . $this->name($value['column']) . ')';
+ }
+ $alter[] = $out;
+ }
+ }
+ return $alter;
+ }
+
+/**
+ * Returns an detailed array of sources (tables) in the database.
+ *
+ * @param string $name Table name to get parameters
+ * @return array Array of table names in the database
+ */
+ public function listDetailedSources($name = null) {
+ $condition = '';
+ if (is_string($name)) {
+ $condition = ' WHERE name = ' . $this->value($name);
+ }
+ $result = $this->_connection->query('SHOW TABLE STATUS ' . $condition, PDO::FETCH_ASSOC);
+
+ if (!$result) {
+ $result->closeCursor();
+ return array();
+ } else {
+ $tables = array();
+ foreach ($result as $row) {
+ $tables[$row['Name']] = (array)$row;
+ unset($tables[$row['Name']]['queryString']);
+ if (!empty($row['Collation'])) {
+ $charset = $this->getCharsetName($row['Collation']);
+ if ($charset) {
+ $tables[$row['Name']]['charset'] = $charset;
+ }
+ }
+ }
+ $result->closeCursor();
+ if (is_string($name) && isset($tables[$name])) {
+ return $tables[$name];
+ }
+ return $tables;
+ }
+ }
+
+/**
+ * Converts database-layer column types to basic types
+ *
+ * @param string $real Real database-layer column type (i.e. "varchar(255)")
+ * @return string Abstract column type (i.e. "string")
+ */
+ public function column($real) {
+ if (is_array($real)) {
+ $col = $real['name'];
+ if (isset($real['limit'])) {
+ $col .= '(' . $real['limit'] . ')';
+ }
+ return $col;
+ }
+
+ $col = str_replace(')', '', $real);
+ $limit = $this->length($real);
+ if (strpos($col, '(') !== false) {
+ list($col, $vals) = explode('(', $col);
+ }
+
+ if (in_array($col, array('date', 'time', 'datetime', 'timestamp'))) {
+ return $col;
+ }
+ if (($col === 'tinyint' && $limit == 1) || $col === 'boolean') {
+ return 'boolean';
+ }
+ if (strpos($col, 'int') !== false) {
+ return 'integer';
+ }
+ if (strpos($col, 'char') !== false || $col === 'tinytext') {
+ return 'string';
+ }
+ if (strpos($col, 'text') !== false) {
+ return 'text';
+ }
+ if (strpos($col, 'blob') !== false || $col === 'binary') {
+ return 'binary';
+ }
+ if (strpos($col, 'float') !== false || strpos($col, 'double') !== false || strpos($col, 'decimal') !== false) {
+ return 'float';
+ }
+ if (strpos($col, 'enum') !== false) {
+ return "enum($vals)";
+ }
+ return 'text';
+ }
+
+/**
+ * Gets the schema name
+ *
+ * @return string The schema name
+ */
+ public function getSchemaName() {
+ return $this->config['database'];
+ }
+
+/**
+ * Check if the server support nested transactions
+ *
+ * @return boolean
+ */
+ public function nestedTransactionSupported() {
+ return $this->useNestedTransactions && version_compare($this->getVersion(), '4.1', '>=');
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Postgres.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Postgres.php
new file mode 100644
index 0000000..74da346
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Postgres.php
@@ -0,0 +1,907 @@
+<?php
+/**
+ * PostgreSQL layer for DBO.
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Datasource.Database
+ * @since CakePHP(tm) v 0.9.1.114
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('DboSource', 'Model/Datasource');
+
+/**
+ * PostgreSQL layer for DBO.
+ *
+ * @package Cake.Model.Datasource.Database
+ */
+class Postgres extends DboSource {
+
+/**
+ * Driver description
+ *
+ * @var string
+ */
+ public $description = "PostgreSQL DBO Driver";
+
+/**
+ * Base driver configuration settings. Merged with user settings.
+ *
+ * @var array
+ */
+ protected $_baseConfig = array(
+ 'persistent' => true,
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'database' => 'cake',
+ 'schema' => 'public',
+ 'port' => 5432,
+ 'encoding' => ''
+ );
+
+/**
+ * Columns
+ *
+ * @var array
+ */
+ public $columns = array(
+ 'primary_key' => array('name' => 'serial NOT NULL'),
+ 'string' => array('name' => 'varchar', 'limit' => '255'),
+ 'text' => array('name' => 'text'),
+ 'integer' => array('name' => 'integer', 'formatter' => 'intval'),
+ 'float' => array('name' => 'float', 'formatter' => 'floatval'),
+ 'datetime' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
+ 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
+ 'time' => array('name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'),
+ 'date' => array('name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'),
+ 'binary' => array('name' => 'bytea'),
+ 'boolean' => array('name' => 'boolean'),
+ 'number' => array('name' => 'numeric'),
+ 'inet' => array('name' => 'inet')
+ );
+
+/**
+ * Starting Quote
+ *
+ * @var string
+ */
+ public $startQuote = '"';
+
+/**
+ * Ending Quote
+ *
+ * @var string
+ */
+ public $endQuote = '"';
+
+/**
+ * Contains mappings of custom auto-increment sequences, if a table uses a sequence name
+ * other than what is dictated by convention.
+ *
+ * @var array
+ */
+ protected $_sequenceMap = array();
+
+/**
+ * Connects to the database using options in the given configuration array.
+ *
+ * @return boolean True if successfully connected.
+ * @throws MissingConnectionException
+ */
+ public function connect() {
+ $config = $this->config;
+ $this->connected = false;
+ try {
+ $flags = array(
+ PDO::ATTR_PERSISTENT => $config['persistent'],
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+ );
+ $this->_connection = new PDO(
+ "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}",
+ $config['login'],
+ $config['password'],
+ $flags
+ );
+
+ $this->connected = true;
+ if (!empty($config['encoding'])) {
+ $this->setEncoding($config['encoding']);
+ }
+ if (!empty($config['schema'])) {
+ $this->_execute('SET search_path TO ' . $config['schema']);
+ }
+ } catch (PDOException $e) {
+ throw new MissingConnectionException(array('class' => $e->getMessage()));
+ }
+
+ return $this->connected;
+ }
+
+/**
+ * Check if PostgreSQL is enabled/loaded
+ *
+ * @return boolean
+ */
+ public function enabled() {
+ return in_array('pgsql', PDO::getAvailableDrivers());
+ }
+
+/**
+ * Returns an array of tables in the database. If there are no tables, an error is raised and the application exits.
+ *
+ * @param mixed $data
+ * @return array Array of table names in the database
+ */
+ public function listSources($data = null) {
+ $cache = parent::listSources();
+
+ if ($cache != null) {
+ return $cache;
+ }
+
+ $schema = $this->config['schema'];
+ $sql = "SELECT table_name as name FROM INFORMATION_SCHEMA.tables WHERE table_schema = ?";
+ $result = $this->_execute($sql, array($schema));
+
+ if (!$result) {
+ return array();
+ } else {
+ $tables = array();
+
+ foreach ($result as $item) {
+ $tables[] = $item->name;
+ }
+
+ $result->closeCursor();
+ parent::listSources($tables);
+ return $tables;
+ }
+ }
+
+/**
+ * Returns an array of the fields in given table name.
+ *
+ * @param Model|string $model Name of database table to inspect
+ * @return array Fields in table. Keys are name and type
+ */
+ public function describe($model) {
+ $table = $this->fullTableName($model, false, false);
+ $fields = parent::describe($table);
+ $this->_sequenceMap[$table] = array();
+ $cols = null;
+
+ if ($fields === null) {
+ $cols = $this->_execute(
+ "SELECT DISTINCT table_schema AS schema, column_name AS name, data_type AS type, is_nullable AS null,
+ column_default AS default, ordinal_position AS position, character_maximum_length AS char_length,
+ character_octet_length AS oct_length FROM information_schema.columns
+ WHERE table_name = ? AND table_schema = ? ORDER BY position",
+ array($table, $this->config['schema'])
+ );
+
+ // @codingStandardsIgnoreStart
+ // Postgres columns don't match the coding standards.
+ foreach ($cols as $c) {
+ $type = $c->type;
+ if (!empty($c->oct_length) && $c->char_length === null) {
+ if ($c->type == 'character varying') {
+ $length = null;
+ $type = 'text';
+ } elseif ($c->type == 'uuid') {
+ $length = 36;
+ } else {
+ $length = intval($c->oct_length);
+ }
+ } elseif (!empty($c->char_length)) {
+ $length = intval($c->char_length);
+ } else {
+ $length = $this->length($c->type);
+ }
+ if (empty($length)) {
+ $length = null;
+ }
+ $fields[$c->name] = array(
+ 'type' => $this->column($type),
+ 'null' => ($c->null == 'NO' ? false : true),
+ 'default' => preg_replace(
+ "/^'(.*)'$/",
+ "$1",
+ preg_replace('/::.*/', '', $c->default)
+ ),
+ 'length' => $length
+ );
+ if ($model instanceof Model) {
+ if ($c->name == $model->primaryKey) {
+ $fields[$c->name]['key'] = 'primary';
+ if ($fields[$c->name]['type'] !== 'string') {
+ $fields[$c->name]['length'] = 11;
+ }
+ }
+ }
+ if (
+ $fields[$c->name]['default'] == 'NULL' ||
+ preg_match('/nextval\([\'"]?([\w.]+)/', $c->default, $seq)
+ ) {
+ $fields[$c->name]['default'] = null;
+ if (!empty($seq) && isset($seq[1])) {
+ if (strpos($seq[1], '.') === false) {
+ $sequenceName = $c->schema . '.' . $seq[1];
+ } else {
+ $sequenceName = $seq[1];
+ }
+ $this->_sequenceMap[$table][$c->name] = $sequenceName;
+ }
+ }
+ if ($fields[$c->name]['type'] == 'boolean' && !empty($fields[$c->name]['default'])) {
+ $fields[$c->name]['default'] = constant($fields[$c->name]['default']);
+ }
+ }
+ $this->_cacheDescription($table, $fields);
+ }
+ // @codingStandardsIgnoreEnd
+
+ if (isset($model->sequence)) {
+ $this->_sequenceMap[$table][$model->primaryKey] = $model->sequence;
+ }
+
+ if ($cols) {
+ $cols->closeCursor();
+ }
+ return $fields;
+ }
+
+/**
+ * Returns the ID generated from the previous INSERT operation.
+ *
+ * @param string $source Name of the database table
+ * @param string $field Name of the ID database field. Defaults to "id"
+ * @return integer
+ */
+ public function lastInsertId($source = null, $field = 'id') {
+ $seq = $this->getSequence($source, $field);
+ return $this->_connection->lastInsertId($seq);
+ }
+
+/**
+ * Gets the associated sequence for the given table/field
+ *
+ * @param string|Model $table Either a full table name (with prefix) as a string, or a model object
+ * @param string $field Name of the ID database field. Defaults to "id"
+ * @return string The associated sequence name from the sequence map, defaults to "{$table}_{$field}_seq"
+ */
+ public function getSequence($table, $field = 'id') {
+ if (is_object($table)) {
+ $table = $this->fullTableName($table, false, false);
+ }
+ if (isset($this->_sequenceMap[$table]) && isset($this->_sequenceMap[$table][$field])) {
+ return $this->_sequenceMap[$table][$field];
+ } else {
+ return "{$table}_{$field}_seq";
+ }
+ }
+
+/**
+ * Deletes all the records in a table and drops all associated auto-increment sequences
+ *
+ * @param string|Model $table A string or model class representing the table to be truncated
+ * @param boolean $reset true for resetting the sequence, false to leave it as is.
+ * and if 1, sequences are not modified
+ * @return boolean SQL TRUNCATE TABLE statement, false if not applicable.
+ */
+ public function truncate($table, $reset = false) {
+ $table = $this->fullTableName($table, false, false);
+ if (!isset($this->_sequenceMap[$table])) {
+ $cache = $this->cacheSources;
+ $this->cacheSources = false;
+ $this->describe($table);
+ $this->cacheSources = $cache;
+ }
+ if ($this->execute('DELETE FROM ' . $this->fullTableName($table))) {
+ $schema = $this->config['schema'];
+ if (isset($this->_sequenceMap[$table]) && $reset != true) {
+ foreach ($this->_sequenceMap[$table] as $field => $sequence) {
+ list($schema, $sequence) = explode('.', $sequence);
+ $this->_execute("ALTER SEQUENCE \"{$schema}\".\"{$sequence}\" RESTART WITH 1");
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+/**
+ * Prepares field names to be quoted by parent
+ *
+ * @param string $data
+ * @return string SQL field
+ */
+ public function name($data) {
+ if (is_string($data)) {
+ $data = str_replace('"__"', '__', $data);
+ }
+ return parent::name($data);
+ }
+
+/**
+ * Generates the fields list of an SQL query.
+ *
+ * @param Model $model
+ * @param string $alias Alias table name
+ * @param mixed $fields
+ * @param boolean $quote
+ * @return array
+ */
+ public function fields(Model $model, $alias = null, $fields = array(), $quote = true) {
+ if (empty($alias)) {
+ $alias = $model->alias;
+ }
+ $fields = parent::fields($model, $alias, $fields, false);
+
+ if (!$quote) {
+ return $fields;
+ }
+ $count = count($fields);
+
+ if ($count >= 1 && !preg_match('/^\s*COUNT\(\*/', $fields[0])) {
+ $result = array();
+ for ($i = 0; $i < $count; $i++) {
+ if (!preg_match('/^.+\\(.*\\)/', $fields[$i]) && !preg_match('/\s+AS\s+/', $fields[$i])) {
+ if (substr($fields[$i], -1) == '*') {
+ if (strpos($fields[$i], '.') !== false && $fields[$i] != $alias . '.*') {
+ $build = explode('.', $fields[$i]);
+ $AssociatedModel = $model->{$build[0]};
+ } else {
+ $AssociatedModel = $model;
+ }
+
+ $_fields = $this->fields($AssociatedModel, $AssociatedModel->alias, array_keys($AssociatedModel->schema()));
+ $result = array_merge($result, $_fields);
+ continue;
+ }
+
+ $prepend = '';
+ if (strpos($fields[$i], 'DISTINCT') !== false) {
+ $prepend = 'DISTINCT ';
+ $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i]));
+ }
+
+ if (strrpos($fields[$i], '.') === false) {
+ $fields[$i] = $prepend . $this->name($alias) . '.' . $this->name($fields[$i]) . ' AS ' . $this->name($alias . '__' . $fields[$i]);
+ } else {
+ $build = explode('.', $fields[$i]);
+ $fields[$i] = $prepend . $this->name($build[0]) . '.' . $this->name($build[1]) . ' AS ' . $this->name($build[0] . '__' . $build[1]);
+ }
+ } else {
+ $fields[$i] = preg_replace_callback('/\(([\s\.\w]+)\)/', array(&$this, '_quoteFunctionField'), $fields[$i]);
+ }
+ $result[] = $fields[$i];
+ }
+ return $result;
+ }
+ return $fields;
+ }
+
+/**
+ * Auxiliary function to quote matched `(Model.fields)` from a preg_replace_callback call
+ * Quotes the fields in a function call.
+ *
+ * @param string $match matched string
+ * @return string quoted string
+ */
+ protected function _quoteFunctionField($match) {
+ $prepend = '';
+ if (strpos($match[1], 'DISTINCT') !== false) {
+ $prepend = 'DISTINCT ';
+ $match[1] = trim(str_replace('DISTINCT', '', $match[1]));
+ }
+ $constant = preg_match('/^\d+|NULL|FALSE|TRUE$/i', $match[1]);
+
+ if (!$constant && strpos($match[1], '.') === false) {
+ $match[1] = $this->name($match[1]);
+ } elseif (!$constant) {
+ $parts = explode('.', $match[1]);
+ if (!Hash::numeric($parts)) {
+ $match[1] = $this->name($match[1]);
+ }
+ }
+ return '(' . $prepend . $match[1] . ')';
+ }
+
+/**
+ * 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) {
+ $index = array();
+ $table = $this->fullTableName($model, false, false);
+ if ($table) {
+ $indexes = $this->query("SELECT c2.relname, i.indisprimary, i.indisunique, i.indisclustered, i.indisvalid, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) as statement, c2.reltablespace
+ FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i
+ WHERE c.oid = (
+ SELECT c.oid
+ FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relname ~ '^(" . $table . ")$'
+ AND pg_catalog.pg_table_is_visible(c.oid)
+ AND n.nspname ~ '^(" . $this->config['schema'] . ")$'
+ )
+ AND c.oid = i.indrelid AND i.indexrelid = c2.oid
+ ORDER BY i.indisprimary DESC, i.indisunique DESC, c2.relname", false);
+ foreach ($indexes as $i => $info) {
+ $key = array_pop($info);
+ if ($key['indisprimary']) {
+ $key['relname'] = 'PRIMARY';
+ }
+ preg_match('/\(([^\)]+)\)/', $key['statement'], $indexColumns);
+ $parsedColumn = $indexColumns[1];
+ if (strpos($indexColumns[1], ',') !== false) {
+ $parsedColumn = explode(', ', $indexColumns[1]);
+ }
+ $index[$key['relname']]['unique'] = $key['indisunique'];
+ $index[$key['relname']]['column'] = $parsedColumn;
+ }
+ }
+ return $index;
+ }
+
+/**
+ * Alter the Schema of a table.
+ *
+ * @param array $compare Results of CakeSchema::compare()
+ * @param string $table name of the table
+ * @return array
+ */
+ public function alterSchema($compare, $table = null) {
+ if (!is_array($compare)) {
+ return false;
+ }
+ $out = '';
+ $colList = array();
+ foreach ($compare as $curTable => $types) {
+ $indexes = $colList = array();
+ if (!$table || $table == $curTable) {
+ $out .= 'ALTER TABLE ' . $this->fullTableName($curTable) . " \n";
+ foreach ($types as $type => $column) {
+ if (isset($column['indexes'])) {
+ $indexes[$type] = $column['indexes'];
+ unset($column['indexes']);
+ }
+ switch ($type) {
+ case 'add':
+ foreach ($column as $field => $col) {
+ $col['name'] = $field;
+ $colList[] = 'ADD COLUMN ' . $this->buildColumn($col);
+ }
+ break;
+ case 'drop':
+ foreach ($column as $field => $col) {
+ $col['name'] = $field;
+ $colList[] = 'DROP COLUMN ' . $this->name($field);
+ }
+ break;
+ case 'change':
+ foreach ($column as $field => $col) {
+ if (!isset($col['name'])) {
+ $col['name'] = $field;
+ }
+ $fieldName = $this->name($field);
+
+ $default = isset($col['default']) ? $col['default'] : null;
+ $nullable = isset($col['null']) ? $col['null'] : null;
+ unset($col['default'], $col['null']);
+ $colList[] = 'ALTER COLUMN ' . $fieldName . ' TYPE ' . str_replace(array($fieldName, 'NOT NULL'), '', $this->buildColumn($col));
+ if (isset($nullable)) {
+ $nullable = ($nullable) ? 'DROP NOT NULL' : 'SET NOT NULL';
+ $colList[] = 'ALTER COLUMN ' . $fieldName . ' ' . $nullable;
+ }
+
+ if (isset($default)) {
+ $colList[] = 'ALTER COLUMN ' . $fieldName . ' SET DEFAULT ' . $this->value($default, $col['type']);
+ } else {
+ $colList[] = 'ALTER COLUMN ' . $fieldName . ' DROP DEFAULT';
+ }
+
+ }
+ break;
+ }
+ }
+ if (isset($indexes['drop']['PRIMARY'])) {
+ $colList[] = 'DROP CONSTRAINT ' . $curTable . '_pkey';
+ }
+ if (isset($indexes['add']['PRIMARY'])) {
+ $cols = $indexes['add']['PRIMARY']['column'];
+ if (is_array($cols)) {
+ $cols = implode(', ', $cols);
+ }
+ $colList[] = 'ADD PRIMARY KEY (' . $cols . ')';
+ }
+
+ if (!empty($colList)) {
+ $out .= "\t" . implode(",\n\t", $colList) . ";\n\n";
+ } else {
+ $out = '';
+ }
+ $out .= implode(";\n\t", $this->_alterIndexes($curTable, $indexes));
+ }
+ }
+ return $out;
+ }
+
+/**
+ * Generate PostgreSQL index alteration statements for a table.
+ *
+ * @param string $table Table to alter indexes for
+ * @param array $indexes Indexes to add and drop
+ * @return array Index alteration statements
+ */
+ protected function _alterIndexes($table, $indexes) {
+ $alter = array();
+ if (isset($indexes['drop'])) {
+ foreach ($indexes['drop'] as $name => $value) {
+ $out = 'DROP ';
+ if ($name == 'PRIMARY') {
+ continue;
+ } else {
+ $out .= 'INDEX ' . $name;
+ }
+ $alter[] = $out;
+ }
+ }
+ if (isset($indexes['add'])) {
+ foreach ($indexes['add'] as $name => $value) {
+ $out = 'CREATE ';
+ if ($name == 'PRIMARY') {
+ continue;
+ } else {
+ if (!empty($value['unique'])) {
+ $out .= 'UNIQUE ';
+ }
+ $out .= 'INDEX ';
+ }
+ if (is_array($value['column'])) {
+ $out .= $name . ' ON ' . $table . ' (' . implode(', ', array_map(array(&$this, 'name'), $value['column'])) . ')';
+ } else {
+ $out .= $name . ' ON ' . $table . ' (' . $this->name($value['column']) . ')';
+ }
+ $alter[] = $out;
+ }
+ }
+ return $alter;
+ }
+
+/**
+ * Returns a limit statement in the correct format for the particular database.
+ *
+ * @param integer $limit Limit of results returned
+ * @param integer $offset Offset from which to start results
+ * @return string SQL limit/offset statement
+ */
+ public function limit($limit, $offset = null) {
+ if ($limit) {
+ $rt = '';
+ if (!strpos(strtolower($limit), 'limit') || strpos(strtolower($limit), 'limit') === 0) {
+ $rt = ' LIMIT';
+ }
+
+ $rt .= ' ' . $limit;
+ if ($offset) {
+ $rt .= ' OFFSET ' . $offset;
+ }
+
+ return $rt;
+ }
+ return null;
+ }
+
+/**
+ * Converts database-layer column types to basic types
+ *
+ * @param string $real Real database-layer column type (i.e. "varchar(255)")
+ * @return string Abstract column type (i.e. "string")
+ */
+ public function column($real) {
+ if (is_array($real)) {
+ $col = $real['name'];
+ if (isset($real['limit'])) {
+ $col .= '(' . $real['limit'] . ')';
+ }
+ return $col;
+ }
+
+ $col = str_replace(')', '', $real);
+ $limit = null;
+
+ if (strpos($col, '(') !== false) {
+ list($col, $limit) = explode('(', $col);
+ }
+
+ $floats = array(
+ 'float', 'float4', 'float8', 'double', 'double precision', 'decimal', 'real', 'numeric'
+ );
+
+ switch (true) {
+ case (in_array($col, array('date', 'time', 'inet', 'boolean'))):
+ return $col;
+ case (strpos($col, 'timestamp') !== false):
+ return 'datetime';
+ case (strpos($col, 'time') === 0):
+ return 'time';
+ case (strpos($col, 'int') !== false && $col != 'interval'):
+ return 'integer';
+ case (strpos($col, 'char') !== false || $col == 'uuid'):
+ return 'string';
+ case (strpos($col, 'text') !== false):
+ return 'text';
+ case (strpos($col, 'bytea') !== false):
+ return 'binary';
+ case (in_array($col, $floats)):
+ return 'float';
+ default:
+ return 'text';
+ break;
+ }
+ }
+
+/**
+ * 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 integer An integer representing the length of the column
+ */
+ public function length($real) {
+ $col = str_replace(array(')', 'unsigned'), '', $real);
+ $limit = null;
+
+ if (strpos($col, '(') !== false) {
+ list($col, $limit) = explode('(', $col);
+ }
+ if ($col == 'uuid') {
+ return 36;
+ }
+ if ($limit != null) {
+ return intval($limit);
+ }
+ return null;
+ }
+
+/**
+ * resultSet method
+ *
+ * @param array $results
+ * @return void
+ */
+ public function resultSet(&$results) {
+ $this->map = array();
+ $numFields = $results->columnCount();
+ $index = 0;
+ $j = 0;
+
+ while ($j < $numFields) {
+ $column = $results->getColumnMeta($j);
+ if (strpos($column['name'], '__')) {
+ list($table, $name) = explode('__', $column['name']);
+ $this->map[$index++] = array($table, $name, $column['native_type']);
+ } else {
+ $this->map[$index++] = array(0, $column['name'], $column['native_type']);
+ }
+ $j++;
+ }
+ }
+
+/**
+ * Fetches the next row from the current result set
+ *
+ * @return array
+ */
+ public function fetchResult() {
+ if ($row = $this->_result->fetch(PDO::FETCH_NUM)) {
+ $resultRow = array();
+
+ foreach ($this->map as $index => $meta) {
+ list($table, $column, $type) = $meta;
+
+ switch ($type) {
+ case 'bool':
+ $resultRow[$table][$column] = is_null($row[$index]) ? null : $this->boolean($row[$index]);
+ break;
+ case 'binary':
+ case 'bytea':
+ $resultRow[$table][$column] = is_null($row[$index]) ? null : stream_get_contents($row[$index]);
+ break;
+ default:
+ $resultRow[$table][$column] = $row[$index];
+ break;
+ }
+ }
+ return $resultRow;
+ } else {
+ $this->_result->closeCursor();
+ return false;
+ }
+ }
+
+/**
+ * Translates between PHP boolean values and PostgreSQL boolean values
+ *
+ * @param mixed $data Value to be translated
+ * @param boolean $quote true to quote a boolean to be used in a query, false to return the boolean value
+ * @return boolean Converted boolean value
+ */
+ public function boolean($data, $quote = false) {
+ switch (true) {
+ case ($data === true || $data === false):
+ $result = $data;
+ break;
+ case ($data === 't' || $data === 'f'):
+ $result = ($data === 't');
+ break;
+ case ($data === 'true' || $data === 'false'):
+ $result = ($data === 'true');
+ break;
+ case ($data === 'TRUE' || $data === 'FALSE'):
+ $result = ($data === 'TRUE');
+ break;
+ default:
+ $result = (bool)$data;
+ break;
+ }
+
+ if ($quote) {
+ return ($result) ? 'TRUE' : 'FALSE';
+ }
+ return (bool)$result;
+ }
+
+/**
+ * Sets the database encoding
+ *
+ * @param mixed $enc Database encoding
+ * @return boolean True on success, false on failure
+ */
+ public function setEncoding($enc) {
+ return $this->_execute('SET NAMES ' . $this->value($enc)) !== false;
+ }
+
+/**
+ * Gets the database encoding
+ *
+ * @return string The database encoding
+ */
+ public function getEncoding() {
+ $result = $this->_execute('SHOW client_encoding')->fetch();
+ if ($result === false) {
+ return false;
+ }
+ return (isset($result['client_encoding'])) ? $result['client_encoding'] : false;
+ }
+
+/**
+ * Generate a Postgres-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) {
+ $col = $this->columns[$column['type']];
+ if (!isset($col['length']) && !isset($col['limit'])) {
+ unset($column['length']);
+ }
+ $out = preg_replace('/integer\([0-9]+\)/', 'integer', parent::buildColumn($column));
+ $out = str_replace('integer serial', 'serial', $out);
+ if (strpos($out, 'timestamp DEFAULT')) {
+ if (isset($column['null']) && $column['null']) {
+ $out = str_replace('DEFAULT NULL', '', $out);
+ } else {
+ $out = str_replace('DEFAULT NOT NULL', '', $out);
+ }
+ }
+ if (strpos($out, 'DEFAULT DEFAULT')) {
+ if (isset($column['null']) && $column['null']) {
+ $out = str_replace('DEFAULT DEFAULT', 'DEFAULT NULL', $out);
+ } elseif (in_array($column['type'], array('integer', 'float'))) {
+ $out = str_replace('DEFAULT DEFAULT', 'DEFAULT 0', $out);
+ } elseif ($column['type'] == 'boolean') {
+ $out = str_replace('DEFAULT DEFAULT', 'DEFAULT FALSE', $out);
+ }
+ }
+ return $out;
+ }
+
+/**
+ * Format indexes for create table
+ *
+ * @param array $indexes
+ * @param string $table
+ * @return string
+ */
+ public function buildIndex($indexes, $table = null) {
+ $join = array();
+ if (!is_array($indexes)) {
+ return array();
+ }
+ foreach ($indexes as $name => $value) {
+ if ($name == 'PRIMARY') {
+ $out = 'PRIMARY KEY (' . $this->name($value['column']) . ')';
+ } else {
+ $out = 'CREATE ';
+ if (!empty($value['unique'])) {
+ $out .= 'UNIQUE ';
+ }
+ if (is_array($value['column'])) {
+ $value['column'] = implode(', ', array_map(array(&$this, 'name'), $value['column']));
+ } else {
+ $value['column'] = $this->name($value['column']);
+ }
+ $out .= "INDEX {$name} ON {$table}({$value['column']});";
+ }
+ $join[] = $out;
+ }
+ return $join;
+ }
+
+/**
+ * Overrides DboSource::renderStatement to handle schema generation with Postgres-style indexes
+ *
+ * @param string $type
+ * @param array $data
+ * @return string
+ */
+ public function renderStatement($type, $data) {
+ switch (strtolower($type)) {
+ case 'schema':
+ extract($data);
+
+ foreach ($indexes as $i => $index) {
+ if (preg_match('/PRIMARY KEY/', $index)) {
+ unset($indexes[$i]);
+ $columns[] = $index;
+ break;
+ }
+ }
+ $join = array('columns' => ",\n\t", 'indexes' => "\n");
+
+ foreach (array('columns', 'indexes') as $var) {
+ if (is_array(${$var})) {
+ ${$var} = implode($join[$var], array_filter(${$var}));
+ }
+ }
+ return "CREATE TABLE {$table} (\n\t{$columns}\n);\n{$indexes}";
+ break;
+ default:
+ return parent::renderStatement($type, $data);
+ break;
+ }
+ }
+
+/**
+ * Gets the schema name
+ *
+ * @return string The schema name
+ */
+ public function getSchemaName() {
+ return $this->config['schema'];
+ }
+
+/**
+ * Check if the server support nested transactions
+ *
+ * @return boolean
+ */
+ public function nestedTransactionSupported() {
+ return $this->useNestedTransactions && version_compare($this->getVersion(), '8.0', '>=');
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlite.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlite.php
new file mode 100644
index 0000000..63ed603
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlite.php
@@ -0,0 +1,571 @@
+<?php
+/**
+ * SQLite layer for DBO
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Datasource.Database
+ * @since CakePHP(tm) v 0.9.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('DboSource', 'Model/Datasource');
+App::uses('String', 'Utility');
+
+/**
+ * DBO implementation for the SQLite3 DBMS.
+ *
+ * A DboSource adapter for SQLite 3 using PDO
+ *
+ * @package Cake.Model.Datasource.Database
+ */
+class Sqlite extends DboSource {
+
+/**
+ * Datasource Description
+ *
+ * @var string
+ */
+ public $description = "SQLite DBO Driver";
+
+/**
+ * Quote Start
+ *
+ * @var string
+ */
+ public $startQuote = '"';
+
+/**
+ * Quote End
+ *
+ * @var string
+ */
+ public $endQuote = '"';
+
+/**
+ * Base configuration settings for SQLite3 driver
+ *
+ * @var array
+ */
+ protected $_baseConfig = array(
+ 'persistent' => false,
+ 'database' => null
+ );
+
+/**
+ * SQLite3 column definition
+ *
+ * @var array
+ */
+ public $columns = array(
+ 'primary_key' => array('name' => 'integer primary key autoincrement'),
+ 'string' => array('name' => 'varchar', 'limit' => '255'),
+ 'text' => array('name' => 'text'),
+ 'integer' => array('name' => 'integer', 'limit' => null, 'formatter' => 'intval'),
+ 'float' => array('name' => 'float', 'formatter' => 'floatval'),
+ 'datetime' => array('name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
+ 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
+ 'time' => array('name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'),
+ 'date' => array('name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'),
+ 'binary' => array('name' => 'blob'),
+ 'boolean' => array('name' => 'boolean')
+ );
+
+/**
+ * List of engine specific additional field parameters used on table creating
+ *
+ * @var array
+ */
+ public $fieldParameters = array(
+ 'collate' => array(
+ 'value' => 'COLLATE',
+ 'quote' => false,
+ 'join' => ' ',
+ 'column' => 'Collate',
+ 'position' => 'afterDefault',
+ 'options' => array(
+ 'BINARY', 'NOCASE', 'RTRIM'
+ )
+ ),
+ );
+
+/**
+ * Connects to the database using config['database'] as a filename.
+ *
+ * @return boolean
+ * @throws MissingConnectionException
+ */
+ public function connect() {
+ $config = $this->config;
+ $flags = array(
+ PDO::ATTR_PERSISTENT => $config['persistent'],
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+ );
+ try {
+ $this->_connection = new PDO('sqlite:' . $config['database'], null, null, $flags);
+ $this->connected = true;
+ } catch(PDOException $e) {
+ throw new MissingConnectionException(array('class' => $e->getMessage()));
+ }
+ return $this->connected;
+ }
+
+/**
+ * Check whether the SQLite extension is installed/loaded
+ *
+ * @return boolean
+ */
+ public function enabled() {
+ return in_array('sqlite', PDO::getAvailableDrivers());
+ }
+
+/**
+ * Returns an array of tables in the database. If there are no tables, an error is raised and the application exits.
+ *
+ * @param mixed $data
+ * @return array Array of table names in the database
+ */
+ public function listSources($data = null) {
+ $cache = parent::listSources();
+ if ($cache != null) {
+ return $cache;
+ }
+
+ $result = $this->fetchAll("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;", false);
+
+ if (!$result || empty($result)) {
+ return array();
+ } else {
+ $tables = array();
+ foreach ($result as $table) {
+ $tables[] = $table[0]['name'];
+ }
+ parent::listSources($tables);
+ return $tables;
+ }
+ }
+
+/**
+ * Returns an array of the fields in given table name.
+ *
+ * @param Model|string $model Either the model or table name you want described.
+ * @return array Fields in table. Keys are name and type
+ */
+ public function describe($model) {
+ $table = $this->fullTableName($model, false, false);
+ $cache = parent::describe($table);
+ if ($cache != null) {
+ return $cache;
+ }
+ $fields = array();
+ $result = $this->_execute(
+ 'PRAGMA table_info(' . $this->value($table, 'string') . ')'
+ );
+
+ foreach ($result as $column) {
+ $column = (array)$column;
+ $default = ($column['dflt_value'] === 'NULL') ? null : trim($column['dflt_value'], "'");
+
+ $fields[$column['name']] = array(
+ 'type' => $this->column($column['type']),
+ 'null' => !$column['notnull'],
+ 'default' => $default,
+ 'length' => $this->length($column['type'])
+ );
+ if ($column['pk'] == 1) {
+ $fields[$column['name']]['key'] = $this->index['PRI'];
+ $fields[$column['name']]['null'] = false;
+ if (empty($fields[$column['name']]['length'])) {
+ $fields[$column['name']]['length'] = 11;
+ }
+ }
+ }
+
+ $result->closeCursor();
+ $this->_cacheDescription($table, $fields);
+ return $fields;
+ }
+
+/**
+ * Generates and executes an SQL UPDATE statement for given model, fields, and values.
+ *
+ * @param Model $model
+ * @param array $fields
+ * @param array $values
+ * @param mixed $conditions
+ * @return array
+ */
+ public function update(Model $model, $fields = array(), $values = null, $conditions = null) {
+ if (empty($values) && !empty($fields)) {
+ foreach ($fields as $field => $value) {
+ if (strpos($field, $model->alias . '.') !== false) {
+ unset($fields[$field]);
+ $field = str_replace($model->alias . '.', "", $field);
+ $field = str_replace($model->alias . '.', "", $field);
+ $fields[$field] = $value;
+ }
+ }
+ }
+ return parent::update($model, $fields, $values, $conditions);
+ }
+
+/**
+ * Deletes all the records in a table and resets the count of the auto-incrementing
+ * primary key, where applicable.
+ *
+ * @param string|Model $table A string or model class representing the table to be truncated
+ * @return boolean SQL TRUNCATE TABLE statement, false if not applicable.
+ */
+ public function truncate($table) {
+ $this->_execute('DELETE FROM sqlite_sequence where name=' . $this->startQuote . $this->fullTableName($table, false, false) . $this->endQuote);
+ return $this->execute('DELETE FROM ' . $this->fullTableName($table));
+ }
+
+/**
+ * Converts database-layer column types to basic types
+ *
+ * @param string $real Real database-layer column type (i.e. "varchar(255)")
+ * @return string Abstract column type (i.e. "string")
+ */
+ public function column($real) {
+ if (is_array($real)) {
+ $col = $real['name'];
+ if (isset($real['limit'])) {
+ $col .= '(' . $real['limit'] . ')';
+ }
+ return $col;
+ }
+
+ $col = strtolower(str_replace(')', '', $real));
+ $limit = null;
+ @list($col, $limit) = explode('(', $col);
+
+ if (in_array($col, array('text', 'integer', 'float', 'boolean', 'timestamp', 'date', 'datetime', 'time'))) {
+ return $col;
+ }
+ if (strpos($col, 'char') !== false) {
+ return 'string';
+ }
+ if (in_array($col, array('blob', 'clob'))) {
+ return 'binary';
+ }
+ if (strpos($col, 'numeric') !== false || strpos($col, 'decimal') !== false) {
+ return 'float';
+ }
+ return 'text';
+ }
+
+/**
+ * Generate ResultSet
+ *
+ * @param mixed $results
+ * @return void
+ */
+ public function resultSet($results) {
+ $this->results = $results;
+ $this->map = array();
+ $numFields = $results->columnCount();
+ $index = 0;
+ $j = 0;
+
+ //PDO::getColumnMeta is experimental and does not work with sqlite3,
+ // so try to figure it out based on the querystring
+ $querystring = $results->queryString;
+ if (stripos($querystring, 'SELECT') === 0) {
+ $last = strripos($querystring, 'FROM');
+ if ($last !== false) {
+ $selectpart = substr($querystring, 7, $last - 8);
+ $selects = String::tokenize($selectpart, ',', '(', ')');
+ }
+ } elseif (strpos($querystring, 'PRAGMA table_info') === 0) {
+ $selects = array('cid', 'name', 'type', 'notnull', 'dflt_value', 'pk');
+ } elseif (strpos($querystring, 'PRAGMA index_list') === 0) {
+ $selects = array('seq', 'name', 'unique');
+ } elseif (strpos($querystring, 'PRAGMA index_info') === 0) {
+ $selects = array('seqno', 'cid', 'name');
+ }
+ while ($j < $numFields) {
+ if (!isset($selects[$j])) {
+ $j++;
+ continue;
+ }
+ if (preg_match('/\bAS\s+(.*)/i', $selects[$j], $matches)) {
+ $columnName = trim($matches[1], '"');
+ } else {
+ $columnName = trim(str_replace('"', '', $selects[$j]));
+ }
+
+ if (strpos($selects[$j], 'DISTINCT') === 0) {
+ $columnName = str_ireplace('DISTINCT', '', $columnName);
+ }
+
+ $metaType = false;
+ try {
+ $metaData = (array)$results->getColumnMeta($j);
+ if (!empty($metaData['sqlite:decl_type'])) {
+ $metaType = trim($metaData['sqlite:decl_type']);
+ }
+ } catch (Exception $e) {
+ }
+
+ if (strpos($columnName, '.')) {
+ $parts = explode('.', $columnName);
+ $this->map[$index++] = array(trim($parts[0]), trim($parts[1]), $metaType);
+ } else {
+ $this->map[$index++] = array(0, $columnName, $metaType);
+ }
+ $j++;
+ }
+ }
+
+/**
+ * Fetches the next row from the current result set
+ *
+ * @return mixed array with results fetched and mapped to column names or false if there is no results left to fetch
+ */
+ public function fetchResult() {
+ if ($row = $this->_result->fetch(PDO::FETCH_NUM)) {
+ $resultRow = array();
+ foreach ($this->map as $col => $meta) {
+ list($table, $column, $type) = $meta;
+ $resultRow[$table][$column] = $row[$col];
+ if ($type == 'boolean' && !is_null($row[$col])) {
+ $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]);
+ }
+ }
+ return $resultRow;
+ } else {
+ $this->_result->closeCursor();
+ return false;
+ }
+ }
+
+/**
+ * Returns a limit statement in the correct format for the particular database.
+ *
+ * @param integer $limit Limit of results returned
+ * @param integer $offset Offset from which to start results
+ * @return string SQL limit/offset statement
+ */
+ public function limit($limit, $offset = null) {
+ if ($limit) {
+ $rt = '';
+ if (!strpos(strtolower($limit), 'limit') || strpos(strtolower($limit), 'limit') === 0) {
+ $rt = ' LIMIT';
+ }
+ $rt .= ' ' . $limit;
+ if ($offset) {
+ $rt .= ' OFFSET ' . $offset;
+ }
+ return $rt;
+ }
+ return null;
+ }
+
+/**
+ * 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;
+ $column = array_merge(array('null' => true), $column);
+ extract($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;
+ }
+
+ if (isset($column['key']) && $column['key'] == 'primary' && $type == 'integer') {
+ return $this->name($name) . ' ' . $this->columns['primary_key']['name'];
+ }
+ return parent::buildColumn($column);
+ }
+
+/**
+ * Sets the database encoding
+ *
+ * @param string $enc Database encoding
+ * @return boolean
+ */
+ public function setEncoding($enc) {
+ if (!in_array($enc, array("UTF-8", "UTF-16", "UTF-16le", "UTF-16be"))) {
+ return false;
+ }
+ return $this->_execute("PRAGMA encoding = \"{$enc}\"") !== false;
+ }
+
+/**
+ * Gets the database encoding
+ *
+ * @return string The database encoding
+ */
+ public function getEncoding() {
+ return $this->fetchRow('PRAGMA encoding');
+ }
+
+/**
+ * Removes redundant primary key indexes, as they are handled in the column def of the key.
+ *
+ * @param array $indexes
+ * @param string $table
+ * @return string
+ */
+ public function buildIndex($indexes, $table = null) {
+ $join = array();
+
+ $table = str_replace('"', '', $table);
+ list($dbname, $table) = explode('.', $table);
+ $dbname = $this->name($dbname);
+
+ foreach ($indexes as $name => $value) {
+
+ if ($name == 'PRIMARY') {
+ continue;
+ }
+ $out = 'CREATE ';
+
+ if (!empty($value['unique'])) {
+ $out .= 'UNIQUE ';
+ }
+ if (is_array($value['column'])) {
+ $value['column'] = join(', ', array_map(array(&$this, 'name'), $value['column']));
+ } else {
+ $value['column'] = $this->name($value['column']);
+ }
+ $t = trim($table, '"');
+ $indexname = $this->name($t . '_' . $name);
+ $table = $this->name($table);
+ $out .= "INDEX {$dbname}.{$indexname} ON {$table}({$value['column']});";
+ $join[] = $out;
+ }
+ return $join;
+ }
+
+/**
+ * Overrides DboSource::index to handle SQLite index introspection
+ * Returns an array of the indexes in given table name.
+ *
+ * @param string $model Name of model to inspect
+ * @return array Fields in table. Keys are column and unique
+ */
+ public function index($model) {
+ $index = array();
+ $table = $this->fullTableName($model, false, false);
+ if ($table) {
+ $indexes = $this->query('PRAGMA index_list(' . $table . ')');
+
+ if (is_bool($indexes)) {
+ return array();
+ }
+ foreach ($indexes as $i => $info) {
+ $key = array_pop($info);
+ $keyInfo = $this->query('PRAGMA index_info("' . $key['name'] . '")');
+ foreach ($keyInfo as $keyCol) {
+ if (!isset($index[$key['name']])) {
+ $col = array();
+ if (preg_match('/autoindex/', $key['name'])) {
+ $key['name'] = 'PRIMARY';
+ }
+ $index[$key['name']]['column'] = $keyCol[0]['name'];
+ $index[$key['name']]['unique'] = intval($key['unique'] == 1);
+ } else {
+ if (!is_array($index[$key['name']]['column'])) {
+ $col[] = $index[$key['name']]['column'];
+ }
+ $col[] = $keyCol[0]['name'];
+ $index[$key['name']]['column'] = $col;
+ }
+ }
+ }
+ }
+ return $index;
+ }
+
+/**
+ * Overrides DboSource::renderStatement to handle schema generation with SQLite-style indexes
+ *
+ * @param string $type
+ * @param array $data
+ * @return string
+ */
+ public function renderStatement($type, $data) {
+ switch (strtolower($type)) {
+ case 'schema':
+ extract($data);
+ if (is_array($columns)) {
+ $columns = "\t" . join(",\n\t", array_filter($columns));
+ }
+ if (is_array($indexes)) {
+ $indexes = "\t" . join("\n\t", array_filter($indexes));
+ }
+ return "CREATE TABLE {$table} (\n{$columns});\n{$indexes}";
+ break;
+ default:
+ return parent::renderStatement($type, $data);
+ break;
+ }
+ }
+
+/**
+ * PDO deals in objects, not resources, so overload accordingly.
+ *
+ * @return boolean
+ */
+ public function hasResult() {
+ return is_object($this->_result);
+ }
+
+/**
+ * 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 = '';
+ foreach ($schema->tables as $curTable => $columns) {
+ if (!$table || $table == $curTable) {
+ $out .= 'DROP TABLE IF EXISTS ' . $this->fullTableName($curTable) . ";\n";
+ }
+ }
+ return $out;
+ }
+
+/**
+ * Gets the schema name
+ *
+ * @return string The schema name
+ */
+ public function getSchemaName() {
+ return "main"; // Sqlite Datasource does not support multidb
+ }
+
+/**
+ * Check if the server support nested transactions
+ *
+ * @return boolean
+ */
+ public function nestedTransactionSupported() {
+ return $this->useNestedTransactions && version_compare($this->getVersion(), '3.6.8', '>=');
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlserver.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlserver.php
new file mode 100644
index 0000000..b23e59a
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Database/Sqlserver.php
@@ -0,0 +1,783 @@
+<?php
+/**
+ * MS SQL Server layer for DBO
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Datasource.Database
+ * @since CakePHP(tm) v 0.10.5.1790
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('DboSource', 'Model/Datasource');
+
+/**
+ * Dbo driver for SQLServer
+ *
+ * A Dbo driver for SQLServer 2008 and higher. Requires the `sqlsrv`
+ * and `pdo_sqlsrv` extensions to be enabled.
+ *
+ * @package Cake.Model.Datasource.Database
+ */
+class Sqlserver extends DboSource {
+
+/**
+ * Driver description
+ *
+ * @var string
+ */
+ public $description = "SQL Server DBO Driver";
+
+/**
+ * Starting quote character for quoted identifiers
+ *
+ * @var string
+ */
+ public $startQuote = "[";
+
+/**
+ * Ending quote character for quoted identifiers
+ *
+ * @var string
+ */
+ public $endQuote = "]";
+
+/**
+ * Creates a map between field aliases and numeric indexes. Workaround for the
+ * SQL Server driver's 30-character column name limitation.
+ *
+ * @var array
+ */
+ protected $_fieldMappings = array();
+
+/**
+ * Storing the last affected value
+ *
+ * @var mixed
+ */
+ protected $_lastAffected = false;
+
+/**
+ * Base configuration settings for MS SQL driver
+ *
+ * @var array
+ */
+ protected $_baseConfig = array(
+ 'persistent' => true,
+ 'host' => 'localhost\SQLEXPRESS',
+ 'login' => '',
+ 'password' => '',
+ 'database' => 'cake',
+ 'schema' => '',
+ );
+
+/**
+ * MS SQL column definition
+ *
+ * @var array
+ */
+ public $columns = array(
+ 'primary_key' => array('name' => 'IDENTITY (1, 1) NOT NULL'),
+ 'string' => array('name' => 'nvarchar', 'limit' => '255'),
+ 'text' => array('name' => 'nvarchar', 'limit' => 'MAX'),
+ 'integer' => array('name' => 'int', 'formatter' => 'intval'),
+ 'float' => array('name' => 'numeric', 'formatter' => 'floatval'),
+ 'datetime' => array('name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
+ 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
+ 'time' => array('name' => 'datetime', 'format' => 'H:i:s', 'formatter' => 'date'),
+ 'date' => array('name' => 'datetime', 'format' => 'Y-m-d', 'formatter' => 'date'),
+ 'binary' => array('name' => 'varbinary'),
+ 'boolean' => array('name' => 'bit')
+ );
+
+/**
+ * Magic column name used to provide pagination support for SQLServer 2008
+ * which lacks proper limit/offset support.
+ */
+ const ROW_COUNTER = '_cake_page_rownum_';
+
+/**
+ * Connects to the database using options in the given configuration array.
+ *
+ * @return boolean True if the database could be connected, else false
+ * @throws MissingConnectionException
+ */
+ public function connect() {
+ $config = $this->config;
+ $this->connected = false;
+ try {
+ $flags = array(
+ PDO::ATTR_PERSISTENT => $config['persistent'],
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+ );
+ if (!empty($config['encoding'])) {
+ $flags[PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
+ }
+ $this->_connection = new PDO(
+ "sqlsrv:server={$config['host']};Database={$config['database']}",
+ $config['login'],
+ $config['password'],
+ $flags
+ );
+ $this->connected = true;
+ } catch (PDOException $e) {
+ throw new MissingConnectionException(array('class' => $e->getMessage()));
+ }
+
+ return $this->connected;
+ }
+
+/**
+ * Check that PDO SQL Server is installed/loaded
+ *
+ * @return boolean
+ */
+ public function enabled() {
+ return in_array('sqlsrv', PDO::getAvailableDrivers());
+ }
+
+/**
+ * Returns an array of sources (tables) in the database.
+ *
+ * @param mixed $data
+ * @return array Array of table names in the database
+ */
+ public function listSources($data = null) {
+ $cache = parent::listSources();
+ if ($cache !== null) {
+ return $cache;
+ }
+ $result = $this->_execute("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES");
+
+ if (!$result) {
+ $result->closeCursor();
+ return array();
+ } else {
+ $tables = array();
+
+ while ($line = $result->fetch(PDO::FETCH_NUM)) {
+ $tables[] = $line[0];
+ }
+
+ $result->closeCursor();
+ parent::listSources($tables);
+ return $tables;
+ }
+ }
+
+/**
+ * Returns an array of the fields in given table name.
+ *
+ * @param Model|string $model Model object to describe, or a string table name.
+ * @return array Fields in table. Keys are name and type
+ * @throws CakeException
+ */
+ public function describe($model) {
+ $table = $this->fullTableName($model, false);
+ $cache = parent::describe($table);
+ if ($cache != null) {
+ return $cache;
+ }
+ $fields = array();
+ $table = $this->fullTableName($model, false);
+ $cols = $this->_execute(
+ "SELECT
+ COLUMN_NAME as Field,
+ DATA_TYPE as Type,
+ COL_LENGTH('" . $table . "', COLUMN_NAME) as Length,
+ IS_NULLABLE As [Null],
+ COLUMN_DEFAULT as [Default],
+ COLUMNPROPERTY(OBJECT_ID('" . $table . "'), COLUMN_NAME, 'IsIdentity') as [Key],
+ NUMERIC_SCALE as Size
+ FROM INFORMATION_SCHEMA.COLUMNS
+ WHERE TABLE_NAME = '" . $table . "'"
+ );
+ if (!$cols) {
+ throw new CakeException(__d('cake_dev', 'Could not describe table for %s', $table));
+ }
+
+ while ($column = $cols->fetch(PDO::FETCH_OBJ)) {
+ $field = $column->Field;
+ $fields[$field] = array(
+ 'type' => $this->column($column),
+ 'null' => ($column->Null === 'YES' ? true : false),
+ 'default' => preg_replace("/^[(]{1,2}'?([^')]*)?'?[)]{1,2}$/", "$1", $column->Default),
+ 'length' => $this->length($column),
+ 'key' => ($column->Key == '1') ? 'primary' : false
+ );
+
+ if ($fields[$field]['default'] === 'null') {
+ $fields[$field]['default'] = null;
+ } else {
+ $this->value($fields[$field]['default'], $fields[$field]['type']);
+ }
+
+ if ($fields[$field]['key'] !== false && $fields[$field]['type'] == 'integer') {
+ $fields[$field]['length'] = 11;
+ } elseif ($fields[$field]['key'] === false) {
+ unset($fields[$field]['key']);
+ }
+ if (in_array($fields[$field]['type'], array('date', 'time', 'datetime', 'timestamp'))) {
+ $fields[$field]['length'] = null;
+ }
+ if ($fields[$field]['type'] == 'float' && !empty($column->Size)) {
+ $fields[$field]['length'] = $fields[$field]['length'] . ',' . $column->Size;
+ }
+ }
+ $this->_cacheDescription($table, $fields);
+ $cols->closeCursor();
+ return $fields;
+ }
+
+/**
+ * Generates the fields list of an SQL query.
+ *
+ * @param Model $model
+ * @param string $alias Alias table name
+ * @param array $fields
+ * @param boolean $quote
+ * @return array
+ */
+ public function fields(Model $model, $alias = null, $fields = array(), $quote = true) {
+ if (empty($alias)) {
+ $alias = $model->alias;
+ }
+ $fields = parent::fields($model, $alias, $fields, false);
+ $count = count($fields);
+
+ if ($count >= 1 && strpos($fields[0], 'COUNT(*)') === false) {
+ $result = array();
+ for ($i = 0; $i < $count; $i++) {
+ $prepend = '';
+
+ if (strpos($fields[$i], 'DISTINCT') !== false) {
+ $prepend = 'DISTINCT ';
+ $fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i]));
+ }
+
+ if (!preg_match('/\s+AS\s+/i', $fields[$i])) {
+ if (substr($fields[$i], -1) == '*') {
+ if (strpos($fields[$i], '.') !== false && $fields[$i] != $alias . '.*') {
+ $build = explode('.', $fields[$i]);
+ $AssociatedModel = $model->{$build[0]};
+ } else {
+ $AssociatedModel = $model;
+ }
+
+ $_fields = $this->fields($AssociatedModel, $AssociatedModel->alias, array_keys($AssociatedModel->schema()));
+ $result = array_merge($result, $_fields);
+ continue;
+ }
+
+ if (strpos($fields[$i], '.') === false) {
+ $this->_fieldMappings[$alias . '__' . $fields[$i]] = $alias . '.' . $fields[$i];
+ $fieldName = $this->name($alias . '.' . $fields[$i]);
+ $fieldAlias = $this->name($alias . '__' . $fields[$i]);
+ } else {
+ $build = explode('.', $fields[$i]);
+ $this->_fieldMappings[$build[0] . '__' . $build[1]] = $fields[$i];
+ $fieldName = $this->name($build[0] . '.' . $build[1]);
+ $fieldAlias = $this->name(preg_replace("/^\[(.+)\]$/", "$1", $build[0]) . '__' . $build[1]);
+ }
+ if ($model->getColumnType($fields[$i]) == 'datetime') {
+ $fieldName = "CONVERT(VARCHAR(20), {$fieldName}, 20)";
+ }
+ $fields[$i] = "{$fieldName} AS {$fieldAlias}";
+ }
+ $result[] = $prepend . $fields[$i];
+ }
+ return $result;
+ } else {
+ return $fields;
+ }
+ }
+
+/**
+ * Generates and executes an SQL INSERT statement for given model, fields, and values.
+ * Removes Identity (primary key) column from update data before returning to parent, if
+ * value is empty.
+ *
+ * @param Model $model
+ * @param array $fields
+ * @param array $values
+ * @return array
+ */
+ public function create(Model $model, $fields = null, $values = null) {
+ if (!empty($values)) {
+ $fields = array_combine($fields, $values);
+ }
+ $primaryKey = $this->_getPrimaryKey($model);
+
+ if (array_key_exists($primaryKey, $fields)) {
+ if (empty($fields[$primaryKey])) {
+ unset($fields[$primaryKey]);
+ } else {
+ $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($model) . ' ON');
+ }
+ }
+ $result = parent::create($model, array_keys($fields), array_values($fields));
+ if (array_key_exists($primaryKey, $fields) && !empty($fields[$primaryKey])) {
+ $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($model) . ' OFF');
+ }
+ return $result;
+ }
+
+/**
+ * Generates and executes an SQL UPDATE statement for given model, fields, and values.
+ * Removes Identity (primary key) column from update data before returning to parent.
+ *
+ * @param Model $model
+ * @param array $fields
+ * @param array $values
+ * @param mixed $conditions
+ * @return array
+ */
+ public function update(Model $model, $fields = array(), $values = null, $conditions = null) {
+ if (!empty($values)) {
+ $fields = array_combine($fields, $values);
+ }
+ if (isset($fields[$model->primaryKey])) {
+ unset($fields[$model->primaryKey]);
+ }
+ if (empty($fields)) {
+ return true;
+ }
+ return parent::update($model, array_keys($fields), array_values($fields), $conditions);
+ }
+
+/**
+ * Returns a limit statement in the correct format for the particular database.
+ *
+ * @param integer $limit Limit of results returned
+ * @param integer $offset Offset from which to start results
+ * @return string SQL limit/offset statement
+ */
+ public function limit($limit, $offset = null) {
+ if ($limit) {
+ $rt = '';
+ if (!strpos(strtolower($limit), 'top') || strpos(strtolower($limit), 'top') === 0) {
+ $rt = ' TOP';
+ }
+ $rt .= ' ' . $limit;
+ if (is_int($offset) && $offset > 0) {
+ $rt = ' OFFSET ' . intval($offset) . ' ROWS FETCH FIRST ' . intval($limit) . ' ROWS ONLY';
+ }
+ return $rt;
+ }
+ return null;
+ }
+
+/**
+ * Converts database-layer column types to basic types
+ *
+ * @param mixed $real Either the string value of the fields type.
+ * or the Result object from Sqlserver::describe()
+ * @return string Abstract column type (i.e. "string")
+ */
+ public function column($real) {
+ $limit = null;
+ $col = $real;
+ if (is_object($real) && isset($real->Field)) {
+ $limit = $real->Length;
+ $col = $real->Type;
+ }
+
+ if ($col == 'datetime2') {
+ return 'datetime';
+ }
+ if (in_array($col, array('date', 'time', 'datetime', 'timestamp'))) {
+ return $col;
+ }
+ if ($col == 'bit') {
+ return 'boolean';
+ }
+ if (strpos($col, 'int') !== false) {
+ return 'integer';
+ }
+ if (strpos($col, 'char') !== false && $limit == -1) {
+ return 'text';
+ }
+ if (strpos($col, 'char') !== false) {
+ return 'string';
+ }
+ if (strpos($col, 'text') !== false) {
+ return 'text';
+ }
+ if (strpos($col, 'binary') !== false || $col == 'image') {
+ return 'binary';
+ }
+ if (in_array($col, array('float', 'real', 'decimal', 'numeric'))) {
+ return 'float';
+ }
+ return 'text';
+ }
+
+/**
+ * Handle SQLServer specific length properties.
+ * SQLServer handles text types as nvarchar/varchar with a length of -1.
+ *
+ * @param mixed $length Either the length as a string, or a Column descriptor object.
+ * @return mixed null|integer with length of column.
+ */
+ public function length($length) {
+ if (is_object($length) && isset($length->Length)) {
+ if ($length->Length == -1 && strpos($length->Type, 'char') !== false) {
+ return null;
+ }
+ if (in_array($length->Type, array('nchar', 'nvarchar'))) {
+ return floor($length->Length / 2);
+ }
+ return $length->Length;
+ }
+ return parent::length($length);
+ }
+
+/**
+ * Builds a map of the columns contained in a result
+ *
+ * @param PDOStatement $results
+ * @return void
+ */
+ public function resultSet($results) {
+ $this->map = array();
+ $numFields = $results->columnCount();
+ $index = 0;
+
+ while ($numFields-- > 0) {
+ $column = $results->getColumnMeta($index);
+ $name = $column['name'];
+
+ if (strpos($name, '__')) {
+ if (isset($this->_fieldMappings[$name]) && strpos($this->_fieldMappings[$name], '.')) {
+ $map = explode('.', $this->_fieldMappings[$name]);
+ } elseif (isset($this->_fieldMappings[$name])) {
+ $map = array(0, $this->_fieldMappings[$name]);
+ } else {
+ $map = array(0, $name);
+ }
+ } else {
+ $map = array(0, $name);
+ }
+ $map[] = ($column['sqlsrv:decl_type'] == 'bit') ? 'boolean' : $column['native_type'];
+ $this->map[$index++] = $map;
+ }
+ }
+
+/**
+ * Builds final SQL statement
+ *
+ * @param string $type Query type
+ * @param array $data Query data
+ * @return string
+ */
+ public function renderStatement($type, $data) {
+ switch (strtolower($type)) {
+ case 'select':
+ extract($data);
+ $fields = trim($fields);
+
+ if (strpos($limit, 'TOP') !== false && strpos($fields, 'DISTINCT ') === 0) {
+ $limit = 'DISTINCT ' . trim($limit);
+ $fields = substr($fields, 9);
+ }
+
+ // hack order as SQLServer requires an order if there is a limit.
+ if ($limit && !$order) {
+ $order = 'ORDER BY (SELECT NULL)';
+ }
+
+ // For older versions use the subquery version of pagination.
+ if (version_compare($this->getVersion(), '11', '<') && preg_match('/FETCH\sFIRST\s+([0-9]+)/i', $limit, $offset)) {
+ preg_match('/OFFSET\s*(\d+)\s*.*?(\d+)\s*ROWS/', $limit, $limitOffset);
+
+ $limit = 'TOP ' . intval($limitOffset[2]);
+ $page = intval($limitOffset[1] / $limitOffset[2]);
+ $offset = intval($limitOffset[2] * $page);
+
+ $rowCounter = self::ROW_COUNTER;
+ return "
+ SELECT {$limit} * FROM (
+ SELECT {$fields}, ROW_NUMBER() OVER ({$order}) AS {$rowCounter}
+ FROM {$table} {$alias} {$joins} {$conditions} {$group}
+ ) AS _cake_paging_
+ WHERE _cake_paging_.{$rowCounter} > {$offset}
+ ORDER BY _cake_paging_.{$rowCounter}
+ ";
+ } elseif (strpos($limit, 'FETCH') !== false) {
+ return "SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}";
+ } else {
+ return "SELECT {$limit} {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order}";
+ }
+ break;
+ case "schema":
+ extract($data);
+
+ foreach ($indexes as $i => $index) {
+ if (preg_match('/PRIMARY KEY/', $index)) {
+ unset($indexes[$i]);
+ break;
+ }
+ }
+
+ foreach (array('columns', 'indexes') as $var) {
+ if (is_array(${$var})) {
+ ${$var} = "\t" . implode(",\n\t", array_filter(${$var}));
+ }
+ }
+ return "CREATE TABLE {$table} (\n{$columns});\n{$indexes}";
+ break;
+ default:
+ return parent::renderStatement($type, $data);
+ break;
+ }
+ }
+
+/**
+ * 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 into which this data will be inserted
+ * @return string Quoted and escaped data
+ */
+ public function value($data, $column = null) {
+ if (is_array($data) || is_object($data)) {
+ return parent::value($data, $column);
+ } elseif (in_array($data, array('{$__cakeID__$}', '{$__cakeForeignKey__$}'), true)) {
+ return $data;
+ }
+
+ if (empty($column)) {
+ $column = $this->introspectType($data);
+ }
+
+ switch ($column) {
+ case 'string':
+ case 'text':
+ return 'N' . $this->_connection->quote($data, PDO::PARAM_STR);
+ default:
+ return parent::value($data, $column);
+ }
+ }
+
+/**
+ * Returns an array of all result rows for a given SQL query.
+ * Returns false if no rows matched.
+ *
+ * @param Model $model
+ * @param array $queryData
+ * @param integer $recursive
+ * @return array Array of resultset rows, or false if no rows matched
+ */
+ public function read(Model $model, $queryData = array(), $recursive = null) {
+ $results = parent::read($model, $queryData, $recursive);
+ $this->_fieldMappings = array();
+ return $results;
+ }
+
+/**
+ * Fetches the next row from the current result set.
+ * Eats the magic ROW_COUNTER variable.
+ *
+ * @return mixed
+ */
+ public function fetchResult() {
+ if ($row = $this->_result->fetch(PDO::FETCH_NUM)) {
+ $resultRow = array();
+ foreach ($this->map as $col => $meta) {
+ list($table, $column, $type) = $meta;
+ if ($table === 0 && $column === self::ROW_COUNTER) {
+ continue;
+ }
+ $resultRow[$table][$column] = $row[$col];
+ if ($type === 'boolean' && !is_null($row[$col])) {
+ $resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]);
+ }
+ }
+ return $resultRow;
+ }
+ $this->_result->closeCursor();
+ return false;
+ }
+
+/**
+ * Inserts multiple values into a table
+ *
+ * @param string $table
+ * @param string $fields
+ * @param array $values
+ * @return void
+ */
+ public function insertMulti($table, $fields, $values) {
+ $primaryKey = $this->_getPrimaryKey($table);
+ $hasPrimaryKey = $primaryKey != null && (
+ (is_array($fields) && in_array($primaryKey, $fields)
+ || (is_string($fields) && strpos($fields, $this->startQuote . $primaryKey . $this->endQuote) !== false))
+ );
+
+ if ($hasPrimaryKey) {
+ $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($table) . ' ON');
+ }
+
+ parent::insertMulti($table, $fields, $values);
+
+ if ($hasPrimaryKey) {
+ $this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($table) . ' OFF');
+ }
+ }
+
+/**
+ * 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) {
+ $result = parent::buildColumn($column);
+ $result = preg_replace('/(int|integer)\([0-9]+\)/i', '$1', $result);
+ $result = preg_replace('/(bit)\([0-9]+\)/i', '$1', $result);
+ if (strpos($result, 'DEFAULT NULL') !== false) {
+ if (isset($column['default']) && $column['default'] === '') {
+ $result = str_replace('DEFAULT NULL', "DEFAULT ''", $result);
+ } else {
+ $result = str_replace('DEFAULT NULL', 'NULL', $result);
+ }
+ } elseif (array_keys($column) == array('type', 'name')) {
+ $result .= ' NULL';
+ } elseif (strpos($result, "DEFAULT N'")) {
+ $result = str_replace("DEFAULT N'", "DEFAULT '", $result);
+ }
+ return $result;
+ }
+
+/**
+ * Format indexes for create table
+ *
+ * @param array $indexes
+ * @param string $table
+ * @return string
+ */
+ public function buildIndex($indexes, $table = null) {
+ $join = array();
+
+ foreach ($indexes as $name => $value) {
+ if ($name == 'PRIMARY') {
+ $join[] = 'PRIMARY KEY (' . $this->name($value['column']) . ')';
+ } elseif (isset($value['unique']) && $value['unique']) {
+ $out = "ALTER TABLE {$table} ADD CONSTRAINT {$name} UNIQUE";
+
+ if (is_array($value['column'])) {
+ $value['column'] = implode(', ', array_map(array(&$this, 'name'), $value['column']));
+ } else {
+ $value['column'] = $this->name($value['column']);
+ }
+ $out .= "({$value['column']});";
+ $join[] = $out;
+ }
+ }
+ return $join;
+ }
+
+/**
+ * Makes sure it will return the primary key
+ *
+ * @param Model|string $model Model instance of table name
+ * @return string
+ */
+ protected function _getPrimaryKey($model) {
+ $schema = $this->describe($model);
+ foreach ($schema as $field => $props) {
+ if (isset($props['key']) && $props['key'] == 'primary') {
+ return $field;
+ }
+ }
+ return null;
+ }
+
+/**
+ * Returns number of affected rows in previous database operation. If no previous operation exists,
+ * this returns false.
+ *
+ * @param mixed $source
+ * @return integer Number of affected rows
+ */
+ public function lastAffected($source = null) {
+ $affected = parent::lastAffected();
+ if ($affected === null && $this->_lastAffected !== false) {
+ return $this->_lastAffected;
+ }
+ return $affected;
+ }
+
+/**
+ * Executes given SQL statement.
+ *
+ * @param string $sql SQL statement
+ * @param array $params list of params to be bound to query (supported only in select)
+ * @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()) {
+ $this->_lastAffected = false;
+ if (strncasecmp($sql, 'SELECT', 6) == 0 || preg_match('/^EXEC(?:UTE)?\s/mi', $sql) > 0) {
+ $prepareOptions += array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL);
+ return parent::_execute($sql, $params, $prepareOptions);
+ }
+ try {
+ $this->_lastAffected = $this->_connection->exec($sql);
+ if ($this->_lastAffected === false) {
+ $this->_results = null;
+ $error = $this->_connection->errorInfo();
+ $this->error = $error[2];
+ return false;
+ }
+ return true;
+ } catch (PDOException $e) {
+ if (isset($query->queryString)) {
+ $e->queryString = $query->queryString;
+ } else {
+ $e->queryString = $sql;
+ }
+ throw $e;
+ }
+ }
+
+/**
+ * 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 = '';
+ foreach ($schema->tables as $curTable => $columns) {
+ if (!$table || $table == $curTable) {
+ $out .= "IF OBJECT_ID('" . $this->fullTableName($curTable, false) . "', 'U') IS NOT NULL DROP TABLE " . $this->fullTableName($curTable) . ";\n";
+ }
+ }
+ return $out;
+ }
+
+/**
+ * Gets the schema name
+ *
+ * @return string The schema name
+ */
+ public function getSchemaName() {
+ return $this->config['schema'];
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DboSource.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DboSource.php
new file mode 100644
index 0000000..ce19daa
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/DboSource.php
@@ -0,0 +1,3268 @@
+<?php
+/**
+ * Dbo Source
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, 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 MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('DataSource', 'Model/Datasource');
+App::uses('String', '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 boolean.
+ */
+ 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 boolean
+ */
+ public $useNestedTransactions = false;
+
+/**
+ * Print full query debug info?
+ *
+ * @var boolean
+ */
+ 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 integer
+ */
+ public $numRows = null;
+
+/**
+ * Time the last query took
+ *
+ * @var integer
+ */
+ public $took = null;
+
+/**
+ * Result
+ *
+ * @var array
+ */
+ protected $_result = null;
+
+/**
+ * Queries count.
+ *
+ * @var integer
+ */
+ protected $_queriesCnt = 0;
+
+/**
+ * Total duration of all queries.
+ *
+ * @var integer
+ */
+ 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 integer Maximum number of queries in the queries log.
+ */
+ protected $_queriesLogMax = 200;
+
+/**
+ * Caches serialized results of executed queries
+ *
+ * @var array Cache of results from executed sql queries.
+ */
+ 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', 'or', 'not', 'in', 'between', 'regexp', 'similar to');
+
+/**
+ * Indicates the level of nested transactions
+ *
+ * @var integer
+ */
+ 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 boolean
+ */
+ protected $_methodCacheChange = false;
+
+/**
+ * Constructor
+ *
+ * @param array $config Array of configuration information for the Datasource.
+ * @param boolean $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),
+ '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 boolean 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 boolean True if the database could be disconnected, else false
+ */
+ 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 into which this data will be inserted
+ * @return string Quoted and escaped data
+ */
+ public function value($data, $column = null) {
+ 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);
+ break;
+ case 'boolean':
+ return $this->_connection->quote($this->boolean($data, true), PDO::PARAM_BOOL);
+ break;
+ case 'string':
+ case 'text':
+ return $this->_connection->quote($data, PDO::PARAM_STR);
+ default:
+ if ($data === '') {
+ return '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);
+ break;
+ }
+ }
+
+/**
+ * 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 boolean
+ */
+ public function rawQuery($sql, $params = array()) {
+ $this->took = $this->numRows = false;
+ return $this->execute($sql, $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
+ * @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)/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
+ * @return integer 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 integer 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'));
+ } else {
+ if (isset($params[3 + $off])) {
+ $recursive = $params[3 + $off];
+ }
+ return $args[2]->find('first', compact('conditions', 'fields', 'order', 'recursive'));
+ }
+ } else {
+ 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;
+ } else {
+ 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 $params parameters to be bound as values for the SQL statement
+ * @param array $options additional options for the query.
+ * @return 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;
+ }
+ if ($result = $this->execute($sql, array(), $params)) {
+ $out = array();
+
+ if ($this->hasResult()) {
+ $first = $this->fetchRow();
+ if ($first != null) {
+ $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 boolean
+ */
+ 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;
+ self::$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 (empty(self::$methodCache)) {
+ self::$methodCache = Cache::read('method_cache', '_cake_core_');
+ }
+ if ($value === null) {
+ return (isset(self::$methodCache[$method][$key])) ? self::$methodCache[$method][$key] : null;
+ }
+ $this->_methodCacheChange = true;
+ return self::$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.string
+ if (strpos($data, '.') === false) { // string
+ return $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)) { // Functions
+ return $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 boolean True if the database is connected, else false
+ */
+ public function isConnected() {
+ return $this->connected;
+ }
+
+/**
+ * Checks if the result is valid
+ *
+ * @return boolean True if the result is valid else false
+ */
+ public function hasResult() {
+ return is_a($this->_result, 'PDOStatement');
+ }
+
+/**
+ * Get the query log as an array.
+ *
+ * @param boolean $sorted Get the queries sorted by time taken, defaults to false.
+ * @param boolean $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 boolean $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('logs', 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_pop($this->_queriesLog);
+ }
+ }
+
+/**
+ * Gets full table name including prefix
+ *
+ * @param Model|string $model Either a Model object or a string table name.
+ * @param boolean $quote Whether you want the table name quoted.
+ * @param boolean $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 (false == strstr($table, '.')) {
+ return $this->name($schemaName) . '.' . $this->name($table);
+ }
+ }
+ return $this->name($table);
+ }
+ if ($schema && !empty($schemaName)) {
+ if (false == strstr($table, '.')) {
+ 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 boolean Success
+ */
+ public function create(Model $model, $fields = null, $values = null) {
+ $id = null;
+
+ if ($fields == null) {
+ unset($fields, $values);
+ $fields = array_keys($model->data);
+ $values = array_values($model->data);
+ }
+ $count = count($fields);
+
+ for ($i = 0; $i < $count; $i++) {
+ $valueInsert[] = $this->value($values[$i], $model->getColumnType($fields[$i]));
+ $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 integer $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);
+
+ $null = null;
+ $array = array('callbacks' => $queryData['callbacks']);
+ $linkedModels = array();
+ $bypass = false;
+
+ if ($recursive === null && isset($queryData['recursive'])) {
+ $recursive = $queryData['recursive'];
+ }
+
+ if (!is_null($recursive)) {
+ $_recursive = $model->recursive;
+ $model->recursive = $recursive;
+ }
+
+ if (!empty($queryData['fields'])) {
+ $bypass = true;
+ $queryData['fields'] = $this->fields($model, null, $queryData['fields']);
+ } else {
+ $queryData['fields'] = $this->fields($model);
+ }
+
+ $_associations = $model->associations();
+
+ if ($model->recursive == -1) {
+ $_associations = array();
+ } elseif ($model->recursive == 0) {
+ unset($_associations[2], $_associations[3]);
+ }
+
+ foreach ($_associations as $type) {
+ foreach ($model->{$type} as $assoc => $assocData) {
+ $linkModel = $model->{$assoc};
+ $external = isset($assocData['external']);
+
+ $linkModel->getDataSource();
+ if ($model->useDbConfig === $linkModel->useDbConfig) {
+ if ($bypass) {
+ $assocData['fields'] = false;
+ }
+ if (true === $this->generateAssociationQuery($model, $linkModel, $type, $assoc, $assocData, $queryData, $external, $null)) {
+ $linkedModels[$type . '/' . $assoc] = true;
+ }
+ }
+ }
+ }
+
+ $query = trim($this->generateAssociationQuery($model, null, null, null, null, $queryData, false, $null));
+
+ $resultSet = $this->fetchAll($query, $model->cacheQueries);
+
+ if ($resultSet === false) {
+ $model->onError();
+ return false;
+ }
+
+ $filtered = array();
+
+ if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {
+ $filtered = $this->_filterResults($resultSet, $model);
+ }
+
+ if ($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])) {
+ if ($model->useDbConfig === $linkModel->useDbConfig) {
+ $db = $this;
+ } else {
+ $db = ConnectionManager::getDataSource($linkModel->useDbConfig);
+ }
+ } elseif ($model->recursive > 1 && ($type === 'belongsTo' || $type === 'hasOne')) {
+ $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') {
+ $filtered[] = $assoc;
+ }
+ }
+ }
+ }
+ if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {
+ $this->_filterResults($resultSet, $model, $filtered);
+ }
+ }
+
+ if (!is_null($recursive)) {
+ $model->recursive = $_recursive;
+ }
+ return $resultSet;
+ }
+
+/**
+ * Passes association results thru afterFind filters of corresponding model
+ *
+ * @param array $results 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(&$results, Model $model, $filtered = array()) {
+ $current = reset($results);
+ 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 ($results as &$result) {
+ $data = $linkedModel->afterFind(array(array($className => $result[$className])), false);
+ if (isset($data[0][$className])) {
+ $result[$className] = $data[0][$className];
+ }
+ }
+ }
+ return $filtering;
+ }
+
+/**
+ * Queries associations. Used to fetch results on recursive models.
+ *
+ * @param Model $model Primary Model object
+ * @param Model $linkModel Linked model that
+ * @param string $type Association type, one of the model association types ie. hasMany
+ * @param string $association
+ * @param array $assocData
+ * @param array $queryData
+ * @param boolean $external Whether or not the association query is on an external datasource.
+ * @param array $resultSet Existing results
+ * @param integer $recursive Number of levels of association
+ * @param array $stack
+ * @return mixed
+ * @throws CakeException when results cannot be created.
+ */
+ public function queryAssociation(Model $model, &$linkModel, $type, $association, $assocData, &$queryData, $external, &$resultSet, $recursive, $stack) {
+ if (isset($stack['_joined'])) {
+ $joined = $stack['_joined'];
+ unset($stack['_joined']);
+ }
+
+ if ($query = $this->generateAssociationQuery($model, $linkModel, $type, $association, $assocData, $queryData, $external, $resultSet)) {
+ 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'])) {
+ $ins = $fetch = array();
+ foreach ($resultSet as &$result) {
+ if ($in = $this->insertQueryData('{$__cakeID__$}', $result, $association, $assocData, $model, $linkModel, $stack)) {
+ $ins[] = $in;
+ }
+ }
+
+ if (!empty($ins)) {
+ $ins = array_unique($ins);
+ $fetch = $this->fetchAssociated($model, $query, $ins);
+ }
+
+ if (!empty($fetch) && is_array($fetch)) {
+ if ($recursive > 0) {
+ foreach ($linkModel->associations() as $type1) {
+ foreach ($linkModel->{$type1} as $assoc1 => $assocData1) {
+ $deepModel = $linkModel->{$assoc1};
+ $tmpStack = $stack;
+ $tmpStack[] = $assoc1;
+
+ if ($linkModel->useDbConfig === $deepModel->useDbConfig) {
+ $db = $this;
+ } else {
+ $db = ConnectionManager::getDataSource($deepModel->useDbConfig);
+ }
+ $db->queryAssociation($linkModel, $deepModel, $type1, $assoc1, $assocData1, $queryData, true, $fetch, $recursive - 1, $tmpStack);
+ }
+ }
+ }
+ }
+ if ($queryData['callbacks'] === true || $queryData['callbacks'] === 'after') {
+ $this->_filterResults($fetch, $model);
+ }
+ return $this->_mergeHasMany($resultSet, $fetch, $association, $model, $linkModel);
+ } elseif ($type === 'hasAndBelongsToMany') {
+ $ins = $fetch = array();
+ foreach ($resultSet as &$result) {
+ if ($in = $this->insertQueryData('{$__cakeID__$}', $result, $association, $assocData, $model, $linkModel, $stack)) {
+ $ins[] = $in;
+ }
+ }
+ if (!empty($ins)) {
+ $ins = array_unique($ins);
+ if (count($ins) > 1) {
+ $query = str_replace('{$__cakeID__$}', '(' . implode(', ', $ins) . ')', $query);
+ $query = str_replace('= (', 'IN (', $query);
+ } else {
+ $query = str_replace('{$__cakeID__$}', $ins[0], $query);
+ }
+ $query = str_replace(' WHERE 1 = 1', '', $query);
+ }
+
+ $foreignKey = $model->hasAndBelongsToMany[$association]['foreignKey'];
+ $joinKeys = array($foreignKey, $model->hasAndBelongsToMany[$association]['associationForeignKey']);
+ list($with, $habtmFields) = $model->joinModel($model->hasAndBelongsToMany[$association]['with'], $joinKeys);
+ $habtmFieldsCount = count($habtmFields);
+ $q = $this->insertQueryData($query, null, $association, $assocData, $model, $linkModel, $stack);
+
+ if ($q !== false) {
+ $fetch = $this->fetchAll($q, $model->cacheQueries);
+ } else {
+ $fetch = null;
+ }
+ }
+
+ $modelAlias = $model->alias;
+ $modelPK = $model->primaryKey;
+ foreach ($resultSet as &$row) {
+ if ($type !== 'hasAndBelongsToMany') {
+ $q = $this->insertQueryData($query, $row, $association, $assocData, $model, $linkModel, $stack);
+ $fetch = null;
+ if ($q !== false) {
+ $joinedData = array();
+ if (($type === 'belongsTo' || $type === 'hasOne') && isset($row[$linkModel->alias], $joined[$model->alias]) && in_array($linkModel->alias, $joined[$model->alias])) {
+ $joinedData = Hash::filter($row[$linkModel->alias]);
+ if (!empty($joinedData)) {
+ $fetch[0] = array($linkModel->alias => $row[$linkModel->alias]);
+ }
+ } else {
+ $fetch = $this->fetchAll($q, $model->cacheQueries);
+ }
+ }
+ }
+ $selfJoin = $linkModel->name === $model->name;
+
+ if (!empty($fetch) && is_array($fetch)) {
+ if ($recursive > 0) {
+ foreach ($linkModel->associations() as $type1) {
+ foreach ($linkModel->{$type1} as $assoc1 => $assocData1) {
+ $deepModel = $linkModel->{$assoc1};
+
+ if ($type1 === 'belongsTo' || ($deepModel->alias === $modelAlias && $type === 'belongsTo') || ($deepModel->alias !== $modelAlias)) {
+ $tmpStack = $stack;
+ $tmpStack[] = $assoc1;
+ if ($linkModel->useDbConfig == $deepModel->useDbConfig) {
+ $db = $this;
+ } else {
+ $db = ConnectionManager::getDataSource($deepModel->useDbConfig);
+ }
+ $db->queryAssociation($linkModel, $deepModel, $type1, $assoc1, $assocData1, $queryData, true, $fetch, $recursive - 1, $tmpStack);
+ }
+ }
+ }
+ }
+ if ($type === 'hasAndBelongsToMany') {
+ $uniqueIds = $merge = array();
+
+ foreach ($fetch as $j => $data) {
+ if (isset($data[$with]) && $data[$with][$foreignKey] === $row[$modelAlias][$modelPK]) {
+ 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 {
+ $this->_mergeAssociation($row, $fetch, $association, $type, $selfJoin);
+ }
+ if (isset($row[$association])) {
+ $row[$association] = $linkModel->afterFind($row[$association], false);
+ }
+ } else {
+ $tempArray[0][$association] = false;
+ $this->_mergeAssociation($row, $tempArray, $association, $type, $selfJoin);
+ }
+ }
+ }
+ }
+
+/**
+ * A more efficient way to fetch associations. Woohoo!
+ *
+ * @param Model $model Primary model object
+ * @param string $query Association query
+ * @param array $ids Array of IDs of associated records
+ * @return array Association results
+ */
+ public function fetchAssociated(Model $model, $query, $ids) {
+ $query = str_replace('{$__cakeID__$}', implode(', ', $ids), $query);
+ if (count($ids) > 1) {
+ $query = str_replace('= (', 'IN (', $query);
+ }
+ return $this->fetchAll($query, $model->cacheQueries);
+ }
+
+/**
+ * mergeHasMany - Merge the results of hasMany relations.
+ *
+ *
+ * @param array $resultSet Data to merge into
+ * @param array $merge Data to merge
+ * @param string $association Name of Model being Merged
+ * @param Model $model Model being merged onto
+ * @param Model $linkModel Model being merged
+ * @return void
+ */
+ protected function _mergeHasMany(&$resultSet, $merge, $association, $model, $linkModel) {
+ $modelAlias = $model->alias;
+ $modelPK = $model->primaryKey;
+ $modelFK = $model->hasMany[$association]['foreignKey'];
+ foreach ($resultSet as &$result) {
+ if (!isset($result[$modelAlias])) {
+ continue;
+ }
+ $merged = array();
+ foreach ($merge as $data) {
+ if ($result[$modelAlias][$modelPK] === $data[$association][$modelFK]) {
+ 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
+ * @param array $merge
+ * @param string $association
+ * @param string $type
+ * @param boolean $selfJoin
+ * @return void
+ */
+ protected function _mergeAssociation(&$data, &$merge, $association, $type, $selfJoin = false) {
+ if (isset($merge[0]) && !isset($merge[0][$association])) {
+ $association = Inflector::pluralize($association);
+ }
+
+ if ($type === 'belongsTo' || $type === 'hasOne') {
+ if (isset($merge[$association])) {
+ $data[$association] = $merge[$association][0];
+ } else {
+ if (count($merge[0][$association]) > 1) {
+ foreach ($merge[0] as $assoc => $data2) {
+ if ($assoc !== $association) {
+ $merge[0][$association][$assoc] = $data2;
+ }
+ }
+ }
+ if (!isset($data[$association])) {
+ if ($merge[0][$association] != null) {
+ $data[$association] = $merge[0][$association];
+ } else {
+ $data[$association] = array();
+ }
+ } else {
+ if (is_array($merge[0][$association])) {
+ foreach ($data[$association] 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) {
+ $data[$association][$association] = $merge[0][$association];
+ } else {
+ $diff = Hash::diff($dataAssocTmp, $mergeAssocTmp);
+ $data[$association] = array_merge($merge[0][$association], $diff);
+ }
+ } elseif ($selfJoin && array_key_exists($association, $merge[0])) {
+ $data[$association] = array_merge($data[$association], array($association => array()));
+ }
+ }
+ }
+ } else {
+ if (isset($merge[0][$association]) && $merge[0][$association] === false) {
+ if (!isset($data[$association])) {
+ $data[$association] = array();
+ }
+ } else {
+ foreach ($merge as $i => $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($data[$association]) || (isset($data[$association]) && !in_array($insert, $data[$association], true))) {
+ $data[$association][] = $insert;
+ }
+ }
+ }
+ }
+ }
+
+/**
+ * Generates an array representing a query or part of a query from a single model or two associated models
+ *
+ * @param Model $model
+ * @param Model $linkModel
+ * @param string $type
+ * @param string $association
+ * @param array $assocData
+ * @param array $queryData
+ * @param boolean $external
+ * @param array $resultSet
+ * @return mixed
+ */
+ public function generateAssociationQuery(Model $model, $linkModel, $type, $association, $assocData, &$queryData, $external, &$resultSet) {
+ $queryData = $this->_scrubQueryData($queryData);
+ $assocData = $this->_scrubQueryData($assocData);
+ $modelAlias = $model->alias;
+
+ if (empty($queryData['fields'])) {
+ $queryData['fields'] = $this->fields($model, $modelAlias);
+ } elseif (!empty($model->hasMany) && $model->recursive > -1) {
+ $assocFields = $this->fields($model, $modelAlias, array("{$modelAlias}.{$model->primaryKey}"));
+ $passedFields = $queryData['fields'];
+ if (count($passedFields) === 1) {
+ if (strpos($passedFields[0], $assocFields[0]) === false && !preg_match('/^[a-z]+\(/i', $passedFields[0])) {
+ $queryData['fields'] = array_merge($passedFields, $assocFields);
+ } else {
+ $queryData['fields'] = $passedFields;
+ }
+ } else {
+ $queryData['fields'] = array_merge($passedFields, $assocFields);
+ }
+ unset($assocFields, $passedFields);
+ }
+
+ if ($linkModel === null) {
+ return $this->buildStatement(
+ array(
+ 'fields' => array_unique($queryData['fields']),
+ 'table' => $this->fullTableName($model),
+ 'alias' => $modelAlias,
+ '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'];
+ }
+
+ $self = $model->name === $linkModel->name;
+ $fields = array();
+
+ if ($external || (in_array($type, array('hasOne', 'belongsTo')) && $assocData['fields'] !== false)) {
+ $fields = $this->fields($linkModel, $association, $assocData['fields']);
+ }
+ if (empty($assocData['offset']) && !empty($assocData['page'])) {
+ $assocData['offset'] = ($assocData['page'] - 1) * $assocData['limit'];
+ }
+ $assocData['limit'] = $this->limit($assocData['limit'], $assocData['offset']);
+
+ switch ($type) {
+ case 'hasOne':
+ case 'belongsTo':
+ $conditions = $this->_mergeConditions(
+ $assocData['conditions'],
+ $this->getConstraint($type, $model, $linkModel, $association, array_merge($assocData, compact('external', 'self')))
+ );
+
+ if (!$self && $external) {
+ foreach ($conditions as $key => $condition) {
+ if (is_numeric($key) && strpos($condition, $modelAlias . '.') !== false) {
+ unset($conditions[$key]);
+ }
+ }
+ }
+
+ if ($external) {
+ $query = array_merge($assocData, array(
+ 'conditions' => $conditions,
+ 'table' => $this->fullTableName($linkModel),
+ 'fields' => $fields,
+ 'alias' => $association,
+ 'group' => null
+ ));
+ $query += array('order' => $assocData['order'], 'limit' => $assocData['limit']);
+ } else {
+ $join = array(
+ 'table' => $linkModel,
+ 'alias' => $association,
+ 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT',
+ 'conditions' => trim($this->conditions($conditions, true, false, $model))
+ );
+ $queryData['fields'] = array_merge($queryData['fields'], $fields);
+
+ if (!empty($assocData['order'])) {
+ $queryData['order'][] = $assocData['order'];
+ }
+ if (!in_array($join, $queryData['joins'])) {
+ $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'],
+ '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 = $model->{$with}->alias;
+ $joinFields = $this->fields($model->{$with}, $joinAlias, $joinFields);
+ } else {
+ $joinFields = array();
+ }
+ } else {
+ $joinTbl = $assocData['joinTable'];
+ $joinAlias = $this->fullTableName($assocData['joinTable']);
+ }
+ $query = array(
+ 'conditions' => $assocData['conditions'],
+ 'limit' => $assocData['limit'],
+ '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 Model object
+ * @param string $linkModel
+ * @param string $alias
+ * @param array $assoc
+ * @param string $alias2
+ * @return array Conditions array defining the constraint between $model and $association
+ */
+ public function getConstraint($type, $model, $linkModel, $alias, $assoc, $alias2 = null) {
+ $assoc += array('external' => false, 'self' => false);
+
+ if (empty($assoc['foreignKey'])) {
+ return array();
+ }
+
+ switch (true) {
+ case ($assoc['external'] && $type === 'hasOne'):
+ return array("{$alias}.{$assoc['foreignKey']}" => '{$__cakeID__$}');
+ case ($assoc['external'] && $type === 'belongsTo'):
+ return array("{$alias}.{$linkModel->primaryKey}" => '{$__cakeForeignKey__$}');
+ case (!$assoc['external'] && $type === 'hasOne'):
+ return array("{$alias}.{$assoc['foreignKey']}" => $this->identifier("{$model->alias}.{$model->primaryKey}"));
+ case (!$assoc['external'] && $type === 'belongsTo'):
+ return array("{$model->alias}.{$assoc['foreignKey']}" => $this->identifier("{$alias}.{$linkModel->primaryKey}"));
+ case ($type === 'hasMany'):
+ return array("{$alias}.{$assoc['foreignKey']}" => array('{$__cakeID__$}'));
+ case ($type === 'hasAndBelongsToMany'):
+ return array(
+ array("{$alias}.{$assoc['foreignKey']}" => '{$__cakeID__$}'),
+ array("{$alias}.{$assoc['associationForeignKey']}" => $this->identifier("{$alias2}.{$linkModel->primaryKey}"))
+ );
+ }
+ return array();
+ }
+
+/**
+ * Builds and generates a JOIN statement from an array. Handles final clean-up before conversion.
+ *
+ * @param array $join An array defining a JOIN statement in a query
+ * @return string An SQL JOIN statement 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' => array()
+ ), $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'])) {
+ $schema = !(is_string($data['table']) && strpos($data['table'], '(') === 0);
+ $data['table'] = $this->fullTableName($data['table'], true, $schema);
+ }
+ 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) {
+ $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
+ * @return string
+ */
+ public function renderJoinStatement($data) {
+ extract($data);
+ return trim("{$type} JOIN {$table} {$alias} ON ({$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 "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 "UPDATE {$table} {$aliases}SET {$fields} {$conditions}";
+ case 'delete':
+ if (!empty($alias)) {
+ $aliases = "{$this->alias}{$alias} {$joins} ";
+ }
+ return "DELETE {$alias} FROM {$table} {$aliases}{$conditions}";
+ case 'schema':
+ foreach (array('columns', 'indexes', 'tableParameters') as $var) {
+ if (is_array(${$var})) {
+ ${$var} = "\t" . join(",\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
+ * @param mixed $assoc
+ * @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
+ * @param array $fields
+ * @param array $values
+ * @param mixed $conditions
+ * @return boolean Success
+ */
+ public function update(Model $model, $fields = array(), $values = null, $conditions = null) {
+ if ($values == null) {
+ $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
+ * @param array $fields
+ * @param boolean $quoteValues If values should be quoted, or treated as SQL snippets
+ * @param boolean $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;
+
+ $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));
+ } 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
+ * @param mixed $conditions
+ * @return boolean 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
+ * @param mixed $conditions
+ * @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($alias, $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 fragments from a model's associations
+ *
+ * @param Model $model
+ * @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}) && $model->useDbConfig == $model->{$assoc}->useDbConfig && $model->{$assoc}->getDataSource()) {
+ $assocData = $model->getAssociated($assoc);
+ $join[] = $this->buildJoinStatement(array(
+ 'table' => $model->{$assoc},
+ 'alias' => $assoc,
+ 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT',
+ 'conditions' => trim($this->conditions(
+ $this->_mergeConditions($assocData['conditions'], $this->getConstraint($assocData['association'], $model, $model->{$assoc}, $assoc, $assocData)),
+ true, false, $model
+ ))
+ ));
+ }
+ }
+ return $join;
+ }
+
+/**
+ * Returns an SQL calculation, i.e. COUNT() or MAX()
+ *
+ * @param Model $model
+ * @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 (is_object($model) && $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 (is_object($model) && $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]);
+ break;
+ }
+ }
+
+/**
+ * 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 boolean 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 boolean
+ */
+ public function nestedTransactionSupported() {
+ return false;
+ }
+
+/**
+ * Begin a transaction
+ *
+ * @return boolean 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 boolean
+ */
+ protected function _beginNested() {
+ $query = 'SAVEPOINT LEVEL' . ++$this->_transactionNesting;
+ if ($this->fullDebug) {
+ $this->logQuery($query);
+ }
+ $this->_connection->exec($query);
+ return true;
+ }
+
+/**
+ * Commit a transaction
+ *
+ * @return boolean 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 boolean
+ */
+ 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 boolean 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 boolean
+ */
+ 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
+ * @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
+ * @param string|array|boolean $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 boolean $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) {
+ 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
+ * @param string $key
+ * @param string $assoc
+ * @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
+ * @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
+ * @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
+ * @param string $alias Alias table name
+ * @param mixed $fields
+ * @param boolean $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)
+ );
+ $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 = String::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 boolean $quoteValues If true, values should be quoted
+ * @param boolean $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 = 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, $match)) {
+ $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 boolean $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 = 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[] = $not . $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[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) . ' = (';
+ } else {
+ $data = $this->_quoteFields($key) . ' IN (';
+ }
+ if ($quoteValues) {
+ if (is_object($model)) {
+ $columnType = $model->getColumnType($key);
+ }
+ $data .= implode(', ', $this->value($value, $columnType));
+ }
+ $data .= ')';
+ } 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($model, trim($key), $value);
+ }
+
+ if ($data != null) {
+ $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 Model $model Model object initiating the query
+ * @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
+ * @return string
+ */
+ protected function _parseKey($model, $key, $value) {
+ $operatorMatch = '/^(((' . implode(')|(', $this->_sqlOps);
+ $operatorMatch .= ')\\x20?)|<[>=]?(?![^>]+>)\\x20?|[>=!]{1,3}(?!<)\\x20?)/is';
+ $bound = (strpos($key, '?') !== false || (is_array($value) && strpos($key, ':') !== false));
+
+ if (strpos($key, ' ') === false) {
+ $operator = '=';
+ } else {
+ list($key, $operator) = explode(' ', trim($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;
+ if (is_object($model) && $model->isVirtualField($key)) {
+ $key = $this->_quoteFields($model->getVirtualField($key));
+ $virtual = true;
+ }
+
+ $type = is_object($model) ? $model->getColumnType($key) : null;
+ $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);
+ $key = $isKey ? $this->_quoteFields($key) : $this->name($key);
+ }
+
+ if ($bound) {
+ return String::insert($key . ' ' . trim($operator), $value);
+ }
+
+ if (!preg_match($operatorMatch, trim($operator))) {
+ $operator .= ' =';
+ }
+ $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
+ * @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);
+ }
+ $conditions = str_replace(array($start, $end), '', $conditions);
+ $conditions = preg_replace_callback('/(?:[\'\"][^\'\"\\\]*(?:\\\.[^\'\"\\\]*)*[\'\"])|([a-z0-9_' . $start . $end . ']*\\.[a-z0-9_' . $start . $end . ']*)/i', array(&$this, '_quoteMatchedField'), $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 integer $limit Limit of results returned
+ * @param integer $offset Offset from which to start results
+ * @return string SQL limit/offset statement
+ */
+ public function limit($limit, $offset = null) {
+ if ($limit) {
+ $rt = '';
+ if (!strpos(strtolower($limit), 'limit')) {
+ $rt = ' LIMIT';
+ }
+
+ if ($offset) {
+ $rt .= ' ' . $offset . ',';
+ }
+
+ $rt .= ' ' . $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 = 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 (is_object($model) && $model->isVirtualField($key)) {
+ $key = '(' . $this->_quoteFields($model->getVirtualField($key)) . ')';
+ }
+ list($alias, $field) = pluginSplit($key);
+ if (is_object($model) && $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 $group Group By Condition
+ * @param Model $model
+ * @return string string condition or null
+ */
+ public function group($group, $model = null) {
+ if ($group) {
+ if (!is_array($group)) {
+ $group = array($group);
+ }
+ foreach ($group as $index => $key) {
+ if (is_object($model) && $model->isVirtualField($key)) {
+ $group[$index] = '(' . $model->getVirtualField($key) . ')';
+ }
+ }
+ $group = implode(', ', $group);
+ return ' GROUP BY ' . $this->_quoteFields($group);
+ }
+ return null;
+ }
+
+/**
+ * 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 boolean 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 intval($limit);
+ }
+ return null;
+ }
+
+ $types = array(
+ 'int' => 1, 'tinyint' => 1, 'smallint' => 1, 'mediumint' => 1, 'integer' => 1, 'bigint' => 1
+ );
+
+ list($real, $type, $length, $offset, $sign, $zerofill) = $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 intval($length);
+ }
+
+/**
+ * Translates between PHP boolean values and Database (faked) boolean values
+ *
+ * @param mixed $data Value to be translated
+ * @param boolean $quote
+ * @return string|boolean 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 boolean
+ */
+ 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 $row => $value) {
+ $i = 1;
+ foreach ($value as $col => $val) {
+ $statement->bindValue($i, $val, $columnMap[$col]);
+ $i += 1;
+ }
+ $statement->execute();
+ $statement->closeCursor();
+ }
+ return $this->commit();
+ }
+
+/**
+ * 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 false;
+ }
+
+/**
+ * Generate a database-native schema for the given Schema object
+ *
+ * @param Model $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 (!is_a($schema, '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 = $colList = $indexes = $tableParameters = array();
+ $primary = null;
+ $table = $this->fullTableName($curTable);
+
+ foreach ($columns as $name => $col) {
+ if (is_string($col)) {
+ $col = array('type' => $col);
+ }
+ if (isset($col['key']) && $col['key'] === 'primary') {
+ $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 (empty($indexes) && !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 a alter syntax from CakeSchema::compare()
+ *
+ * @param mixed $compare
+ * @param string $table
+ * @return boolean
+ */
+ 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 = '';
+
+ foreach ($schema->tables as $curTable => $columns) {
+ if (!$table || $table == $curTable) {
+ $out .= 'DROP TABLE ' . $this->fullTableName($curTable) . ";\n";
+ }
+ }
+ return $out;
+ }
+
+/**
+ * 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') {
+ $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'])) {
+ continue;
+ }
+ $val = $columnData[$paramName];
+ if ($value['quote']) {
+ $val = $this->value($val);
+ }
+ $columnString .= ' ' . $value['value'] . $value['join'] . $val;
+ }
+ }
+ return $columnString;
+ }
+
+/**
+ * Format indexes for create table
+ *
+ * @param array $indexes
+ * @param string $table
+ * @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
+ * @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
+ * @param string $table
+ * @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
+ * @return void
+ */
+ public function introspectType($value) {
+ if (!is_array($value)) {
+ if (is_bool($value)) {
+ return 'boolean';
+ }
+ if (is_float($value) && floatval($value) === $value) {
+ return 'float';
+ }
+ if (is_int($value) && intval($value) === $value) {
+ return 'integer';
+ }
+ if (is_string($value) && strlen($value) > 255) {
+ return 'text';
+ }
+ return 'string';
+ }
+
+ $isAllFloat = $isAllInt = true;
+ $containsFloat = $containsInt = $containsString = false;
+ foreach ($value as $key => $valElement) {
+ $valElement = trim($valElement);
+ if (!is_float($valElement) && !preg_match('/^[\d]+\.[\d]+$/', $valElement)) {
+ $isAllFloat = false;
+ } else {
+ $containsFloat = true;
+ 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', self::$methodCache, '_cake_core_');
+ }
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CacheSession.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CacheSession.php
new file mode 100644
index 0000000..c9f14e4
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CacheSession.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * Cache Session save handler. Allows saving session information into Cache.
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Datasource.Session
+ * @since CakePHP(tm) v 2.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('Cache', 'Cache');
+App::uses('CakeSessionHandlerInterface', 'Model/Datasource/Session');
+
+/**
+ * CacheSession provides method for saving sessions into a Cache engine. Used with CakeSession
+ *
+ * @package Cake.Model.Datasource.Session
+ * @see CakeSession for configuration information.
+ */
+class CacheSession implements CakeSessionHandlerInterface {
+
+/**
+ * Method called on open of a database session.
+ *
+ * @return boolean Success
+ */
+ public function open() {
+ return true;
+ }
+
+/**
+ * Method called on close of a database session.
+ *
+ * @return boolean Success
+ */
+ public function close() {
+ return true;
+ }
+
+/**
+ * Method used to read from a database session.
+ *
+ * @param string $id The key of the value to read
+ * @return mixed The value of the key or false if it does not exist
+ */
+ public function read($id) {
+ return Cache::read($id, Configure::read('Session.handler.config'));
+ }
+
+/**
+ * Helper function called on write for database sessions.
+ *
+ * @param integer $id ID that uniquely identifies session in database
+ * @param mixed $data The value of the data to be saved.
+ * @return boolean True for successful write, false otherwise.
+ */
+ public function write($id, $data) {
+ return Cache::write($id, $data, Configure::read('Session.handler.config'));
+ }
+
+/**
+ * Method called on the destruction of a database session.
+ *
+ * @param integer $id ID that uniquely identifies session in cache
+ * @return boolean True for successful delete, false otherwise.
+ */
+ public function destroy($id) {
+ return Cache::delete($id, Configure::read('Session.handler.config'));
+ }
+
+/**
+ * Helper function called on gc for cache sessions.
+ *
+ * @param integer $expires Timestamp (defaults to current time)
+ * @return boolean Success
+ */
+ public function gc($expires = null) {
+ return Cache::gc(Configure::read('Session.handler.config'), $expires);
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CakeSessionHandlerInterface.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CakeSessionHandlerInterface.php
new file mode 100644
index 0000000..26c9cad
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/CakeSessionHandlerInterface.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Datasource
+ * @since CakePHP(tm) v 2.1
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+/**
+ * Interface for Session handlers. Custom session handler classes should implement
+ * this interface as it allows CakeSession know how to map methods to session_set_save_handler()
+ *
+ * @package Cake.Model.Datasource.Session
+ */
+interface CakeSessionHandlerInterface {
+
+/**
+ * Method called on open of a session.
+ *
+ * @return boolean Success
+ */
+ public function open();
+
+/**
+ * Method called on close of a session.
+ *
+ * @return boolean Success
+ */
+ public function close();
+
+/**
+ * Method used to read from a session.
+ *
+ * @param string $id The key of the value to read
+ * @return mixed The value of the key or false if it does not exist
+ */
+ public function read($id);
+
+/**
+ * Helper function called on write for sessions.
+ *
+ * @param integer $id ID that uniquely identifies session in database
+ * @param mixed $data The value of the data to be saved.
+ * @return boolean True for successful write, false otherwise.
+ */
+ public function write($id, $data);
+
+/**
+ * Method called on the destruction of a session.
+ *
+ * @param integer $id ID that uniquely identifies session in database
+ * @return boolean True for successful delete, false otherwise.
+ */
+ public function destroy($id);
+
+/**
+ * Run the Garbage collection on the session storage. This method should vacuum all
+ * expired or dead sessions.
+ *
+ * @param integer $expires Timestamp (defaults to current time)
+ * @return boolean Success
+ */
+ public function gc($expires = null);
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/DatabaseSession.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/DatabaseSession.php
new file mode 100644
index 0000000..17207f9
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Datasource/Session/DatabaseSession.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Database Session save handler. Allows saving session information into a model.
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Datasource.Session
+ * @since CakePHP(tm) v 2.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('CakeSessionHandlerInterface', 'Model/Datasource/Session');
+App::uses('ClassRegistry', 'Utility');
+
+/**
+ * DatabaseSession provides methods to be used with CakeSession.
+ *
+ * @package Cake.Model.Datasource.Session
+ */
+class DatabaseSession implements CakeSessionHandlerInterface {
+
+/**
+ * Reference to the model handling the session data
+ *
+ * @var Model
+ */
+ protected $_model;
+
+/**
+ * Number of seconds to mark the session as expired
+ *
+ * @var int
+ */
+ protected $_timeout;
+
+/**
+ * Constructor. Looks at Session configuration information and
+ * sets up the session model.
+ *
+ */
+ public function __construct() {
+ $modelName = Configure::read('Session.handler.model');
+
+ if (empty($modelName)) {
+ $settings = array(
+ 'class' => 'Session',
+ 'alias' => 'Session',
+ 'table' => 'cake_sessions',
+ );
+ } else {
+ $settings = array(
+ 'class' => $modelName,
+ 'alias' => 'Session',
+ );
+ }
+ $this->_model = ClassRegistry::init($settings);
+ $this->_timeout = Configure::read('Session.timeout') * 60;
+ }
+
+/**
+ * Method called on open of a database session.
+ *
+ * @return boolean Success
+ */
+ public function open() {
+ return true;
+ }
+
+/**
+ * Method called on close of a database session.
+ *
+ * @return boolean Success
+ */
+ public function close() {
+ return true;
+ }
+
+/**
+ * Method used to read from a database session.
+ *
+ * @param integer|string $id The key of the value to read
+ * @return mixed The value of the key or false if it does not exist
+ */
+ public function read($id) {
+ $row = $this->_model->find('first', array(
+ 'conditions' => array($this->_model->primaryKey => $id)
+ ));
+
+ if (empty($row[$this->_model->alias]['data'])) {
+ return false;
+ }
+
+ return $row[$this->_model->alias]['data'];
+ }
+
+/**
+ * Helper function called on write for database sessions.
+ *
+ * @param integer $id ID that uniquely identifies session in database
+ * @param mixed $data The value of the data to be saved.
+ * @return boolean True for successful write, false otherwise.
+ */
+ public function write($id, $data) {
+ if (!$id) {
+ return false;
+ }
+ $expires = time() + $this->_timeout;
+ $record = compact('id', 'data', 'expires');
+ $record[$this->_model->primaryKey] = $id;
+ return $this->_model->save($record);
+ }
+
+/**
+ * Method called on the destruction of a database session.
+ *
+ * @param integer $id ID that uniquely identifies session in database
+ * @return boolean True for successful delete, false otherwise.
+ */
+ public function destroy($id) {
+ return $this->_model->delete($id);
+ }
+
+/**
+ * Helper function called on gc for database sessions.
+ *
+ * @param integer $expires Timestamp (defaults to current time)
+ * @return boolean Success
+ */
+ public function gc($expires = null) {
+ if (!$expires) {
+ $expires = time();
+ }
+ return $this->_model->deleteAll(array($this->_model->alias . ".expires <" => $expires), false, false);
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/I18nModel.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/I18nModel.php
new file mode 100644
index 0000000..9b18506
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/I18nModel.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Behavior
+ * @since CakePHP(tm) v 1.2.0.4525
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+/**
+ * A model used by TranslateBehavior to access the translation tables.
+ *
+ * @package Cake.Model
+ */
+class I18nModel extends AppModel {
+
+/**
+ * Model name
+ *
+ * @var string
+ */
+ public $name = 'I18nModel';
+
+/**
+ * Table name
+ *
+ * @var string
+ */
+ public $useTable = 'i18n';
+
+/**
+ * Display field
+ *
+ * @var string
+ */
+ public $displayField = 'field';
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Model.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Model.php
new file mode 100644
index 0000000..8909483
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Model.php
@@ -0,0 +1,3402 @@
+<?php
+/**
+ * Object-relational mapper.
+ *
+ * DBO-backed object data model, for mapping database tables to Cake objects.
+ *
+ * PHP versions 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 0.10.0.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('ClassRegistry', 'Utility');
+App::uses('Validation', 'Utility');
+App::uses('String', 'Utility');
+App::uses('Hash', 'Utility');
+App::uses('BehaviorCollection', 'Model');
+App::uses('ModelBehavior', 'Model');
+App::uses('ModelValidator', 'Model');
+App::uses('ConnectionManager', 'Model');
+App::uses('Xml', 'Utility');
+App::uses('CakeEvent', 'Event');
+App::uses('CakeEventListener', 'Event');
+App::uses('CakeEventManager', 'Event');
+
+/**
+ * Object-relational mapper.
+ *
+ * DBO-backed object data model.
+ * Automatically selects a database table name based on a pluralized lowercase object class name
+ * (i.e. class 'User' => table 'users'; class 'Man' => table 'men')
+ * The table is required to have at least 'id auto_increment' primary key.
+ *
+ * @package Cake.Model
+ * @link http://book.cakephp.org/2.0/en/models.html
+ */
+class Model extends Object implements CakeEventListener {
+
+/**
+ * The name of the DataSource connection that this Model uses
+ *
+ * The value must be an attribute name that you defined in `app/Config/database.php`
+ * or created using `ConnectionManager::create()`.
+ *
+ * @var string
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#usedbconfig
+ */
+ public $useDbConfig = 'default';
+
+/**
+ * Custom database table name, or null/false if no table association is desired.
+ *
+ * @var string
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#usetable
+ */
+ public $useTable = null;
+
+/**
+ * Custom display field name. Display fields are used by Scaffold, in SELECT boxes' OPTION elements.
+ *
+ * This field is also used in `find('list')` when called with no extra parameters in the fields list
+ *
+ * @var string
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#displayfield
+ */
+ public $displayField = null;
+
+/**
+ * Value of the primary key ID of the record that this model is currently pointing to.
+ * Automatically set after database insertions.
+ *
+ * @var mixed
+ */
+ public $id = false;
+
+/**
+ * Container for the data that this model gets from persistent storage (usually, a database).
+ *
+ * @var array
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#data
+ */
+ public $data = array();
+
+/**
+ * Holds physical schema/database name for this model. Automatically set during Model creation.
+ *
+ * @var string
+ * @access public
+ */
+ public $schemaName = null;
+
+/**
+ * Table name for this Model.
+ *
+ * @var string
+ */
+ public $table = false;
+
+/**
+ * The name of the primary key field for this model.
+ *
+ * @var string
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#primaryKey
+ */
+ public $primaryKey = null;
+
+/**
+ * Field-by-field table metadata.
+ *
+ * @var array
+ */
+ protected $_schema = null;
+
+/**
+ * List of validation rules. It must be an array with the field name as key and using
+ * as value one of the following possibilities
+ *
+ * ### Validating using regular expressions
+ *
+ * {{{
+ * public $validate = array(
+ * 'name' => '/^[a-z].+$/i'
+ * );
+ * }}}
+ *
+ * ### Validating using methods (no parameters)
+ *
+ * {{{
+ * public $validate = array(
+ * 'name' => 'notEmpty'
+ * );
+ * }}}
+ *
+ * ### Validating using methods (with parameters)
+ *
+ * {{{
+ * public $validate = array(
+ * 'age' => array(
+ * 'rule' => array('between', 5, 25)
+ * )
+ * );
+ * }}}
+ *
+ * ### Validating using custom method
+ *
+ * {{{
+ * public $validate = array(
+ * 'password' => array(
+ * 'rule' => array('customValidation')
+ * )
+ * );
+ * public function customValidation($data) {
+ * // $data will contain array('password' => 'value')
+ * if (isset($this->data[$this->alias]['password2'])) {
+ * return $this->data[$this->alias]['password2'] === current($data);
+ * }
+ * return true;
+ * }
+ * }}}
+ *
+ * ### Validations with messages
+ *
+ * The messages will be used in Model::$validationErrors and can be used in the FormHelper
+ *
+ * {{{
+ * public $validate = array(
+ * 'age' => array(
+ * 'rule' => array('between', 5, 25),
+ * 'message' => array('The age must be between %d and %d.')
+ * )
+ * );
+ * }}}
+ *
+ * ### Multiple validations to the same field
+ *
+ * {{{
+ * public $validate = array(
+ * 'login' => array(
+ * array(
+ * 'rule' => 'alphaNumeric',
+ * 'message' => 'Only alphabets and numbers allowed',
+ * 'last' => true
+ * ),
+ * array(
+ * 'rule' => array('minLength', 8),
+ * 'message' => array('Minimum length of %d characters')
+ * )
+ * )
+ * );
+ * }}}
+ *
+ * ### Valid keys in validations
+ *
+ * - `rule`: String with method name, regular expression (started by slash) or array with method and parameters
+ * - `message`: String with the message or array if have multiple parameters. See http://php.net/sprintf
+ * - `last`: Boolean value to indicate if continue validating the others rules if the current fail [Default: true]
+ * - `required`: Boolean value to indicate if the field must be present on save
+ * - `allowEmpty`: Boolean value to indicate if the field can be empty
+ * - `on`: Possible values: `update`, `create`. Indicate to apply this rule only on update or create
+ *
+ * @var array
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#validate
+ * @link http://book.cakephp.org/2.0/en/models/data-validation.html
+ */
+ public $validate = array();
+
+/**
+ * List of validation errors.
+ *
+ * @var array
+ */
+ public $validationErrors = array();
+
+/**
+ * Name of the validation string domain to use when translating validation errors.
+ *
+ * @var string
+ */
+ public $validationDomain = null;
+
+/**
+ * Database table prefix for tables in model.
+ *
+ * @var string
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#tableprefix
+ */
+ public $tablePrefix = null;
+
+/**
+ * Name of the model.
+ *
+ * @var string
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#name
+ */
+ public $name = null;
+
+/**
+ * Alias name for model.
+ *
+ * @var string
+ */
+ public $alias = null;
+
+/**
+ * List of table names included in the model description. Used for associations.
+ *
+ * @var array
+ */
+ public $tableToModel = array();
+
+/**
+ * Whether or not to cache queries for this model. This enables in-memory
+ * caching only, the results are not stored beyond the current request.
+ *
+ * @var boolean
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#cachequeries
+ */
+ public $cacheQueries = false;
+
+/**
+ * Detailed list of belongsTo associations.
+ *
+ * ### Basic usage
+ *
+ * `public $belongsTo = array('Group', 'Department');`
+ *
+ * ### Detailed configuration
+ *
+ * {{{
+ * public $belongsTo = array(
+ * 'Group',
+ * 'Department' => array(
+ * 'className' => 'Department',
+ * 'foreignKey' => 'department_id'
+ * )
+ * );
+ * }}}
+ *
+ * ### Possible keys in association
+ *
+ * - `className`: the classname of the model being associated to the current model.
+ * If you're defining a 'Profile belongsTo User' relationship, the className key should equal 'User.'
+ * - `foreignKey`: the name of the foreign key found in the current model. This is
+ * especially handy if you need to define multiple belongsTo relationships. The default
+ * value for this key is the underscored, singular name of the other model, suffixed with '_id'.
+ * - `conditions`: An SQL fragment used to filter related model records. It's good
+ * practice to use model names in SQL fragments: 'User.active = 1' is always
+ * better than just 'active = 1.'
+ * - `type`: the type of the join to use in the SQL query, default is LEFT which
+ * may not fit your needs in all situations, INNER may be helpful when you want
+ * everything from your main and associated models or nothing at all!(effective
+ * when used with some conditions of course). (NB: type value is in lower case - i.e. left, inner)
+ * - `fields`: A list of fields to be retrieved when the associated model data is
+ * fetched. Returns all fields by default.
+ * - `order`: An SQL fragment that defines the sorting order for the returned associated rows.
+ * - `counterCache`: If set to true the associated Model will automatically increase or
+ * decrease the "[singular_model_name]_count" field in the foreign table whenever you do
+ * a save() or delete(). If its a string then its the field name to use. The value in the
+ * counter field represents the number of related rows.
+ * - `counterScope`: Optional conditions array to use for updating counter cache field.
+ *
+ * @var array
+ * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#belongsto
+ */
+ public $belongsTo = array();
+
+/**
+ * Detailed list of hasOne associations.
+ *
+ * ### Basic usage
+ *
+ * `public $hasOne = array('Profile', 'Address');`
+ *
+ * ### Detailed configuration
+ *
+ * {{{
+ * public $hasOne = array(
+ * 'Profile',
+ * 'Address' => array(
+ * 'className' => 'Address',
+ * 'foreignKey' => 'user_id'
+ * )
+ * );
+ * }}}
+ *
+ * ### Possible keys in association
+ *
+ * - `className`: the classname of the model being associated to the current model.
+ * If you're defining a 'User hasOne Profile' relationship, the className key should equal 'Profile.'
+ * - `foreignKey`: the name of the foreign key found in the other model. This is
+ * especially handy if you need to define multiple hasOne relationships.
+ * The default value for this key is the underscored, singular name of the
+ * current model, suffixed with '_id'. In the example above it would default to 'user_id'.
+ * - `conditions`: An SQL fragment used to filter related model records. It's good
+ * practice to use model names in SQL fragments: "Profile.approved = 1" is
+ * always better than just "approved = 1."
+ * - `fields`: A list of fields to be retrieved when the associated model data is
+ * fetched. Returns all fields by default.
+ * - `order`: An SQL fragment that defines the sorting order for the returned associated rows.
+ * - `dependent`: When the dependent key is set to true, and the model's delete()
+ * method is called with the cascade parameter set to true, associated model
+ * records are also deleted. In this case we set it true so that deleting a
+ * User will also delete her associated Profile.
+ *
+ * @var array
+ * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasone
+ */
+ public $hasOne = array();
+
+/**
+ * Detailed list of hasMany associations.
+ *
+ * ### Basic usage
+ *
+ * `public $hasMany = array('Comment', 'Task');`
+ *
+ * ### Detailed configuration
+ *
+ * {{{
+ * public $hasMany = array(
+ * 'Comment',
+ * 'Task' => array(
+ * 'className' => 'Task',
+ * 'foreignKey' => 'user_id'
+ * )
+ * );
+ * }}}
+ *
+ * ### Possible keys in association
+ *
+ * - `className`: the classname of the model being associated to the current model.
+ * If you're defining a 'User hasMany Comment' relationship, the className key should equal 'Comment.'
+ * - `foreignKey`: the name of the foreign key found in the other model. This is
+ * especially handy if you need to define multiple hasMany relationships. The default
+ * value for this key is the underscored, singular name of the actual model, suffixed with '_id'.
+ * - `conditions`: An SQL fragment used to filter related model records. It's good
+ * practice to use model names in SQL fragments: "Comment.status = 1" is always
+ * better than just "status = 1."
+ * - `fields`: A list of fields to be retrieved when the associated model data is
+ * fetched. Returns all fields by default.
+ * - `order`: An SQL fragment that defines the sorting order for the returned associated rows.
+ * - `limit`: The maximum number of associated rows you want returned.
+ * - `offset`: The number of associated rows to skip over (given the current
+ * conditions and order) before fetching and associating.
+ * - `dependent`: When dependent is set to true, recursive model deletion is
+ * possible. In this example, Comment records will be deleted when their
+ * associated User record has been deleted.
+ * - `exclusive`: When exclusive is set to true, recursive model deletion does
+ * the delete with a deleteAll() call, instead of deleting each entity separately.
+ * This greatly improves performance, but may not be ideal for all circumstances.
+ * - `finderQuery`: A complete SQL query CakePHP can use to fetch associated model
+ * records. This should be used in situations that require very custom results.
+ *
+ * @var array
+ * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasmany
+ */
+ public $hasMany = array();
+
+/**
+ * Detailed list of hasAndBelongsToMany associations.
+ *
+ * ### Basic usage
+ *
+ * `public $hasAndBelongsToMany = array('Role', 'Address');`
+ *
+ * ### Detailed configuration
+ *
+ * {{{
+ * public $hasAndBelongsToMany = array(
+ * 'Role',
+ * 'Address' => array(
+ * 'className' => 'Address',
+ * 'foreignKey' => 'user_id',
+ * 'associationForeignKey' => 'address_id',
+ * 'joinTable' => 'addresses_users'
+ * )
+ * );
+ * }}}
+ *
+ * ### Possible keys in association
+ *
+ * - `className`: the classname of the model being associated to the current model.
+ * If you're defining a 'Recipe HABTM Tag' relationship, the className key should equal 'Tag.'
+ * - `joinTable`: The name of the join table used in this association (if the
+ * current table doesn't adhere to the naming convention for HABTM join tables).
+ * - `with`: Defines the name of the model for the join table. By default CakePHP
+ * will auto-create a model for you. Using the example above it would be called
+ * RecipesTag. By using this key you can override this default name. The join
+ * table model can be used just like any "regular" model to access the join table directly.
+ * - `foreignKey`: the name of the foreign key found in the current model.
+ * This is especially handy if you need to define multiple HABTM relationships.
+ * The default value for this key is the underscored, singular name of the
+ * current model, suffixed with '_id'.
+ * - `associationForeignKey`: the name of the foreign key found in the other model.
+ * This is especially handy if you need to define multiple HABTM relationships.
+ * The default value for this key is the underscored, singular name of the other
+ * model, suffixed with '_id'.
+ * - `unique`: If true (default value) cake will first delete existing relationship
+ * records in the foreign keys table before inserting new ones, when updating a
+ * record. So existing associations need to be passed again when updating.
+ * To prevent deletion of existing relationship records, set this key to a string 'keepExisting'.
+ * - `conditions`: An SQL fragment used to filter related model records. It's good
+ * practice to use model names in SQL fragments: "Comment.status = 1" is always
+ * better than just "status = 1."
+ * - `fields`: A list of fields to be retrieved when the associated model data is
+ * fetched. Returns all fields by default.
+ * - `order`: An SQL fragment that defines the sorting order for the returned associated rows.
+ * - `limit`: The maximum number of associated rows you want returned.
+ * - `offset`: The number of associated rows to skip over (given the current
+ * conditions and order) before fetching and associating.
+ * - `finderQuery`, `deleteQuery`, `insertQuery`: A complete SQL query CakePHP
+ * can use to fetch, delete, or create new associated model records. This should
+ * be used in situations that require very custom results.
+ *
+ * @var array
+ * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasandbelongstomany-habtm
+ */
+ public $hasAndBelongsToMany = array();
+
+/**
+ * List of behaviors to load when the model object is initialized. Settings can be
+ * passed to behaviors by using the behavior name as index. Eg:
+ *
+ * public $actsAs = array('Translate', 'MyBehavior' => array('setting1' => 'value1'))
+ *
+ * @var array
+ * @link http://book.cakephp.org/2.0/en/models/behaviors.html#using-behaviors
+ */
+ public $actsAs = null;
+
+/**
+ * Holds the Behavior objects currently bound to this model.
+ *
+ * @var BehaviorCollection
+ */
+ public $Behaviors = null;
+
+/**
+ * Whitelist of fields allowed to be saved.
+ *
+ * @var array
+ */
+ public $whitelist = array();
+
+/**
+ * Whether or not to cache sources for this model.
+ *
+ * @var boolean
+ */
+ public $cacheSources = true;
+
+/**
+ * Type of find query currently executing.
+ *
+ * @var string
+ */
+ public $findQueryType = null;
+
+/**
+ * Number of associations to recurse through during find calls. Fetches only
+ * the first level by default.
+ *
+ * @var integer
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#recursive
+ */
+ public $recursive = 1;
+
+/**
+ * The column name(s) and direction(s) to order find results by default.
+ *
+ * public $order = "Post.created DESC";
+ * public $order = array("Post.view_count DESC", "Post.rating DESC");
+ *
+ * @var string
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#order
+ */
+ public $order = null;
+
+/**
+ * Array of virtual fields this model has. Virtual fields are aliased
+ * SQL expressions. Fields added to this property will be read as other fields in a model
+ * but will not be saveable.
+ *
+ * `public $virtualFields = array('two' => '1 + 1');`
+ *
+ * Is a simplistic example of how to set virtualFields
+ *
+ * @var array
+ * @link http://book.cakephp.org/2.0/en/models/model-attributes.html#virtualfields
+ */
+ public $virtualFields = array();
+
+/**
+ * Default list of association keys.
+ *
+ * @var array
+ */
+ protected $_associationKeys = array(
+ 'belongsTo' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'counterCache'),
+ 'hasOne' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'dependent'),
+ 'hasMany' => array('className', 'foreignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'dependent', 'exclusive', 'finderQuery', 'counterQuery'),
+ 'hasAndBelongsToMany' => array('className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery', 'deleteQuery', 'insertQuery')
+ );
+
+/**
+ * Holds provided/generated association key names and other data for all associations.
+ *
+ * @var array
+ */
+ protected $_associations = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
+
+/**
+ * Holds model associations temporarily to allow for dynamic (un)binding.
+ *
+ * @var array
+ */
+ public $__backAssociation = array();
+
+/**
+ * Back inner association
+ *
+ * @var array
+ */
+ public $__backInnerAssociation = array();
+
+/**
+ * Back original association
+ *
+ * @var array
+ */
+ public $__backOriginalAssociation = array();
+
+/**
+ * Back containable association
+ *
+ * @var array
+ */
+ public $__backContainableAssociation = array();
+
+/**
+ * The ID of the model record that was last inserted.
+ *
+ * @var integer
+ */
+ protected $_insertID = null;
+
+/**
+ * Has the datasource been configured.
+ *
+ * @var boolean
+ * @see Model::getDataSource
+ */
+ protected $_sourceConfigured = false;
+
+/**
+ * List of valid finder method options, supplied as the first parameter to find().
+ *
+ * @var array
+ */
+ public $findMethods = array(
+ 'all' => true, 'first' => true, 'count' => true,
+ 'neighbors' => true, 'list' => true, 'threaded' => true
+ );
+
+/**
+ * Instance of the CakeEventManager this model is using
+ * to dispatch inner events.
+ *
+ * @var CakeEventManager
+ */
+ protected $_eventManager = null;
+
+/**
+ * Instance of the ModelValidator
+ *
+ * @var ModelValidator
+ */
+ protected $_validator = null;
+
+/**
+ * Constructor. Binds the model's database table to the object.
+ *
+ * If `$id` is an array it can be used to pass several options into the model.
+ *
+ * - id - The id to start the model on.
+ * - table - The table to use for this model.
+ * - ds - The connection name this model is connected to.
+ * - name - The name of the model eg. Post.
+ * - alias - The alias of the model, this is used for registering the instance in the `ClassRegistry`.
+ * eg. `ParentThread`
+ *
+ * ### Overriding Model's __construct method.
+ *
+ * When overriding Model::__construct() be careful to include and pass in all 3 of the
+ * arguments to `parent::__construct($id, $table, $ds);`
+ *
+ * ### Dynamically creating models
+ *
+ * You can dynamically create model instances using the $id array syntax.
+ *
+ * {{{
+ * $Post = new Model(array('table' => 'posts', 'name' => 'Post', 'ds' => 'connection2'));
+ * }}}
+ *
+ * Would create a model attached to the posts table on connection2. Dynamic model creation is useful
+ * when you want a model object that contains no associations or attached behaviors.
+ *
+ * @param integer|string|array $id Set this ID for this model on startup, can also be an array of options, see above.
+ * @param string $table Name of database table to use.
+ * @param string $ds DataSource connection name.
+ */
+ public function __construct($id = false, $table = null, $ds = null) {
+ parent::__construct();
+
+ if (is_array($id)) {
+ extract(array_merge(
+ array(
+ 'id' => $this->id, 'table' => $this->useTable, 'ds' => $this->useDbConfig,
+ 'name' => $this->name, 'alias' => $this->alias
+ ),
+ $id
+ ));
+ }
+
+ if ($this->name === null) {
+ $this->name = (isset($name) ? $name : get_class($this));
+ }
+
+ if ($this->alias === null) {
+ $this->alias = (isset($alias) ? $alias : $this->name);
+ }
+
+ if ($this->primaryKey === null) {
+ $this->primaryKey = 'id';
+ }
+
+ ClassRegistry::addObject($this->alias, $this);
+
+ $this->id = $id;
+ unset($id);
+
+ if ($table === false) {
+ $this->useTable = false;
+ } elseif ($table) {
+ $this->useTable = $table;
+ }
+
+ if ($ds !== null) {
+ $this->useDbConfig = $ds;
+ }
+
+ if (is_subclass_of($this, 'AppModel')) {
+ $merge = array('actsAs', 'findMethods');
+ $parentClass = get_parent_class($this);
+ if ($parentClass !== 'AppModel') {
+ $this->_mergeVars($merge, $parentClass);
+ }
+ $this->_mergeVars($merge, 'AppModel');
+ }
+ $this->_mergeVars(array('findMethods'), 'Model');
+
+ $this->Behaviors = new BehaviorCollection();
+
+ if ($this->useTable !== false) {
+
+ if ($this->useTable === null) {
+ $this->useTable = Inflector::tableize($this->name);
+ }
+
+ if ($this->displayField == null) {
+ unset($this->displayField);
+ }
+ $this->table = $this->useTable;
+ $this->tableToModel[$this->table] = $this->alias;
+ } elseif ($this->table === false) {
+ $this->table = Inflector::tableize($this->name);
+ }
+
+ if ($this->tablePrefix === null) {
+ unset($this->tablePrefix);
+ }
+
+ $this->_createLinks();
+ $this->Behaviors->init($this->alias, $this->actsAs);
+ }
+
+/**
+ * Returns a list of all events that will fire in the model during it's lifecycle.
+ * You can override this function to add you own listener callbacks
+ *
+ * @return array
+ */
+ public function implementedEvents() {
+ return array(
+ 'Model.beforeFind' => array('callable' => 'beforeFind', 'passParams' => true),
+ 'Model.afterFind' => array('callable' => 'afterFind', 'passParams' => true),
+ 'Model.beforeValidate' => array('callable' => 'beforeValidate', 'passParams' => true),
+ 'Model.afterValidate' => array('callable' => 'afterValidate'),
+ 'Model.beforeSave' => array('callable' => 'beforeSave', 'passParams' => true),
+ 'Model.afterSave' => array('callable' => 'afterSave', 'passParams' => true),
+ 'Model.beforeDelete' => array('callable' => 'beforeDelete', 'passParams' => true),
+ 'Model.afterDelete' => array('callable' => 'afterDelete'),
+ );
+ }
+
+/**
+ * Returns the CakeEventManager manager instance that is handling any callbacks.
+ * You can use this instance to register any new listeners or callbacks to the
+ * model events, or create your own events and trigger them at will.
+ *
+ * @return CakeEventManager
+ */
+ public function getEventManager() {
+ if (empty($this->_eventManager)) {
+ $this->_eventManager = new CakeEventManager();
+ $this->_eventManager->attach($this->Behaviors);
+ $this->_eventManager->attach($this);
+ }
+ return $this->_eventManager;
+ }
+
+/**
+ * Handles custom method calls, like findBy<field> for DB models,
+ * and custom RPC calls for remote data sources.
+ *
+ * @param string $method Name of method to call.
+ * @param array $params Parameters for the method.
+ * @return mixed Whatever is returned by called method
+ */
+ public function __call($method, $params) {
+ $result = $this->Behaviors->dispatchMethod($this, $method, $params);
+ if ($result !== array('unhandled')) {
+ return $result;
+ }
+ $return = $this->getDataSource()->query($method, $params, $this);
+ return $return;
+ }
+
+/**
+ * Handles the lazy loading of model associations by looking in the association arrays for the requested variable
+ *
+ * @param string $name variable tested for existence in class
+ * @return boolean true if the variable exists (if is a not loaded model association it will be created), false otherwise
+ */
+ public function __isset($name) {
+ $className = false;
+
+ foreach ($this->_associations as $type) {
+ if (isset($name, $this->{$type}[$name])) {
+ $className = empty($this->{$type}[$name]['className']) ? $name : $this->{$type}[$name]['className'];
+ break;
+ } elseif (isset($name, $this->__backAssociation[$type][$name])) {
+ $className = empty($this->__backAssociation[$type][$name]['className']) ?
+ $name : $this->__backAssociation[$type][$name]['className'];
+ break;
+ } elseif ($type == 'hasAndBelongsToMany') {
+ foreach ($this->{$type} as $k => $relation) {
+ if (empty($relation['with'])) {
+ continue;
+ }
+ if (is_array($relation['with'])) {
+ if (key($relation['with']) === $name) {
+ $className = $name;
+ }
+ } else {
+ list($plugin, $class) = pluginSplit($relation['with']);
+ if ($class === $name) {
+ $className = $relation['with'];
+ }
+ }
+ if ($className) {
+ $assocKey = $k;
+ $dynamic = !empty($relation['dynamicWith']);
+ break(2);
+ }
+ }
+ }
+ }
+
+ if (!$className) {
+ return false;
+ }
+
+ list($plugin, $className) = pluginSplit($className);
+
+ if (!ClassRegistry::isKeySet($className) && !empty($dynamic)) {
+ $this->{$className} = new AppModel(array(
+ 'name' => $className,
+ 'table' => $this->hasAndBelongsToMany[$assocKey]['joinTable'],
+ 'ds' => $this->useDbConfig
+ ));
+ } else {
+ $this->_constructLinkedModel($name, $className, $plugin);
+ }
+
+ if (!empty($assocKey)) {
+ $this->hasAndBelongsToMany[$assocKey]['joinTable'] = $this->{$name}->table;
+ if (count($this->{$name}->schema()) <= 2 && $this->{$name}->primaryKey !== false) {
+ $this->{$name}->primaryKey = $this->hasAndBelongsToMany[$assocKey]['foreignKey'];
+ }
+ }
+
+ return true;
+ }
+
+/**
+ * Returns the value of the requested variable if it can be set by __isset()
+ *
+ * @param string $name variable requested for it's value or reference
+ * @return mixed value of requested variable if it is set
+ */
+ public function __get($name) {
+ if ($name === 'displayField') {
+ return $this->displayField = $this->hasField(array('title', 'name', $this->primaryKey));
+ }
+ if ($name === 'tablePrefix') {
+ $this->setDataSource();
+ if (property_exists($this, 'tablePrefix') && !empty($this->tablePrefix)) {
+ return $this->tablePrefix;
+ }
+ return $this->tablePrefix = null;
+ }
+ if (isset($this->{$name})) {
+ return $this->{$name};
+ }
+ }
+
+/**
+ * Bind model associations on the fly.
+ *
+ * If `$reset` is false, association will not be reset
+ * to the originals defined in the model
+ *
+ * Example: Add a new hasOne binding to the Profile model not
+ * defined in the model source code:
+ *
+ * `$this->User->bindModel( array('hasOne' => array('Profile')) );`
+ *
+ * Bindings that are not made permanent will be reset by the next Model::find() call on this
+ * model.
+ *
+ * @param array $params Set of bindings (indexed by binding type)
+ * @param boolean $reset Set to false to make the binding permanent
+ * @return boolean Success
+ * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly
+ */
+ public function bindModel($params, $reset = true) {
+ foreach ($params as $assoc => $model) {
+ if ($reset === true && !isset($this->__backAssociation[$assoc])) {
+ $this->__backAssociation[$assoc] = $this->{$assoc};
+ }
+ foreach ($model as $key => $value) {
+ $assocName = $key;
+
+ if (is_numeric($key)) {
+ $assocName = $value;
+ $value = array();
+ }
+ $this->{$assoc}[$assocName] = $value;
+ if (property_exists($this, $assocName)) {
+ unset($this->{$assocName});
+ }
+ if ($reset === false && isset($this->__backAssociation[$assoc])) {
+ $this->__backAssociation[$assoc][$assocName] = $value;
+ }
+ }
+ }
+ $this->_createLinks();
+ return true;
+ }
+
+/**
+ * Turn off associations on the fly.
+ *
+ * If $reset is false, association will not be reset
+ * to the originals defined in the model
+ *
+ * Example: Turn off the associated Model Support request,
+ * to temporarily lighten the User model:
+ *
+ * `$this->User->unbindModel( array('hasMany' => array('Supportrequest')) );`
+ *
+ * unbound models that are not made permanent will reset with the next call to Model::find()
+ *
+ * @param array $params Set of bindings to unbind (indexed by binding type)
+ * @param boolean $reset Set to false to make the unbinding permanent
+ * @return boolean Success
+ * @link http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly
+ */
+ public function unbindModel($params, $reset = true) {
+ foreach ($params as $assoc => $models) {
+ if ($reset === true && !isset($this->__backAssociation[$assoc])) {
+ $this->__backAssociation[$assoc] = $this->{$assoc};
+ }
+ foreach ($models as $model) {
+ if ($reset === false && isset($this->__backAssociation[$assoc][$model])) {
+ unset($this->__backAssociation[$assoc][$model]);
+ }
+ unset($this->{$assoc}[$model]);
+ }
+ }
+ return true;
+ }
+
+/**
+ * Create a set of associations.
+ *
+ * @return void
+ */
+ protected function _createLinks() {
+ foreach ($this->_associations as $type) {
+ if (!is_array($this->{$type})) {
+ $this->{$type} = explode(',', $this->{$type});
+
+ foreach ($this->{$type} as $i => $className) {
+ $className = trim($className);
+ unset ($this->{$type}[$i]);
+ $this->{$type}[$className] = array();
+ }
+ }
+
+ if (!empty($this->{$type})) {
+ foreach ($this->{$type} as $assoc => $value) {
+ $plugin = null;
+
+ if (is_numeric($assoc)) {
+ unset ($this->{$type}[$assoc]);
+ $assoc = $value;
+ $value = array();
+
+ if (strpos($assoc, '.') !== false) {
+ list($plugin, $assoc) = pluginSplit($assoc, true);
+ $this->{$type}[$assoc] = array('className' => $plugin . $assoc);
+ } else {
+ $this->{$type}[$assoc] = $value;
+ }
+ }
+ $this->_generateAssociation($type, $assoc);
+ }
+ }
+ }
+ }
+
+/**
+ * Protected helper method to create associated models of a given class.
+ *
+ * @param string $assoc Association name
+ * @param string $className Class name
+ * @param string $plugin name of the plugin where $className is located
+ * examples: public $hasMany = array('Assoc' => array('className' => 'ModelName'));
+ * usage: $this->Assoc->modelMethods();
+ *
+ * public $hasMany = array('ModelName');
+ * usage: $this->ModelName->modelMethods();
+ * @return void
+ */
+ protected function _constructLinkedModel($assoc, $className = null, $plugin = null) {
+ if (empty($className)) {
+ $className = $assoc;
+ }
+
+ if (!isset($this->{$assoc}) || $this->{$assoc}->name !== $className) {
+ if ($plugin) {
+ $plugin .= '.';
+ }
+ $model = array('class' => $plugin . $className, 'alias' => $assoc);
+ $this->{$assoc} = ClassRegistry::init($model);
+ if ($plugin) {
+ ClassRegistry::addObject($plugin . $className, $this->{$assoc});
+ }
+ if ($assoc) {
+ $this->tableToModel[$this->{$assoc}->table] = $assoc;
+ }
+ }
+ }
+
+/**
+ * Build an array-based association from string.
+ *
+ * @param string $type 'belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany'
+ * @param string $assocKey
+ * @return void
+ */
+ protected function _generateAssociation($type, $assocKey) {
+ $class = $assocKey;
+ $dynamicWith = false;
+
+ foreach ($this->_associationKeys[$type] as $key) {
+
+ if (!isset($this->{$type}[$assocKey][$key]) || $this->{$type}[$assocKey][$key] === null) {
+ $data = '';
+
+ switch ($key) {
+ case 'fields':
+ $data = '';
+ break;
+
+ case 'foreignKey':
+ $data = (($type == 'belongsTo') ? Inflector::underscore($assocKey) : Inflector::singularize($this->table)) . '_id';
+ break;
+
+ case 'associationForeignKey':
+ $data = Inflector::singularize($this->{$class}->table) . '_id';
+ break;
+
+ case 'with':
+ $data = Inflector::camelize(Inflector::singularize($this->{$type}[$assocKey]['joinTable']));
+ $dynamicWith = true;
+ break;
+
+ case 'joinTable':
+ $tables = array($this->table, $this->{$class}->table);
+ sort ($tables);
+ $data = $tables[0] . '_' . $tables[1];
+ break;
+
+ case 'className':
+ $data = $class;
+ break;
+
+ case 'unique':
+ $data = true;
+ break;
+ }
+ $this->{$type}[$assocKey][$key] = $data;
+ }
+
+ if ($dynamicWith) {
+ $this->{$type}[$assocKey]['dynamicWith'] = true;
+ }
+
+ }
+ }
+
+/**
+ * Sets a custom table for your controller class. Used by your controller to select a database table.
+ *
+ * @param string $tableName Name of the custom table
+ * @throws MissingTableException when database table $tableName is not found on data source
+ * @return void
+ */
+ public function setSource($tableName) {
+ $this->setDataSource($this->useDbConfig);
+ $db = ConnectionManager::getDataSource($this->useDbConfig);
+ $db->cacheSources = ($this->cacheSources && $db->cacheSources);
+
+ if (method_exists($db, 'listSources')) {
+ $sources = $db->listSources();
+ if (is_array($sources) && !in_array(strtolower($this->tablePrefix . $tableName), array_map('strtolower', $sources))) {
+ throw new MissingTableException(array(
+ 'table' => $this->tablePrefix . $tableName,
+ 'class' => $this->alias,
+ 'ds' => $this->useDbConfig,
+ ));
+ }
+ $this->_schema = null;
+ }
+ $this->table = $this->useTable = $tableName;
+ $this->tableToModel[$this->table] = $this->alias;
+ }
+
+/**
+ * This function does two things:
+ *
+ * 1. it scans the array $one for the primary key,
+ * and if that's found, it sets the current id to the value of $one[id].
+ * For all other keys than 'id' the keys and values of $one are copied to the 'data' property of this object.
+ * 2. Returns an array with all of $one's keys and values.
+ * (Alternative indata: two strings, which are mangled to
+ * a one-item, two-dimensional array using $one for a key and $two as its value.)
+ *
+ * @param string|array|SimpleXmlElement|DomNode $one Array or string of data
+ * @param string $two Value string for the alternative indata method
+ * @return array Data with all of $one's keys and values
+ * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html
+ */
+ public function set($one, $two = null) {
+ if (!$one) {
+ return;
+ }
+ if (is_object($one)) {
+ if ($one instanceof SimpleXMLElement || $one instanceof DOMNode) {
+ $one = $this->_normalizeXmlData(Xml::toArray($one));
+ } else {
+ $one = Set::reverse($one);
+ }
+ }
+
+ if (is_array($one)) {
+ $data = $one;
+ if (empty($one[$this->alias])) {
+ $data = $this->_setAliasData($one);
+ }
+ } else {
+ $data = array($this->alias => array($one => $two));
+ }
+
+ foreach ($data as $modelName => $fieldSet) {
+ if (is_array($fieldSet)) {
+
+ foreach ($fieldSet as $fieldName => $fieldValue) {
+ if (isset($this->validationErrors[$fieldName])) {
+ unset ($this->validationErrors[$fieldName]);
+ }
+
+ if ($modelName === $this->alias) {
+ if ($fieldName === $this->primaryKey) {
+ $this->id = $fieldValue;
+ }
+ }
+ if (is_array($fieldValue) || is_object($fieldValue)) {
+ $fieldValue = $this->deconstruct($fieldName, $fieldValue);
+ }
+ $this->data[$modelName][$fieldName] = $fieldValue;
+ }
+ }
+ }
+ return $data;
+ }
+
+/**
+ * Move values to alias
+ *
+ * @param array $data
+ * @return array
+ */
+ protected function _setAliasData($data) {
+ $models = array_keys($this->getAssociated());
+ $schema = array_keys((array)$this->schema());
+ foreach ($data as $field => $value) {
+ if (in_array($field, $schema) || !in_array($field, $models)) {
+ $data[$this->alias][$field] = $value;
+ unset($data[$field]);
+ }
+ }
+ return $data;
+ }
+
+/**
+ * Normalize Xml::toArray() to use in Model::save()
+ *
+ * @param array $xml XML as array
+ * @return array
+ */
+ protected function _normalizeXmlData(array $xml) {
+ $return = array();
+ foreach ($xml as $key => $value) {
+ if (is_array($value)) {
+ $return[Inflector::camelize($key)] = $this->_normalizeXmlData($value);
+ } elseif ($key[0] === '@') {
+ $return[substr($key, 1)] = $value;
+ } else {
+ $return[$key] = $value;
+ }
+ }
+ return $return;
+ }
+
+/**
+ * Deconstructs a complex data type (array or object) into a single field value.
+ *
+ * @param string $field The name of the field to be deconstructed
+ * @param array|object $data An array or object to be deconstructed into a field
+ * @return mixed The resulting data that should be assigned to a field
+ */
+ public function deconstruct($field, $data) {
+ if (!is_array($data)) {
+ return $data;
+ }
+
+ $type = $this->getColumnType($field);
+
+ if (in_array($type, array('datetime', 'timestamp', 'date', 'time'))) {
+ $useNewDate = (isset($data['year']) || isset($data['month']) ||
+ isset($data['day']) || isset($data['hour']) || isset($data['minute']));
+
+ $dateFields = array('Y' => 'year', 'm' => 'month', 'd' => 'day', 'H' => 'hour', 'i' => 'min', 's' => 'sec');
+ $timeFields = array('H' => 'hour', 'i' => 'min', 's' => 'sec');
+ $date = array();
+
+ if (isset($data['meridian']) && empty($data['meridian'])) {
+ return null;
+ }
+
+ if (
+ isset($data['hour']) &&
+ isset($data['meridian']) &&
+ !empty($data['hour']) &&
+ $data['hour'] != 12 &&
+ 'pm' == $data['meridian']
+ ) {
+ $data['hour'] = $data['hour'] + 12;
+ }
+ if (isset($data['hour']) && isset($data['meridian']) && $data['hour'] == 12 && 'am' == $data['meridian']) {
+ $data['hour'] = '00';
+ }
+ if ($type == 'time') {
+ foreach ($timeFields as $key => $val) {
+ if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') {
+ $data[$val] = '00';
+ } elseif ($data[$val] !== '') {
+ $data[$val] = sprintf('%02d', $data[$val]);
+ }
+ if (!empty($data[$val])) {
+ $date[$key] = $data[$val];
+ } else {
+ return null;
+ }
+ }
+ }
+
+ if ($type == 'datetime' || $type == 'timestamp' || $type == 'date') {
+ foreach ($dateFields as $key => $val) {
+ if ($val == 'hour' || $val == 'min' || $val == 'sec') {
+ if (!isset($data[$val]) || $data[$val] === '0' || $data[$val] === '00') {
+ $data[$val] = '00';
+ } else {
+ $data[$val] = sprintf('%02d', $data[$val]);
+ }
+ }
+ if (!isset($data[$val]) || isset($data[$val]) && (empty($data[$val]) || $data[$val][0] === '-')) {
+ return null;
+ }
+ if (isset($data[$val]) && !empty($data[$val])) {
+ $date[$key] = $data[$val];
+ }
+ }
+ }
+
+ if ($useNewDate && !empty($date)) {
+ $format = $this->getDataSource()->columns[$type]['format'];
+ foreach (array('m', 'd', 'H', 'i', 's') as $index) {
+ if (isset($date[$index])) {
+ $date[$index] = sprintf('%02d', $date[$index]);
+ }
+ }
+ return str_replace(array_keys($date), array_values($date), $format);
+ }
+ }
+ return $data;
+ }
+
+/**
+ * Returns an array of table metadata (column names and types) from the database.
+ * $field => keys(type, null, default, key, length, extra)
+ *
+ * @param boolean|string $field Set to true to reload schema, or a string to return a specific field
+ * @return array Array of table metadata
+ */
+ public function schema($field = false) {
+ if ($this->useTable !== false && (!is_array($this->_schema) || $field === true)) {
+ $db = $this->getDataSource();
+ $db->cacheSources = ($this->cacheSources && $db->cacheSources);
+ if (method_exists($db, 'describe') && $this->useTable !== false) {
+ $this->_schema = $db->describe($this);
+ } elseif ($this->useTable === false) {
+ $this->_schema = array();
+ }
+ }
+ if (is_string($field)) {
+ if (isset($this->_schema[$field])) {
+ return $this->_schema[$field];
+ } else {
+ return null;
+ }
+ }
+ return $this->_schema;
+ }
+
+/**
+ * Returns an associative array of field names and column types.
+ *
+ * @return array Field types indexed by field name
+ */
+ public function getColumnTypes() {
+ $columns = $this->schema();
+ if (empty($columns)) {
+ trigger_error(__d('cake_dev', '(Model::getColumnTypes) Unable to build model field data. If you are using a model without a database table, try implementing schema()'), E_USER_WARNING);
+ }
+ $cols = array();
+ foreach ($columns as $field => $values) {
+ $cols[$field] = $values['type'];
+ }
+ return $cols;
+ }
+
+/**
+ * Returns the column type of a column in the model.
+ *
+ * @param string $column The name of the model column
+ * @return string Column type
+ */
+ public function getColumnType($column) {
+ $db = $this->getDataSource();
+ $cols = $this->schema();
+ $model = null;
+
+ $startQuote = isset($db->startQuote) ? $db->startQuote : null;
+ $endQuote = isset($db->endQuote) ? $db->endQuote : null;
+ $column = str_replace(array($startQuote, $endQuote), '', $column);
+
+ if (strpos($column, '.')) {
+ list($model, $column) = explode('.', $column);
+ }
+ if ($model != $this->alias && isset($this->{$model})) {
+ return $this->{$model}->getColumnType($column);
+ }
+ if (isset($cols[$column]) && isset($cols[$column]['type'])) {
+ return $cols[$column]['type'];
+ }
+ return null;
+ }
+
+/**
+ * Returns true if the supplied field exists in the model's database table.
+ *
+ * @param string|array $name Name of field to look for, or an array of names
+ * @param boolean $checkVirtual checks if the field is declared as virtual
+ * @return mixed If $name is a string, returns a boolean indicating whether the field exists.
+ * If $name is an array of field names, returns the first field that exists,
+ * or false if none exist.
+ */
+ public function hasField($name, $checkVirtual = false) {
+ if (is_array($name)) {
+ foreach ($name as $n) {
+ if ($this->hasField($n, $checkVirtual)) {
+ return $n;
+ }
+ }
+ return false;
+ }
+
+ if ($checkVirtual && !empty($this->virtualFields)) {
+ if ($this->isVirtualField($name)) {
+ return true;
+ }
+ }
+
+ if (empty($this->_schema)) {
+ $this->schema();
+ }
+
+ if ($this->_schema != null) {
+ return isset($this->_schema[$name]);
+ }
+ return false;
+ }
+
+/**
+ * Check that a method is callable on a model. This will check both the model's own methods, its
+ * inherited methods and methods that could be callable through behaviors.
+ *
+ * @param string $method The method to be called.
+ * @return boolean True on method being callable.
+ */
+ public function hasMethod($method) {
+ if (method_exists($this, $method)) {
+ return true;
+ }
+ if ($this->Behaviors->hasMethod($method)) {
+ return true;
+ }
+ return false;
+ }
+
+/**
+ * Returns true if the supplied field is a model Virtual Field
+ *
+ * @param string $field Name of field to look for
+ * @return boolean indicating whether the field exists as a model virtual field.
+ */
+ public function isVirtualField($field) {
+ if (empty($this->virtualFields) || !is_string($field)) {
+ return false;
+ }
+ if (isset($this->virtualFields[$field])) {
+ return true;
+ }
+ if (strpos($field, '.') !== false) {
+ list($model, $field) = explode('.', $field);
+ if ($model == $this->alias && isset($this->virtualFields[$field])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+/**
+ * Returns the expression for a model virtual field
+ *
+ * @param string $field Name of field to look for
+ * @return mixed If $field is string expression bound to virtual field $field
+ * If $field is null, returns an array of all model virtual fields
+ * or false if none $field exist.
+ */
+ public function getVirtualField($field = null) {
+ if ($field == null) {
+ return empty($this->virtualFields) ? false : $this->virtualFields;
+ }
+ if ($this->isVirtualField($field)) {
+ if (strpos($field, '.') !== false) {
+ list($model, $field) = explode('.', $field);
+ }
+ return $this->virtualFields[$field];
+ }
+ return false;
+ }
+
+/**
+ * Initializes the model for writing a new record, loading the default values
+ * for those fields that are not defined in $data, and clearing previous validation errors.
+ * Especially helpful for saving data in loops.
+ *
+ * @param boolean|array $data Optional data array to assign to the model after it is created. If null or false,
+ * schema data defaults are not merged.
+ * @param boolean $filterKey If true, overwrites any primary key input with an empty value
+ * @return array The current Model::data; after merging $data and/or defaults from database
+ * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-create-array-data-array
+ */
+ public function create($data = array(), $filterKey = false) {
+ $defaults = array();
+ $this->id = false;
+ $this->data = array();
+ $this->validationErrors = array();
+
+ if ($data !== null && $data !== false) {
+ foreach ($this->schema() as $field => $properties) {
+ if ($this->primaryKey !== $field && isset($properties['default']) && $properties['default'] !== '') {
+ $defaults[$field] = $properties['default'];
+ }
+ }
+ $this->set($defaults);
+ $this->set($data);
+ }
+ if ($filterKey) {
+ $this->set($this->primaryKey, false);
+ }
+ return $this->data;
+ }
+
+/**
+ * Returns a list of fields from the database, and sets the current model
+ * data (Model::$data) with the record found.
+ *
+ * @param string|array $fields String of single field name, or an array of field names.
+ * @param integer|string $id The ID of the record to read
+ * @return array Array of database fields, or false if not found
+ * @link http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-read
+ */
+ public function read($fields = null, $id = null) {
+ $this->validationErrors = array();
+
+ if ($id != null) {
+ $this->id = $id;
+ }
+
+ $id = $this->id;
+
+ if (is_array($this->id)) {
+ $id = $this->id[0];
+ }
+
+ if ($id !== null && $id !== false) {
+ $this->data = $this->find('first', array(
+ 'conditions' => array($this->alias . '.' . $this->primaryKey => $id),
+ 'fields' => $fields
+ ));
+ return $this->data;
+ } else {
+ return false;
+ }
+ }
+
+/**
+ * Returns the contents of a single field given the supplied conditions, in the
+ * supplied order.
+ *
+ * @param string $name Name of field to get
+ * @param array $conditions SQL conditions (defaults to NULL)
+ * @param string $order SQL ORDER BY fragment
+ * @return string field contents, or false if not found
+ * @link http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-field
+ */
+ public function field($name, $conditions = null, $order = null) {
+ if ($conditions === null && $this->id !== false) {
+ $conditions = array($this->alias . '.' . $this->primaryKey => $this->id);
+ }
+ if ($this->recursive >= 1) {
+ $recursive = -1;
+ } else {
+ $recursive = $this->recursive;
+ }
+ $fields = $name;
+ if ($data = $this->find('first', compact('conditions', 'fields', 'order', 'recursive'))) {
+ if (strpos($name, '.') === false) {
+ if (isset($data[$this->alias][$name])) {
+ return $data[$this->alias][$name];
+ }
+ } else {
+ $name = explode('.', $name);
+ if (isset($data[$name[0]][$name[1]])) {
+ return $data[$name[0]][$name[1]];
+ }
+ }
+ if (isset($data[0]) && count($data[0]) > 0) {
+ return array_shift($data[0]);
+ }
+ } else {
+ return false;
+ }
+ }
+
+/**
+ * Saves the value of a single field to the database, based on the current
+ * model ID.
+ *
+ * @param string $name Name of the table field
+ * @param mixed $value Value of the field
+ * @param array $validate See $options param in Model::save(). Does not respect 'fieldList' key if passed
+ * @return boolean See Model::save()
+ * @see Model::save()
+ * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-savefield-string-fieldname-string-fieldvalue-validate-false
+ */
+ public function saveField($name, $value, $validate = false) {
+ $id = $this->id;
+ $this->create(false);
+
+ if (is_array($validate)) {
+ $options = array_merge(array('validate' => false, 'fieldList' => array($name)), $validate);
+ } else {
+ $options = array('validate' => $validate, 'fieldList' => array($name));
+ }
+ return $this->save(array($this->alias => array($this->primaryKey => $id, $name => $value)), $options);
+ }
+
+/**
+ * Saves model data (based on white-list, if supplied) to the database. By
+ * default, validation occurs before save.
+ *
+ * @param array $data Data to save.
+ * @param boolean|array $validate Either a boolean, or an array.
+ * If a boolean, indicates whether or not to validate before saving.
+ * If an array, allows control of validate, callbacks, and fieldList
+ * @param array $fieldList List of fields to allow to be written
+ * @return mixed On success Model::$data if its not empty or true, false on failure
+ * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html
+ */
+ public function save($data = null, $validate = true, $fieldList = array()) {
+ $defaults = array('validate' => true, 'fieldList' => array(), 'callbacks' => true);
+ $_whitelist = $this->whitelist;
+ $fields = array();
+
+ if (!is_array($validate)) {
+ $options = array_merge($defaults, compact('validate', 'fieldList', 'callbacks'));
+ } else {
+ $options = array_merge($defaults, $validate);
+ }
+
+ if (!empty($options['fieldList'])) {
+ if (!empty($options['fieldList'][$this->alias]) && is_array($options['fieldList'][$this->alias])) {
+ $this->whitelist = $options['fieldList'][$this->alias];
+ } else {
+ $this->whitelist = $options['fieldList'];
+ }
+ } elseif ($options['fieldList'] === null) {
+ $this->whitelist = array();
+ }
+ $this->set($data);
+
+ if (empty($this->data) && !$this->hasField(array('created', 'updated', 'modified'))) {
+ return false;
+ }
+
+ foreach (array('created', 'updated', 'modified') as $field) {
+ $keyPresentAndEmpty = (
+ isset($this->data[$this->alias]) &&
+ array_key_exists($field, $this->data[$this->alias]) &&
+ $this->data[$this->alias][$field] === null
+ );
+ if ($keyPresentAndEmpty) {
+ unset($this->data[$this->alias][$field]);
+ }
+ }
+
+ $exists = $this->exists();
+ $dateFields = array('modified', 'updated');
+
+ if (!$exists) {
+ $dateFields[] = 'created';
+ }
+ if (isset($this->data[$this->alias])) {
+ $fields = array_keys($this->data[$this->alias]);
+ }
+ if ($options['validate'] && !$this->validates($options)) {
+ $this->whitelist = $_whitelist;
+ return false;
+ }
+
+ $db = $this->getDataSource();
+
+ foreach ($dateFields as $updateCol) {
+ if ($this->hasField($updateCol) && !in_array($updateCol, $fields)) {
+ $default = array('formatter' => 'date');
+ $colType = array_merge($default, $db->columns[$this->getColumnType($updateCol)]);
+ if (!array_key_exists('format', $colType)) {
+ $time = strtotime('now');
+ } else {
+ $time = call_user_func($colType['formatter'], $colType['format']);
+ }
+ if (!empty($this->whitelist)) {
+ $this->whitelist[] = $updateCol;
+ }
+ $this->set($updateCol, $time);
+ }
+ }
+
+ if ($options['callbacks'] === true || $options['callbacks'] === 'before') {
+ $event = new CakeEvent('Model.beforeSave', $this, array($options));
+ list($event->break, $event->breakOn) = array(true, array(false, null));
+ $this->getEventManager()->dispatch($event);
+ if (!$event->result) {
+ $this->whitelist = $_whitelist;
+ return false;
+ }
+ }
+
+ if (empty($this->data[$this->alias][$this->primaryKey])) {
+ unset($this->data[$this->alias][$this->primaryKey]);
+ }
+ $fields = $values = array();
+
+ foreach ($this->data as $n => $v) {
+ if (isset($this->hasAndBelongsToMany[$n])) {
+ if (isset($v[$n])) {
+ $v = $v[$n];
+ }
+ $joined[$n] = $v;
+ } else {
+ if ($n === $this->alias) {
+ foreach (array('created', 'updated', 'modified') as $field) {
+ if (array_key_exists($field, $v) && empty($v[$field])) {
+ unset($v[$field]);
+ }
+ }
+
+ foreach ($v as $x => $y) {
+ if ($this->hasField($x) && (empty($this->whitelist) || in_array($x, $this->whitelist))) {
+ list($fields[], $values[]) = array($x, $y);
+ }
+ }
+ }
+ }
+ }
+ $count = count($fields);
+
+ if (!$exists && $count > 0) {
+ $this->id = false;
+ }
+ $success = true;
+ $created = false;
+
+ if ($count > 0) {
+ $cache = $this->_prepareUpdateFields(array_combine($fields, $values));
+
+ if (!empty($this->id)) {
+ $success = (bool)$db->update($this, $fields, $values);
+ } else {
+ $fInfo = $this->schema($this->primaryKey);
+ $isUUID = ($fInfo['length'] == 36 &&
+ ($fInfo['type'] === 'string' || $fInfo['type'] === 'binary')
+ );
+ if (empty($this->data[$this->alias][$this->primaryKey]) && $isUUID) {
+ if (array_key_exists($this->primaryKey, $this->data[$this->alias])) {
+ $j = array_search($this->primaryKey, $fields);
+ $values[$j] = String::uuid();
+ } else {
+ list($fields[], $values[]) = array($this->primaryKey, String::uuid());
+ }
+ }
+
+ if (!$db->create($this, $fields, $values)) {
+ $success = $created = false;
+ } else {
+ $created = true;
+ }
+ }
+
+ if ($success && !empty($this->belongsTo)) {
+ $this->updateCounterCache($cache, $created);
+ }
+ }
+
+ if (!empty($joined) && $success === true) {
+ $this->_saveMulti($joined, $this->id, $db);
+ }
+
+ if ($success && $count > 0) {
+ if (!empty($this->data)) {
+ $success = $this->data;
+ if ($created) {
+ $this->data[$this->alias][$this->primaryKey] = $this->id;
+ }
+ }
+ if ($options['callbacks'] === true || $options['callbacks'] === 'after') {
+ $event = new CakeEvent('Model.afterSave', $this, array($created, $options));
+ $this->getEventManager()->dispatch($event);
+ }
+ if (!empty($this->data)) {
+ $success = Hash::merge($success, $this->data);
+ }
+ $this->data = false;
+ $this->_clearCache();
+ $this->validationErrors = array();
+ }
+ $this->whitelist = $_whitelist;
+ return $success;
+ }
+
+/**
+ * Saves model hasAndBelongsToMany data to the database.
+ *
+ * @param array $joined Data to save
+ * @param integer|string $id ID of record in this model
+ * @param DataSource $db
+ * @return void
+ */
+ protected function _saveMulti($joined, $id, $db) {
+ foreach ($joined as $assoc => $data) {
+
+ if (isset($this->hasAndBelongsToMany[$assoc])) {
+ list($join) = $this->joinModel($this->hasAndBelongsToMany[$assoc]['with']);
+
+ $keyInfo = $this->{$join}->schema($this->{$join}->primaryKey);
+ if ($with = $this->hasAndBelongsToMany[$assoc]['with']) {
+ $withModel = is_array($with) ? key($with) : $with;
+ list($pluginName, $withModel) = pluginSplit($withModel);
+ $dbMulti = $this->{$withModel}->getDataSource();
+ } else {
+ $dbMulti = $db;
+ }
+
+ $isUUID = !empty($this->{$join}->primaryKey) && (
+ $keyInfo['length'] == 36 && (
+ $keyInfo['type'] === 'string' ||
+ $keyInfo['type'] === 'binary'
+ )
+ );
+
+ $newData = $newValues = $newJoins = array();
+ $primaryAdded = false;
+
+ $fields = array(
+ $dbMulti->name($this->hasAndBelongsToMany[$assoc]['foreignKey']),
+ $dbMulti->name($this->hasAndBelongsToMany[$assoc]['associationForeignKey'])
+ );
+
+ $idField = $db->name($this->{$join}->primaryKey);
+ if ($isUUID && !in_array($idField, $fields)) {
+ $fields[] = $idField;
+ $primaryAdded = true;
+ }
+
+ foreach ((array)$data as $row) {
+ if ((is_string($row) && (strlen($row) == 36 || strlen($row) == 16)) || is_numeric($row)) {
+ $newJoins[] = $row;
+ $values = array($id, $row);
+ if ($isUUID && $primaryAdded) {
+ $values[] = String::uuid();
+ }
+ $newValues[$row] = $values;
+ unset($values);
+ } elseif (isset($row[$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
+ if (!empty($row[$this->{$join}->primaryKey])) {
+ $newJoins[] = $row[$this->hasAndBelongsToMany[$assoc]['associationForeignKey']];
+ }
+ $newData[] = $row;
+ } elseif (isset($row[$join]) && isset($row[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
+ if (!empty($row[$join][$this->{$join}->primaryKey])) {
+ $newJoins[] = $row[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']];
+ }
+ $newData[] = $row[$join];
+ }
+ }
+
+ $keepExisting = $this->hasAndBelongsToMany[$assoc]['unique'] === 'keepExisting';
+ if ($this->hasAndBelongsToMany[$assoc]['unique']) {
+ $conditions = array(
+ $join . '.' . $this->hasAndBelongsToMany[$assoc]['foreignKey'] => $id
+ );
+ if (!empty($this->hasAndBelongsToMany[$assoc]['conditions'])) {
+ $conditions = array_merge($conditions, (array)$this->hasAndBelongsToMany[$assoc]['conditions']);
+ }
+ $associationForeignKey = $this->{$join}->alias . '.' . $this->hasAndBelongsToMany[$assoc]['associationForeignKey'];
+ $links = $this->{$join}->find('all', array(
+ 'conditions' => $conditions,
+ 'recursive' => empty($this->hasAndBelongsToMany[$assoc]['conditions']) ? -1 : 0,
+ 'fields' => $associationForeignKey,
+ ));
+
+ $oldLinks = Hash::extract($links, "{n}.{$associationForeignKey}");
+ if (!empty($oldLinks)) {
+ if ($keepExisting && !empty($newJoins)) {
+ $conditions[$associationForeignKey] = array_diff($oldLinks, $newJoins);
+ } else {
+ $conditions[$associationForeignKey] = $oldLinks;
+ }
+ $dbMulti->delete($this->{$join}, $conditions);
+ }
+ }
+
+ if (!empty($newData)) {
+ foreach ($newData as $data) {
+ $data[$this->hasAndBelongsToMany[$assoc]['foreignKey']] = $id;
+ if (empty($data[$this->{$join}->primaryKey])) {
+ $this->{$join}->create();
+ }
+ $this->{$join}->save($data);
+ }
+ }
+
+ if (!empty($newValues)) {
+ if ($keepExisting && !empty($links)) {
+ foreach ($links as $link) {
+ $oldJoin = $link[$join][$this->hasAndBelongsToMany[$assoc]['associationForeignKey']];
+ if (! in_array($oldJoin, $newJoins) ) {
+ $conditions[$associationForeignKey] = $oldJoin;
+ $db->delete($this->{$join}, $conditions);
+ } else {
+ unset($newValues[$oldJoin]);
+ }
+ }
+ $newValues = array_values($newValues);
+ }
+ if (!empty($newValues)) {
+ $dbMulti->insertMulti($this->{$join}, $fields, $newValues);
+ }
+ }
+ }
+ }
+ }
+
+/**
+ * Updates the counter cache of belongsTo associations after a save or delete operation
+ *
+ * @param array $keys Optional foreign key data, defaults to the information $this->data
+ * @param boolean $created True if a new record was created, otherwise only associations with
+ * 'counterScope' defined get updated
+ * @return void
+ */
+ public function updateCounterCache($keys = array(), $created = false) {
+ $keys = empty($keys) ? $this->data[$this->alias] : $keys;
+ $keys['old'] = isset($keys['old']) ? $keys['old'] : array();
+
+ foreach ($this->belongsTo as $parent => $assoc) {
+ if (!empty($assoc['counterCache'])) {
+ if (!is_array($assoc['counterCache'])) {
+ if (isset($assoc['counterScope'])) {
+ $assoc['counterCache'] = array($assoc['counterCache'] => $assoc['counterScope']);
+ } else {
+ $assoc['counterCache'] = array($assoc['counterCache'] => array());
+ }
+ }
+
+ $foreignKey = $assoc['foreignKey'];
+ $fkQuoted = $this->escapeField($assoc['foreignKey']);
+
+ foreach ($assoc['counterCache'] as $field => $conditions) {
+ if (!is_string($field)) {
+ $field = Inflector::underscore($this->alias) . '_count';
+ }
+ if (!$this->{$parent}->hasField($field)) {
+ continue;
+ }
+ if ($conditions === true) {
+ $conditions = array();
+ } else {
+ $conditions = (array)$conditions;
+ }
+
+ if (!array_key_exists($foreignKey, $keys)) {
+ $keys[$foreignKey] = $this->field($foreignKey);
+ }
+ $recursive = (empty($conditions) ? -1 : 0);
+
+ if (isset($keys['old'][$foreignKey])) {
+ if ($keys['old'][$foreignKey] != $keys[$foreignKey]) {
+ $conditions[$fkQuoted] = $keys['old'][$foreignKey];
+ $count = intval($this->find('count', compact('conditions', 'recursive')));
+
+ $this->{$parent}->updateAll(
+ array($field => $count),
+ array($this->{$parent}->escapeField() => $keys['old'][$foreignKey])
+ );
+ }
+ }
+ $conditions[$fkQuoted] = $keys[$foreignKey];
+
+ if ($recursive === 0) {
+ $conditions = array_merge($conditions, (array)$conditions);
+ }
+ $count = intval($this->find('count', compact('conditions', 'recursive')));
+
+ $this->{$parent}->updateAll(
+ array($field => $count),
+ array($this->{$parent}->escapeField() => $keys[$foreignKey])
+ );
+ }
+ }
+ }
+ }
+
+/**
+ * Helper method for Model::updateCounterCache(). Checks the fields to be updated for
+ *
+ * @param array $data The fields of the record that will be updated
+ * @return array Returns updated foreign key values, along with an 'old' key containing the old
+ * values, or empty if no foreign keys are updated.
+ */
+ protected function _prepareUpdateFields($data) {
+ $foreignKeys = array();
+ foreach ($this->belongsTo as $assoc => $info) {
+ if ($info['counterCache']) {
+ $foreignKeys[$assoc] = $info['foreignKey'];
+ }
+ }
+ $included = array_intersect($foreignKeys, array_keys($data));
+
+ if (empty($included) || empty($this->id)) {
+ return array();
+ }
+ $old = $this->find('first', array(
+ 'conditions' => array($this->alias . '.' . $this->primaryKey => $this->id),
+ 'fields' => array_values($included),
+ 'recursive' => -1
+ ));
+ return array_merge($data, array('old' => $old[$this->alias]));
+ }
+
+/**
+ * Backwards compatible passthrough method for:
+ * saveMany(), validateMany(), saveAssociated() and validateAssociated()
+ *
+ * Saves multiple individual records for a single model; Also works with a single record, as well as
+ * all its associated records.
+ *
+ * #### Options
+ *
+ * - validate: Set to false to disable validation, true to validate each record before saving,
+ * 'first' to validate *all* records before any are saved (default),
+ * or 'only' to only validate the records, but not save them.
+ * - atomic: If true (default), will attempt to save all records in a single transaction.
+ * Should be set to false if database/table does not support transactions.
+ * - fieldList: Equivalent to the $fieldList parameter in Model::save().
+ * It should be an associate array with model name as key and array of fields as value. Eg.
+ * {{{
+ * array(
+ * 'SomeModel' => array('field'),
+ * 'AssociatedModel' => array('field', 'otherfield')
+ * )
+ * }}}
+ * - deep: see saveMany/saveAssociated
+ *
+ * @param array $data Record data to save. This can be either a numerically-indexed array (for saving multiple
+ * records of the same type), or an array indexed by association name.
+ * @param array $options Options to use when saving record data, See $options above.
+ * @return mixed If atomic: True on success, or false on failure.
+ * Otherwise: array similar to the $data array passed, but values are set to true/false
+ * depending on whether each record saved successfully.
+ * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array
+ * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveall-array-data-null-array-options-array
+ */
+ public function saveAll($data, $options = array()) {
+ $options = array_merge(array('validate' => 'first'), $options);
+ if (Hash::numeric(array_keys($data))) {
+ if ($options['validate'] === 'only') {
+ return $this->validateMany($data, $options);
+ }
+ return $this->saveMany($data, $options);
+ }
+ if ($options['validate'] === 'only') {
+ return $this->validateAssociated($data, $options);
+ }
+ return $this->saveAssociated($data, $options);
+ }
+
+/**
+ * Saves multiple individual records for a single model
+ *
+ * #### Options
+ *
+ * - validate: Set to false to disable validation, true to validate each record before saving,
+ * 'first' to validate *all* records before any are saved (default),
+ * - atomic: If true (default), will attempt to save all records in a single transaction.
+ * Should be set to false if database/table does not support transactions.
+ * - fieldList: Equivalent to the $fieldList parameter in Model::save()
+ * - deep: If set to true, all associated data will be saved as well.
+ *
+ * @param array $data Record data to save. This should be a numerically-indexed array
+ * @param array $options Options to use when saving record data, See $options above.
+ * @return mixed If atomic: True on success, or false on failure.
+ * Otherwise: array similar to the $data array passed, but values are set to true/false
+ * depending on whether each record saved successfully.
+ * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-savemany-array-data-null-array-options-array
+ */
+ public function saveMany($data = null, $options = array()) {
+ if (empty($data)) {
+ $data = $this->data;
+ }
+
+ $options = array_merge(array('validate' => 'first', 'atomic' => true, 'deep' => false), $options);
+ $this->validationErrors = $validationErrors = array();
+
+ if (empty($data) && $options['validate'] !== false) {
+ $result = $this->save($data, $options);
+ if (!$options['atomic']) {
+ return array(!empty($result));
+ }
+ return !empty($result);
+ }
+
+ if ($options['validate'] === 'first') {
+ $validates = $this->validateMany($data, $options);
+ if ((!$validates && $options['atomic']) || (!$options['atomic'] && in_array(false, $validates, true))) {
+ return $validates;
+ }
+ $options['validate'] = false;
+ }
+
+ if ($options['atomic']) {
+ $db = $this->getDataSource();
+ $transactionBegun = $db->begin();
+ }
+ $return = array();
+ foreach ($data as $key => $record) {
+ $validates = $this->create(null) !== null;
+ $saved = false;
+ if ($validates) {
+ if ($options['deep']) {
+ $saved = $this->saveAssociated($record, array_merge($options, array('atomic' => false)));
+ } else {
+ $saved = $this->save($record, $options);
+ }
+ }
+ $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true))));
+ if (!$validates) {
+ $validationErrors[$key] = $this->validationErrors;
+ }
+ if (!$options['atomic']) {
+ $return[$key] = $validates;
+ } elseif (!$validates) {
+ break;
+ }
+ }
+ $this->validationErrors = $validationErrors;
+
+ if (!$options['atomic']) {
+ return $return;
+ }
+ if ($validates) {
+ if ($transactionBegun) {
+ return $db->commit() !== false;
+ } else {
+ return true;
+ }
+ }
+ $db->rollback();
+ return false;
+ }
+
+/**
+ * Validates multiple individual records for a single model
+ *
+ * #### Options
+ *
+ * - atomic: If true (default), returns boolean. If false returns array.
+ * - fieldList: Equivalent to the $fieldList parameter in Model::save()
+ * - deep: If set to true, all associated data will be validated as well.
+ *
+ * Warning: This method could potentially change the passed argument `$data`,
+ * If you do not want this to happen, make a copy of `$data` before passing it
+ * to this method
+ *
+ * @param array $data Record data to validate. This should be a numerically-indexed array
+ * @param array $options Options to use when validating record data (see above), See also $options of validates().
+ * @return boolean True on success, or false on failure.
+ * @return mixed If atomic: True on success, or false on failure.
+ * Otherwise: array similar to the $data array passed, but values are set to true/false
+ * depending on whether each record validated successfully.
+ */
+ public function validateMany(&$data, $options = array()) {
+ return $this->validator()->validateMany($data, $options);
+ }
+
+/**
+ * Saves a single record, as well as all its directly associated records.
+ *
+ * #### Options
+ *
+ * - `validate` Set to `false` to disable validation, `true` to validate each record before saving,
+ * 'first' to validate *all* records before any are saved(default),
+ * - `atomic` If true (default), will attempt to save all records in a single transaction.
+ * Should be set to false if database/table does not support transactions.
+ * - fieldList: Equivalent to the $fieldList parameter in Model::save().
+ * It should be an associate array with model name as key and array of fields as value. Eg.
+ * {{{
+ * array(
+ * 'SomeModel' => array('field'),
+ * 'AssociatedModel' => array('field', 'otherfield')
+ * )
+ * }}}
+ * - deep: If set to true, not only directly associated data is saved, but deeper nested associated data as well.
+ *
+ * @param array $data Record data to save. This should be an array indexed by association name.
+ * @param array $options Options to use when saving record data, See $options above.
+ * @return mixed If atomic: True on success, or false on failure.
+ * Otherwise: array similar to the $data array passed, but values are set to true/false
+ * depending on whether each record saved successfully.
+ * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array
+ */
+ public function saveAssociated($data = null, $options = array()) {
+ if (empty($data)) {
+ $data = $this->data;
+ }
+
+ $options = array_merge(array('validate' => 'first', 'atomic' => true, 'deep' => false), $options);
+ $this->validationErrors = $validationErrors = array();
+
+ if (empty($data) && $options['validate'] !== false) {
+ $result = $this->save($data, $options);
+ if (!$options['atomic']) {
+ return array(!empty($result));
+ }
+ return !empty($result);
+ }
+
+ if ($options['validate'] === 'first') {
+ $validates = $this->validateAssociated($data, $options);
+ if ((!$validates && $options['atomic']) || (!$options['atomic'] && in_array(false, $validates, true))) {
+ return $validates;
+ }
+ $options['validate'] = false;
+ }
+ if ($options['atomic']) {
+ $db = $this->getDataSource();
+ $transactionBegun = $db->begin();
+ }
+
+ $associations = $this->getAssociated();
+ $return = array();
+ $validates = true;
+ foreach ($data as $association => $values) {
+ $notEmpty = !empty($values[$association]) || (!isset($values[$association]) && !empty($values));
+ if (isset($associations[$association]) && $associations[$association] === 'belongsTo' && $notEmpty) {
+ $validates = $this->{$association}->create(null) !== null;
+ $saved = false;
+ if ($validates) {
+ if ($options['deep']) {
+ $saved = $this->{$association}->saveAssociated($values, array_merge($options, array('atomic' => false)));
+ } else {
+ $saved = $this->{$association}->save($values, array_merge($options, array('atomic' => false)));
+ }
+ $validates = ($saved === true || (is_array($saved) && !in_array(false, $saved, true)));
+ }
+ if ($validates) {
+ $key = $this->belongsTo[$association]['foreignKey'];
+ if (isset($data[$this->alias])) {
+ $data[$this->alias][$key] = $this->{$association}->id;
+ } else {
+ $data = array_merge(array($key => $this->{$association}->id), $data, array($key => $this->{$association}->id));
+ }
+ } else {
+ $validationErrors[$association] = $this->{$association}->validationErrors;
+ }
+ $return[$association] = $validates;
+ }
+ }
+ if ($validates && !($this->create(null) !== null && $this->save($data, $options))) {
+ $validationErrors[$this->alias] = $this->validationErrors;
+ $validates = false;
+ }
+ $return[$this->alias] = $validates;
+
+ foreach ($data as $association => $values) {
+ if (!$validates) {
+ break;
+ }
+ $notEmpty = !empty($values[$association]) || (!isset($values[$association]) && !empty($values));
+ if (isset($associations[$association]) && $notEmpty) {
+ $type = $associations[$association];
+ $key = $this->{$type}[$association]['foreignKey'];
+ switch ($type) {
+ case 'hasOne':
+ if (isset($values[$association])) {
+ $values[$association][$key] = $this->id;
+ } else {
+ $values = array_merge(array($key => $this->id), $values, array($key => $this->id));
+ }
+ $validates = $this->{$association}->create(null) !== null;
+ $saved = false;
+ if ($validates) {
+ if ($options['deep']) {
+ $saved = $this->{$association}->saveAssociated($values, array_merge($options, array('atomic' => false)));
+ } else {
+ $saved = $this->{$association}->save($values, $options);
+ }
+ }
+ $validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true))));
+ if (!$validates) {
+ $validationErrors[$association] = $this->{$association}->validationErrors;
+ }
+ $return[$association] = $validates;
+ break;
+ case 'hasMany':
+ foreach ($values as $i => $value) {
+ if (isset($values[$i][$association])) {
+ $values[$i][$association][$key] = $this->id;
+ } else {
+ $values[$i] = array_merge(array($key => $this->id), $value, array($key => $this->id));
+ }
+ }
+ $_return = $this->{$association}->saveMany($values, array_merge($options, array('atomic' => false)));
+ if (in_array(false, $_return, true)) {
+ $validationErrors[$association] = $this->{$association}->validationErrors;
+ $validates = false;
+ }
+ $return[$association] = $_return;
+ break;
+ }
+ }
+ }
+ $this->validationErrors = $validationErrors;
+
+ if (isset($validationErrors[$this->alias])) {
+ $this->validationErrors = $validationErrors[$this->alias];
+ unset($validationErrors[$this->alias]);
+ $this->validationErrors = array_merge($this->validationErrors, $validationErrors);
+ }
+
+ if (!$options['atomic']) {
+ return $return;
+ }
+ if ($validates) {
+ if ($transactionBegun) {
+ return $db->commit() !== false;
+ } else {
+ return true;
+ }
+ }
+ $db->rollback();
+ return false;
+ }
+
+/**
+ * Validates a single record, as well as all its directly associated records.
+ *
+ * #### Options
+ *
+ * - atomic: If true (default), returns boolean. If false returns array.
+ * - fieldList: Equivalent to the $fieldList parameter in Model::save()
+ * - deep: If set to true, not only directly associated data , but deeper nested associated data is validated as well.
+ *
+ * Warning: This method could potentially change the passed argument `$data`,
+ * If you do not want this to happen, make a copy of `$data` before passing it
+ * to this method
+ *
+ * @param array $data Record data to validate. This should be an array indexed by association name.
+ * @param array $options Options to use when validating record data (see above), See also $options of validates().
+ * @return array|boolean If atomic: True on success, or false on failure.
+ * Otherwise: array similar to the $data array passed, but values are set to true/false
+ * depending on whether each record validated successfully.
+ */
+ public function validateAssociated(&$data, $options = array()) {
+ return $this->validator()->validateAssociated($data, $options);
+ }
+
+/**
+ * Updates multiple model records based on a set of conditions.
+ *
+ * @param array $fields Set of fields and values, indexed by fields.
+ * Fields are treated as SQL snippets, to insert literal values manually escape your data.
+ * @param mixed $conditions Conditions to match, true for all records
+ * @return boolean True on success, false on failure
+ * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-updateall-array-fields-array-conditions
+ */
+ public function updateAll($fields, $conditions = true) {
+ return $this->getDataSource()->update($this, $fields, null, $conditions);
+ }
+
+/**
+ * Removes record for given ID. If no ID is given, the current ID is used. Returns true on success.
+ *
+ * @param integer|string $id ID of record to delete
+ * @param boolean $cascade Set to true to delete records that depend on this record
+ * @return boolean True on success
+ * @link http://book.cakephp.org/2.0/en/models/deleting-data.html
+ */
+ public function delete($id = null, $cascade = true) {
+ if (!empty($id)) {
+ $this->id = $id;
+ }
+ $id = $this->id;
+
+ $event = new CakeEvent('Model.beforeDelete', $this, array($cascade));
+ list($event->break, $event->breakOn) = array(true, array(false, null));
+ $this->getEventManager()->dispatch($event);
+ if (!$event->isStopped()) {
+ if (!$this->exists()) {
+ return false;
+ }
+ $db = $this->getDataSource();
+
+ $this->_deleteDependent($id, $cascade);
+ $this->_deleteLinks($id);
+ $this->id = $id;
+
+ $updateCounterCache = false;
+ if (!empty($this->belongsTo)) {
+ foreach ($this->belongsTo as $parent => $assoc) {
+ if (!empty($assoc['counterCache'])) {
+ $updateCounterCache = true;
+ break;
+ }
+ }
+ if ($updateCounterCache) {
+ $keys = $this->find('first', array(
+ 'fields' => $this->_collectForeignKeys(),
+ 'conditions' => array($this->alias . '.' . $this->primaryKey => $id),
+ 'recursive' => -1,
+ 'callbacks' => false
+ ));
+ }
+ }
+
+ if ($db->delete($this, array($this->alias . '.' . $this->primaryKey => $id))) {
+ if ($updateCounterCache) {
+ $this->updateCounterCache($keys[$this->alias]);
+ }
+ $this->getEventManager()->dispatch(new CakeEvent('Model.afterDelete', $this));
+ $this->_clearCache();
+ $this->id = false;
+ return true;
+ }
+ }
+ return false;
+ }
+
+/**
+ * Cascades model deletes through associated hasMany and hasOne child records.
+ *
+ * @param string $id ID of record that was deleted
+ * @param boolean $cascade Set to true to delete records that depend on this record
+ * @return void
+ */
+ protected function _deleteDependent($id, $cascade) {
+ if (!empty($this->__backAssociation)) {
+ $savedAssociatons = $this->__backAssociation;
+ $this->__backAssociation = array();
+ }
+ if ($cascade === true) {
+ foreach (array_merge($this->hasMany, $this->hasOne) as $assoc => $data) {
+ if ($data['dependent'] === true) {
+
+ $model = $this->{$assoc};
+
+ if ($data['foreignKey'] === false && $data['conditions'] && in_array($this->name, $model->getAssociated('belongsTo'))) {
+ $model->recursive = 0;
+ $conditions = array($this->escapeField(null, $this->name) => $id);
+ } else {
+ $model->recursive = -1;
+ $conditions = array($model->escapeField($data['foreignKey']) => $id);
+ if ($data['conditions']) {
+ $conditions = array_merge((array)$data['conditions'], $conditions);
+ }
+ }
+
+ if (isset($data['exclusive']) && $data['exclusive']) {
+ $model->deleteAll($conditions);
+ } else {
+ $records = $model->find('all', array(
+ 'conditions' => $conditions, 'fields' => $model->primaryKey
+ ));
+
+ if (!empty($records)) {
+ foreach ($records as $record) {
+ $model->delete($record[$model->alias][$model->primaryKey]);
+ }
+ }
+ }
+ }
+ }
+ }
+ if (isset($savedAssociatons)) {
+ $this->__backAssociation = $savedAssociatons;
+ }
+ }
+
+/**
+ * Cascades model deletes through HABTM join keys.
+ *
+ * @param string $id ID of record that was deleted
+ * @return void
+ */
+ protected function _deleteLinks($id) {
+ foreach ($this->hasAndBelongsToMany as $assoc => $data) {
+ list($plugin, $joinModel) = pluginSplit($data['with']);
+ $records = $this->{$joinModel}->find('all', array(
+ 'conditions' => array($this->{$joinModel}->escapeField($data['foreignKey']) => $id),
+ 'fields' => $this->{$joinModel}->primaryKey,
+ 'recursive' => -1,
+ 'callbacks' => false
+ ));
+ if (!empty($records)) {
+ foreach ($records as $record) {
+ $this->{$joinModel}->delete($record[$this->{$joinModel}->alias][$this->{$joinModel}->primaryKey]);
+ }
+ }
+ }
+ }
+
+/**
+ * Deletes multiple model records based on a set of conditions.
+ *
+ * @param mixed $conditions Conditions to match
+ * @param boolean $cascade Set to true to delete records that depend on this record
+ * @param boolean $callbacks Run callbacks
+ * @return boolean True on success, false on failure
+ * @link http://book.cakephp.org/2.0/en/models/deleting-data.html#deleteall
+ */
+ public function deleteAll($conditions, $cascade = true, $callbacks = false) {
+ if (empty($conditions)) {
+ return false;
+ }
+ $db = $this->getDataSource();
+
+ if (!$cascade && !$callbacks) {
+ return $db->delete($this, $conditions);
+ } else {
+ $ids = $this->find('all', array_merge(array(
+ 'fields' => "{$this->alias}.{$this->primaryKey}",
+ 'recursive' => 0), compact('conditions'))
+ );
+ if ($ids === false) {
+ return false;
+ }
+
+ $ids = Hash::extract($ids, "{n}.{$this->alias}.{$this->primaryKey}");
+ if (empty($ids)) {
+ return true;
+ }
+
+ if ($callbacks) {
+ $_id = $this->id;
+ $result = true;
+ foreach ($ids as $id) {
+ $result = ($result && $this->delete($id, $cascade));
+ }
+ $this->id = $_id;
+ return $result;
+ } else {
+ foreach ($ids as $id) {
+ $this->_deleteLinks($id);
+ if ($cascade) {
+ $this->_deleteDependent($id, $cascade);
+ }
+ }
+ return $db->delete($this, array($this->alias . '.' . $this->primaryKey => $ids));
+ }
+ }
+ }
+
+/**
+ * Collects foreign keys from associations.
+ *
+ * @param string $type
+ * @return array
+ */
+ protected function _collectForeignKeys($type = 'belongsTo') {
+ $result = array();
+
+ foreach ($this->{$type} as $assoc => $data) {
+ if (isset($data['foreignKey']) && is_string($data['foreignKey'])) {
+ $result[$assoc] = $data['foreignKey'];
+ }
+ }
+ return $result;
+ }
+
+/**
+ * Returns true if a record with particular ID exists.
+ *
+ * If $id is not passed it calls Model::getID() to obtain the current record ID,
+ * and then performs a Model::find('count') on the currently configured datasource
+ * to ascertain the existence of the record in persistent storage.
+ *
+ * @param integer|string $id ID of record to check for existence
+ * @return boolean True if such a record exists
+ */
+ public function exists($id = null) {
+ if ($id === null) {
+ $id = $this->getID();
+ }
+ if ($id === false) {
+ return false;
+ }
+ $conditions = array($this->alias . '.' . $this->primaryKey => $id);
+ $query = array('conditions' => $conditions, 'recursive' => -1, 'callbacks' => false);
+ return ($this->find('count', $query) > 0);
+ }
+
+/**
+ * Returns true if a record that meets given conditions exists.
+ *
+ * @param array $conditions SQL conditions array
+ * @return boolean True if such a record exists
+ */
+ public function hasAny($conditions = null) {
+ return ($this->find('count', array('conditions' => $conditions, 'recursive' => -1)) != false);
+ }
+
+/**
+ * Queries the datasource and returns a result set array.
+ *
+ * Also used to perform notation finds, where the first argument is type of find operation to perform
+ * (all / first / count / neighbors / list / threaded),
+ * second parameter options for finding ( indexed array, including: 'conditions', 'limit',
+ * 'recursive', 'page', 'fields', 'offset', 'order')
+ *
+ * Eg:
+ * {{{
+ * find('all', array(
+ * 'conditions' => array('name' => 'Thomas Anderson'),
+ * 'fields' => array('name', 'email'),
+ * 'order' => 'field3 DESC',
+ * 'recursive' => 2,
+ * 'group' => 'type'
+ * ));
+ * }}}
+ *
+ * In addition to the standard query keys above, you can provide Datasource, and behavior specific
+ * keys. For example, when using a SQL based datasource you can use the joins key to specify additional
+ * joins that should be part of the query.
+ *
+ * {{{
+ * find('all', array(
+ * 'conditions' => array('name' => 'Thomas Anderson'),
+ * 'joins' => array(
+ * array(
+ * 'alias' => 'Thought',
+ * 'table' => 'thoughts',
+ * 'type' => 'LEFT',
+ * 'conditions' => '`Thought`.`person_id` = `Person`.`id`'
+ * )
+ * )
+ * ));
+ * }}}
+ *
+ * Behaviors and find types can also define custom finder keys which are passed into find().
+ *
+ * Specifying 'fields' for notation 'list':
+ *
+ * - If no fields are specified, then 'id' is used for key and 'model->displayField' is used for value.
+ * - If a single field is specified, 'id' is used for key and specified field is used for value.
+ * - If three fields are specified, they are used (in order) for key, value and group.
+ * - Otherwise, first and second fields are used for key and value.
+ *
+ * Note: find(list) + database views have issues with MySQL 5.0. Try upgrading to MySQL 5.1 if you
+ * have issues with database views.
+ * @param string $type Type of find operation (all / first / count / neighbors / list / threaded)
+ * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks)
+ * @return array Array of records
+ * @link http://book.cakephp.org/2.0/en/models/deleting-data.html#deleteall
+ */
+ public function find($type = 'first', $query = array()) {
+ $this->findQueryType = $type;
+ $this->id = $this->getID();
+
+ $query = $this->buildQuery($type, $query);
+ if (is_null($query)) {
+ return null;
+ }
+
+ $results = $this->getDataSource()->read($this, $query);
+ $this->resetAssociations();
+
+ if ($query['callbacks'] === true || $query['callbacks'] === 'after') {
+ $results = $this->_filterResults($results);
+ }
+
+ $this->findQueryType = null;
+
+ if ($type === 'all') {
+ return $results;
+ } else {
+ if ($this->findMethods[$type] === true) {
+ return $this->{'_find' . ucfirst($type)}('after', $query, $results);
+ }
+ }
+ }
+
+/**
+ * Builds the query array that is used by the data source to generate the query to fetch the data.
+ *
+ * @param string $type Type of find operation (all / first / count / neighbors / list / threaded)
+ * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks)
+ * @return array Query array or null if it could not be build for some reasons
+ * @see Model::find()
+ */
+ public function buildQuery($type = 'first', $query = array()) {
+ $query = array_merge(
+ array(
+ 'conditions' => null, 'fields' => null, 'joins' => array(), 'limit' => null,
+ 'offset' => null, 'order' => null, 'page' => 1, 'group' => null, 'callbacks' => true,
+ ),
+ (array)$query
+ );
+
+ if ($type !== 'all') {
+ if ($this->findMethods[$type] === true) {
+ $query = $this->{'_find' . ucfirst($type)}('before', $query);
+ }
+ }
+
+ if (!is_numeric($query['page']) || intval($query['page']) < 1) {
+ $query['page'] = 1;
+ }
+ if ($query['page'] > 1 && !empty($query['limit'])) {
+ $query['offset'] = ($query['page'] - 1) * $query['limit'];
+ }
+ if ($query['order'] === null && $this->order !== null) {
+ $query['order'] = $this->order;
+ }
+ $query['order'] = array($query['order']);
+
+ if ($query['callbacks'] === true || $query['callbacks'] === 'before') {
+ $event = new CakeEvent('Model.beforeFind', $this, array($query));
+ list($event->break, $event->breakOn, $event->modParams) = array(true, array(false, null), 0);
+ $this->getEventManager()->dispatch($event);
+ if ($event->isStopped()) {
+ return null;
+ }
+ $query = $event->result === true ? $event->data[0] : $event->result;
+ }
+
+ return $query;
+ }
+
+/**
+ * Handles the before/after filter logic for find('first') operations. Only called by Model::find().
+ *
+ * @param string $state Either "before" or "after"
+ * @param array $query
+ * @param array $results
+ * @return array
+ * @see Model::find()
+ */
+ protected function _findFirst($state, $query, $results = array()) {
+ if ($state === 'before') {
+ $query['limit'] = 1;
+ return $query;
+ } elseif ($state === 'after') {
+ if (empty($results[0])) {
+ return false;
+ }
+ return $results[0];
+ }
+ }
+
+/**
+ * Handles the before/after filter logic for find('count') operations. Only called by Model::find().
+ *
+ * @param string $state Either "before" or "after"
+ * @param array $query
+ * @param array $results
+ * @return integer The number of records found, or false
+ * @see Model::find()
+ */
+ protected function _findCount($state, $query, $results = array()) {
+ if ($state === 'before') {
+ if (!empty($query['type']) && isset($this->findMethods[$query['type']]) && $query['type'] !== 'count' ) {
+ $query['operation'] = 'count';
+ $query = $this->{'_find' . ucfirst($query['type'])}('before', $query);
+ }
+ $db = $this->getDataSource();
+ $query['order'] = false;
+ if (!method_exists($db, 'calculate')) {
+ return $query;
+ }
+ if (!empty($query['fields']) && is_array($query['fields'])) {
+ if (!preg_match('/^count/i', current($query['fields']))) {
+ unset($query['fields']);
+ }
+ }
+ if (empty($query['fields'])) {
+ $query['fields'] = $db->calculate($this, 'count');
+ } elseif (method_exists($db, 'expression') && is_string($query['fields']) && !preg_match('/count/i', $query['fields'])) {
+ $query['fields'] = $db->calculate($this, 'count', array(
+ $db->expression($query['fields']), 'count'
+ ));
+ }
+ return $query;
+ } elseif ($state === 'after') {
+ foreach (array(0, $this->alias) as $key) {
+ if (isset($results[0][$key]['count'])) {
+ if (($count = count($results)) > 1) {
+ return $count;
+ } else {
+ return intval($results[0][$key]['count']);
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+/**
+ * Handles the before/after filter logic for find('list') operations. Only called by Model::find().
+ *
+ * @param string $state Either "before" or "after"
+ * @param array $query
+ * @param array $results
+ * @return array Key/value pairs of primary keys/display field values of all records found
+ * @see Model::find()
+ */
+ protected function _findList($state, $query, $results = array()) {
+ if ($state === 'before') {
+ if (empty($query['fields'])) {
+ $query['fields'] = array("{$this->alias}.{$this->primaryKey}", "{$this->alias}.{$this->displayField}");
+ $list = array("{n}.{$this->alias}.{$this->primaryKey}", "{n}.{$this->alias}.{$this->displayField}", null);
+ } else {
+ if (!is_array($query['fields'])) {
+ $query['fields'] = String::tokenize($query['fields']);
+ }
+
+ if (count($query['fields']) === 1) {
+ if (strpos($query['fields'][0], '.') === false) {
+ $query['fields'][0] = $this->alias . '.' . $query['fields'][0];
+ }
+
+ $list = array("{n}.{$this->alias}.{$this->primaryKey}", '{n}.' . $query['fields'][0], null);
+ $query['fields'] = array("{$this->alias}.{$this->primaryKey}", $query['fields'][0]);
+ } elseif (count($query['fields']) === 3) {
+ for ($i = 0; $i < 3; $i++) {
+ if (strpos($query['fields'][$i], '.') === false) {
+ $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i];
+ }
+ }
+
+ $list = array('{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], '{n}.' . $query['fields'][2]);
+ } else {
+ for ($i = 0; $i < 2; $i++) {
+ if (strpos($query['fields'][$i], '.') === false) {
+ $query['fields'][$i] = $this->alias . '.' . $query['fields'][$i];
+ }
+ }
+
+ $list = array('{n}.' . $query['fields'][0], '{n}.' . $query['fields'][1], null);
+ }
+ }
+ if (!isset($query['recursive']) || $query['recursive'] === null) {
+ $query['recursive'] = -1;
+ }
+ list($query['list']['keyPath'], $query['list']['valuePath'], $query['list']['groupPath']) = $list;
+ return $query;
+ } elseif ($state === 'after') {
+ if (empty($results)) {
+ return array();
+ }
+ $lst = $query['list'];
+ return Hash::combine($results, $lst['keyPath'], $lst['valuePath'], $lst['groupPath']);
+ }
+ }
+
+/**
+ * Detects the previous field's value, then uses logic to find the 'wrapping'
+ * rows and return them.
+ *
+ * @param string $state Either "before" or "after"
+ * @param array $query
+ * @param array $results
+ * @return array
+ */
+ protected function _findNeighbors($state, $query, $results = array()) {
+ if ($state === 'before') {
+ extract($query);
+ $conditions = (array)$conditions;
+ if (isset($field) && isset($value)) {
+ if (strpos($field, '.') === false) {
+ $field = $this->alias . '.' . $field;
+ }
+ } else {
+ $field = $this->alias . '.' . $this->primaryKey;
+ $value = $this->id;
+ }
+ $query['conditions'] = array_merge($conditions, array($field . ' <' => $value));
+ $query['order'] = $field . ' DESC';
+ $query['limit'] = 1;
+ $query['field'] = $field;
+ $query['value'] = $value;
+ return $query;
+ } elseif ($state === 'after') {
+ extract($query);
+ unset($query['conditions'][$field . ' <']);
+ $return = array();
+ if (isset($results[0])) {
+ $prevVal = Hash::get($results[0], $field);
+ $query['conditions'][$field . ' >='] = $prevVal;
+ $query['conditions'][$field . ' !='] = $value;
+ $query['limit'] = 2;
+ } else {
+ $return['prev'] = null;
+ $query['conditions'][$field . ' >'] = $value;
+ $query['limit'] = 1;
+ }
+ $query['order'] = $field . ' ASC';
+ $neighbors = $this->find('all', $query);
+ if (!array_key_exists('prev', $return)) {
+ $return['prev'] = $neighbors[0];
+ }
+ if (count($neighbors) === 2) {
+ $return['next'] = $neighbors[1];
+ } elseif (count($neighbors) === 1 && !$return['prev']) {
+ $return['next'] = $neighbors[0];
+ } else {
+ $return['next'] = null;
+ }
+ return $return;
+ }
+ }
+
+/**
+ * In the event of ambiguous results returned (multiple top level results, with different parent_ids)
+ * top level results with different parent_ids to the first result will be dropped
+ *
+ * @param string $state
+ * @param mixed $query
+ * @param array $results
+ * @return array Threaded results
+ */
+ protected function _findThreaded($state, $query, $results = array()) {
+ if ($state === 'before') {
+ return $query;
+ } elseif ($state === 'after') {
+ $parent = 'parent_id';
+ if (isset($query['parent'])) {
+ $parent = $query['parent'];
+ }
+ return Hash::nest($results, array(
+ 'idPath' => '{n}.' . $this->alias . '.' . $this->primaryKey,
+ 'parentPath' => '{n}.' . $this->alias . '.' . $parent
+ ));
+ }
+ }
+
+/**
+ * Passes query results through model and behavior afterFilter() methods.
+ *
+ * @param array $results Results to filter
+ * @param boolean $primary If this is the primary model results (results from model where the find operation was performed)
+ * @return array Set of filtered results
+ */
+ protected function _filterResults($results, $primary = true) {
+ $event = new CakeEvent('Model.afterFind', $this, array($results, $primary));
+ $event->modParams = 0;
+ $this->getEventManager()->dispatch($event);
+ return $event->result;
+ }
+
+/**
+ * This resets the association arrays for the model back
+ * to those originally defined in the model. Normally called at the end
+ * of each call to Model::find()
+ *
+ * @return boolean Success
+ */
+ public function resetAssociations() {
+ if (!empty($this->__backAssociation)) {
+ foreach ($this->_associations as $type) {
+ if (isset($this->__backAssociation[$type])) {
+ $this->{$type} = $this->__backAssociation[$type];
+ }
+ }
+ $this->__backAssociation = array();
+ }
+
+ foreach ($this->_associations as $type) {
+ foreach ($this->{$type} as $key => $name) {
+ if (property_exists($this, $key) && !empty($this->{$key}->__backAssociation)) {
+ $this->{$key}->resetAssociations();
+ }
+ }
+ }
+ $this->__backAssociation = array();
+ return true;
+ }
+
+/**
+ * Returns false if any fields passed match any (by default, all if $or = false) of their matching values.
+ *
+ * @param array $fields Field/value pairs to search (if no values specified, they are pulled from $this->data)
+ * @param boolean $or If false, all fields specified must match in order for a false return value
+ * @return boolean False if any records matching any fields are found
+ */
+ public function isUnique($fields, $or = true) {
+ if (!is_array($fields)) {
+ $fields = func_get_args();
+ if (is_bool($fields[count($fields) - 1])) {
+ $or = $fields[count($fields) - 1];
+ unset($fields[count($fields) - 1]);
+ }
+ }
+
+ foreach ($fields as $field => $value) {
+ if (is_numeric($field)) {
+ unset($fields[$field]);
+
+ $field = $value;
+ if (isset($this->data[$this->alias][$field])) {
+ $value = $this->data[$this->alias][$field];
+ } else {
+ $value = null;
+ }
+ }
+
+ if (strpos($field, '.') === false) {
+ unset($fields[$field]);
+ $fields[$this->alias . '.' . $field] = $value;
+ }
+ }
+ if ($or) {
+ $fields = array('or' => $fields);
+ }
+ if (!empty($this->id)) {
+ $fields[$this->alias . '.' . $this->primaryKey . ' !='] = $this->id;
+ }
+ return ($this->find('count', array('conditions' => $fields, 'recursive' => -1)) == 0);
+ }
+
+/**
+ * Returns a resultset for a given SQL statement. Custom SQL queries should be performed with this method.
+ *
+ * @param string $sql,... SQL statement
+ * @return mixed Resultset array or boolean indicating success / failure depending on the query executed
+ * @link http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#model-query
+ */
+ public function query($sql) {
+ $params = func_get_args();
+ $db = $this->getDataSource();
+ return call_user_func_array(array(&$db, 'query'), $params);
+ }
+
+/**
+ * Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations
+ * that use the 'with' key as well. Since _saveMulti is incapable of exiting a save operation.
+ *
+ * Will validate the currently set data. Use Model::set() or Model::create() to set the active data.
+ *
+ * @param array $options An optional array of custom options to be made available in the beforeValidate callback
+ * @return boolean True if there are no errors
+ */
+ public function validates($options = array()) {
+ return $this->validator()->validates($options);
+ }
+
+/**
+ * Returns an array of fields that have failed validation. On the current model.
+ *
+ * @param string $options An optional array of custom options to be made available in the beforeValidate callback
+ * @return array Array of invalid fields
+ * @see Model::validates()
+ */
+ public function invalidFields($options = array()) {
+ return $this->validator()->errors($options);
+ }
+
+/**
+ * Marks a field as invalid, optionally setting the name of validation
+ * rule (in case of multiple validation for field) that was broken.
+ *
+ * @param string $field The name of the field to invalidate
+ * @param mixed $value Name of validation rule that was not failed, or validation message to
+ * be returned. If no validation key is provided, defaults to true.
+ * @return void
+ */
+ public function invalidate($field, $value = true) {
+ $this->validator()->invalidate($field, $value);
+ }
+
+/**
+ * Returns true if given field name is a foreign key in this model.
+ *
+ * @param string $field Returns true if the input string ends in "_id"
+ * @return boolean True if the field is a foreign key listed in the belongsTo array.
+ */
+ public function isForeignKey($field) {
+ $foreignKeys = array();
+ if (!empty($this->belongsTo)) {
+ foreach ($this->belongsTo as $assoc => $data) {
+ $foreignKeys[] = $data['foreignKey'];
+ }
+ }
+ return in_array($field, $foreignKeys);
+ }
+
+/**
+ * Escapes the field name and prepends the model name. Escaping is done according to the
+ * current database driver's rules.
+ *
+ * @param string $field Field to escape (e.g: id)
+ * @param string $alias Alias for the model (e.g: Post)
+ * @return string The name of the escaped field for this Model (i.e. id becomes `Post`.`id`).
+ */
+ public function escapeField($field = null, $alias = null) {
+ if (empty($alias)) {
+ $alias = $this->alias;
+ }
+ if (empty($field)) {
+ $field = $this->primaryKey;
+ }
+ $db = $this->getDataSource();
+ if (strpos($field, $db->name($alias) . '.') === 0) {
+ return $field;
+ }
+ return $db->name($alias . '.' . $field);
+ }
+
+/**
+ * Returns the current record's ID
+ *
+ * @param integer $list Index on which the composed ID is located
+ * @return mixed The ID of the current record, false if no ID
+ */
+ public function getID($list = 0) {
+ if (empty($this->id) || (is_array($this->id) && isset($this->id[0]) && empty($this->id[0]))) {
+ return false;
+ }
+
+ if (!is_array($this->id)) {
+ return $this->id;
+ }
+
+ if (isset($this->id[$list]) && !empty($this->id[$list])) {
+ return $this->id[$list];
+ } elseif (isset($this->id[$list])) {
+ return false;
+ }
+
+ return current($this->id);
+ }
+
+/**
+ * Returns the ID of the last record this model inserted.
+ *
+ * @return mixed Last inserted ID
+ */
+ public function getLastInsertID() {
+ return $this->getInsertID();
+ }
+
+/**
+ * Returns the ID of the last record this model inserted.
+ *
+ * @return mixed Last inserted ID
+ */
+ public function getInsertID() {
+ return $this->_insertID;
+ }
+
+/**
+ * Sets the ID of the last record this model inserted
+ *
+ * @param integer|string $id Last inserted ID
+ * @return void
+ */
+ public function setInsertID($id) {
+ $this->_insertID = $id;
+ }
+
+/**
+ * Returns the number of rows returned from the last query.
+ *
+ * @return integer Number of rows
+ */
+ public function getNumRows() {
+ return $this->getDataSource()->lastNumRows();
+ }
+
+/**
+ * Returns the number of rows affected by the last query.
+ *
+ * @return integer Number of rows
+ */
+ public function getAffectedRows() {
+ return $this->getDataSource()->lastAffected();
+ }
+
+/**
+ * Sets the DataSource to which this model is bound.
+ *
+ * @param string $dataSource The name of the DataSource, as defined in app/Config/database.php
+ * @return void
+ * @throws MissingConnectionException
+ */
+ public function setDataSource($dataSource = null) {
+ $oldConfig = $this->useDbConfig;
+
+ if ($dataSource != null) {
+ $this->useDbConfig = $dataSource;
+ }
+ $db = ConnectionManager::getDataSource($this->useDbConfig);
+ if (!empty($oldConfig) && isset($db->config['prefix'])) {
+ $oldDb = ConnectionManager::getDataSource($oldConfig);
+
+ if (!isset($this->tablePrefix) || (!isset($oldDb->config['prefix']) || $this->tablePrefix == $oldDb->config['prefix'])) {
+ $this->tablePrefix = $db->config['prefix'];
+ }
+ } elseif (isset($db->config['prefix'])) {
+ $this->tablePrefix = $db->config['prefix'];
+ }
+
+ $this->schemaName = $db->getSchemaName();
+ }
+
+/**
+ * Gets the DataSource to which this model is bound.
+ *
+ * @return DataSource A DataSource object
+ */
+ public function getDataSource() {
+ if (!$this->_sourceConfigured && $this->useTable !== false) {
+ $this->_sourceConfigured = true;
+ $this->setSource($this->useTable);
+ }
+ return ConnectionManager::getDataSource($this->useDbConfig);
+ }
+
+/**
+ * Get associations
+ *
+ * @return array
+ */
+ public function associations() {
+ return $this->_associations;
+ }
+
+/**
+ * Gets all the models with which this model is associated.
+ *
+ * @param string $type Only result associations of this type
+ * @return array Associations
+ */
+ public function getAssociated($type = null) {
+ if ($type == null) {
+ $associated = array();
+ foreach ($this->_associations as $assoc) {
+ if (!empty($this->{$assoc})) {
+ $models = array_keys($this->{$assoc});
+ foreach ($models as $m) {
+ $associated[$m] = $assoc;
+ }
+ }
+ }
+ return $associated;
+ } elseif (in_array($type, $this->_associations)) {
+ if (empty($this->{$type})) {
+ return array();
+ }
+ return array_keys($this->{$type});
+ } else {
+ $assoc = array_merge(
+ $this->hasOne,
+ $this->hasMany,
+ $this->belongsTo,
+ $this->hasAndBelongsToMany
+ );
+ if (array_key_exists($type, $assoc)) {
+ foreach ($this->_associations as $a) {
+ if (isset($this->{$a}[$type])) {
+ $assoc[$type]['association'] = $a;
+ break;
+ }
+ }
+ return $assoc[$type];
+ }
+ return null;
+ }
+ }
+
+/**
+ * Gets the name and fields to be used by a join model. This allows specifying join fields
+ * in the association definition.
+ *
+ * @param string|array $assoc The model to be joined
+ * @param array $keys Any join keys which must be merged with the keys queried
+ * @return array
+ */
+ public function joinModel($assoc, $keys = array()) {
+ if (is_string($assoc)) {
+ list(, $assoc) = pluginSplit($assoc);
+ return array($assoc, array_keys($this->{$assoc}->schema()));
+ } elseif (is_array($assoc)) {
+ $with = key($assoc);
+ return array($with, array_unique(array_merge($assoc[$with], $keys)));
+ }
+ trigger_error(
+ __d('cake_dev', 'Invalid join model settings in %s', $model->alias),
+ E_USER_WARNING
+ );
+ }
+
+/**
+ * Called before each find operation. Return false if you want to halt the find
+ * call, otherwise return the (modified) query data.
+ *
+ * @param array $queryData Data used to execute this query, i.e. conditions, order, etc.
+ * @return mixed true if the operation should continue, false if it should abort; or, modified
+ * $queryData to continue with new $queryData
+ * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforefind
+ */
+ public function beforeFind($queryData) {
+ return true;
+ }
+
+/**
+ * Called after each find operation. Can be used to modify any results returned by find().
+ * Return value should be the (modified) results.
+ *
+ * @param mixed $results The results of the find operation
+ * @param boolean $primary Whether this model is being queried directly (vs. being queried as an association)
+ * @return mixed Result of the find operation
+ * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#afterfind
+ */
+ public function afterFind($results, $primary = false) {
+ return $results;
+ }
+
+/**
+ * Called before each save operation, after validation. Return a non-true result
+ * to halt the save.
+ *
+ * @param array $options
+ * @return boolean True if the operation should continue, false if it should abort
+ * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforesave
+ */
+ public function beforeSave($options = array()) {
+ return true;
+ }
+
+/**
+ * Called after each successful save operation.
+ *
+ * @param boolean $created True if this save created a new record
+ * @return void
+ * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#aftersave
+ */
+ public function afterSave($created) {
+ }
+
+/**
+ * Called before every deletion operation.
+ *
+ * @param boolean $cascade If true records that depend on this record will also be deleted
+ * @return boolean True if the operation should continue, false if it should abort
+ * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforedelete
+ */
+ public function beforeDelete($cascade = true) {
+ return true;
+ }
+
+/**
+ * Called after every deletion operation.
+ *
+ * @return void
+ * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#afterdelete
+ */
+ public function afterDelete() {
+ }
+
+/**
+ * Called during validation operations, before validation. Please note that custom
+ * validation rules can be defined in $validate.
+ *
+ * @param array $options Options passed from model::save(), see $options of model::save().
+ * @return boolean True if validate operation should continue, false to abort
+ * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforevalidate
+ */
+ public function beforeValidate($options = array()) {
+ return true;
+ }
+
+/**
+ * Called after data has been checked for errors
+ *
+ * @return void
+ */
+ public function afterValidate() {
+ }
+
+/**
+ * Called when a DataSource-level error occurs.
+ *
+ * @return void
+ * @link http://book.cakephp.org/2.0/en/models/callback-methods.html#onerror
+ */
+ public function onError() {
+ }
+
+/**
+ * Clears cache for this model.
+ *
+ * @param string $type If null this deletes cached views if Cache.check is true
+ * Will be used to allow deleting query cache also
+ * @return boolean true on delete
+ */
+ protected function _clearCache($type = null) {
+ if ($type === null) {
+ if (Configure::read('Cache.check') === true) {
+ $assoc[] = strtolower(Inflector::pluralize($this->alias));
+ $assoc[] = strtolower(Inflector::underscore(Inflector::pluralize($this->alias)));
+ foreach ($this->_associations as $key => $association) {
+ foreach ($this->$association as $key => $className) {
+ $check = strtolower(Inflector::pluralize($className['className']));
+ if (!in_array($check, $assoc)) {
+ $assoc[] = strtolower(Inflector::pluralize($className['className']));
+ $assoc[] = strtolower(Inflector::underscore(Inflector::pluralize($className['className'])));
+ }
+ }
+ }
+ clearCache($assoc);
+ return true;
+ }
+ } else {
+ //Will use for query cache deleting
+ }
+ }
+
+/**
+ * Retunrs an instance of a model validator for this class
+ *
+ * @return ModelValidator
+ */
+ public function validator($instance = null) {
+ if ($instance instanceof ModelValidator) {
+ return $this->_validator = $instance;
+ }
+
+ if (empty($this->_validator) && is_null($instance)) {
+ $this->_validator = new ModelValidator($this);
+ }
+
+ return $this->_validator;
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelBehavior.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelBehavior.php
new file mode 100644
index 0000000..141b9d5
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelBehavior.php
@@ -0,0 +1,236 @@
+<?php
+/**
+ * Model behaviors base class.
+ *
+ * Adds methods and automagic functionality to Cake Models.
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 1.2.0.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+/**
+ * Model behavior base class.
+ *
+ * Defines the Behavior interface, and contains common model interaction functionality. Behaviors
+ * allow you to simulate mixins, and create reusable blocks of application logic, that can be reused across
+ * several models. Behaviors also provide a way to hook into model callbacks and augment their behavior.
+ *
+ * ### Mixin methods
+ *
+ * Behaviors can provide mixin like features by declaring public methods. These methods should expect
+ * the model instance to be shifted onto the parameter list.
+ *
+ * {{{
+ * function doSomething(Model $model, $arg1, $arg2) {
+ * //do something
+ * }
+ * }}}
+ *
+ * Would be called like `$this->Model->doSomething($arg1, $arg2);`.
+ *
+ * ### Mapped methods
+ *
+ * Behaviors can also define mapped methods. Mapped methods use pattern matching for method invocation. This
+ * allows you to create methods similar to Model::findAllByXXX methods on your behaviors. Mapped methods need to
+ * be declared in your behaviors `$mapMethods` array. The method signature for a mapped method is slightly different
+ * than a normal behavior mixin method.
+ *
+ * {{{
+ * public $mapMethods = array('/do(\w+)/' => 'doSomething');
+ *
+ * function doSomething(Model $model, $method, $arg1, $arg2) {
+ * //do something
+ * }
+ * }}}
+ *
+ * The above will map every doXXX() method call to the behavior. As you can see, the model is
+ * still the first parameter, but the called method name will be the 2nd parameter. This allows
+ * you to munge the method name for additional information, much like Model::findAllByXX.
+ *
+ * @package Cake.Model
+ * @see Model::$actsAs
+ * @see BehaviorCollection::load()
+ */
+class ModelBehavior extends Object {
+
+/**
+ * Contains configuration settings for use with individual model objects. This
+ * is used because if multiple models use this Behavior, each will use the same
+ * object instance. Individual model settings should be stored as an
+ * associative array, keyed off of the model name.
+ *
+ * @var array
+ * @see Model::$alias
+ */
+ public $settings = array();
+
+/**
+ * Allows the mapping of preg-compatible regular expressions to public or
+ * private methods in this class, where the array key is a /-delimited regular
+ * expression, and the value is a class method. Similar to the functionality of
+ * the findBy* / findAllBy* magic methods.
+ *
+ * @var array
+ */
+ public $mapMethods = array();
+
+/**
+ * Setup this behavior with the specified configuration settings.
+ *
+ * @param Model $model Model using this behavior
+ * @param array $config Configuration settings for $model
+ * @return void
+ */
+ public function setup(Model $model, $config = array()) {
+ }
+
+/**
+ * Clean up any initialization this behavior has done on a model. Called when a behavior is dynamically
+ * detached from a model using Model::detach().
+ *
+ * @param Model $model Model using this behavior
+ * @return void
+ * @see BehaviorCollection::detach()
+ */
+ public function cleanup(Model $model) {
+ if (isset($this->settings[$model->alias])) {
+ unset($this->settings[$model->alias]);
+ }
+ }
+
+/**
+ * beforeFind can be used to cancel find operations, or modify the query that will be executed.
+ * By returning null/false you can abort a find. By returning an array you can modify/replace the query
+ * that is going to be run.
+ *
+ * @param Model $model Model using this behavior
+ * @param array $query Data used to execute this query, i.e. conditions, order, etc.
+ * @return boolean|array False or null will abort the operation. You can return an array to replace the
+ * $query that will be eventually run.
+ */
+ public function beforeFind(Model $model, $query) {
+ return true;
+ }
+
+/**
+ * After find callback. Can be used to modify any results returned by find.
+ *
+ * @param Model $model Model using this behavior
+ * @param mixed $results The results of the find operation
+ * @param boolean $primary Whether this model is being queried directly (vs. being queried as an association)
+ * @return mixed An array value will replace the value of $results - any other value will be ignored.
+ */
+ public function afterFind(Model $model, $results, $primary) {
+ }
+
+/**
+ * beforeValidate is called before a model is validated, you can use this callback to
+ * add behavior validation rules into a models validate array. Returning false
+ * will allow you to make the validation fail.
+ *
+ * @param Model $model Model using this behavior
+ * @return mixed False or null will abort the operation. Any other result will continue.
+ */
+ public function beforeValidate(Model $model) {
+ return true;
+ }
+
+/**
+ * afterValidate is called just after model data was validated, you can use this callback
+ * to perform any data cleanup or preparation if needed
+ *
+ * @param Model $model Model using this behavior
+ * @return mixed False will stop this event from being passed to other behaviors
+ */
+ public function afterValidate(Model $model) {
+ return true;
+ }
+
+/**
+ * beforeSave is called before a model is saved. Returning false from a beforeSave callback
+ * will abort the save operation.
+ *
+ * @param Model $model Model using this behavior
+ * @return mixed False if the operation should abort. Any other result will continue.
+ */
+ public function beforeSave(Model $model) {
+ return true;
+ }
+
+/**
+ * afterSave is called after a model is saved.
+ *
+ * @param Model $model Model using this behavior
+ * @param boolean $created True if this save created a new record
+ * @return boolean
+ */
+ public function afterSave(Model $model, $created) {
+ return true;
+ }
+
+/**
+ * Before delete is called before any delete occurs on the attached model, but after the model's
+ * beforeDelete is called. Returning false from a beforeDelete will abort the delete.
+ *
+ * @param Model $model Model using this behavior
+ * @param boolean $cascade If true records that depend on this record will also be deleted
+ * @return mixed False if the operation should abort. Any other result will continue.
+ */
+ public function beforeDelete(Model $model, $cascade = true) {
+ return true;
+ }
+
+/**
+ * After delete is called after any delete occurs on the attached model.
+ *
+ * @param Model $model Model using this behavior
+ * @return void
+ */
+ public function afterDelete(Model $model) {
+ }
+
+/**
+ * DataSource error callback
+ *
+ * @param Model $model Model using this behavior
+ * @param string $error Error generated in DataSource
+ * @return void
+ */
+ public function onError(Model $model, $error) {
+ }
+
+/**
+ * If $model's whitelist property is non-empty, $field will be added to it.
+ * Note: this method should *only* be used in beforeValidate or beforeSave to ensure
+ * that it only modifies the whitelist for the current save operation. Also make sure
+ * you explicitly set the value of the field which you are allowing.
+ *
+ * @param Model $model Model using this behavior
+ * @param string $field Field to be added to $model's whitelist
+ * @return void
+ */
+ protected function _addToWhitelist(Model $model, $field) {
+ if (is_array($field)) {
+ foreach ($field as $f) {
+ $this->_addToWhitelist($model, $f);
+ }
+ return;
+ }
+ if (!empty($model->whitelist) && !in_array($field, $model->whitelist)) {
+ $model->whitelist[] = $field;
+ }
+ }
+
+}
+
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelValidator.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelValidator.php
new file mode 100644
index 0000000..dbe58ce
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/ModelValidator.php
@@ -0,0 +1,599 @@
+<?php
+/**
+ * ModelValidator.
+ *
+ * Provides the Model validation logic.
+ *
+ * PHP versions 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 2.2.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('CakeValidationSet', 'Model/Validator');
+
+/**
+ * ModelValidator object encapsulates all methods related to data validations for a model
+ * It also provides an API to dynamically change validation rules for each model field.
+ *
+ * Implements ArrayAccess to easily modify rules as usually done with `Model::$validate`
+ * definition array
+ *
+ * @package Cake.Model
+ * @link http://book.cakephp.org/2.0/en/data-validation.html
+ */
+class ModelValidator implements ArrayAccess, IteratorAggregate, Countable {
+
+/**
+ * Holds the CakeValidationSet objects array
+ *
+ * @var array
+ */
+ protected $_fields = array();
+
+/**
+ * Holds the reference to the model this Validator is attached to
+ *
+ * @var Model
+ */
+ protected $_model = array();
+
+/**
+ * The validators $validate property, used for checking wheter validation
+ * rules definition changed in the model and should be refreshed in this class
+ *
+ * @var array
+ */
+ protected $_validate = array();
+
+/**
+ * Holds the available custom callback methods, usually taken from model methods
+ * and behavior methods
+ *
+ * @var array
+ */
+ protected $_methods = array();
+
+/**
+ * Constructor
+ *
+ * @param Model $Model A reference to the Model the Validator is attached to
+ */
+ public function __construct(Model $Model) {
+ $this->_model = $Model;
+ }
+
+/**
+ * Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations
+ * that use the 'with' key as well. Since `Model::_saveMulti` is incapable of exiting a save operation.
+ *
+ * Will validate the currently set data. Use `Model::set()` or `Model::create()` to set the active data.
+ *
+ * @param array $options An optional array of custom options to be made available in the beforeValidate callback
+ * @return boolean True if there are no errors
+ */
+ public function validates($options = array()) {
+ $errors = $this->errors($options);
+ if (empty($errors) && $errors !== false) {
+ $errors = $this->_validateWithModels($options);
+ }
+ if (is_array($errors)) {
+ return count($errors) === 0;
+ }
+ return $errors;
+ }
+
+/**
+ * Validates a single record, as well as all its directly associated records.
+ *
+ * #### Options
+ *
+ * - atomic: If true (default), returns boolean. If false returns array.
+ * - fieldList: Equivalent to the $fieldList parameter in Model::save()
+ * - deep: If set to true, not only directly associated data , but deeper nested associated data is validated as well.
+ *
+ * Warning: This method could potentially change the passed argument `$data`,
+ * If you do not want this to happen, make a copy of `$data` before passing it
+ * to this method
+ *
+ * @param array $data Record data to validate. This should be an array indexed by association name.
+ * @param array $options Options to use when validating record data (see above), See also $options of validates().
+ * @return array|boolean If atomic: True on success, or false on failure.
+ * Otherwise: array similar to the $data array passed, but values are set to true/false
+ * depending on whether each record validated successfully.
+ */
+ public function validateAssociated(&$data, $options = array()) {
+ $model = $this->getModel();
+ $options = array_merge(array('atomic' => true, 'deep' => false), $options);
+ $model->validationErrors = $validationErrors = $return = array();
+ $model->create(null);
+ if (!($model->set($data) && $model->validates($options))) {
+ $validationErrors[$model->alias] = $model->validationErrors;
+ $return[$model->alias] = false;
+ } else {
+ $return[$model->alias] = true;
+ }
+ $data = $model->data;
+ if (!empty($options['deep']) && isset($data[$model->alias])) {
+ $recordData = $data[$model->alias];
+ unset($data[$model->alias]);
+ $data = array_merge($data, $recordData);
+ }
+
+ $associations = $model->getAssociated();
+ foreach ($data as $association => &$values) {
+ $validates = true;
+ if (isset($associations[$association])) {
+ if (in_array($associations[$association], array('belongsTo', 'hasOne'))) {
+ if ($options['deep']) {
+ $validates = $model->{$association}->validateAssociated($values, $options);
+ } else {
+ $model->{$association}->create(null);
+ $validates = $model->{$association}->set($values) && $model->{$association}->validates($options);
+ $data[$association] = $model->{$association}->data[$model->{$association}->alias];
+ }
+ if (is_array($validates)) {
+ if (in_array(false, $validates, true)) {
+ $validates = false;
+ } else {
+ $validates = true;
+ }
+ }
+ $return[$association] = $validates;
+ } elseif ($associations[$association] === 'hasMany') {
+ $validates = $model->{$association}->validateMany($values, $options);
+ $return[$association] = $validates;
+ }
+ if (!$validates || (is_array($validates) && in_array(false, $validates, true))) {
+ $validationErrors[$association] = $model->{$association}->validationErrors;
+ }
+ }
+ }
+
+ $model->validationErrors = $validationErrors;
+ if (isset($validationErrors[$model->alias])) {
+ $model->validationErrors = $validationErrors[$model->alias];
+ unset($validationErrors[$model->alias]);
+ $model->validationErrors = array_merge($model->validationErrors, $validationErrors);
+ }
+ if (!$options['atomic']) {
+ return $return;
+ }
+ if ($return[$model->alias] === false || !empty($model->validationErrors)) {
+ return false;
+ }
+ return true;
+ }
+
+/**
+ * Validates multiple individual records for a single model
+ *
+ * #### Options
+ *
+ * - atomic: If true (default), returns boolean. If false returns array.
+ * - fieldList: Equivalent to the $fieldList parameter in Model::save()
+ * - deep: If set to true, all associated data will be validated as well.
+ *
+ * Warning: This method could potentially change the passed argument `$data`,
+ * If you do not want this to happen, make a copy of `$data` before passing it
+ * to this method
+ *
+ * @param array $data Record data to validate. This should be a numerically-indexed array
+ * @param array $options Options to use when validating record data (see above), See also $options of validates().
+ * @return boolean True on success, or false on failure.
+ * @return mixed If atomic: True on success, or false on failure.
+ * Otherwise: array similar to the $data array passed, but values are set to true/false
+ * depending on whether each record validated successfully.
+ */
+ public function validateMany(&$data, $options = array()) {
+ $model = $this->getModel();
+ $options = array_merge(array('atomic' => true, 'deep' => false), $options);
+ $model->validationErrors = $validationErrors = $return = array();
+ foreach ($data as $key => &$record) {
+ if ($options['deep']) {
+ $validates = $model->validateAssociated($record, $options);
+ } else {
+ $model->create(null);
+ $validates = $model->set($record) && $model->validates($options);
+ $data[$key] = $model->data;
+ }
+ if ($validates === false || (is_array($validates) && in_array(false, $validates, true))) {
+ $validationErrors[$key] = $model->validationErrors;
+ $validates = false;
+ } else {
+ $validates = true;
+ }
+ $return[$key] = $validates;
+ }
+ $model->validationErrors = $validationErrors;
+ if (!$options['atomic']) {
+ return $return;
+ }
+ if (empty($model->validationErrors)) {
+ return true;
+ }
+ return false;
+ }
+
+/**
+ * Returns an array of fields that have failed validation. On the current model. This method will
+ * actually run validation rules over data, not just return the messages.
+ *
+ * @param string $options An optional array of custom options to be made available in the beforeValidate callback
+ * @return array Array of invalid fields
+ * @see ModelValidator::validates()
+ */
+ public function errors($options = array()) {
+ if (!$this->_triggerBeforeValidate($options)) {
+ return false;
+ }
+ $model = $this->getModel();
+
+ if (!$this->_parseRules()) {
+ return $model->validationErrors;
+ }
+
+ $fieldList = isset($options['fieldList']) ? $options['fieldList'] : array();
+ $exists = $model->exists();
+ $methods = $this->getMethods();
+ $fields = $this->_validationList($fieldList);
+
+ foreach ($fields as $field) {
+ $field->setMethods($methods);
+ $field->setValidationDomain($model->validationDomain);
+ $data = isset($model->data[$model->alias]) ? $model->data[$model->alias] : array();
+ $errors = $field->validate($data, $exists);
+ foreach ($errors as $error) {
+ $this->invalidate($field->field, $error);
+ }
+ }
+
+ $model->getEventManager()->dispatch(new CakeEvent('Model.afterValidate', $model));
+ return $model->validationErrors;
+ }
+
+/**
+ * Marks a field as invalid, optionally setting a message explaining
+ * why the rule failed
+ *
+ * @param string $field The name of the field to invalidate
+ * @param string $message Validation message explaining why the rule failed, defaults to true.
+ * @return void
+ */
+ public function invalidate($field, $message = true) {
+ $this->getModel()->validationErrors[$field][] = $message;
+ }
+
+/**
+ * Gets all possible custom methods from the Model and attached Behaviors
+ * to be used as validators
+ *
+ * @return array List of callables to be used as validation methods
+ */
+ public function getMethods() {
+ if (!empty($this->_methods)) {
+ return $this->_methods;
+ }
+
+ $methods = array();
+ foreach (get_class_methods($this->_model) as $method) {
+ $methods[strtolower($method)] = array($this->_model, $method);
+ }
+
+ foreach (array_keys($this->_model->Behaviors->methods()) as $method) {
+ $methods += array(strtolower($method) => array($this->_model, $method));
+ }
+
+ return $this->_methods = $methods;
+ }
+
+/**
+ * Returns a CakeValidationSet object containing all validation rules for a field, if no
+ * params are passed then it returns an array with all CakeValidationSet objects for each field
+ *
+ * @param string $name [optional] The fieldname to fetch. Defaults to null.
+ * @return CakeValidationSet|array
+ */
+ public function getField($name = null) {
+ $this->_parseRules();
+ if ($name !== null && !empty($this->_fields[$name])) {
+ return $this->_fields[$name];
+ } elseif ($name !== null) {
+ return null;
+ }
+ return $this->_fields;
+ }
+
+/**
+ * Sets the CakeValidationSet objects from the `Model::$validate` property
+ * If `Model::$validate` is not set or empty, this method returns false. True otherwise.
+ *
+ * @return boolean true if `Model::$validate` was processed, false otherwise
+ */
+ protected function _parseRules() {
+ if ($this->_validate === $this->_model->validate) {
+ return true;
+ }
+
+ if (empty($this->_model->validate)) {
+ $this->_validate = array();
+ $this->_fields = array();
+ return false;
+ }
+
+ $this->_validate = $this->_model->validate;
+ $this->_fields = array();
+ $methods = $this->getMethods();
+ foreach ($this->_validate as $fieldName => $ruleSet) {
+ $this->_fields[$fieldName] = new CakeValidationSet($fieldName, $ruleSet);
+ $this->_fields[$fieldName]->setMethods($methods);
+ }
+ return true;
+ }
+
+/**
+ * Sets the I18n domain for validation messages. This method is chainable.
+ *
+ * @param string $validationDomain [optional] The validation domain to be used.
+ * @return ModelValidator
+ */
+ public function setValidationDomain($validationDomain = null) {
+ if (empty($validationDomain)) {
+ $validationDomain = 'default';
+ }
+ $this->getModel()->validationDomain = $validationDomain;
+ return $this;
+ }
+
+/**
+ * Gets the model related to this validator
+ *
+ * @return Model
+ */
+ public function getModel() {
+ return $this->_model;
+ }
+
+/**
+ * Processes the Model's whitelist or passed fieldList and returns the list of fields
+ * to be validated
+ *
+ * @param array $fieldList list of fields to be used for validation
+ * @return array List of validation rules to be applied
+ */
+ protected function _validationList($fieldList = array()) {
+ $model = $this->getModel();
+ $whitelist = $model->whitelist;
+
+ if (!empty($fieldList)) {
+ if (!empty($fieldList[$model->alias]) && is_array($fieldList[$model->alias])) {
+ $whitelist = $fieldList[$model->alias];
+ } else {
+ $whitelist = $fieldList;
+ }
+ }
+ unset($fieldList);
+
+ $validateList = array();
+ if (!empty($whitelist)) {
+ $this->validationErrors = array();
+
+ foreach ((array)$whitelist as $f) {
+ if (!empty($this->_fields[$f])) {
+ $validateList[$f] = $this->_fields[$f];
+ }
+ }
+ } else {
+ return $this->_fields;
+ }
+
+ return $validateList;
+ }
+
+/**
+ * Runs validation for hasAndBelongsToMany associations that have 'with' keys
+ * set and data in the data set.
+ *
+ * @param array $options Array of options to use on Validation of with models
+ * @return boolean Failure of validation on with models.
+ * @see Model::validates()
+ */
+ protected function _validateWithModels($options) {
+ $valid = true;
+ $model = $this->getModel();
+
+ foreach ($model->hasAndBelongsToMany as $assoc => $association) {
+ if (empty($association['with']) || !isset($model->data[$assoc])) {
+ continue;
+ }
+ list($join) = $model->joinModel($model->hasAndBelongsToMany[$assoc]['with']);
+ $data = $model->data[$assoc];
+
+ $newData = array();
+ foreach ((array)$data as $row) {
+ if (isset($row[$model->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
+ $newData[] = $row;
+ } elseif (isset($row[$join]) && isset($row[$join][$model->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
+ $newData[] = $row[$join];
+ }
+ }
+ if (empty($newData)) {
+ continue;
+ }
+ foreach ($newData as $data) {
+ $data[$model->hasAndBelongsToMany[$assoc]['foreignKey']] = $model->id;
+ $model->{$join}->create($data);
+ $valid = ($valid && $model->{$join}->validator()->validates($options));
+ }
+ }
+ return $valid;
+ }
+
+/**
+ * Propagates beforeValidate event
+ *
+ * @param array $options
+ * @return boolean
+ */
+ protected function _triggerBeforeValidate($options = array()) {
+ $model = $this->getModel();
+ $event = new CakeEvent('Model.beforeValidate', $model, array($options));
+ list($event->break, $event->breakOn) = array(true, false);
+ $model->getEventManager()->dispatch($event);
+ if ($event->isStopped()) {
+ return false;
+ }
+ return true;
+ }
+
+/**
+ * Returns wheter a rule set is defined for a field or not
+ *
+ * @param string $field name of the field to check
+ * @return boolean
+ **/
+ public function offsetExists($field) {
+ $this->_parseRules();
+ return isset($this->_fields[$field]);
+ }
+
+/**
+ * Returns the rule set for a field
+ *
+ * @param string $field name of the field to check
+ * @return CakeValidationSet
+ **/
+ public function offsetGet($field) {
+ $this->_parseRules();
+ return $this->_fields[$field];
+ }
+
+/**
+ * Sets the rule set for a field
+ *
+ * @param string $field name of the field to set
+ * @param array|CakeValidationSet $rules set of rules to apply to field
+ * @return void
+ **/
+ public function offsetSet($field, $rules) {
+ $this->_parseRules();
+ if (!$rules instanceof CakeValidationSet) {
+ $rules = new CakeValidationSet($field, $rules);
+ $methods = $this->getMethods();
+ $rules->setMethods($methods);
+ }
+ $this->_fields[$field] = $rules;
+ }
+
+/**
+ * Unsets the rulset for a field
+ *
+ * @param string $field name of the field to unset
+ * @return void
+ **/
+ public function offsetUnset($field) {
+ $this->_parseRules();
+ unset($this->_fields[$field]);
+ }
+
+/**
+ * Returns an iterator for each of the fields to be validated
+ *
+ * @return ArrayIterator
+ **/
+ public function getIterator() {
+ $this->_parseRules();
+ return new ArrayIterator($this->_fields);
+ }
+
+/**
+ * Returns the number of fields having validation rules
+ *
+ * @return int
+ **/
+ public function count() {
+ $this->_parseRules();
+ return count($this->_fields);
+ }
+
+/**
+ * Adds a new rule to a field's rule set. If second argumet is an array or instance of
+ * CakeValidationSet then rules list for the field will be replaced with second argument and
+ * third argument will be ignored.
+ *
+ * ## Example:
+ *
+ * {{{
+ * $validator
+ * ->add('title', 'required', array('rule' => 'notEmpty', 'required' => true))
+ * ->add('user_id', 'valid', array('rule' => 'numeric', 'message' => 'Invalid User'))
+ *
+ * $validator->add('password', array(
+ * 'size' => array('rule' => array('between', 8, 20)),
+ * 'hasSpecialCharacter' => array('rule' => 'validateSpecialchar', 'message' => 'not valid')
+ * ));
+ * }}}
+ *
+ * @param string $field The name of the field from wich the rule will be removed
+ * @param string|array|CakeValidationSet $name name of the rule to be added or list of rules for the field
+ * @param array|CakeValidationRule $rule or list of rules to be added to the field's rule set
+ * @return ModelValidator this instance
+ **/
+ public function add($field, $name, $rule = null) {
+ $this->_parseRules();
+ if ($name instanceof CakeValidationSet) {
+ $this->_fields[$field] = $name;
+ return $this;
+ }
+
+ if (!isset($this->_fields[$field])) {
+ $rule = (is_string($name)) ? array($name => $rule) : $name;
+ $this->_fields[$field] = new CakeValidationSet($field, $rule);
+ } else {
+ if (is_string($name)) {
+ $this->_fields[$field]->setRule($name, $rule);
+ } else {
+ $this->_fields[$field]->setRules($name);
+ }
+ }
+
+ $methods = $this->getMethods();
+ $this->_fields[$field]->setMethods($methods);
+
+ return $this;
+ }
+
+/**
+ * Removes a rule from the set by its name
+ *
+ * ## Example:
+ *
+ * {{{
+ * $validator
+ * ->remove('title', 'required')
+ * ->remove('user_id')
+ * }}}
+ *
+ * @param string $field The name of the field from wich the rule will be removed
+ * @param string $rule the name of the rule to be removed
+ * @return ModelValidator this instance
+ **/
+ public function remove($field, $rule = null) {
+ $this->_parseRules();
+ if ($rule === null) {
+ unset($this->_fields[$field]);
+ } else {
+ $this->_fields[$field]->removeRule($rule);
+ }
+ return $this;
+ }
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Permission.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Permission.php
new file mode 100644
index 0000000..8decc7f
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Permission.php
@@ -0,0 +1,257 @@
+<?php
+/**
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model
+ * @since CakePHP(tm) v 0.2.9
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+App::uses('AppModel', 'Model');
+
+/**
+ * Permissions linking AROs with ACOs
+ *
+ * @package Cake.Model
+ */
+class Permission extends AppModel {
+
+/**
+ * Model name
+ *
+ * @var string
+ */
+ public $name = 'Permission';
+
+/**
+ * Explicitly disable in-memory query caching
+ *
+ * @var boolean
+ */
+ public $cacheQueries = false;
+
+/**
+ * Override default table name
+ *
+ * @var string
+ */
+ public $useTable = 'aros_acos';
+
+/**
+ * Permissions link AROs with ACOs
+ *
+ * @var array
+ */
+ public $belongsTo = array('Aro', 'Aco');
+
+/**
+ * No behaviors for this model
+ *
+ * @var array
+ */
+ public $actsAs = null;
+
+/**
+ * Constructor, used to tell this model to use the
+ * database configured for ACL
+ */
+ public function __construct() {
+ $config = Configure::read('Acl.database');
+ if (!empty($config)) {
+ $this->useDbConfig = $config;
+ }
+ parent::__construct();
+ }
+
+/**
+ * Checks if the given $aro has access to action $action in $aco
+ *
+ * @param string $aro ARO The requesting object identifier.
+ * @param string $aco ACO The controlled object identifier.
+ * @param string $action Action (defaults to *)
+ * @return boolean Success (true if ARO has access to action in ACO, false otherwise)
+ */
+ public function check($aro, $aco, $action = "*") {
+ if ($aro == null || $aco == null) {
+ return false;
+ }
+
+ $permKeys = $this->getAcoKeys($this->schema());
+ $aroPath = $this->Aro->node($aro);
+ $acoPath = $this->Aco->node($aco);
+
+ if (empty($aroPath) || empty($acoPath)) {
+ trigger_error(__d('cake_dev', "DbAcl::check() - Failed ARO/ACO node lookup in permissions check. Node references:\nAro: ") . print_r($aro, true) . "\nAco: " . print_r($aco, true), E_USER_WARNING);
+ return false;
+ }
+
+ if ($acoPath == null || $acoPath == array()) {
+ trigger_error(__d('cake_dev', "DbAcl::check() - Failed ACO node lookup in permissions check. Node references:\nAro: ") . print_r($aro, true) . "\nAco: " . print_r($aco, true), E_USER_WARNING);
+ return false;
+ }
+
+ if ($action != '*' && !in_array('_' . $action, $permKeys)) {
+ trigger_error(__d('cake_dev', "ACO permissions key %s does not exist in DbAcl::check()", $action), E_USER_NOTICE);
+ return false;
+ }
+
+ $inherited = array();
+ $acoIDs = Hash::extract($acoPath, '{n}.' . $this->Aco->alias . '.id');
+
+ $count = count($aroPath);
+ for ($i = 0; $i < $count; $i++) {
+ $permAlias = $this->alias;
+
+ $perms = $this->find('all', array(
+ 'conditions' => array(
+ "{$permAlias}.aro_id" => $aroPath[$i][$this->Aro->alias]['id'],
+ "{$permAlias}.aco_id" => $acoIDs
+ ),
+ 'order' => array($this->Aco->alias . '.lft' => 'desc'),
+ 'recursive' => 0
+ ));
+
+ if (empty($perms)) {
+ continue;
+ } else {
+ $perms = Hash::extract($perms, '{n}.' . $this->alias);
+ foreach ($perms as $perm) {
+ if ($action == '*') {
+
+ foreach ($permKeys as $key) {
+ if (!empty($perm)) {
+ if ($perm[$key] == -1) {
+ return false;
+ } elseif ($perm[$key] == 1) {
+ $inherited[$key] = 1;
+ }
+ }
+ }
+
+ if (count($inherited) === count($permKeys)) {
+ return true;
+ }
+ } else {
+ switch ($perm['_' . $action]) {
+ case -1:
+ return false;
+ case 0:
+ continue;
+ break;
+ case 1:
+ return true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+/**
+ * Allow $aro to have access to action $actions in $aco
+ *
+ * @param string $aro ARO The requesting object identifier.
+ * @param string $aco ACO The controlled object identifier.
+ * @param string $actions Action (defaults to *)
+ * @param integer $value Value to indicate access type (1 to give access, -1 to deny, 0 to inherit)
+ * @return boolean Success
+ */
+ public function allow($aro, $aco, $actions = "*", $value = 1) {
+ $perms = $this->getAclLink($aro, $aco);
+ $permKeys = $this->getAcoKeys($this->schema());
+ $save = array();
+
+ if ($perms == false) {
+ trigger_error(__d('cake_dev', 'DbAcl::allow() - Invalid node'), E_USER_WARNING);
+ return false;
+ }
+ if (isset($perms[0])) {
+ $save = $perms[0][$this->alias];
+ }
+
+ if ($actions == "*") {
+ $save = array_combine($permKeys, array_pad(array(), count($permKeys), $value));
+ } else {
+ if (!is_array($actions)) {
+ $actions = array('_' . $actions);
+ }
+ if (is_array($actions)) {
+ foreach ($actions as $action) {
+ if ($action{0} != '_') {
+ $action = '_' . $action;
+ }
+ if (in_array($action, $permKeys)) {
+ $save[$action] = $value;
+ }
+ }
+ }
+ }
+ list($save['aro_id'], $save['aco_id']) = array($perms['aro'], $perms['aco']);
+
+ if ($perms['link'] != null && !empty($perms['link'])) {
+ $save['id'] = $perms['link'][0][$this->alias]['id'];
+ } else {
+ unset($save['id']);
+ $this->id = null;
+ }
+ return ($this->save($save) !== false);
+ }
+
+/**
+ * Get an array of access-control links between the given Aro and Aco
+ *
+ * @param string $aro ARO The requesting object identifier.
+ * @param string $aco ACO The controlled object identifier.
+ * @return array Indexed array with: 'aro', 'aco' and 'link'
+ */
+ public function getAclLink($aro, $aco) {
+ $obj = array();
+ $obj['Aro'] = $this->Aro->node($aro);
+ $obj['Aco'] = $this->Aco->node($aco);
+
+ if (empty($obj['Aro']) || empty($obj['Aco'])) {
+ return false;
+ }
+ $aro = Hash::extract($obj, 'Aro.0.' . $this->Aro->alias . '.id');
+ $aco = Hash::extract($obj, 'Aco.0.' . $this->Aco->alias . '.id');
+ $aro = current($aro);
+ $aco = current($aco);
+
+ return array(
+ 'aro' => $aro,
+ 'aco' => $aco,
+ 'link' => $this->find('all', array('conditions' => array(
+ $this->alias . '.aro_id' => $aro,
+ $this->alias . '.aco_id' => $aco
+ )))
+ );
+ }
+
+/**
+ * Get the crud type keys
+ *
+ * @param array $keys Permission schema
+ * @return array permission keys
+ */
+ public function getAcoKeys($keys) {
+ $newKeys = array();
+ $keys = array_keys($keys);
+ foreach ($keys as $key) {
+ if (!in_array($key, array('id', 'aro_id', 'aco_id'))) {
+ $newKeys[] = $key;
+ }
+ }
+ return $newKeys;
+ }
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationRule.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationRule.php
new file mode 100644
index 0000000..8ebf825
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationRule.php
@@ -0,0 +1,333 @@
+<?php
+/**
+ * CakeValidationRule.
+ *
+ * Provides the Model validation logic.
+ *
+ * PHP versions 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Validator
+ * @since CakePHP(tm) v 2.2.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+App::uses('ModelValidator', 'Model');
+App::uses('CakeValidationSet', 'Model/Validator');
+App::uses('Validation', 'Utility');
+
+/**
+ * CakeValidationRule object. Represents a validation method, error message and
+ * rules for applying such method to a field.
+ *
+ * @package Cake.Model.Validator
+ * @link http://book.cakephp.org/2.0/en/data-validation.html
+ */
+class CakeValidationRule {
+
+/**
+ * Whether the field passed this validation rule
+ *
+ * @var mixed
+ */
+ protected $_valid = true;
+
+/**
+ * Holds whether the record being validated exists in datasource or not
+ *
+ * @var boolean
+ */
+ protected $_recordExists = false;
+
+/**
+ * Validation method
+ *
+ * @var mixed
+ */
+ protected $_rule = null;
+
+/**
+ * Validation method arguments
+ *
+ * @var array
+ */
+ protected $_ruleParams = array();
+
+/**
+ * Holds passed in options
+ *
+ * @var array
+ */
+ protected $_passedOptions = array();
+
+/**
+ * The 'rule' key
+ *
+ * @var mixed
+ */
+ public $rule = 'blank';
+
+/**
+ * The 'required' key
+ *
+ * @var mixed
+ */
+ public $required = null;
+
+/**
+ * The 'allowEmpty' key
+ *
+ * @var boolean
+ */
+ public $allowEmpty = null;
+
+/**
+ * The 'on' key
+ *
+ * @var string
+ */
+ public $on = null;
+
+/**
+ * The 'last' key
+ *
+ * @var boolean
+ */
+ public $last = true;
+
+/**
+ * The 'message' key
+ *
+ * @var string
+ */
+ public $message = null;
+
+/**
+ * Constructor
+ *
+ * @param array $validator [optional] The validator properties
+ */
+ public function __construct($validator = array()) {
+ $this->_addValidatorProps($validator);
+ }
+
+/**
+ * Checks if the rule is valid
+ *
+ * @return boolean
+ */
+ public function isValid() {
+ if (!$this->_valid || (is_string($this->_valid) && !empty($this->_valid))) {
+ return false;
+ }
+
+ return true;
+ }
+
+/**
+ * Returns whether the field can be left blank according to this rule
+ *
+ * @return boolean
+ */
+ public function isEmptyAllowed() {
+ return $this->skip() || $this->allowEmpty === true;
+ }
+
+/**
+ * Checks if the field is required according to the `required` property
+ *
+ * @return boolean
+ */
+ public function isRequired() {
+ if (in_array($this->required, array('create', 'update'), true)) {
+ if ($this->required === 'create' && !$this->isUpdate() || $this->required === 'update' && $this->isUpdate()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ return $this->required;
+ }
+
+/**
+ * Checks whether the field failed the `field should be present` validation
+ *
+ * @param array $data data to check rule against
+ * @return boolean
+ */
+ public function checkRequired($field, &$data) {
+ return (
+ (!isset($data[$field]) && $this->isRequired() === true) ||
+ (
+ isset($data[$field]) && (empty($data[$field]) &&
+ !is_numeric($data[$field])) && $this->allowEmpty === false
+ )
+ );
+ }
+
+/**
+ * Checks if the allowEmpty key applies
+ *
+ * @param array $data data to check rule against
+ * @return boolean
+ */
+ public function checkEmpty($field, &$data) {
+ if (empty($data[$field]) && $data[$field] != '0' && $this->allowEmpty === true) {
+ return true;
+ }
+ return false;
+ }
+
+/**
+ * Checks if the validation rule should be skipped
+ *
+ * @return boolean True if the ValidationRule can be skipped
+ */
+ public function skip() {
+ if (!empty($this->on)) {
+ if ($this->on == 'create' && $this->isUpdate() || $this->on == 'update' && !$this->isUpdate()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+/**
+ * Returns whethere this rule should break validation process for associated field
+ * after it fails
+ *
+ * @return boolean
+ */
+ public function isLast() {
+ return (bool)$this->last;
+ }
+
+/**
+ * Gets the validation error message
+ *
+ * @return string
+ */
+ public function getValidationResult() {
+ return $this->_valid;
+ }
+
+/**
+ * Gets an array with the rule properties
+ *
+ * @return array
+ */
+ protected function _getPropertiesArray() {
+ $rule = $this->rule;
+ if (!is_string($rule)) {
+ unset($rule[0]);
+ }
+ return array(
+ 'rule' => $rule,
+ 'required' => $this->required,
+ 'allowEmpty' => $this->allowEmpty,
+ 'on' => $this->on,
+ 'last' => $this->last,
+ 'message' => $this->message
+ );
+ }
+
+/**
+ * Sets the recordExists configuration value for this rule,
+ * ir refers to wheter the model record it is validating exists
+ * exists in the collection or not (create or update operation)
+ *
+ * If called with no parameters it will return whether this rule
+ * is configured for update operations or not.
+ *
+ * @return boolean
+ **/
+ public function isUpdate($exists = null) {
+ if ($exists === null) {
+ return $this->_recordExists;
+ }
+ return $this->_recordExists = $exists;
+ }
+
+/**
+ * Dispatches the validation rule to the given validator method
+ *
+ * @return boolean True if the rule could be dispatched, false otherwise
+ */
+ public function process($field, &$data, &$methods) {
+ $this->_valid = true;
+ $this->_parseRule($field, $data);
+
+ $validator = $this->_getPropertiesArray();
+ $rule = strtolower($this->_rule);
+ if (isset($methods[$rule])) {
+ $this->_ruleParams[] = array_merge($validator, $this->_passedOptions);
+ $this->_ruleParams[0] = array($field => $this->_ruleParams[0]);
+ $this->_valid = call_user_func_array($methods[$rule], $this->_ruleParams);
+ } elseif (class_exists('Validation') && method_exists('Validation', $this->_rule)) {
+ $this->_valid = call_user_func_array(array('Validation', $this->_rule), $this->_ruleParams);
+ } elseif (is_string($validator['rule'])) {
+ $this->_valid = preg_match($this->_rule, $data[$field]);
+ } elseif (Configure::read('debug') > 0) {
+ trigger_error(__d('cake_dev', 'Could not find validation handler %s for %s', $this->_rule, $field), E_USER_WARNING);
+ return false;
+ }
+
+ return true;
+ }
+
+/**
+ * Returns passed options for this rule
+ *
+ * @return array
+ **/
+ public function getOptions($key) {
+ if (!isset($this->_passedOptions[$key])) {
+ return null;
+ }
+ return $this->_passedOptions[$key];
+ }
+
+/**
+ * Sets the rule properties from the rule entry in validate
+ *
+ * @param array $validator [optional]
+ * @return void
+ */
+ protected function _addValidatorProps($validator = array()) {
+ if (!is_array($validator)) {
+ $validator = array('rule' => $validator);
+ }
+ foreach ($validator as $key => $value) {
+ if (isset($value) || !empty($value)) {
+ if (in_array($key, array('rule', 'required', 'allowEmpty', 'on', 'message', 'last'))) {
+ $this->{$key} = $validator[$key];
+ } else {
+ $this->_passedOptions[$key] = $value;
+ }
+ }
+ }
+ }
+
+/**
+ * Parses the rule and sets the rule and ruleParams
+ *
+ * @return void
+ */
+ protected function _parseRule($field, &$data) {
+ if (is_array($this->rule)) {
+ $this->_rule = $this->rule[0];
+ $this->_ruleParams = array_merge(array($data[$field]), array_values(array_slice($this->rule, 1)));
+ } else {
+ $this->_rule = $this->rule;
+ $this->_ruleParams = array($data[$field]);
+ }
+ }
+
+}
diff --git a/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationSet.php b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationSet.php
new file mode 100644
index 0000000..fdecdc1
--- /dev/null
+++ b/poc/poc02-compiling-cake/src/vendor/cakephp-2.2.1-0-gcc44130/lib/Cake/Model/Validator/CakeValidationSet.php
@@ -0,0 +1,350 @@
+<?php
+/**
+ * CakeValidationSet.
+ *
+ * Provides the Model validation logic.
+ *
+ * PHP versions 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Validator
+ * @since CakePHP(tm) v 2.2.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+App::uses('ModelValidator', 'Model');
+App::uses('CakeValidationRule', 'Model/Validator');
+
+/**
+ * CakeValidationSet object. Holds all validation rules for a field and exposes
+ * methods to dynamically add or remove validation rules
+ *
+ * @package Cake.Model.Validator
+ * @link http://book.cakephp.org/2.0/en/data-validation.html
+ */
+class CakeValidationSet implements ArrayAccess, IteratorAggregate, Countable {
+
+/**
+ * Holds the CakeValidationRule objects
+ *
+ * @var array
+ */
+ protected $_rules = array();
+
+/**
+ * List of methods available for validation
+ *
+ * @var array
+ **/
+ protected $_methods = array();
+
+/**
+ * I18n domain for validation messages.
+ *
+ * @var string
+ **/
+ protected $_validationDomain = null;
+
+/**
+ * Whether the validation is stopped
+ *
+ * @var boolean
+ */
+ public $isStopped = false;
+
+/**
+ * Holds the fieldname
+ *
+ * @var string
+ */
+ public $field = null;
+
+/**
+ * Holds the original ruleSet
+ *
+ * @var array
+ */
+ public $ruleSet = array();
+
+/**
+ * Constructor
+ *
+ * @param string $fieldName The fieldname
+ * @param array $ruleset
+ */
+ public function __construct($fieldName, $ruleSet) {
+ $this->field = $fieldName;
+
+ if (!is_array($ruleSet) || (is_array($ruleSet) && isset($ruleSet['rule']))) {
+ $ruleSet = array($ruleSet);
+ }
+
+ foreach ($ruleSet as $index => $validateProp) {
+ $this->_rules[$index] = new CakeValidationRule($validateProp);
+ }
+ $this->ruleSet = $ruleSet;
+ }
+
+/**
+ * Sets the list of methods to use for validation
+ *
+ * @return void
+ **/
+ public function setMethods(&$methods) {
+ $this->_methods =& $methods;
+ }
+
+/**
+ * Sets the I18n domain for validation messages.
+ *
+ * @param string $validationDomain The validation domain to be used.
+ * @return void
+ */
+ public function setValidationDomain($validationDomain) {
+ $this->_validationDomain = $validationDomain;
+ }
+
+/**
+ * Runs all validation rules in this set and returns a list of
+ * validation errors
+ *
+ * @return array list of validation errors for this field
+ */
+ public function validate($data, $isUpdate = false) {
+ $errors = array();
+ foreach ($this->getRules() as $name => $rule) {
+ $rule->isUpdate($isUpdate);
+ if ($rule->skip()) {
+ continue;
+ }
+
+ $checkRequired = $rule->checkRequired($this->field, $data);
+ if (!$checkRequired && array_key_exists($this->field, $data)) {
+ if ($rule->checkEmpty($this->field, $data)) {
+ break;
+ }
+ $rule->process($this->field, $data, $this->_methods);
+ }
+
+ if ($checkRequired || !$rule->isValid()) {
+ $errors[] = $this->_processValidationResponse($name, $rule);
+ if ($rule->isLast()) {
+ break;
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+/**
+ * Gets a rule for a given name if exists
+ *
+ * @param string $name
+ * @return CakeValidationRule
+ */
+ public function getRule($name) {
+ if (!empty($this->_rules[$name])) {
+ return $this->_rules[$name];
+ }
+ }
+
+/**
+ * Returns all rules for this validation set
+ *
+ * @return array
+ */
+ public function getRules() {
+ return $this->_rules;
+ }
+
+/**
+ * Sets a CakeValidationRule $rule with a $name
+ *
+ * ## Example:
+ *
+ * {{{
+ * $set
+ * ->setRule('required', array('rule' => 'notEmpty', 'required' => true))
+ * ->setRule('inRange', array('rule' => array('between', 4, 10))
+ * }}}
+ *
+ * @param string $name The name under which the rule should be set
+ * @param CakeValidationRule|array $rule The validation rule to be set
+ * @return CakeValidationSet this instance
+ */
+ public function setRule($name, $rule) {
+ if (!$rule instanceof CakeValidationRule) {
+ $rule = new CakeValidationRule($rule);
+ }
+ $this->_rules[$name] = $rule;
+ return $this;
+ }
+
+/**
+ * Removes a validation rule from the set
+ *
+ * ## Example:
+ *
+ * {{{
+ * $set
+ * ->removeRule('required')
+ * ->removeRule('inRange')
+ * }}}
+ *
+ * @param string $name The name under which the rule should be unset
+ * @return CakeValidationSet this instance
+ */
+ public function removeRule($name) {
+ unset($this->_rules[$name]);
+ return $this;
+ }
+
+/**
+ * Sets the rules for a given field
+ *
+ * ## Example:
+ *
+ * {{{
+ * $set->setRules(array(
+ * 'required' => array('rule' => 'notEmpty', 'required' => true),
+ * 'inRange' => array('rule' => array('between', 4, 10)
+ * ));
+ * }}}
+ *
+ * @param array $rules The rules to be set
+ * @param bolean $mergeVars [optional] If true, merges vars instead of replace. Defaults to true.
+ * @return ModelField
+ */
+ public function setRules($rules = array(), $mergeVars = true) {
+ if ($mergeVars === false) {
+ $this->_rules = $rules;
+ } else {
+ $this->_rules = array_merge($this->_rules, $rules);
+ }
+ return $this;
+ }
+
+/**
+ * Fetches the correct error message for a failed validation
+ *
+ * @param string $name the name of the rule as it was configured
+ * @param CakeValidationRule $rule the object containing validation information
+ * @return string
+ */
+ protected function _processValidationResponse($name, $rule) {
+ $message = $rule->getValidationResult();
+ if (is_string($message)) {
+ return $message;
+ }
+ $message = $rule->message;
+
+ if ($message !== null) {
+ $args = null;
+ if (is_array($message)) {
+ $result = $message[0];
+ $args = array_slice($message, 1);
+ } else {
+ $result = $message;
+ }
+ if (is_array($rule->rule) && $args === null) {
+ $args = array_slice($rule->rule, 1);
+ }
+ $args = $this->_translateArgs($args);
+
+ $message = __d($this->_validationDomain, $result, $args);
+ } elseif (is_string($name)) {
+ if (is_array($rule->rule)) {
+ $args = array_slice($rule->rule, 1);
+ $args = $this->_translateArgs($args);
+ $message = __d($this->_validationDomain, $name, $args);
+ } else {
+ $message = __d($this->_validationDomain, $name);
+ }
+ } else {
+ $message = __d('cake_dev', 'This field cannot be left blank');
+ }
+
+ return $message;
+ }
+
+/**
+ * Applies translations to validator arguments.
+ *
+ * @param array $args The args to translate
+ * @return array Translated args.
+ */
+ protected function _translateArgs($args) {
+ foreach ((array)$args as $k => $arg) {
+ if (is_string($arg)) {
+ $args[$k] = __d($this->_validationDomain, $arg);
+ }
+ }
+ return $args;
+ }
+
+/**
+ * Returns wheter an index exists in the rule set
+ *
+ * @param string $index name of the rule
+ * @return boolean
+ **/
+ public function offsetExists($index) {
+ return isset($this->_rules[$index]);
+ }
+
+/**
+ * Returns a rule object by its index
+ *
+ * @param string $index name of the rule
+ * @return CakeValidationRule
+ **/
+ public function offsetGet($index) {
+ return $this->_rules[$index];
+ }
+
+/**
+ * Sets or replace a validation rule
+ *
+ * @param string $index name of the rule
+ * @param CakeValidationRule|array rule to add to $index
+ **/
+ public function offsetSet($index, $rule) {
+ $this->setRule($index, $rule);
+ }
+
+/**
+ * Unsets a validation rule
+ *
+ * @param string $index name of the rule
+ * @return void
+ **/
+ public function offsetUnset($index) {
+ unset($this->_rules[$index]);
+ }
+
+/**
+ * Returns an iterator for each of the rules to be applied
+ *
+ * @return ArrayIterator
+ **/
+ public function getIterator() {
+ return new ArrayIterator($this->_rules);
+ }
+
+/**
+ * Returns the number of rules in this set
+ *
+ * @return int
+ **/
+ public function count() {
+ return count($this->_rules);
+ }
+
+}