assignAttributes($attrs, $options); $new_model->_create_do(); return $new_model; } /** * Can receive parameters that will be directly passed to where(); */ static public function destroyAll() { $models = call_user_func_array(get_called_class() . '::where', func_get_args())->take(); foreach ($models as $m) { $m->destroy(); } return $models; } # TODO static public function deleteAll(array $conds) { } /** * Finds model by id and updates it. * * @param string|array $id * @param array $attrs */ static public function update($id, array $attrs) { if (is_array($id)) { foreach ($id as $k => $i) { static::update($i, $attrs[$k]); } } else { $object = static::find($id); $object->updateAttributes($attrs); return $object; } } /** * Searches if record exists by id. * * How to use: * Model::exists(1); * Model::exists(1, 2, 3); * Model::exists([1, 2, 3]); */ static public function exists($params) { $query = self::none(); if (ctype_digit((string)$params)) { $query->where(['id' => $params]); } else { if (is_array($params)) $query->where('id IN (?)', $params); else $query->where('id IN (?)', func_get_args()); } return $query->exists(); } static public function countBySql() { $stmt = call_user_func_array([static::connection(), 'executeSql'], func_get_args()); if (ActiveRecord::lastError()) return false; $rows = $stmt->fetchAll(PDO::FETCH_NUM); if (isset($rows[0][0])) return (int)$rows[0][0]; else return false; } static public function maximum($attr) { return self::connection()->selectValue('SELECT MAX(' . $attr . ') FROM ' . static::tableName()); } static public function count() { return self::connection()->selectValue('SELECT COUNT(*) FROM `' . self::tableName() . '`'); } static public function I18n() { return Rails::application()->I18n(); } static public function transaction($params = [], \Closure $block = null) { self::connection()->transaction($params, $block); } static public function connectionName() { return null; } /** * If called class specifies a connection, such connection will be returned. */ static public function connection() { $name = static::connectionName(); return ActiveRecord::connection($name); } /** * Public properties declared in a model class can be used to avoid defining * setter and getter methods; the value will be set/get directly to/from it. */ static public function hasPublicProperty($propName) { $reflection = self::getReflection(); return $reflection->hasProperty($propName) && $reflection->getProperty($propName)->isPublic(); } /** * It is sometimes required an empty collection. */ static public function emptyCollection() { return new Collection(); } static protected function cn() { return get_called_class(); } static protected function _registry() { if (!self::$registry) self::$registry = new Registry(); return self::$registry->model(get_called_class()); } /** * Creates and returns a non-empty model. * This function is useful to centralize the creation of non-empty models, * since isNewRecord must set to null by passing non_empty. */ static private function _create_model(array $data) { self::$preventInit = true; self::$skipDefaultAttributes = true; $model = new static(); $model->attributes = $data; # Check for primary_key and set init_attrs. if (!static::table()->primaryKey()) { $model->storedAttributes = $data; } $model->isNewRecord = false; $model->_register(); $model->init(); return $model; } static private function _create_collection(array $data, $query = null) { $models = array(); foreach ($data as $d) $models[] = self::_create_model($d); $coll = new Collection($models, $query); return $coll; } static private function getReflection($class = null) { if (!$class) { $class = get_called_class(); } if (!isset(self::$cachedReflections[$class])) { self::$cachedReflections[$class] = new \ReflectionClass($class); } return self::$cachedReflections[$class]; } public function __construct(array $attrs = []) { if (!self::$skipDefaultAttributes) { $this->setDefaultAttributes(); } else { self::$skipDefaultAttributes = true; } $this->assignAttributes($attrs); if (!self::$preventInit) { $this->init(); } else { self::$preventInit = true; } } /** * Checks for the called property in this order * 1. Checks if property is an attribute. * 2. Checks if called property is an already loaded association. * 3. Checks if called property is an association. * If none if these validations is true, the property is actually called * in order to generate an error. */ public function __get($prop) { # See Config/default_config.php for more info. // if (!Rails::config()->ar2) { if (static::isAttribute($prop)) { return $this->getAttribute($prop); } elseif ($this->getAssociation($prop) !== null) { return $this->loadedAssociations[$prop]; } throw new Exception\RuntimeException( sprintf("Tried to get unknown property %s::$%s", get_called_class(), $prop) ); // } // # Force error/default behaviour // return $this->$prop; } /** * 1. Checks for setter method and calls it if available. * 2. If the property is an attribute, the value is set to it (i.e. default setter for attributes). * 3. The value is set as object property. < This shouldn't happen! */ public function __set($prop, $val) { // if (!Rails::config()->ar2) { if (static::isAttribute($prop)) { $this->setAttribute($prop, $val); // } elseif ($this->setterExists($prop)) { // $this->_run_setter($prop, $val); } else { throw new Exception\RuntimeException( sprintf("Trying to set undefined property %s", $prop) ); // # Default PHP behaviour. // $this->$prop = $val; } // } else { // if (static::isAttribute($prop)) { // $this->setAttribute($prop, $val); // } else { // throw new Exception\RuntimeException( // sprintf("Trying to set undefined property %s", $prop) // ); // } // } } public function __isset($prop) { return $this->issetAttribute($prop); } /** * Some model's features can be overloaded: * 1. Check if an attribute changed with attributeNameChanged(); * Calling this normally would be: attributeChanged('attribute_name'); * 2. Get the previous value of an attribute with attributeNameWas(); * Normally: attributeWas('attribute_name'); * 3. Overload attribute setters. Models *should* define setter * methods for each attribute, but they can be called overloadingly. The correct * or expected name of the setter method is camel-cased, like setAttributeName(); * 4. Overload scopes. * 5. Overload associations. * 6. Overload attribute getters. The expected method name is the camel-cased name of * the attribute, like attributeName(), because the "get" prefix is omitted for getters. * Models *should* define getter methods for each attribute. */ public function __call($method, $params) { # Overload attributeChange() if (strlen($method) > 7 && substr($method, -7) == 'Changed') { $attribute = static::properAttrName(substr($method, 0, -7)); return $this->attributeChanged($attribute); } # Overload attributeWas() if (strlen($method) > 3 && substr($method, -3) == 'Was') { $attribute = static::properAttrName(substr($method, 0, -3)); return $this->attributeWas($attribute); } # Overload attribute setter. if (substr($method, 0, 3) == 'set') { $attrName = substr($method, 3); $underscored = Rails::services()->get('inflector')->underscore($method); if (static::isAttribute($underscored)) { $this->setAttribute($underscored, array_shift($params)); return; } } # Overload scopes. if ($rel = static::scope($method, $params)) { return $rel; } // # Overload associations. // if ($this->getAssociation($method) !== null) { // return $this->loadedAssociations[$method]; // } // # Overload attributes. // $underscored = Rails::services()->get('inflector')->underscore($method); // if (static::isAttribute($underscored)) { // return $this->getAttribute($underscored); // } throw new Exception\BadMethodCallException( sprintf("Call to undefined method %s::%s", get_called_class(), $method) ); } public function isNewRecord() { return $this->isNewRecord; } /** * Directly passes the new value to the attributes array and then * saves the record. * * @param string $attrName The name of the attribute. * @param mixed $value The value for the attribute, */ public function updateAttribute($attrName, $value) { $this->attributes[$attrName] = $value; return $this->save(['skip_validation' => true, 'skip_callbacks' => true, 'action' => 'update']); } /** * Save record * * Saves in the database the properties of the object that match * the columns of the corresponding table in the database. * * Is the model is new, create() will be called instead. * * @array $values: If present, object will be updated according * to this array, otherwise, according to its properties. */ public function save(array $opts = array()) { // if (empty($opts['skip_validation'])) { // if (!$this->_validate_data(!empty($opts['action']) ? $opts['action'] : 'save')) // return false; // } if ($this->isNewRecord()) { if (!$this->_create_do($opts)) return false; } else { if (!$this->_validate_data('save')) return false; if (empty($opts['skip_callbacks'])) { if (!$this->runCallbacks('before_save')) return false; } if (!$this->_save_do($opts)) return false; if (empty($opts['skip_callbacks'])) { $this->runCallbacks('after_save'); } } return true; } /** * Delete * * Deletes row from database based on Primary keys. */ static public function delete() { // if ($this->_delete_from_db('delete')) { // foreach (array_keys(get_object_vars($this)) as $p) // unset($this->$p); // return true; // } // return false; } # Deletes current model from database but keeps model's properties. public function destroy() { return $this->_delete_from_db('destroy'); } public function errors() { if (!$this->errors) $this->errors = new ActiveModel\Errors(); return $this->errors; } public function reload() { try { $data = $this->_get_stored_data(); } catch (Exception\ExceptionInterface $e) { return false; } $cn = get_called_class(); $refl = new \ReflectionClass($cn); $reflProps = $refl->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED); $defProps = $refl->getDefaultProperties(); foreach ($reflProps as $reflProp) { if ($reflProp->getDeclaringClass() == $cn && !$reflProp->isStatic()) { $this->{$reflProp->name} = $defProps[$reflProp->name]; } } $this->isNewRecord = false; $this->attributes = $data->attributes(); $this->loadedAssociations = []; return true; } public function asJson() { return $this->attributes(); } public function toJson() { return json_encode($this->asJson()); } # TODO: public function toXml(array $params = []) { if (!isset($params['attributes'])) { // $this->_merge_model_attributes(); $attrs = $this->attributes(); } else { $attrs = $params['attributes']; } !isset($params['root']) && $params['root'] = strtolower(str_replace('_', '-', self::cn())); if (!isset($params['builder'])) { $xml = new \Rails\Xml\Xml($attrs, $params); return $xml->output(); } else { $builder = $params['builder']; unset($params['builder']); $builder->build($attrs, $params); } } /** * *************************** * Default protected methods { * *************************** * attrAccessible and attrProtected can be found in Base\Methods\AttributeMethods. * associations can be found in Base\Methods\AssociationMethods */ /** * Code executed when initializing the object. * Called by _create_model() and _create_do(). */ protected function init() { } /** * Example: * * return [ * // Validate attributes * 'attribute_name' => [ * 'property' => rules... * ], * * // Passing a value or an index that isn't recognized as * // attribute will be considered as custom validation method. * 'methodName', * 'method2Name' => [ 'on' => [ actions... ] ], * ]; * * Custom validation methods must set the error manually. They aren't expected to * return anything. */ protected function validations() { return []; } protected function callbacks() { return []; } /* } */ /** * Returns model's current data in the database or using storedAttributes. */ protected function _get_stored_data() { if (!($prim_key = static::table()->primaryKey()) && !$this->storedAttributes) { throw new Exception\RuntimeException( "Can't find data without primary key nor storedAttributes" ); } $query = static::none(); if ($prim_key) { $query->where('`'.$prim_key.'` = ?', $this->$prim_key); } else { $model = new static(); $cols_names = static::table()->columnNames(); foreach ($this->storedAttributes as $name => $value) { if (in_array($name, $cols_names)) { $model->attributes[$name] = $value; } } if (!$model->attributes) throw new Exception\RuntimeException( "Model rebuilt from storedAttributes failed" ); else return $model; } $current = $query->first(); if (!$current) throw new Exception\RuntimeException( "Row not found in database (searched with storedAttributes)" ); else return $current; } public function runCallbacks($callback_name) { $callbacks = array(); $tmp = $this->callbacks(); if (isset($tmp[$callback_name])) { $callbacks = $tmp[$callback_name]; } $callbacks = array_unique(array_filter(array_merge($callbacks, $this->_get_parents_callbacks($callback_name)))); if ($callbacks) { foreach ($callbacks as $method) { if (false === $this->$method()) { return false; } } } return true; } private function _get_parents_callbacks($callback_name) { $all_callbacks = array(); if (($class = get_parent_class($this)) != 'Rails\ActiveRecord\Base') { $class = self::cn(); $obj = new $class(); while (($class = get_parent_class($obj)) != 'Rails\ActiveRecord\Base') { $obj = new $class(); if ($callbacks = $obj->callbacks()) { if (isset($callbacks[$callback_name])) $all_callbacks = array_merge($callbacks, $callbacks[$callback_name]); } } } return $all_callbacks; } /** * @return bool * @see validations() */ private function _validate_data($action) { if (!$this->runCallbacks('before_validation')) return false; $validation_success = true; $modelClass = get_called_class(); $classProps = get_class_vars($modelClass); foreach ($this->validations() as $attrName => $validations) { /** * This should only happen when passing a custom validation method with * no validation options. */ if (is_int($attrName)) { $attrName = $validations; $validations = []; } if (static::isAttribute($attrName) || array_key_exists($attrName, $classProps)) { foreach ($validations as $type => $params) { if (!is_array($params)) { $params = [$params]; } if ($modelClass::isAttribute($attrName)) { $value = $this->getAttribute($attrName); } else { $value = $this->$attrName; } $validation = new Validator($type, $value, $params); $validation->set_params($action, $this, $attrName); if (!$validation->validate()->success()) { $validation->set_error_message(); $validation_success = false; } } } else { /** * The attrName passed isn't an attribute nor a property, so we assume it's a method. * * $attrName becomes the name of the method. * $validations becomes validation options. */ if (!empty($validations['on']) && !in_array($action, $validations['on'])) { continue; } // $this->getAttribute($attrName); $this->$attrName(); if ($this->errors()->any()) { $validation_success = false; } } } return $validation_success; } private function _create_do() { if (!$this->runCallbacks('before_validation_on_create')) { return false; } elseif (!$this->_validate_data('create')) { return false; } $this->runCallbacks('after_validation_on_create'); if (!$this->runCallbacks('before_save')) return false; if (!$this->runCallbacks('before_create')) return false; $this->_check_time_column('created_at'); $this->_check_time_column('updated_at'); $this->_check_time_column('created_on'); $this->_check_time_column('updated_on'); $cols_values = $cols_names = array(); // $this->_merge_model_attributes(); foreach ($this->attributes() as $attr => $val) { $proper = static::properAttrName($attr); if (!static::table()->columnExists($proper)) { continue; } $cols_names[] = '`'.$attr.'`'; $cols_values[] = $val; $init_attrs[$attr] = $val; } if (!$cols_values) return false; $binding_marks = implode(', ', array_fill(0, (count($cols_names)), '?')); $cols_names = implode(', ', $cols_names); $sql = 'INSERT INTO `'.static::tableName().'` ('.$cols_names.') VALUES ('.$binding_marks.')'; array_unshift($cols_values, $sql); static::connection()->executeSql($cols_values); $id = static::connection()->lastInsertId(); $primary_key = static::table()->primaryKey(); if ($primary_key && count($primary_key) == 1) { if (!$id) { $this->errors()->addToBase('Couldn\'t retrieve new primary key.'); return false; } if ($pri_key = static::table()->primaryKey()) { $this->setAttribute($pri_key, $id); } } else { $this->storedAttributes = $init_attrs; } $this->isNewRecord = false; // $this->init(); $this->runCallbacks('after_create'); $this->runCallbacks('after_save'); return true; } private function _save_do() { $w = $wd = $q = $d = array(); $dt = static::table()->columnNames(); try { $current = $this->_get_stored_data(); } catch (Exception\ExceptionInterface $e) { $this->errors()->addToBase($e->getMessage()); return; } $has_primary_keys = false; foreach (static::table()->indexes() as $idx) { $w[] = '`'.$idx.'` = ?'; $wd[] = $current->$idx; } if ($w) { $has_primary_keys = true; } // $this->_merge_model_attributes(); foreach ($this->attributes() as $prop => $val) { # Can't update properties that don't have a column in DB, or # PRImary keys, or time columns. if (!in_array($prop, $dt) || $prop == 'created_at' || $prop == 'updated_at' || $prop == 'created_on' || $prop == 'updated_on' ) { continue; } elseif (!$has_primary_keys) { $w[] = '`'.$prop.'` = ?'; $wd[] = $current->$prop; // foreach ($current->attributes() as $ } // } elseif (!$has_primary_keys && $val === $current->$prop) { // $w[] = '`'.$prop.'` = ?'; // $wd[] = $current->$prop; // } else if ($val != $current->$prop) { $this->setChangedAttribute($prop, $current->$prop); $q[] = '`'.$prop.'` = ?'; $d[] = $val; } } # Update `updated_at|on` if exists. if ($this->_check_time_column('updated_at')) { $q[] = "`updated_at` = ?"; $d[] = $this->updated_at; } elseif ($this->_check_time_column('updated_on')) { $q[] = "`updated_on` = ?"; $d[] = $this->updated_on; } if ($q) { $q = "UPDATE `" . static::tableName() . "` SET " . implode(', ', $q); $w && $q .= ' WHERE '.implode(' AND ', $w); $q .= ' LIMIT 1'; $d = array_merge($d, $wd); array_unshift($d, $q); static::connection()->executeSql($d); if (ActiveRecord::lastError()) { $this->errors()->addToBase(ActiveRecord::lastError()); return false; } } $this->_update_init_attrs(); return true; } private function _delete_from_db($type) { if (!$this->runCallbacks('before_'.$type)) { return false; } $w = $wd = []; if ($keys = self::table()->indexes()) { foreach ($keys as $k) { $w[] = '`' . static::tableName() . '`.`'.$k.'` = ?'; $wd[] = $this->$k; } } elseif ($this->storedAttributes) { foreach ($this->storedAttributes as $attr => $val) { $w[] = '`'.$attr.'` = ?'; $wd[] = $val; } } else { throw new Exception\LogicException( "Can't delete model without attributes" ); } $w = implode(' AND ', $w); $query = 'DELETE FROM `' . static::tableName() . '` WHERE '.$w; array_unshift($wd, $query); static::connection()->executeSql($wd); $this->runCallbacks('after_'.$type); return true; } private function _register() { self::_registry()->register($this); } /** * Check time column * * Called by save() and create(), checks if $column * exists and automatically sets a value to it. */ private function _check_time_column($column) { if (!static::table()->columnExists($column)) return false; $type = static::table()->columnType($column); if ($type == 'datetime') $time = date('Y-m-d H:i:s'); elseif ($type == 'year') $time = date('Y'); elseif ($type == 'date') $time = date('Y-m-d'); elseif ($type == 'time') $time = date('H:i:s'); elseif ($type == 'timestamp') $time = time(); else return false; $this->attributes[$column] = $time; return true; } private function _update_init_attrs() { foreach (array_keys($this->storedAttributes) as $name) { if (isset($this->attributes[$name])) $this->storedAttributes[$name] = $this->attributes[$name]; } } private function _getter_for($prop) { $camelized = Rails::services()->get('inflector')->camelize($prop); $method = 'get' . ucfirst($camelized); if (method_exists($this, $method)) { return $this->$method(); } elseif (method_exists($this, $camelized)) { return $this->$camelized(); } return null; } private function setterExists($attrName) { $inflector = Rails::services()->get('inflector'); $reflection = self::getReflection(); $setter = 'set' . $inflector->camelize($attrName); if ($reflection->hasMethod($setter) && $reflection->getMethod($setter)->isPublic()) { return $setter; } else { false; } } }