a grand renaming so that the most significant portion of the name comes first
This commit is contained in:
123
php-practice/web-cake/html/cake/libs/model/behaviors/acl.php
Normal file
123
php-practice/web-cake/html/cake/libs/model/behaviors/acl.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* ACL behavior class.
|
||||
*
|
||||
* Enables objects to easily tie into an ACL system
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* CakePHP : Rapid Development Framework (http://cakephp.org)
|
||||
* Copyright 2006-2010, Cake Software Foundation, Inc.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright 2006-2010, Cake Software Foundation, Inc.
|
||||
* @link http://cakephp.org CakePHP Project
|
||||
* @package cake
|
||||
* @subpackage cake.cake.libs.model.behaviors
|
||||
* @since CakePHP v 1.2.0.4487
|
||||
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
*/
|
||||
|
||||
/**
|
||||
* ACL behavior
|
||||
*
|
||||
* @package cake
|
||||
* @subpackage cake.cake.libs.model.behaviors
|
||||
* @link http://book.cakephp.org/view/1320/ACL
|
||||
*/
|
||||
class AclBehavior extends ModelBehavior {
|
||||
|
||||
/**
|
||||
* Maps ACL type options to ACL models
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
var $__typeMaps = array('requester' => 'Aro', 'controlled' => 'Aco');
|
||||
|
||||
/**
|
||||
* Sets up the configuation for the model, and loads ACL models if they haven't been already
|
||||
*
|
||||
* @param mixed $config
|
||||
* @return void
|
||||
* @access public
|
||||
*/
|
||||
function setup(&$model, $config = array()) {
|
||||
if (is_string($config)) {
|
||||
$config = array('type' => $config);
|
||||
}
|
||||
$this->settings[$model->name] = array_merge(array('type' => 'requester'), (array)$config);
|
||||
$this->settings[$model->name]['type'] = strtolower($this->settings[$model->name]['type']);
|
||||
|
||||
$type = $this->__typeMaps[$this->settings[$model->name]['type']];
|
||||
if (!class_exists('AclNode')) {
|
||||
require LIBS . 'model' . DS . 'db_acl.php';
|
||||
}
|
||||
if (PHP5) {
|
||||
$model->{$type} = ClassRegistry::init($type);
|
||||
} else {
|
||||
$model->{$type} =& ClassRegistry::init($type);
|
||||
}
|
||||
if (!method_exists($model, 'parentNode')) {
|
||||
trigger_error(sprintf(__('Callback parentNode() not defined in %s', true), $model->alias), E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Aro/Aco node for this model
|
||||
*
|
||||
* @param mixed $ref
|
||||
* @return array
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1322/node
|
||||
*/
|
||||
function node(&$model, $ref = null) {
|
||||
$type = $this->__typeMaps[$this->settings[$model->name]['type']];
|
||||
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 boolean $created True if this is a new record
|
||||
* @return void
|
||||
* @access public
|
||||
*/
|
||||
function afterSave(&$model, $created) {
|
||||
$type = $this->__typeMaps[$this->settings[$model->name]['type']];
|
||||
$parent = $model->parentNode();
|
||||
if (!empty($parent)) {
|
||||
$parent = $this->node($model, $parent);
|
||||
}
|
||||
$data = array(
|
||||
'parent_id' => isset($parent[0][$type]['id']) ? $parent[0][$type]['id'] : null,
|
||||
'model' => $model->alias,
|
||||
'foreign_key' => $model->id
|
||||
);
|
||||
if (!$created) {
|
||||
$node = $this->node($model);
|
||||
$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
|
||||
*
|
||||
* @return void
|
||||
* @access public
|
||||
*/
|
||||
function afterDelete(&$model) {
|
||||
$type = $this->__typeMaps[$this->settings[$model->name]['type']];
|
||||
$node = Set::extract($this->node($model), "0.{$type}.id");
|
||||
if (!empty($node)) {
|
||||
$model->{$type}->delete($node);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,453 @@
|
||||
<?php
|
||||
/**
|
||||
* Behavior for binding management.
|
||||
*
|
||||
* Behavior to simplify manipulating a model's bindings when doing a find operation
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
||||
* Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
* @link http://cakephp.org CakePHP(tm) Project
|
||||
* @package cake
|
||||
* @subpackage cake.cake.console.libs
|
||||
* @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
|
||||
* @subpackage cake.cake.console.libs
|
||||
* @link http://book.cakephp.org/view/1323/Containable
|
||||
*/
|
||||
class ContainableBehavior extends ModelBehavior {
|
||||
|
||||
/**
|
||||
* Types of relationships available for models
|
||||
*
|
||||
* @var array
|
||||
* @access private
|
||||
*/
|
||||
var $types = array('belongsTo', 'hasOne', 'hasMany', 'hasAndBelongsToMany');
|
||||
|
||||
/**
|
||||
* Runtime configuration for this behavior
|
||||
*
|
||||
* @var array
|
||||
* @access private
|
||||
*/
|
||||
var $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 object $Model Model using the behavior
|
||||
* @param array $settings Settings to override for model.
|
||||
* @access public
|
||||
*/
|
||||
function setup(&$Model, $settings = array()) {
|
||||
if (!isset($this->settings[$Model->alias])) {
|
||||
$this->settings[$Model->alias] = array('recursive' => true, 'notices' => true, 'autoFields' => true);
|
||||
}
|
||||
if (!is_array($settings)) {
|
||||
$settings = array();
|
||||
}
|
||||
$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 object $Model Model using the behavior
|
||||
* @param array $query Query parameters as set by cake
|
||||
* @return array
|
||||
* @access public
|
||||
*/
|
||||
function beforeFind(&$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;
|
||||
} else if ($reset && empty($instance->__backContainableAssociation)) {
|
||||
$instance->__backContainableAssociation = $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;
|
||||
} else if ($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;
|
||||
} else if (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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets original associations on models that may have receive multiple,
|
||||
* subsequent unbindings.
|
||||
*
|
||||
* @param object $Model Model on which we are resetting
|
||||
* @param array $results Results of the find operation
|
||||
* @param bool $primary true if this is the primary model that issued the find operation, false otherwise
|
||||
* @access public
|
||||
*/
|
||||
function afterFind(&$Model, $results, $primary) {
|
||||
if (!empty($Model->__backContainableAssociation)) {
|
||||
foreach ($Model->__backContainableAssociation as $relation => $bindings) {
|
||||
$Model->{$relation} = $bindings;
|
||||
unset($Model->__backContainableAssociation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds all relations from a model except the specified ones. Calling this function without
|
||||
* parameters unbinds all related models.
|
||||
*
|
||||
* @param object $Model Model on which binding restriction is being applied
|
||||
* @return void
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1323/Containable#Using-Containable-1324
|
||||
*/
|
||||
function contain(&$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 object $Model Model on which to reset bindings
|
||||
* @return void
|
||||
* @access public
|
||||
*/
|
||||
function resetBindings(&$Model) {
|
||||
if (!empty($Model->__backOriginalAssociation)) {
|
||||
$Model->__backAssociation = $Model->__backOriginalAssociation;
|
||||
unset($Model->__backOriginalAssociation);
|
||||
}
|
||||
$Model->resetAssociations();
|
||||
if (!empty($Model->__backInnerAssociation)) {
|
||||
$assocs = $Model->__backInnerAssociation;
|
||||
unset($Model->__backInnerAssociation);
|
||||
foreach ($assocs as $currentModel) {
|
||||
$this->resetBindings($Model->$currentModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process containments for model.
|
||||
*
|
||||
* @param object $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 bool $throwErrors Wether unexisting bindings show throw errors
|
||||
* @return array Containments
|
||||
* @access public
|
||||
*/
|
||||
function containments(&$Model, $contain, $containments = array(), $throwErrors = null) {
|
||||
$options = array('className', 'joinTable', 'with', 'foreignKey', 'associationForeignKey', 'conditions', 'fields', 'order', 'limit', 'offset', 'unique', 'finderQuery', 'deleteQuery', 'insertQuery');
|
||||
$keep = array();
|
||||
$depth = 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(substr($key, 1), 0, -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 = Set::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(sprintf(__('Model "%s" is not associated with model "%s"', true), $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 object $Model Model
|
||||
* @param array $map Map of relations for given model
|
||||
* @param mixed $fields If array, fields to initially load, if false use $Model as primary model
|
||||
* @return array Fields
|
||||
* @access public
|
||||
*/
|
||||
function fieldDependencies(&$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--';
|
||||
} else if ($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
|
||||
* @access 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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,540 @@
|
||||
<?php
|
||||
/**
|
||||
* Translate behavior
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
||||
* Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||||
* @link http://cakephp.org CakePHP(tm) Project
|
||||
* @package cake
|
||||
* @subpackage cake.cake.libs.model.behaviors
|
||||
* @since CakePHP(tm) v 1.2.0.4525
|
||||
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Translate behavior
|
||||
*
|
||||
* @package cake
|
||||
* @subpackage cake.cake.libs.model.behaviors
|
||||
* @link http://book.cakephp.org/view/1328/Translate
|
||||
*/
|
||||
class TranslateBehavior extends ModelBehavior {
|
||||
|
||||
/**
|
||||
* Used for runtime configuration of model
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
var $runtime = array();
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @access public
|
||||
*/
|
||||
function setup(&$model, $config = array()) {
|
||||
$db =& ConnectionManager::getDataSource($model->useDbConfig);
|
||||
if (!$db->connected) {
|
||||
trigger_error(
|
||||
sprintf(__('Datasource %s for TranslateBehavior of model %s is not connected', true), $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
|
||||
* @access public
|
||||
*/
|
||||
function cleanup(&$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
|
||||
* @access public
|
||||
*/
|
||||
function beforeFind(&$model, $query) {
|
||||
$locale = $this->_getLocale($model);
|
||||
if (empty($locale)) {
|
||||
return $query;
|
||||
}
|
||||
$db =& ConnectionManager::getDataSource($model->useDbConfig);
|
||||
$RuntimeModel =& $this->translateModel($model);
|
||||
if (!empty($RuntimeModel->tablePrefix)) {
|
||||
$tablePrefix = $RuntimeModel->tablePrefix;
|
||||
} else {
|
||||
$tablePrefix = $db->config['prefix'];
|
||||
}
|
||||
|
||||
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' => $db->name($tablePrefix . $RuntimeModel->useTable),
|
||||
'conditions' => array(
|
||||
$model->alias . '.' . $model->primaryKey => $db->identifier($RuntimeModel->alias.'.foreign_key'),
|
||||
$RuntimeModel->alias.'.model' => $model->name,
|
||||
$RuntimeModel->alias.'.locale' => $locale
|
||||
)
|
||||
);
|
||||
return $query;
|
||||
}
|
||||
$autoFields = false;
|
||||
|
||||
if (empty($query['fields'])) {
|
||||
$query['fields'] = array($model->alias.'.*');
|
||||
|
||||
$recursive = $model->recursive;
|
||||
if (isset($query['recursive'])) {
|
||||
$recursive = $query['recursive'];
|
||||
}
|
||||
|
||||
if ($recursive >= 0) {
|
||||
foreach (array('hasOne', 'belongsTo') as $type) {
|
||||
foreach ($model->{$type} as $key => $value) {
|
||||
|
||||
if (empty($value['fields'])) {
|
||||
$query['fields'][] = $key.'.*';
|
||||
} else {
|
||||
foreach ($value['fields'] as $field) {
|
||||
$query['fields'][] = $key.'.'.$field;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$autoFields = true;
|
||||
}
|
||||
$fields = array_merge($this->settings[$model->alias], $this->runtime[$model->alias]['fields']);
|
||||
$addFields = array();
|
||||
if (is_array($query['fields'])) {
|
||||
foreach ($fields as $key => $value) {
|
||||
$field = (is_numeric($key)) ? $value : $key;
|
||||
|
||||
if (in_array($model->alias.'.*', $query['fields']) || $autoFields || in_array($model->alias.'.'.$field, $query['fields']) || in_array($field, $query['fields'])) {
|
||||
$addFields[] = $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($addFields) {
|
||||
foreach ($addFields as $field) {
|
||||
foreach (array($field, $model->alias.'.'.$field) as $_field) {
|
||||
$key = array_search($_field, $query['fields']);
|
||||
|
||||
if ($key !== false) {
|
||||
unset($query['fields'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($locale)) {
|
||||
foreach ($locale as $_locale) {
|
||||
$query['fields'][] = 'I18n__'.$field.'__'.$_locale.'.content';
|
||||
$query['joins'][] = array(
|
||||
'type' => 'LEFT',
|
||||
'alias' => 'I18n__'.$field.'__'.$_locale,
|
||||
'table' => $db->name($tablePrefix . $RuntimeModel->useTable),
|
||||
'conditions' => array(
|
||||
$model->alias . '.' . $model->primaryKey => $db->identifier("I18n__{$field}__{$_locale}.foreign_key"),
|
||||
'I18n__'.$field.'__'.$_locale.'.model' => $model->name,
|
||||
'I18n__'.$field.'__'.$_locale.'.'.$RuntimeModel->displayField => $field,
|
||||
'I18n__'.$field.'__'.$_locale.'.locale' => $_locale
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$query['fields'][] = 'I18n__'.$field.'.content';
|
||||
$query['joins'][] = array(
|
||||
'type' => 'LEFT',
|
||||
'alias' => 'I18n__'.$field,
|
||||
'table' => $db->name($tablePrefix . $RuntimeModel->useTable),
|
||||
'conditions' => array(
|
||||
$model->alias . '.' . $model->primaryKey => $db->identifier("I18n__{$field}.foreign_key"),
|
||||
'I18n__'.$field.'.model' => $model->name,
|
||||
'I18n__'.$field.'.'.$RuntimeModel->displayField => $field
|
||||
)
|
||||
);
|
||||
|
||||
if (is_string($query['conditions'])) {
|
||||
$query['conditions'] = $db->conditions($query['conditions'], true, false, $model) . ' AND '.$db->name('I18n__'.$field.'.locale').' = \''.$locale.'\'';
|
||||
} else {
|
||||
$query['conditions'][$db->name("I18n__{$field}.locale")] = $locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_array($query['fields'])) {
|
||||
$query['fields'] = array_merge($query['fields']);
|
||||
}
|
||||
$this->runtime[$model->alias]['beforeFind'] = $addFields;
|
||||
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
|
||||
* @access public
|
||||
*/
|
||||
function afterFind(&$model, $results, $primary) {
|
||||
$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)) ? @$locale[0] : $locale;
|
||||
|
||||
foreach ($beforeFind as $field) {
|
||||
if (is_array($locale)) {
|
||||
foreach ($locale as $_locale) {
|
||||
if (!isset($results[$key][$model->alias][$field]) && !empty($results[$key]['I18n__'.$field.'__'.$_locale]['content'])) {
|
||||
$results[$key][$model->alias][$field] = $results[$key]['I18n__'.$field.'__'.$_locale]['content'];
|
||||
}
|
||||
unset($results[$key]['I18n__'.$field.'__'.$_locale]);
|
||||
}
|
||||
|
||||
if (!isset($results[$key][$model->alias][$field])) {
|
||||
$results[$key][$model->alias][$field] = '';
|
||||
}
|
||||
} else {
|
||||
$value = '';
|
||||
if (!empty($results[$key]['I18n__'.$field]['content'])) {
|
||||
$value = $results[$key]['I18n__'.$field]['content'];
|
||||
}
|
||||
$results[$key][$model->alias][$field] = $value;
|
||||
unset($results[$key]['I18n__'.$field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* beforeValidate Callback
|
||||
*
|
||||
* @param Model $model Model invalidFields was called on.
|
||||
* @return boolean
|
||||
* @access public
|
||||
*/
|
||||
function beforeValidate(&$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;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* afterSave Callback
|
||||
*
|
||||
* @param Model $model Model the callback is called on
|
||||
* @param boolean $created Whether or not the save created a record.
|
||||
* @return void
|
||||
* @access public
|
||||
*/
|
||||
function afterSave(&$model, $created) {
|
||||
if (!isset($this->runtime[$model->alias]['beforeSave'])) {
|
||||
return true;
|
||||
}
|
||||
$locale = $this->_getLocale($model);
|
||||
$tempData = $this->runtime[$model->alias]['beforeSave'];
|
||||
unset($this->runtime[$model->alias]['beforeSave']);
|
||||
$conditions = array('model' => $model->alias, 'foreign_key' => $model->id);
|
||||
$RuntimeModel =& $this->translateModel($model);
|
||||
|
||||
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
|
||||
* @access public
|
||||
*/
|
||||
function afterDelete(&$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
|
||||
* @access protected
|
||||
*/
|
||||
function _getLocale(&$model) {
|
||||
if (!isset($model->locale) || is_null($model->locale)) {
|
||||
if (!class_exists('I18n')) {
|
||||
App::import('Core', 'i18n');
|
||||
}
|
||||
$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 object
|
||||
* @access public
|
||||
*/
|
||||
function &translateModel(&$model) {
|
||||
if (!isset($this->runtime[$model->alias]['model'])) {
|
||||
if (!isset($model->translateModel) || empty($model->translateModel)) {
|
||||
$className = 'I18nModel';
|
||||
} else {
|
||||
$className = $model->translateModel;
|
||||
}
|
||||
|
||||
if (PHP5) {
|
||||
$this->runtime[$model->alias]['model'] = ClassRegistry::init($className, 'Model');
|
||||
} else {
|
||||
$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');
|
||||
}
|
||||
$model =& $this->runtime[$model->alias]['model'];
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind translation for fields, optionally with hasMany association for
|
||||
* fake field
|
||||
*
|
||||
* @param object instance of model
|
||||
* @param mixed string with field or array(field1, field2=>AssocName, field3)
|
||||
* @param boolean $reset
|
||||
* @return bool
|
||||
*/
|
||||
function bindTranslation(&$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 (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_assoc($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_assoc($this->runtime[$model->alias]['fields'], array($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(
|
||||
sprintf(__('Association %s is already binded to model %s', true), $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbind translation for fields, optionally unbinds hasMany association for
|
||||
* fake field
|
||||
*
|
||||
* @param object $model instance of model
|
||||
* @param mixed $fields string with field, or array(field1, field2=>AssocName, field3), or null for
|
||||
* unbind all original translations
|
||||
* @return bool
|
||||
*/
|
||||
function unbindTranslation(&$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;
|
||||
}
|
||||
|
||||
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_assoc($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_assoc($this->runtime[$model->alias]['fields'], array($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;
|
||||
}
|
||||
}
|
||||
if (!defined('CAKEPHP_UNIT_TEST_EXECUTION')) {
|
||||
|
||||
/**
|
||||
* @package cake
|
||||
* @subpackage cake.cake.libs.model.behaviors
|
||||
*/
|
||||
class I18nModel extends AppModel {
|
||||
var $name = 'I18nModel';
|
||||
var $useTable = 'i18n';
|
||||
var $displayField = 'field';
|
||||
}
|
||||
}
|
977
php-practice/web-cake/html/cake/libs/model/behaviors/tree.php
Normal file
977
php-practice/web-cake/html/cake/libs/model/behaviors/tree.php
Normal file
@@ -0,0 +1,977 @@
|
||||
<?php
|
||||
/**
|
||||
* Tree behavior class.
|
||||
*
|
||||
* Enables a model object to act as a node-based tree.
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* CakePHP : Rapid Development Framework (http://cakephp.org)
|
||||
* Copyright 2006-2010, Cake Software Foundation, Inc.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright 2006-2010, Cake Software Foundation, Inc.
|
||||
* @link http://cakephp.org CakePHP Project
|
||||
* @package cake
|
||||
* @subpackage cake.cake.libs.model.behaviors
|
||||
* @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
|
||||
* @subpackage cake.cake.libs.model.behaviors
|
||||
* @link http://book.cakephp.org/view/1339/Tree
|
||||
*/
|
||||
class TreeBehavior extends ModelBehavior {
|
||||
|
||||
/**
|
||||
* Errors
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
var $errors = array();
|
||||
|
||||
/**
|
||||
* Defaults
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
var $_defaults = array(
|
||||
'parent' => 'parent_id', 'left' => 'lft', 'right' => 'rght',
|
||||
'scope' => '1 = 1', 'type' => 'nested', '__parentChange' => false, 'recursive' => -1
|
||||
);
|
||||
|
||||
/**
|
||||
* Initiate Tree behavior
|
||||
*
|
||||
* @param object $Model instance of model
|
||||
* @param array $config array of configuration settings.
|
||||
* @return void
|
||||
* @access public
|
||||
*/
|
||||
function setup(&$Model, $config = array()) {
|
||||
if (!is_array($config)) {
|
||||
$config = array('type' => $config);
|
||||
}
|
||||
$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
|
||||
*
|
||||
* Overriden 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 AppModel $Model Model instance.
|
||||
* @param boolean $created indicates whether the node just saved was created or updated
|
||||
* @return boolean true on success, false on failure
|
||||
* @access public
|
||||
*/
|
||||
function afterSave(&$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 ($__parentChange) {
|
||||
$this->settings[$Model->alias]['__parentChange'] = false;
|
||||
return $this->_setParent($Model, $Model->data[$Model->alias][$parent]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Before delete method. Called before all deletes
|
||||
*
|
||||
* Will delete the current node and all children using the deleteAll method and sync the table
|
||||
*
|
||||
* @param AppModel $Model Model instance
|
||||
* @return boolean true to continue, false to abort the delete
|
||||
* @access public
|
||||
*/
|
||||
function beforeDelete(&$Model) {
|
||||
extract($this->settings[$Model->alias]);
|
||||
list($name, $data) = array($Model->alias, $Model->read());
|
||||
$data = $data[$name];
|
||||
|
||||
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
|
||||
*
|
||||
* Overriden 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 AppModel $Model Model instance
|
||||
* @return boolean true to continue, false to abort the save
|
||||
* @access public
|
||||
*/
|
||||
function beforeSave(&$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; //$parentNode[$right];
|
||||
$Model->data[$Model->alias][$right] = 0; //$parentNode[$right] + 1;
|
||||
} 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 AppModel $Model Model instance
|
||||
* @param mixed $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
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1347/Counting-children
|
||||
*/
|
||||
function childcount(&$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 (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 AppModel $Model Model instance
|
||||
* @param mixed $id The ID of the record to read
|
||||
* @param boolean $direct whether to return only the direct, or all, children
|
||||
* @param mixed $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
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1346/Children
|
||||
*/
|
||||
function children(&$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;
|
||||
}
|
||||
$name = $Model->alias;
|
||||
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 AppModel $Model Model instance
|
||||
* @param mixed $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
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1348/generatetreelist
|
||||
*/
|
||||
function generatetreelist(&$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('{0}{1}', '{n}.tree_prefix', '{n}.' . $Model->alias . '.' . $Model->displayField);
|
||||
|
||||
} elseif (is_string($valuePath)) {
|
||||
$valuePath = array('{0}{1}', '{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) {
|
||||
while ($stack && ($stack[count($stack) - 1] < $result[$Model->alias][$right])) {
|
||||
array_pop($stack);
|
||||
}
|
||||
$results[$i]['tree_prefix'] = str_repeat($spacer,count($stack));
|
||||
$stack[] = $result[$Model->alias][$right];
|
||||
}
|
||||
if (empty($results)) {
|
||||
return array();
|
||||
}
|
||||
return Set::combine($results, $keyPath, $valuePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent node
|
||||
*
|
||||
* reads the parent id and returns this node
|
||||
*
|
||||
* @param AppModel $Model Model instance
|
||||
* @param mixed $id The ID of the record to read
|
||||
* @param integer $recursive The number of levels deep to fetch associated records
|
||||
* @return array Array of data for the parent node
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1349/getparentnode
|
||||
*/
|
||||
function getparentnode(&$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 AppModel $Model Model instance
|
||||
* @param mixed $id The ID of the record to read
|
||||
* @param mixed $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
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1350/getpath
|
||||
*/
|
||||
function getpath(&$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 AppModel $Model Model instance
|
||||
* @param mixed $id The ID of the record to move
|
||||
* @param mixed $number how many places to move the node or true to move to last position
|
||||
* @return boolean true on success, false on failure
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1352/moveDown
|
||||
*/
|
||||
function movedown(&$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 AppModel $Model Model instance
|
||||
* @param mixed $id The ID of the record to move
|
||||
* @param mixed $number how many places to move the node, or true to move to first position
|
||||
* @return boolean true on success, false on failure
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1353/moveUp
|
||||
*/
|
||||
function moveup(&$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 AppModel $Model Model instance
|
||||
* @param string $mode parent or tree
|
||||
* @param mixed $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
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1628/Recover
|
||||
*/
|
||||
function recover(&$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->alias,
|
||||
'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) {
|
||||
$Model->id = $array[$Model->alias][$Model->primaryKey];
|
||||
$lft = $count++;
|
||||
$rght = $count++;
|
||||
$Model->save(array($left => $lft, $right => $rght), array('callbacks' => false));
|
||||
}
|
||||
foreach ($Model->find('all', array('conditions' => $scope, 'fields' => array($Model->primaryKey, $parent), 'order' => $left)) as $array) {
|
||||
$Model->create();
|
||||
$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 reordeing 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 AppModel $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/view/1355/reorder
|
||||
* @link http://book.cakephp.org/view/1629/Reorder
|
||||
*/
|
||||
function reorder(&$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 AppModel $Model Model instance
|
||||
* @param mixed $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
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1354/removeFromTree
|
||||
*/
|
||||
function removefromtree(&$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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 AppModel $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)
|
||||
* @access public
|
||||
* @link http://book.cakephp.org/view/1630/Verify
|
||||
*/
|
||||
function verify(&$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->alias,
|
||||
'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 AppModel $Model Model instance
|
||||
* @param mixed $parentId
|
||||
* @return boolean true on success, false on failure
|
||||
* @access protected
|
||||
*/
|
||||
function _setParent(&$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 AppModel $Model
|
||||
* @param string $scope
|
||||
* @param string $right
|
||||
* @return int
|
||||
* @access private
|
||||
*/
|
||||
function __getMax($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 AppModel $Model
|
||||
* @param string $scope
|
||||
* @param string $right
|
||||
* @return int
|
||||
* @access private
|
||||
*/
|
||||
function __getMin($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 AppModel $Model
|
||||
* @param integer $shift
|
||||
* @param string $direction
|
||||
* @param array $conditions
|
||||
* @param string $field
|
||||
* @access private
|
||||
*/
|
||||
function __sync(&$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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user