Commit d83b2b98 authored by Patrick Barroca's avatar Patrick Barroca 🐧

fix Storm_Model_Abstract::hasChange() bug

parent 77704143
Pipeline #1867 passed with stage
in 6 seconds
<?php
/*
STORM is under the MIT License (MIT)
Copyright (c) 2010-2011 Agence Française Informatique http://www.afi-sa.fr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
abstract class Storm_Model_Abstract {
/**
* @var array
*/
protected static $_loaders = array();
/**
* @var string
*/
protected $_table_primary = null;
/**
* @var array
*/
protected $_attributes = ['id' => null];
/**
* @var array
*/
protected $_attributes_in_db = array();
/**
* @var array
*/
protected $_has_many_attributes_in_db = array();
/**
* @var array
*/
protected $_has_many_attributes = array();
/**
* Defines me -> * dependents relationship
* Attributes:
* model: dependent class name
* role: the name of the accessor (also used for default id mapping in db)
* order: order statetement while loading models
* through: this relation relies on another one
* unique: if true, a dependent could not appear twice
* dependents: if 'delete', then deleting me will delete dependents
* scope: an array of field / value pair that filters data
* limit: when specified, add a limit for fetching descendents
*
* Ex:
* protected $_has_many = array('sessions' => array(
* 'model' => 'Class_SessionFormation',
* 'role' => 'formation',
* 'dependents' => 'delete',
* 'order' => 'date_debut desc',
* 'scope' => array('type' => 2)),
*
* 'session_formation_inscriptions' => array('through' => 'sessions',
* 'unique' => 'true'),
*
* 'stagiaires' => array('through' => 'session_formation_inscriptions'));
*
* Relationships definition can be overrided at runtime by writing a method named descriptionOfFieldName.
* This can be used to have dynamic relationships between objects.
*
* ex:
*
* protected $_has_many = [ 'animals' => [] ];
*
* public function descriptionOfAnimals() {
* return $this->iWantCats()
* ? [ 'model' => 'Cat', 'role' => 'owner' ]
* : [ 'model' => 'Dog', 'role' => 'owner' ];
* }
*
*
* @var array
*/
protected $_has_many = array();
/**
* @var array
*/
protected $_belongs_to_attributes = array();
/**
* Defines me -> * dependents relationship
* Attributes:
* model: dependent class name
* referenced_in: field name used to reference dependent id
* trough: this relation relies on another one
* protected $_belongs_to = array('librairy' => array('model' => 'Librairy',
* 'referenced_in' => 'id_library'),
*
* 'zone' => array('through' => 'librairy'));
* @var array
*/
protected $_belongs_to = array();
/**
* Store the list of errors found by validate() method.
* See also check($condition, $error)
* @var array
*/
protected $_errors = array();
/**
* Default values for attributes of a new instance
* Should be defined in subclasses like:
* $_default_values = array('title' => 'new article', 'content' => '')
* @var array
*/
protected $_default_attribute_values = array();
/**
* By default, use autoincrement ids generated by the SGBD. When $_fixed_id = true,
* does not take id from SGBD.
* @var boolean
*/
protected $_fixed_id = false;
/**
* Generic mean to handle associations (in order to extent and replace has_many and belongs_to)
* Collection of Storm_Model_Association_Abstract subclasses
* @var Storm_Collection
*/
protected $_associations;
/**
* @param string $class
* @return Storm_Model_Loader
*/
protected static function _buildLoaderFor($class) {
$reflection = new ReflectionClass($class);
$class_vars = $reflection->getdefaultProperties();
if (isset($class_vars['_loader_class'])) {
$loader_class = $class_vars['_loader_class'];
return new $loader_class($class);
}
return new Storm_Model_Loader($class);
}
public static function getClassVar($var) {
$reflection = new ReflectionClass(get_called_class());
$class_vars = $reflection->getdefaultProperties();
return $class_vars[$var];
}
/**
* @return Storm_Model_Loader
*/
public static function getLoader() {
return self::getLoaderFor(get_called_class());
}
/**
* Forward static calls to the Loader so instead of writing:
* My_Class::getLoader()->foo()
* we can write:
* My_Class::()
* @param string $name method name
* @param array $args
* @return mixed
*/
public static function __callStatic($name, $args) {
return call_user_func_array([static::getLoader(), $name], $args);
}
/**
* @param string $class
* @return Storm_Model_Loader
*/
public static function getLoaderFor($class) {
return isset(self::$_loaders[$class])
? self::$_loaders[$class]
: self::setLoaderFor($class, self::_buildLoaderFor($class));
}
/**
* @param string $class
* @param Storm_Model_Loader $loader
*/
public static function setLoaderFor($class, $loader) {
return self::$_loaders[$class] = $loader;
}
public static function unsetLoaders() {
self::$_loaders = [];
}
public static function getLoaders() {
return self::$_loaders;
}
public function __construct() {
$this->_associations = new Storm_Model_Associations();
$this->describeAssociationsOn($this->_associations);
}
public function describeAssociationsOn($associations) {}
/**
* @return bool
*/
public function hasBelongsToRelashionshipWith($field) {
return array_key_exists($field, $this->_belongs_to);
}
/**
* Put the instance in its Loader's cache
* @return Storm_Model_Abstract
*/
public function cache() {
static::getLoader()->cacheInstance($this);
return $this;
}
/**
* @return bool
*/
public function save() {
$this->_updateNullBelongsToIdFieldsFromDependents();
if ($valid = $this->isValid()) {
$this->saveWithoutValidation();
$this->_attributes_in_db=$this->_attributes;
}
return $valid;
}
public function assertSave() {
if (!$this->save())
throw new Storm_Model_Exception('Can\'t save '.get_class($this).': '.implode(',',$this->getErrors()));
return true;
}
public function getClassName() {
return get_class($this);
}
public function saveWithoutValidation() {
$this->beforeSave();
$this->_associations->saveBefore($this);
$this->_updateNullBelongsToIdFieldsFromDependents();
$this->getLoader()->save($this);
$this->_saveDependencies();
$this->afterSave();
}
protected function _updateNullBelongsToIdFieldsFromDependents() {
$this->_belongs_to_attributes = array_filter($this->_belongs_to_attributes);
foreach ($this->_belongs_to_attributes as $field => $dependent) {
$id_field = $this->getIdFieldForDependent($field);
if (null === $this->_get($id_field))
$this->_set($id_field, $dependent->getId());
}
return $this;
}
public function beforeSave() {}
public function afterSave() {}
public function beforeDelete() {}
public function afterDelete() {}
/**
* Is this model valid for saving
*
* You can notify error by using addError or check methods
*
* @see Storm_Model_Abstract::addError
* @see Storm_Model_Abstract::check
*
* @example
* public function validate() {
* $this->check($this->getRole() < 10, 'role should not exceed 10');
* }
*
*/
public function validate() {}
/**
* Try to validate the model. If errors found, return false
* @see Storm_Model_Abstract::validate
* @return bool
*/
public function isValid() {
$this->_errors = [];
$this->validate();
return !$this->hasErrors();
}
/**
* Return true if contains some errors (do not try to validate)
* @see Storm_Model_Abstract::validate
* @return bool
*/
public function hasErrors() {
return count($this->getErrors()) > 0;
}
/**
* @return array
*/
public function getErrors() {
return $this->_errors;
}
/**
* @param string $attribute
* @param string $error
*/
public function addAttributeError($attribute, $error) {
$this->_errors[$attribute] = $error;
}
/**
* @param string $error
*/
public function addError($error) {
$this->_errors[] = $error;
}
/**
* @param bool $condition
* @param string $error
*/
public function check($condition, $error) {
if (!$condition) {
$this->addError($error);
}
return $this;
}
/**
* @param string $attribute
* @param bool $condition
* @param string $error
*/
public function checkAttribute($attribute, $condition, $error) {
if (!$condition) {
$this->addAttributeError($attribute, $error);
}
return $this;
}
/**
* Validate an attribute with a validator
*
* @param $name string attribute name
* @param $validator_class string
* @param $message string
* @return Storm_Model_Abstract
*/
public function validateAttribute($name, $validator_class, $message=null) {
$validator = new $validator_class();
$valid = $validator->isValid($this->_get($name));
if ($message)
return $this->checkAttribute($name, $valid, $message);
foreach($validator->getMessages() as $message)
$this->checkAttribute($name, $valid, $message);
return $this;
}
/**
* Return an associative array with attribute
* name as $key and its value.
*
* Used by Loader while saving in order to build the
* SQL query.
*
* @return array
*/
public function attributesToArray() {
$attributes = array();
$all_attributes = array_merge($this->_default_attribute_values,
$this->_attributes);
foreach ($all_attributes as $name => $value) {
$method = 'get'.$this->attributeNameToAccessor($name);
if (method_exists($this, $method)) {
$attributes[$name] = $this->$method();
} else {
$attributes[$name] = $value;
}
}
return $attributes;
}
/**
* Return field $_attributes
*
* @return array
*/
public function getRawAttributes() {
return array_merge($this->_default_attribute_values,
$this->_attributes);
}
/**
* Return field $_attributes_in_db
*
* @return array
*/
public function getRawDbAttributes() {
return $this->_attributes_in_db;
}
/**
* Return an associative array with attribute
* name as $key and its value.
*
* This method may be redefined in subclasses
* in order to provide some coupling with Zend_Form::populate()
*
* @return array
*/
public function toArray() {
return $this->attributesToArray();
}
/**
* Returns true if this instance has not been loaded from the
* database or saved.
*
* @return bool
*/
public function isNew() {
return (bool)(!array_key_exists('id', $this->_attributes) ||
$this->_attributes['id'] === null);
}
public function delete() {
$this->beforeDelete();
$this->_deleteDependents();
if ($this->isNew() || $this->getLoader()->delete($this)) {
$this->afterDelete();
}
}
/**
* @param string $id
* @return Storm_Model_Abstract
*/
public function setId($id) {
if ($this->_table_primary != null) {
$this->_set(strtolower($this->_table_primary), $id);
}
//return $this->_set('id', $id); //perfs
$this->_attributes['id'] = $id;
return $this;
}
/**
* @param string $field
* @return string
*/
public function getIdFieldForDependent($field) {
if (
array_key_exists($field, $this->_belongs_to)
&& (array_key_exists('referenced_in', $this->_belongs_to[$field]))
) {
return $this->_belongs_to[$field]['referenced_in'];
}
return $field . '_id';
}
/**
* @param string $attribute
* @return bool
*/
public function isAttributeEmpty($attribute) {
if (!array_key_exists($attribute, $this->_attributes)) {
return true;
}
$value = $this->_get($attribute);
return empty($value);
}
protected function _deleteDependents() {
foreach ($this->hasManyRelationships() as $field => $relation) {
if (!array_key_exists('dependents', $relation)) {
continue;
}
if ($relation['dependents'] != 'delete') {
continue;
}
$dependents = $this->_getDependents($field);
foreach ($dependents as $object) {
$this->_removeDependent($field, $object);
$object->delete();
}
}
$this->_associations->delete($this);
}
protected function _saveDependencies() {
foreach (array_keys($this->_has_many_attributes) as $field)
$this->_saveDependents($field);
$this->_associations->save($this);
}
/**
* @param string $field
* @return Storm_Model_Abstract
*/
protected function _saveDependents($field) {
if (array_key_exists('through', $this->descriptionOf($field))) {
return $this;
}
if (!array_key_exists($field, $this->_has_many_attributes_in_db)) {
$this->_has_many_attributes_in_db[$field] = $this->_getDependentsFromLoader($field);
}
$has_many_attributes = (array)$this->_has_many_attributes[$field];
$has_many_attributes_in_db = (array)$this->_has_many_attributes_in_db[$field];
$dependents_to_delete = $this->_array_diff(
$has_many_attributes_in_db,
$has_many_attributes
);
foreach ($dependents_to_delete as $dependent) {
$dependent->delete();
}
foreach($this->_has_many_attributes[$field] as $dependent) {
$dependent->save();
}
$this->_has_many_attributes_in_db[$field] = $this->_has_many_attributes[$field];
return $this;
}
/**
* @param array $array1
* @param array $array2
* @return array
* @todo why not using real array_diff ??
*/
protected function _array_diff(Array &$array1, Array &$array2) {
$diff = array();
foreach($array1 as $element) {
if (! in_array($element, $array2)) {
$diff []= $element;
}
}
return $diff;
}
/**
* Main purpose is to setup generic getters and setters:
*
* $car->getColor();
* $car->setColor('red');
* $car->addWheel($w = new Wheel());
* $car->removeWheel($w);
* $car->getWheels() //return an array containing instances;
* $car->setWheels(array(new Wheel(), new Wheel(), new Wheel(), new Wheel()))
* $car->hasWheels() //return true if attribute wheels not empty;
*
*
* @param string $method
* @param array $args
* @return Storm_Model_Abstract
* @throws Exception
*/
public function __call($method, $args) {
return $this->_associations->handleCall($this, $method, $args);
}
/**
* See __call
*
* ex:
* $car->perform('getColor')
* $car->perform('setColor', ['red'])
*
* @param string $method
* @param array $args
* @return Storm_Model_Abstract
* @throws Exception
*/
public function perform($method, $args = []) {
if (!preg_match('/(get|set|add|remove|has|numberOf)(\w+)/', $method, $matches)) {
throw new Storm_Model_Exception('Tried to call unknown method '.get_class($this).'::'.$method);
}
$attribute = $this->_accessorToAttributeName($matches[2]);
switch ($matches[1]) {
case 'get':
return $this->_get($attribute);
break;
case 'set':
$this->_set($attribute, $args[0]);
return $this;
break;
case 'add':
$this->_addDependent($this->_pluralize($attribute), $args[0]);
break;
case 'remove':
$this->_removeDependent($this->_pluralize($attribute), $args[0]);
break;
case 'has':
return $this->_has($attribute);
break;
case 'numberOf':
return $this->_numberOf($attribute);
break;
}
return $this;
}
/**
* @param string $field
* @return array
*/
public function descriptionOf($field) {
if (method_exists($this, $method = 'descriptionOf'.$field))
return $this->$method();
if (isset($this->_has_many[$field]))
return $this->_has_many[$field];
if (isset($this->_belongs_to[$field]))
return $this->_belongs_to[$field];
return null;
}
public function hasManyRelationships() {
$relations = $this->_has_many;
foreach (array_keys($this->_has_many) as $field)
$relations[$field] = $this->descriptionOf($field);
return $relations;
}
/**
* Return true if $attribute not empty
*
* @param String $attribute
* @return bool
*/
public function _has($attribute) {
$dependent = $this->callGetterByAttributeName($attribute);
return !empty($dependent);