浏览代码

2.x version of slugged

euromark 13 年之前
父节点
当前提交
b04cbe45fc

+ 0 - 1
Model/Behavior/GeocoderBehavior.php

@@ -35,7 +35,6 @@ class GeocoderBehavior extends ModelBehavior {
 	 *
 	 * @param object $Model Model using the behaviour
 	 * @param array $settings Settings to override for model.
-	 * 2011-01-13 ms
 	 */
 	public function setup(Model $Model, $settings = array()) {
 		$default = array(

+ 198 - 0
Model/Behavior/LinkableBehavior.php

@@ -0,0 +1,198 @@
+<?php
+App::uses('ModelBehavior', 'Model');
+
+/**
+ * LinkableBehavior
+ * Light-weight approach for data mining on deep relations between models. 
+ * Join tables based on model relations to easily enable right to left find operations.
+ * Original behavior by rafaelbandeira3 on GitHub. 
+ * Includes modifications from Terr, n8man, and Chad Jablonski
+ *  
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *  
+ * GiulianoB ( https://github.com/giulianob/linkable )
+ *
+ * @version 1.0; 
+ * 
+ * @version 1.1:
+ *  -Brought in improvements and test cases from Terr. However, THIS VERSION OF LINKABLE IS NOT DROP IN COMPATIBLE WITH Terr's VERSION!
+ *  -If fields aren't specified, will now return all columns of that model
+ *  -No need to specify the foreign key condition if a custom condition is given. Linkable will automatically include the foreign key relationship. 
+ *  -Ability to specify the exact condition Linkable should use (e.g. $this->Post->find('first', array('link' => array('User' => array('conditions' => array('exactly' => 'User.last_post_id = Post.id'))))) )
+ *  This is usually required when doing on-the-fly joins since Linkable generally assumes a belongsTo relationship when no specific relationship is found and may produce invalid foreign key conditions.
+ *  -Linkable will no longer break queries that use SQL COUNTs
+ * 
+ * @version 1.2:
+ * @modified Mark Scherer
+ * - works with cakephp2.0 (89.84 test coverage)
+ */
+class LinkableBehavior extends ModelBehavior {
+	
+	protected $_key = 'link';
+	
+	protected $_options = array(
+		'type' => true, 'table' => true, 'alias' => true,
+		'conditions' => true, 'fields' => true, 'reference' => true,
+		'class' => true, 'defaults' => true
+	);
+
+	protected $_defaults = array('type' => 'LEFT');
+	
+	public function beforeFind(Model $Model, $query) {
+		if (isset($query[$this->_key])) {			
+			$optionsDefaults = $this->_defaults + array('reference' => $Model->alias, $this->_key => array());
+			$optionsKeys = $this->_options + array($this->_key => true);
+			
+			// If containable is being used, then let it set the recursive!
+			if (empty($query['contain'])) {
+				$query = am(array('joins' => array()), $query, array('recursive' => -1));
+			} else {				
+				$query = am(array('joins' => array()), $query);
+			}
+			$iterators[] = $query[$this->_key];
+			$cont = 0;
+			do {			
+				$iterator = $iterators[$cont];
+				$defaults = $optionsDefaults;
+				if (isset($iterator['defaults'])) {
+					$defaults = array_merge($defaults, $iterator['defaults']);
+					unset($iterator['defaults']);
+				}
+				$iterations = Set::normalize($iterator);
+				foreach ($iterations as $alias => $options) {
+					if (is_null($options)) {
+						$options = array();
+					}
+					$options = am($defaults, compact('alias'), $options);
+					if (empty($options['alias'])) {
+						throw new InvalidArgumentException(sprintf('%s::%s must receive aliased links', get_class($this), __FUNCTION__));
+					}					
+					if (empty($options['table']) && empty($options['class'])) {
+						$options['class'] = $options['alias'];
+					} elseif (!empty($options['table']) && empty($options['class'])) {
+						$options['class'] = Inflector::classify($options['table']);
+					}
+					
+					// the incoming model to be linked in query
+					$_Model = ClassRegistry::init($options['class']);
+					// the already in query model that links to $_Model
+					$Reference = ClassRegistry::init($options['reference']); 	
+					$db = $_Model->getDataSource();
+					$associations = $_Model->getAssociated();
+					if (isset($Reference->belongsTo[$_Model->alias])) {
+						$type = 'hasOne';
+						$association = $Reference->belongsTo[$_Model->alias];						
+					} elseif (isset($associations[$Reference->alias])) {
+						$type = $associations[$Reference->alias];
+						$association = $_Model->{$type}[$Reference->alias];						
+					} else {
+						$_Model->bindModel(array('belongsTo' => array($Reference->alias)));
+						$type = 'belongsTo';
+						$association = $_Model->{$type}[$Reference->alias];
+						$_Model->unbindModel(array('belongsTo' => array($Reference->alias)));
+					}
+					
+					if (!isset($options['conditions'])) {
+						$options['conditions'] = array();
+					} elseif (!is_array($options['conditions'])) {
+						// Support for string conditions
+						$options['conditions'] = array($options['conditions']);
+					}
+				
+					if (isset($options['conditions']['exactly'])) {
+						if (is_array($options['conditions']['exactly']))
+							$options['conditions'] = reset($options['conditions']['exactly']);
+						else
+							$options['conditions'] = array($options['conditions']['exactly']);
+					} else {
+						if ($type === 'belongsTo') {
+							$modelKey = $_Model->escapeField($association['foreignKey']);
+							$referenceKey = $Reference->escapeField($Reference->primaryKey);
+							$options['conditions'][] = "{$referenceKey} = {$modelKey}";
+						} elseif ($type === 'hasAndBelongsToMany') {
+							if (isset($association['with'])) {
+								$Link = $_Model->{$association['with']};
+								if (isset($Link->belongsTo[$_Model->alias])) {
+									$modelLink = $Link->escapeField($Link->belongsTo[$_Model->alias]['foreignKey']);
+								}
+								if (isset($Link->belongsTo[$Reference->alias])) {
+									$referenceLink = $Link->escapeField($Link->belongsTo[$Reference->alias]['foreignKey']);
+								}								
+							} else {
+								$Link = $_Model->{Inflector::classify($association['joinTable'])};
+							}
+							if (empty($modelLink)) {
+								$modelLink = $Link->escapeField(Inflector::underscore($_Model->alias) . '_id');
+							}
+							if (empty($referenceLink)) {
+								$referenceLink = $Link->escapeField(Inflector::underscore($Reference->alias) . '_id');
+							}
+							$referenceKey = $Reference->escapeField();
+							$query['joins'][] = array(
+								'alias' => $Link->alias,
+								'table' => $Link->table, //$Link->getDataSource()->fullTableName($Link),
+								'conditions' => "{$referenceLink} = {$referenceKey}",
+								'type' => 'LEFT'
+							);
+							$modelKey = $_Model->escapeField();
+							$options['conditions'][] = "{$modelLink} = {$modelKey}";						
+						} else {
+							$referenceKey = $Reference->escapeField($association['foreignKey']);
+							$modelKey = $_Model->escapeField($_Model->primaryKey);
+							$options['conditions'][] = "{$modelKey} = {$referenceKey}";
+						}					
+					}
+						
+					if (empty($options['table'])) {
+						$options['table'] = $_Model->table;
+					}
+					
+					// Decide whether we should mess with the fields or not
+					// If this query is a COUNT query then we just leave it alone
+					if (!isset($query['fields']) || is_array($query['fields']) || strpos($query['fields'], 'COUNT(*)') === FALSE) {
+						if (!empty($options['fields'])) {
+							if ($options['fields'] === true && !empty($association['fields'])) {
+								$options['fields'] = $db->fields($_Model, null, $association['fields']);
+							} elseif ($options['fields'] === true) {
+								$options['fields'] = $db->fields($_Model);
+							} else {
+								$options['fields'] = $db->fields($_Model, null, $options['fields']);	
+							}						
+						} elseif (!isset($options['fields']) || (isset($options['fields']) && !is_array($options['fields']))) {
+							if (!empty($association['fields'])) {
+									$options['fields'] = $db->fields($_Model, null, $association['fields']);
+							} else {
+									$options['fields'] = $db->fields($_Model);
+							}
+						}
+						
+						if (!empty($options['class']) && $options['class'] !== $alias) {
+							$options['fields'] = str_replace($options['class'], $alias, $options['fields']);	
+						}
+						if (is_array($query['fields'])) {
+							$query['fields'] = array_merge($query['fields'], $options['fields']);
+						} else {
+							// If user didn't specify any fields then select all fields by default (just as find would)
+							$query['fields'] = array_merge($db->fields($Model), $options['fields']);
+						}
+					}
+					
+					$options[$this->_key] = am($options[$this->_key], array_diff_key($options, $optionsKeys));
+					$options = array_intersect_key($options, $optionsKeys);
+					if (!empty($options[$this->_key])) {
+						$iterators[] = $options[$this->_key] + array('defaults' => array_merge($defaults, array('reference' => $options['class'])));
+					}
+					
+					$query['joins'][] = array_intersect_key($options, array('type' => true, 'alias' => true, 'table' => true, 'conditions' => true));					
+				}
+				$cont++;
+				$notDone = isset($iterators[$cont]);
+			} while ($notDone);
+		}	
+		
+		unset($query['link']);
+		
+		return $query;
+	}
+}

+ 3 - 2
Model/Behavior/SluggedBehavior.php

@@ -23,13 +23,14 @@
  * Ensure that mb_ functions exist
  */
 App::uses('Multibyte', 'I18n');
+App::uses('ModelBehavior', 'Model');
 
 /**
  * SluggedBehavior class
  *
  * @uses          ModelBehavior
- * @package       mi
- * @subpackage    mi.models.behaviors
+ * @version       2.x
+ * @modified      Mark Scherer
  */
 class SluggedBehavior extends ModelBehavior {
 

+ 334 - 0
Model/Behavior/SoftDeleteBehavior.php

@@ -0,0 +1,334 @@
+<?php
+
+/**
+ * Copyright 2007-2010, Cake Development Corporation (http://cakedc.com)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2007-2010, Cake Development Corporation (http://cakedc.com)
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+App::uses('ModelBehavior', 'Model');
+
+/**
+ * Utils Plugin
+ *
+ * Utils Soft Delete Behavior
+ *
+ * @package utils
+ * @subpackage utils.models.behaviors
+ */
+class SoftDeleteBehavior extends ModelBehavior {
+
+	/**
+	 * Default settings
+	 *
+	 * @var array $default
+	 */
+	public $default = array('deleted' => 'deleted_date');
+
+	/**
+	 * Holds activity flags for models
+	 *
+	 * @var array $runtime
+	 */
+	public $runtime = array();
+
+	/**
+	 * Setup callback
+	 *
+	 * @param object $model
+	 * @param array $settings
+	 */
+	public function setup(Model $model, $settings = array()) {
+		if (empty($settings)) {
+			$settings = $this->default;
+		} elseif (!is_array($settings)) {
+			$settings = array($settings);
+		}
+
+		$error = 'SoftDeleteBehavior::setup(): model ' . $model->alias . ' has no field ';
+		$fields = $this->_normalizeFields($model, $settings);
+		foreach ($fields as $flag => $date) {
+			if ($model->hasField($flag)) {
+				if ($date && !$model->hasField($date)) {
+					trigger_error($error . $date, E_USER_NOTICE);
+					return;
+				}
+				continue;
+			}
+			trigger_error($error . $flag, E_USER_NOTICE);
+			return;
+		}
+
+		$this->settings[$model->alias] = $fields;
+		$this->softDelete($model, true);
+	}
+
+	/**
+	 * Before find callback
+	 *
+	 * @param object $model
+	 * @param array $query
+	 * @return array
+	 */
+	public function beforeFind(Model $model, $query) {
+		$runtime = $this->runtime[$model->alias];
+		if ($runtime) {
+			if (!is_array($query['conditions'])) {
+				$query['conditions'] = array();
+			}
+			$conditions = array_filter(array_keys($query['conditions']));
+
+			$fields = $this->_normalizeFields($model);
+
+			foreach ($fields as $flag => $date) {
+				if (true === $runtime || $flag === $runtime) {
+					if (!in_array($flag, $conditions) && !in_array($model->name . '.' . $flag, $conditions)) {
+						$query['conditions'][$model->alias . '.' . $flag] = false;
+					}
+
+					if ($flag === $runtime) {
+						break;
+					}
+				}
+			}
+			return $query;
+		}
+	}
+
+	/**
+	 * Before delete callback
+	 *
+	 * @param object $model
+	 * @param array $query
+	 * @return boolean
+	 */
+	public function beforeDelete(Model $model) {
+		$runtime = $this->runtime[$model->alias];
+		if ($runtime) {
+			$this->delete($model, $model->id);
+			return false;
+		}
+		return true;
+	}
+
+	/**
+	 * Mark record as deleted
+	 *
+	 * @param object $model
+	 * @param integer $id
+	 * @return boolean
+	 */
+	public function delete(Model $model, $id) {
+		$runtime = $this->runtime[$model->alias];
+
+		$data = array();
+		$fields = $this->_normalizeFields($model);
+		foreach ($fields as $flag => $date) {
+			if (true === $runtime || $flag === $runtime) {
+				$data[$flag] = true;
+				if ($date) {
+					$data[$date] = date('Y-m-d H:i:s');
+				}
+				if ($flag === $runtime) {
+					break;
+				}
+			}
+		}
+
+		$model->create();
+		$model->set($model->primaryKey, $id);
+		return $model->save(array($model->alias => $data), false, array_keys($data));
+	}
+
+	/**
+	 * Mark record as not deleted
+	 *
+	 * @param object $model
+	 * @param integer $id
+	 * @return boolean
+	 */
+	public function undelete(Model $model, $id) {
+		$runtime = $this->runtime[$model->alias];
+		$this->softDelete($model, false);
+
+		$data = array();
+		$fields = $this->_normalizeFields($model);
+		foreach ($fields as $flag => $date) {
+			if (true === $runtime || $flag === $runtime) {
+				$data[$flag] = false;
+				if ($date) {
+					$data[$date] = null;
+				}
+				if ($flag === $runtime) {
+					break;
+				}
+			}
+		}
+
+		$model->create();
+		$model->set($model->primaryKey, $id);
+		$result = $model->save(array($model->alias => $data), false, array_keys($data));
+		$this->softDelete($model, $runtime);
+		return $result;
+	}
+
+	/**
+	 * Enable/disable SoftDelete functionality
+	 *
+	 * Usage from model:
+	 * $this->softDelete(false); deactivate this behavior for model
+	 * $this->softDelete('field_two'); enabled only for this flag field
+	 * $this->softDelete(true); enable again for all flag fields
+	 * $config = $this->softDelete(null); for obtaining current setting
+	 *
+	 * @param object $model
+	 * @param mixed $active
+	 * @return mixed if $active is null, then current setting/null, or boolean if runtime setting for model was changed
+	 */
+	public function softDelete(Model $model, $active) {
+		if (is_null($active)) {
+			return !empty($this->runtime[$model->alias]) ? $this->runtime[$model->alias] : null;
+		}
+
+
+		$result = !isset($this->runtime[$model->alias]) || $this->runtime[$model->alias] !== $active;
+		$this->runtime[$model->alias] = $active;
+		$this->_softDeleteAssociations($model, $active);
+		return $result;
+	}
+
+	/**
+	 * Returns number of outdated softdeleted records prepared for purge
+	 *
+	 * @param object $model
+	 * @param mixed $expiration anything parseable by strtotime(), by default '-90 days'
+	 * @return integer
+	 */
+	public function purgeDeletedCount(Model $model, $expiration = '-90 days') {
+		$this->softDelete($model, false);
+		return $model->find('count', array('conditions' => $this->_purgeDeletedConditions($model, $expiration), 'recursive' => -1));
+	}
+
+	/**
+	 * Purge table
+	 *
+	 * @param object $model
+	 * @param mixed $expiration anything parseable by strtotime(), by default '-90 days'
+	 * @return boolean if there were some outdated records
+	 */
+	public function purgeDeleted(Model $model, $expiration = '-90 days') {
+		$this->softDelete($model, false);
+		$records = $model->find('all', array(
+			'conditions' => $this->_purgeDeletedConditions($model, $expiration),
+			'fields' => array($model->primaryKey),
+			'recursive' => -1));
+		if ($records) {
+			foreach ($records as $record) {
+				$model->delete($record[$model->alias][$model->primaryKey]);
+			}
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Returns conditions for finding outdated records
+	 *
+	 * @param object $model
+	 * @param mixed $expiration anything parseable by strtotime(), by default '-90 days'
+	 * @return array
+	 */
+	protected function _purgeDeletedConditions(Model $model, $expiration = '-90 days') {
+		$purgeDate = date('Y-m-d H:i:s', strtotime($expiration));
+		$conditions = array();
+		foreach ($this->settings[$model->alias] as $flag => $date) {
+			$conditions[$model->alias . '.' . $flag] = true;
+			if ($date) {
+				$conditions[$model->alias . '.' . $date . ' <'] = $purgeDate;
+			}
+		}
+		return $conditions;
+	}
+
+	/**
+	 * Return normalized field array
+	 *
+	 * @param object $model
+	 * @param array $settings
+	 * @return array
+	 */
+	protected function _normalizeFields(Model $model, $settings = array()) {
+		if (empty($settings)) {
+			$settings = @$this->settings[$model->alias];
+		}
+		$result = array();
+		foreach ($settings as $flag => $date) {
+			if (is_numeric($flag)) {
+				$flag = $date;
+				$date = false;
+			}
+			$result[$flag] = $date;
+		}
+		return $result;
+	}
+
+	/**
+	 * Modifies conditions of hasOne and hasMany associations
+	 *
+	 * If multiple delete flags are configured for model, then $active=true doesn't
+	 * do anything - you have to alter conditions in association definition
+	 *
+	 * @param object $model
+	 * @param mixed $active
+	 */
+	protected function _softDeleteAssociations(Model $model, $active) {
+		if (empty($model->belongsTo)) {
+			return;
+		}
+		$fields = array_keys($this->_normalizeFields($model));
+		$parentModels = array_keys($model->belongsTo);
+
+		foreach ($parentModels as $parentModel) {
+			foreach (array('hasOne', 'hasMany') as $assocType) {
+				if (empty($model->{$parentModel}->{$assocType})) {
+					continue;
+				}
+
+				foreach ($model->{$parentModel}->{$assocType} as $assoc => $assocConfig) {
+					$modelName = !empty($assocConfig['className']) ? $assocConfig['className'] : $assoc;
+					if ($model->alias != $modelName) {
+						continue;
+					}
+
+					$conditions = $model->{$parentModel}->{$assocType}[$assoc]['conditions'];
+					if (!is_array($conditions)) {
+						$model->{$parentModel}->{$assocType}[$assoc]['conditions'] = array();
+					}
+
+					$multiFields = 1 < count($fields);
+					foreach ($fields as $field) {
+						if ($active) {
+							if (!isset($conditions[$field]) && !isset($conditions[$assoc . '.' . $field])) {
+								if (is_string($active)) {
+									if ($field == $active) {
+										$conditions[$assoc . '.' . $field] = false;
+									} elseif (isset($conditions[$assoc . '.' . $field])) {
+										unset($conditions[$assoc . '.' . $field]);
+									}
+								} elseif (!$multiFields) {
+									$conditions[$assoc . '.' . $field] = false;
+								}
+							}
+						} elseif (isset($conditions[$assoc . '.' . $field])) {
+							unset($conditions[$assoc . '.' . $field]);
+						}
+					}
+				}
+			}
+		}
+	}
+	
+}