diff --git a/extensions/elasticsearch/ActiveRecord.php b/extensions/elasticsearch/ActiveRecord.php index b04e67d..d6ada20 100644 --- a/extensions/elasticsearch/ActiveRecord.php +++ b/extensions/elasticsearch/ActiveRecord.php @@ -10,6 +10,8 @@ namespace yii\elasticsearch; use yii\base\InvalidCallException; use yii\base\InvalidConfigException; use yii\base\NotSupportedException; +use yii\db\ActiveRecordInterface; +use yii\db\BaseActiveRecord; use yii\helpers\Inflector; use yii\helpers\Json; use yii\helpers\StringHelper; @@ -42,7 +44,7 @@ use yii\helpers\StringHelper; * @author Carsten Brandt * @since 2.0 */ -class ActiveRecord extends \yii\db\ActiveRecord +class ActiveRecord extends BaseActiveRecord { const PRIMARY_KEY_NAME = 'id'; @@ -428,47 +430,4 @@ class ActiveRecord extends \yii\db\ActiveRecord } return $n; } - - /** - * @inheritdoc - */ - public static function updateAllCounters($counters, $condition = null, $params = []) - { - throw new NotSupportedException('Update Counters is not supported by elasticsearch ActiveRecord.'); - } - - /** - * @inheritdoc - */ - public static function getTableSchema() - { - throw new NotSupportedException('getTableSchema() is not supported by elasticsearch ActiveRecord.'); - } - - /** - * @inheritdoc - */ - public static function tableName() - { - return static::index() . '/' . static::type(); - } - - /** - * @inheritdoc - */ - public static function findBySql($sql, $params = []) - { - throw new NotSupportedException('findBySql() is not supported by elasticsearch ActiveRecord.'); - } - - /** - * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. - * This method will always return false as transactional operations are not supported by elasticsearch. - * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. - * @return boolean whether the specified operation is transactional in the current [[scenario]]. - */ - public function isTransactional($operation) - { - return false; - } } diff --git a/extensions/redis/ActiveQuery.php b/extensions/redis/ActiveQuery.php index 755fc6f..607b18e 100644 --- a/extensions/redis/ActiveQuery.php +++ b/extensions/redis/ActiveQuery.php @@ -132,7 +132,7 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface if ($db === null) { $db = $modelClass::getDb(); } - return $db->executeCommand('LLEN', [$modelClass::tableName()]); + return $db->executeCommand('LLEN', [$modelClass::keyPrefix()]); } else { return $this->executeScript($db, 'Count'); } @@ -296,7 +296,7 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface $data = []; foreach($pks as $pk) { if (++$i > $start && ($this->limit === null || $i <= $start + $this->limit)) { - $key = $modelClass::tableName() . ':a:' . $modelClass::buildKey($pk); + $key = $modelClass::keyPrefix() . ':a:' . $modelClass::buildKey($pk); $result = $db->executeCommand('HGETALL', [$key]); if (!empty($result)) { $data[] = $result; diff --git a/extensions/redis/ActiveRecord.php b/extensions/redis/ActiveRecord.php index 1239a6c..f227ed1 100644 --- a/extensions/redis/ActiveRecord.php +++ b/extensions/redis/ActiveRecord.php @@ -9,6 +9,8 @@ namespace yii\redis; use yii\base\InvalidConfigException; use yii\base\NotSupportedException; +use yii\db\BaseActiveRecord; +use yii\helpers\Inflector; use yii\helpers\StringHelper; /** @@ -34,7 +36,7 @@ use yii\helpers\StringHelper; * @author Carsten Brandt * @since 2.0 */ -class ActiveRecord extends \yii\db\ActiveRecord +class ActiveRecord extends BaseActiveRecord { /** * Returns the database connection used by this AR class. @@ -87,6 +89,18 @@ class ActiveRecord extends \yii\db\ActiveRecord } /** + * Declares prefix of the key that represents the keys that store this records in redis. + * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]. + * For example, 'Customer' becomes 'customer', and 'OrderItem' becomes + * 'order_item'. You may override this method if you want different key naming. + * @return string the prefix to apply to all AR keys + */ + public static function keyPrefix() + { + return Inflector::camel2id(StringHelper::basename(get_called_class()), '_'); + } + + /** * @inheritdoc */ public function insert($runValidation = true, $attributes = null) @@ -102,15 +116,15 @@ class ActiveRecord extends \yii\db\ActiveRecord foreach ($this->primaryKey() as $key) { $pk[$key] = $values[$key] = $this->getAttribute($key); if ($pk[$key] === null) { - $pk[$key] = $values[$key] = $db->executeCommand('INCR', [static::tableName() . ':s:' . $key]); + $pk[$key] = $values[$key] = $db->executeCommand('INCR', [static::keyPrefix() . ':s:' . $key]); $this->setAttribute($key, $values[$key]); } } // } // save pk in a findall pool - $db->executeCommand('RPUSH', [static::tableName(), static::buildKey($pk)]); + $db->executeCommand('RPUSH', [static::keyPrefix(), static::buildKey($pk)]); - $key = static::tableName() . ':a:' . static::buildKey($pk); + $key = static::keyPrefix() . ':a:' . static::buildKey($pk); // save attributes $args = [$key]; foreach($values as $attribute => $value) { @@ -150,7 +164,7 @@ class ActiveRecord extends \yii\db\ActiveRecord foreach(static::fetchPks($condition) as $pk) { $newPk = $pk; $pk = static::buildKey($pk); - $key = static::tableName() . ':a:' . $pk; + $key = static::keyPrefix() . ':a:' . $pk; // save attributes $args = [$key]; foreach($attributes as $attribute => $value) { @@ -161,13 +175,13 @@ class ActiveRecord extends \yii\db\ActiveRecord $args[] = $value; } $newPk = static::buildKey($newPk); - $newKey = static::tableName() . ':a:' . $newPk; + $newKey = static::keyPrefix() . ':a:' . $newPk; // rename index if pk changed if ($newPk != $pk) { $db->executeCommand('MULTI'); $db->executeCommand('HMSET', $args); - $db->executeCommand('LINSERT', [static::tableName(), 'AFTER', $pk, $newPk]); - $db->executeCommand('LREM', [static::tableName(), 0, $pk]); + $db->executeCommand('LINSERT', [static::keyPrefix(), 'AFTER', $pk, $newPk]); + $db->executeCommand('LREM', [static::keyPrefix(), 0, $pk]); $db->executeCommand('RENAME', [$key, $newKey]); $db->executeCommand('EXEC'); } else { @@ -201,7 +215,7 @@ class ActiveRecord extends \yii\db\ActiveRecord $db = static::getDb(); $n=0; foreach(static::fetchPks($condition) as $pk) { - $key = static::tableName() . ':a:' . static::buildKey($pk); + $key = static::keyPrefix() . ':a:' . static::buildKey($pk); foreach($counters as $attribute => $value) { $db->executeCommand('HINCRBY', [$key, $attribute, $value]); } @@ -233,8 +247,8 @@ class ActiveRecord extends \yii\db\ActiveRecord $db->executeCommand('MULTI'); foreach($pks as $pk) { $pk = static::buildKey($pk); - $db->executeCommand('LREM', [static::tableName(), 0, $pk]); - $attributeKeys[] = static::tableName() . ':a:' . $pk; + $db->executeCommand('LREM', [static::keyPrefix(), 0, $pk]); + $attributeKeys[] = static::keyPrefix() . ':a:' . $pk; } if (empty($attributeKeys)) { $db->executeCommand('EXEC'); @@ -292,31 +306,4 @@ class ActiveRecord extends \yii\db\ActiveRecord } return md5(json_encode($key)); } - - /** - * @inheritdoc - */ - public static function getTableSchema() - { - throw new NotSupportedException('getTableSchema() is not supported by redis ActiveRecord.'); - } - - /** - * @inheritdoc - */ - public static function findBySql($sql, $params = []) - { - throw new NotSupportedException('findBySql() is not supported by redis ActiveRecord.'); - } - - /** - * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. - * This method will always return false as transactional operations are not supported by redis. - * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. - * @return boolean whether the specified operation is transactional in the current [[scenario]]. - */ - public function isTransactional($operation) - { - return false; - } } diff --git a/extensions/redis/LuaScriptBuilder.php b/extensions/redis/LuaScriptBuilder.php index 81dff3f..55bf455 100644 --- a/extensions/redis/LuaScriptBuilder.php +++ b/extensions/redis/LuaScriptBuilder.php @@ -29,7 +29,7 @@ class LuaScriptBuilder extends \yii\base\Object // TODO add support for orderBy /** @var ActiveRecord $modelClass */ $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); return $this->build($query, "n=n+1 pks[n]=redis.call('HGETALL',$key .. pk)", 'pks'); } @@ -43,7 +43,7 @@ class LuaScriptBuilder extends \yii\base\Object // TODO add support for orderBy /** @var ActiveRecord $modelClass */ $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); return $this->build($query, "do return redis.call('HGETALL',$key .. pk) end", 'pks'); } @@ -58,7 +58,7 @@ class LuaScriptBuilder extends \yii\base\Object // TODO add support for orderBy and indexBy /** @var ActiveRecord $modelClass */ $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); return $this->build($query, "n=n+1 pks[n]=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'pks'); } @@ -82,7 +82,7 @@ class LuaScriptBuilder extends \yii\base\Object { /** @var ActiveRecord $modelClass */ $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); return $this->build($query, "n=n+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'n'); } @@ -96,7 +96,7 @@ class LuaScriptBuilder extends \yii\base\Object { /** @var ActiveRecord $modelClass */ $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); return $this->build($query, "n=n+1 if v==nil then v=0 end v=v+redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ")", 'v/n'); } @@ -110,7 +110,7 @@ class LuaScriptBuilder extends \yii\base\Object { /** @var ActiveRecord $modelClass */ $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or nmodelClass; - $key = $this->quoteValue($modelClass::tableName() . ':a:'); + $key = $this->quoteValue($modelClass::keyPrefix() . ':a:'); return $this->build($query, "n=redis.call('HGET',$key .. pk," . $this->quoteValue($column) . ") if v==nil or n>v then v=n end", 'v'); } @@ -152,7 +152,7 @@ class LuaScriptBuilder extends \yii\base\Object /** @var ActiveRecord $modelClass */ $modelClass = $query->modelClass; - $key = $this->quoteValue($modelClass::tableName()); + $key = $this->quoteValue($modelClass::keyPrefix()); $loadColumnValues = ''; foreach($columns as $column => $alias) { $loadColumnValues .= "local $alias=redis.call('HGET',$key .. ':a:' .. pk, '$column')\n"; diff --git a/framework/yii/db/ActiveRecord.php b/framework/yii/db/ActiveRecord.php index bb2016a..0d79d3e 100644 --- a/framework/yii/db/ActiveRecord.php +++ b/framework/yii/db/ActiveRecord.php @@ -39,45 +39,9 @@ use yii\helpers\Inflector; * @author Carsten Brandt * @since 2.0 */ -class ActiveRecord extends Model +class ActiveRecord extends BaseActiveRecord { /** - * @event Event an event that is triggered when the record is initialized via [[init()]]. - */ - const EVENT_INIT = 'init'; - /** - * @event Event an event that is triggered after the record is created and populated with query result. - */ - const EVENT_AFTER_FIND = 'afterFind'; - /** - * @event ModelEvent an event that is triggered before inserting a record. - * You may set [[ModelEvent::isValid]] to be false to stop the insertion. - */ - const EVENT_BEFORE_INSERT = 'beforeInsert'; - /** - * @event Event an event that is triggered after a record is inserted. - */ - const EVENT_AFTER_INSERT = 'afterInsert'; - /** - * @event ModelEvent an event that is triggered before updating a record. - * You may set [[ModelEvent::isValid]] to be false to stop the update. - */ - const EVENT_BEFORE_UPDATE = 'beforeUpdate'; - /** - * @event Event an event that is triggered after a record is updated. - */ - const EVENT_AFTER_UPDATE = 'afterUpdate'; - /** - * @event ModelEvent an event that is triggered before deleting a record. - * You may set [[ModelEvent::isValid]] to be false to stop the deletion. - */ - const EVENT_BEFORE_DELETE = 'beforeDelete'; - /** - * @event Event an event that is triggered after a record is deleted. - */ - const EVENT_AFTER_DELETE = 'afterDelete'; - - /** * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional. */ const OP_INSERT = 0x01; @@ -96,20 +60,6 @@ class ActiveRecord extends Model const OP_ALL = 0x07; /** - * @var array attribute values indexed by attribute names - */ - private $_attributes = []; - /** - * @var array old attribute values indexed by attribute names. - */ - private $_oldAttributes; - /** - * @var array related models indexed by the relation names - */ - private $_related = []; - - - /** * Returns the database connection used by this AR class. * By default, the "db" application component is used as the database connection. * You may override this method if you want to use a different database connection. @@ -121,41 +71,6 @@ class ActiveRecord extends Model } /** - * Creates an [[ActiveQuery]] instance for query purpose. - * - * @include @yii/db/ActiveRecord-find.md - * - * @param mixed $q the query parameter. This can be one of the followings: - * - * - a scalar value (integer or string): query by a single primary key value and return the - * corresponding record. - * - an array of name-value pairs: query by a set of column values and return a single record matching all of them. - * - null: return a new [[ActiveQuery]] object for further query purpose. - * - * @return ActiveQuery|ActiveRecord|null When `$q` is null, a new [[ActiveQuery]] instance - * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be - * returned (null will be returned if there is no matching). - * @throws InvalidConfigException if the AR class does not have a primary key - * @see createQuery() - */ - public static function find($q = null) - { - $query = static::createQuery(); - if (is_array($q)) { - return $query->where($q)->one(); - } elseif ($q !== null) { - // query by primary key - $primaryKey = static::primaryKey(); - if (isset($primaryKey[0])) { - return $query->where([$primaryKey[0] => $q])->one(); - } else { - throw new InvalidConfigException(get_called_class() . ' must have a primary key.'); - } - } - return $query; - } - - /** * Creates an [[ActiveQuery]] instance with a given SQL statement. * * Note that because the SQL statement is already specified, calling additional @@ -309,31 +224,13 @@ class ActiveRecord extends Model } /** - * Returns the name of the column that stores the lock version for implementing optimistic locking. - * - * Optimistic locking allows multiple users to access the same record for edits and avoids - * potential conflicts. In case when a user attempts to save the record upon some staled data - * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown, - * and the update or deletion is skipped. - * - * Optimistic locking is only supported by [[update()]] and [[delete()]]. - * - * To use Optimistic locking: - * - * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`. - * Override this method to return the name of this column. - * 2. In the Web form that collects the user input, add a hidden field that stores - * the lock version of the recording being updated. - * 3. In the controller action that does the data updating, try to catch the [[StaleObjectException]] - * and implement necessary business logic (e.g. merging the changes, prompting stated data) - * to resolve the conflict. - * - * @return string the column name that stores the lock version of a table row. - * If null is returned (default implemented), optimistic locking will not be supported. + * Returns the list of all attribute names of the model. + * The default implementation will return all column names of the table associated with this AR class. + * @return array list of attribute names. */ - public function optimisticLock() + public function attributes() { - return null; + return array_keys(static::getTableSchema()->columns); } /** @@ -369,161 +266,6 @@ class ActiveRecord extends Model } /** - * PHP getter magic method. - * This method is overridden so that attributes and related objects can be accessed like properties. - * @param string $name property name - * @return mixed property value - * @see getAttribute() - */ - public function __get($name) - { - if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) { - return $this->_attributes[$name]; - } elseif ($this->hasAttribute($name)) { - return null; - } else { - if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) { - return $this->_related[$name]; - } - $value = parent::__get($name); - if ($value instanceof ActiveRelationInterface) { - return $this->_related[$name] = $value->multiple ? $value->all() : $value->one(); - } else { - return $value; - } - } - } - - /** - * PHP setter magic method. - * This method is overridden so that AR attributes can be accessed like properties. - * @param string $name property name - * @param mixed $value property value - */ - public function __set($name, $value) - { - if ($this->hasAttribute($name)) { - $this->_attributes[$name] = $value; - } else { - parent::__set($name, $value); - } - } - - /** - * Checks if a property value is null. - * This method overrides the parent implementation by checking if the named attribute is null or not. - * @param string $name the property name or the event name - * @return boolean whether the property value is null - */ - public function __isset($name) - { - try { - return $this->__get($name) !== null; - } catch (\Exception $e) { - return false; - } - } - - /** - * Sets a component property to be null. - * This method overrides the parent implementation by clearing - * the specified attribute value. - * @param string $name the property name or the event name - */ - public function __unset($name) - { - if ($this->hasAttribute($name)) { - unset($this->_attributes[$name]); - } else { - if (isset($this->_related[$name])) { - unset($this->_related[$name]); - } else { - parent::__unset($name); - } - } - } - - /** - * Declares a `has-one` relation. - * The declaration is returned in terms of an [[ActiveRelation]] instance - * through which the related record can be queried and retrieved back. - * - * A `has-one` relation means that there is at most one related record matching - * the criteria set by this relation, e.g., a customer has one country. - * - * For example, to declare the `country` relation for `Customer` class, we can write - * the following code in the `Customer` class: - * - * ~~~ - * public function getCountry() - * { - * return $this->hasOne(Country::className(), ['id' => 'country_id']); - * } - * ~~~ - * - * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name - * in the related class `Country`, while the 'country_id' value refers to an attribute name - * in the current AR class. - * - * Call methods declared in [[ActiveRelation]] to further customize the relation. - * - * @param string $class the class name of the related record - * @param array $link the primary-foreign key constraint. The keys of the array refer to - * the attributes of the record associated with the `$class` model, while the values of the - * array refer to the corresponding attributes in **this** AR class. - * @return ActiveRelationInterface the relation object. - */ - public function hasOne($class, $link) - { - /** @var ActiveRecord $class */ - return $class::createActiveRelation([ - 'modelClass' => $class, - 'primaryModel' => $this, - 'link' => $link, - 'multiple' => false, - ]); - } - - /** - * Declares a `has-many` relation. - * The declaration is returned in terms of an [[ActiveRelation]] instance - * through which the related record can be queried and retrieved back. - * - * A `has-many` relation means that there are multiple related records matching - * the criteria set by this relation, e.g., a customer has many orders. - * - * For example, to declare the `orders` relation for `Customer` class, we can write - * the following code in the `Customer` class: - * - * ~~~ - * public function getOrders() - * { - * return $this->hasMany(Order::className(), ['customer_id' => 'id']); - * } - * ~~~ - * - * Note that in the above, the 'customer_id' key in the `$link` parameter refers to - * an attribute name in the related class `Order`, while the 'id' value refers to - * an attribute name in the current AR class. - * - * @param string $class the class name of the related record - * @param array $link the primary-foreign key constraint. The keys of the array refer to - * the attributes of the record associated with the `$class` model, while the values of the - * array refer to the corresponding attributes in **this** AR class. - * @return ActiveRelationInterface the relation object. - */ - public function hasMany($class, $link) - { - /** @var ActiveRecord $class */ - return $class::createActiveRelation([ - 'modelClass' => $class, - 'primaryModel' => $this, - 'link' => $link, - 'multiple' => true, - ]); - } - - /** * Creates an [[ActiveRelation]] instance. * This method is called by [[hasOne()]] and [[hasMany()]] to create a relation instance. * You may override this method to return a customized relation. @@ -536,208 +278,6 @@ class ActiveRecord extends Model } /** - * Populates the named relation with the related records. - * Note that this method does not check if the relation exists or not. - * @param string $name the relation name (case-sensitive) - * @param ActiveRecord|array|null the related records to be populated into the relation. - */ - public function populateRelation($name, $records) - { - $this->_related[$name] = $records; - } - - /** - * Check whether the named relation has been populated with records. - * @param string $name the relation name (case-sensitive) - * @return bool whether relation has been populated with records. - */ - public function isRelationPopulated($name) - { - return array_key_exists($name, $this->_related); - } - - /** - * Returns all populated relations. - * @return array an array of relation data indexed by relation names. - */ - public function getPopulatedRelations() - { - return $this->_related; - } - - /** - * Returns the list of all attribute names of the model. - * The default implementation will return all column names of the table associated with this AR class. - * @return array list of attribute names. - */ - public function attributes() - { - return array_keys(static::getTableSchema()->columns); - } - - /** - * Returns a value indicating whether the model has an attribute with the specified name. - * @param string $name the name of the attribute - * @return boolean whether the model has an attribute with the specified name. - */ - public function hasAttribute($name) - { - return isset($this->_attributes[$name]) || in_array($name, $this->attributes()); - } - - /** - * Returns the named attribute value. - * If this record is the result of a query and the attribute is not loaded, - * null will be returned. - * @param string $name the attribute name - * @return mixed the attribute value. Null if the attribute is not set or does not exist. - * @see hasAttribute() - */ - public function getAttribute($name) - { - return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; - } - - /** - * Sets the named attribute value. - * @param string $name the attribute name - * @param mixed $value the attribute value. - * @throws InvalidParamException if the named attribute does not exist. - * @see hasAttribute() - */ - public function setAttribute($name, $value) - { - if ($this->hasAttribute($name)) { - $this->_attributes[$name] = $value; - } else { - throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); - } - } - - /** - * Returns the old attribute values. - * @return array the old attribute values (name-value pairs) - */ - public function getOldAttributes() - { - return $this->_oldAttributes === null ? [] : $this->_oldAttributes; - } - - /** - * Sets the old attribute values. - * All existing old attribute values will be discarded. - * @param array $values old attribute values to be set. - */ - public function setOldAttributes($values) - { - $this->_oldAttributes = $values; - } - - /** - * Returns the old value of the named attribute. - * If this record is the result of a query and the attribute is not loaded, - * null will be returned. - * @param string $name the attribute name - * @return mixed the old attribute value. Null if the attribute is not loaded before - * or does not exist. - * @see hasAttribute() - */ - public function getOldAttribute($name) - { - return isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; - } - - /** - * Sets the old value of the named attribute. - * @param string $name the attribute name - * @param mixed $value the old attribute value. - * @throws InvalidParamException if the named attribute does not exist. - * @see hasAttribute() - */ - public function setOldAttribute($name, $value) - { - if (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name)) { - $this->_oldAttributes[$name] = $value; - } else { - throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); - } - } - - /** - * Returns a value indicating whether the named attribute has been changed. - * @param string $name the name of the attribute - * @return boolean whether the attribute has been changed - */ - public function isAttributeChanged($name) - { - if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) { - return $this->_attributes[$name] !== $this->_oldAttributes[$name]; - } else { - return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]); - } - } - - /** - * Returns the attribute values that have been modified since they are loaded or saved most recently. - * @param string[]|null $names the names of the attributes whose values may be returned if they are - * changed recently. If null, [[attributes()]] will be used. - * @return array the changed attribute values (name-value pairs) - */ - public function getDirtyAttributes($names = null) - { - if ($names === null) { - $names = $this->attributes(); - } - $names = array_flip($names); - $attributes = []; - if ($this->_oldAttributes === null) { - foreach ($this->_attributes as $name => $value) { - if (isset($names[$name])) { - $attributes[$name] = $value; - } - } - } else { - foreach ($this->_attributes as $name => $value) { - if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) { - $attributes[$name] = $value; - } - } - } - return $attributes; - } - - /** - * Saves the current record. - * - * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]] - * when [[isNewRecord]] is false. - * - * For example, to save a customer record: - * - * ~~~ - * $customer = new Customer; // or $customer = Customer::find($id); - * $customer->name = $name; - * $customer->email = $email; - * $customer->save(); - * ~~~ - * - * - * @param boolean $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be saved to database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the saving succeeds - */ - public function save($runValidation = true, $attributes = null) - { - if ($this->getIsNewRecord()) { - return $this->insert($runValidation, $attributes); - } else { - return $this->update($runValidation, $attributes) !== false; - } - } - - /** * Inserts a row into the associated database table using the attribute values of this record. * * This method performs the following steps in order: @@ -810,8 +350,8 @@ class ActiveRecord extends Model } $values = $this->getDirtyAttributes($attributes); if (empty($values)) { - foreach ($this->primaryKey() as $key) { - $values[$key] = isset($this->_attributes[$key]) ? $this->_attributes[$key] : null; + foreach ($this->getPrimaryKey(true) as $key => $value) { + $values[$key] = $value; } } $db = static::getDb(); @@ -822,14 +362,16 @@ class ActiveRecord extends Model $table = $this->getTableSchema(); if ($table->sequenceName !== null) { foreach ($table->primaryKey as $name) { - if (!isset($this->_attributes[$name])) { - $this->_oldAttributes[$name] = $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName); + if ($this->getAttribute($name) === null) { + $id = $db->getLastInsertID($table->sequenceName); + $this->setAttribute($name, $id); + $this->setOldAttribute($name, $id); break; } } } foreach ($values as $name => $value) { - $this->_oldAttributes[$name] = $value; + $this->setOldAttribute($name, $value); } $this->afterSave(true); return true; @@ -911,73 +453,6 @@ class ActiveRecord extends Model } /** - * @see CActiveRecord::update() - * @throws StaleObjectException - */ - private function updateInternal($attributes = null) - { - if (!$this->beforeSave(false)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - if (empty($values)) { - $this->afterSave(false); - return 0; - } - $condition = $this->getOldPrimaryKey(true); - $lock = $this->optimisticLock(); - if ($lock !== null) { - if (!isset($values[$lock])) { - $values[$lock] = $this->$lock + 1; - } - $condition[$lock] = $this->$lock; - } - // We do not check the return value of updateAll() because it's possible - // that the UPDATE statement doesn't change anything and thus returns 0. - $rows = $this->updateAll($values, $condition); - - if ($lock !== null && !$rows) { - throw new StaleObjectException('The object being updated is outdated.'); - } - - foreach ($values as $name => $value) { - $this->_oldAttributes[$name] = $this->_attributes[$name]; - } - $this->afterSave(false); - return $rows; - } - - /** - * Updates one or several counter columns for the current AR object. - * Note that this method differs from [[updateAllCounters()]] in that it only - * saves counters for the current AR object. - * - * An example usage is as follows: - * - * ~~~ - * $post = Post::find($id); - * $post->updateCounters(['view_count' => 1]); - * ~~~ - * - * @param array $counters the counters to be updated (attribute name => increment value) - * Use negative values if you want to decrement the counters. - * @return boolean whether the saving is successful - * @see updateAllCounters() - */ - public function updateCounters($counters) - { - if ($this->updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) { - foreach ($counters as $name => $value) { - $this->_attributes[$name] += $value; - $this->_oldAttributes[$name] = $this->_attributes[$name]; - } - return true; - } else { - return false; - } - } - - /** * Deletes the table row corresponding to this active record. * * This method performs the following steps in order: @@ -1014,7 +489,7 @@ class ActiveRecord extends Model if ($lock !== null && !$result) { throw new StaleObjectException('The object being deleted is outdated.'); } - $this->_oldAttributes = null; + $this->setOldAttributes(null); $this->afterDelete(); } if ($transaction !== null) { @@ -1034,149 +509,6 @@ class ActiveRecord extends Model } /** - * Returns a value indicating whether the current record is new. - * @return boolean whether the record is new and should be inserted when calling [[save()]]. - */ - public function getIsNewRecord() - { - return $this->_oldAttributes === null; - } - - /** - * Sets the value indicating whether the record is new. - * @param boolean $value whether the record is new and should be inserted when calling [[save()]]. - * @see getIsNewRecord() - */ - public function setIsNewRecord($value) - { - $this->_oldAttributes = $value ? null : $this->_attributes; - } - - /** - * Initializes the object. - * This method is called at the end of the constructor. - * The default implementation will trigger an [[EVENT_INIT]] event. - * If you override this method, make sure you call the parent implementation at the end - * to ensure triggering of the event. - */ - public function init() - { - parent::init(); - $this->trigger(self::EVENT_INIT); - } - - /** - * This method is called when the AR object is created and populated with the query result. - * The default implementation will trigger an [[EVENT_AFTER_FIND]] event. - * When overriding this method, make sure you call the parent implementation to ensure the - * event is triggered. - */ - public function afterFind() - { - $this->trigger(self::EVENT_AFTER_FIND); - } - - /** - * This method is called at the beginning of inserting or updating a record. - * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is true, - * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is false. - * When overriding this method, make sure you call the parent implementation like the following: - * - * ~~~ - * public function beforeSave($insert) - * { - * if (parent::beforeSave($insert)) { - * // ...custom code here... - * return true; - * } else { - * return false; - * } - * } - * ~~~ - * - * @param boolean $insert whether this method called while inserting a record. - * If false, it means the method is called while updating a record. - * @return boolean whether the insertion or updating should continue. - * If false, the insertion or updating will be cancelled. - */ - public function beforeSave($insert) - { - $event = new ModelEvent; - $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event); - return $event->isValid; - } - - /** - * This method is called at the end of inserting or updating a record. - * The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is true, - * or an [[EVENT_AFTER_UPDATE]] event if `$insert` is false. - * When overriding this method, make sure you call the parent implementation so that - * the event is triggered. - * @param boolean $insert whether this method called while inserting a record. - * If false, it means the method is called while updating a record. - */ - public function afterSave($insert) - { - $this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE); - } - - /** - * This method is invoked before deleting a record. - * The default implementation raises the [[EVENT_BEFORE_DELETE]] event. - * When overriding this method, make sure you call the parent implementation like the following: - * - * ~~~ - * public function beforeDelete() - * { - * if (parent::beforeDelete()) { - * // ...custom code here... - * return true; - * } else { - * return false; - * } - * } - * ~~~ - * - * @return boolean whether the record should be deleted. Defaults to true. - */ - public function beforeDelete() - { - $event = new ModelEvent; - $this->trigger(self::EVENT_BEFORE_DELETE, $event); - return $event->isValid; - } - - /** - * This method is invoked after deleting a record. - * The default implementation raises the [[EVENT_AFTER_DELETE]] event. - * You may override this method to do postprocessing after the record is deleted. - * Make sure you call the parent implementation so that the event is raised properly. - */ - public function afterDelete() - { - $this->trigger(self::EVENT_AFTER_DELETE); - } - - /** - * Repopulates this active record with the latest data. - * @return boolean whether the row still exists in the database. If true, the latest data - * will be populated to this active record. Otherwise, this record will remain unchanged. - */ - public function refresh() - { - $record = $this->find($this->getPrimaryKey(true)); - if ($record === null) { - return false; - } - foreach ($this->attributes() as $name) { - $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null; - } - $this->_oldAttributes = $this->_attributes; - $this->_related = []; - return true; - } - - /** * Returns a value indicating whether the given active record is the same as the current one. * The comparison is made by comparing the table names and the primary key values of the two active records. * If one of the records [[isNewRecord|is new]] they are also considered not equal. @@ -1192,348 +524,6 @@ class ActiveRecord extends Model } /** - * Returns the primary key value(s). - * @param boolean $asArray whether to return the primary key value as an array. If true, - * the return value will be an array with column names as keys and column values as values. - * Note that for composite primary keys, an array will always be returned regardless of this parameter value. - * @property mixed The primary key value. An array (column name => column value) is returned if - * the primary key is composite. A string is returned otherwise (null will be returned if - * the key value is null). - * @return mixed the primary key value. An array (column name => column value) is returned if the primary key - * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if - * the key value is null). - */ - public function getPrimaryKey($asArray = false) - { - $keys = $this->primaryKey(); - if (count($keys) === 1 && !$asArray) { - return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null; - } else { - $values = []; - foreach ($keys as $name) { - $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; - } - return $values; - } - } - - /** - * Returns the old primary key value(s). - * This refers to the primary key value that is populated into the record - * after executing a find method (e.g. find(), findAll()). - * The value remains unchanged even if the primary key attribute is manually assigned with a different value. - * @param boolean $asArray whether to return the primary key value as an array. If true, - * the return value will be an array with column name as key and column value as value. - * If this is false (default), a scalar value will be returned for non-composite primary key. - * @property mixed The old primary key value. An array (column name => column value) is - * returned if the primary key is composite. A string is returned otherwise (null will be - * returned if the key value is null). - * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key - * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if - * the key value is null). - */ - public function getOldPrimaryKey($asArray = false) - { - $keys = $this->primaryKey(); - if (count($keys) === 1 && !$asArray) { - return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null; - } else { - $values = []; - foreach ($keys as $name) { - $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; - } - return $values; - } - } - - /** - * Creates an active record object using a row of data. - * This method is called by [[ActiveQuery]] to populate the query results - * into Active Records. It is not meant to be used to create new records. - * @param array $row attribute values (name => value) - * @return ActiveRecord the newly created active record. - */ - public static function create($row) - { - $record = static::instantiate($row); - $columns = array_flip($record->attributes()); - foreach ($row as $name => $value) { - if (isset($columns[$name])) { - $record->_attributes[$name] = $value; - } else { - $record->$name = $value; - } - } - $record->_oldAttributes = $record->_attributes; - $record->afterFind(); - return $record; - } - - /** - * Creates an active record instance. - * This method is called by [[create()]]. - * You may override this method if the instance being created - * depends on the row data to be populated into the record. - * For example, by creating a record based on the value of a column, - * you may implement the so-called single-table inheritance mapping. - * @param array $row row data to be populated into the record. - * @return ActiveRecord the newly created active record - */ - public static function instantiate($row) - { - return new static; - } - - /** - * Returns whether there is an element at the specified offset. - * This method is required by the interface ArrayAccess. - * @param mixed $offset the offset to check on - * @return boolean whether there is an element at the specified offset. - */ - public function offsetExists($offset) - { - return $this->__isset($offset); - } - - /** - * Returns the relation object with the specified name. - * A relation is defined by a getter method which returns an [[ActiveRelation]] object. - * It can be declared in either the Active Record class itself or one of its behaviors. - * @param string $name the relation name - * @return ActiveRelation the relation object - * @throws InvalidParamException if the named relation does not exist. - */ - public function getRelation($name) - { - $getter = 'get' . $name; - try { - $relation = $this->$getter(); - if ($relation instanceof ActiveRelationInterface) { - return $relation; - } else { - throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); - } - } catch (UnknownMethodException $e) { - throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e); - } - } - - /** - * Establishes the relationship between two models. - * - * The relationship is established by setting the foreign key value(s) in one model - * to be the corresponding primary key value(s) in the other model. - * The model with the foreign key will be saved into database without performing validation. - * - * If the relationship involves a pivot table, a new row will be inserted into the - * pivot table which contains the primary key values from both models. - * - * Note that this method requires that the primary key value is not null. - * - * @param string $name the case sensitive name of the relationship - * @param ActiveRecord $model the model to be linked with the current one. - * @param array $extraColumns additional column values to be saved into the pivot table. - * This parameter is only meaningful for a relationship involving a pivot table - * (i.e., a relation set with `[[ActiveRelation::via()]]` or `[[ActiveRelation::viaTable()]]`.) - * @throws InvalidCallException if the method is unable to link two models. - */ - public function link($name, $model, $extraColumns = []) - { - $relation = $this->getRelation($name); - - if ($relation->via !== null) { - if ($this->getIsNewRecord() || $model->getIsNewRecord()) { - throw new InvalidCallException('Unable to link models: both models must NOT be newly created.'); - } - if (is_array($relation->via)) { - /** @var ActiveRelation $viaRelation */ - list($viaName, $viaRelation) = $relation->via; - $viaClass = $viaRelation->modelClass; - // unset $viaName so that it can be reloaded to reflect the change - unset($this->_related[$viaName]); - } else { - $viaRelation = $relation->via; - $viaTable = reset($relation->via->from); - } - $columns = []; - foreach ($viaRelation->link as $a => $b) { - $columns[$a] = $this->$b; - } - foreach ($relation->link as $a => $b) { - $columns[$b] = $model->$a; - } - foreach ($extraColumns as $k => $v) { - $columns[$k] = $v; - } - if (is_array($relation->via)) { - /** @var $viaClass ActiveRecord */ - /** @var $record ActiveRecord */ - $record = new $viaClass(); - foreach($columns as $column => $value) { - $record->$column = $value; - } - $record->insert(false); - } else { - /** @var $viaTable string */ - static::getDb()->createCommand() - ->insert($viaTable, $columns)->execute(); - } - } else { - $p1 = $model->isPrimaryKey(array_keys($relation->link)); - $p2 = $this->isPrimaryKey(array_values($relation->link)); - if ($p1 && $p2) { - if ($this->getIsNewRecord() && $model->getIsNewRecord()) { - throw new InvalidCallException('Unable to link models: both models are newly created.'); - } elseif ($this->getIsNewRecord()) { - $this->bindModels(array_flip($relation->link), $this, $model); - } else { - $this->bindModels($relation->link, $model, $this); - } - } elseif ($p1) { - $this->bindModels(array_flip($relation->link), $this, $model); - } elseif ($p2) { - $this->bindModels($relation->link, $model, $this); - } else { - throw new InvalidCallException('Unable to link models: the link does not involve any primary key.'); - } - } - - // update lazily loaded related objects - if (!$relation->multiple) { - $this->_related[$name] = $model; - } elseif (isset($this->_related[$name])) { - if ($relation->indexBy !== null) { - $indexBy = $relation->indexBy; - $this->_related[$name][$model->$indexBy] = $model; - } else { - $this->_related[$name][] = $model; - } - } - } - - /** - * Destroys the relationship between two models. - * - * The model with the foreign key of the relationship will be deleted if `$delete` is true. - * Otherwise, the foreign key will be set null and the model will be saved without validation. - * - * @param string $name the case sensitive name of the relationship. - * @param ActiveRecord $model the model to be unlinked from the current one. - * @param boolean $delete whether to delete the model that contains the foreign key. - * If false, the model's foreign key will be set null and saved. - * If true, the model containing the foreign key will be deleted. - * @throws InvalidCallException if the models cannot be unlinked - */ - public function unlink($name, $model, $delete = false) - { - $relation = $this->getRelation($name); - - if ($relation->via !== null) { - if (is_array($relation->via)) { - /** @var ActiveRelation $viaRelation */ - list($viaName, $viaRelation) = $relation->via; - $viaClass = $viaRelation->modelClass; - unset($this->_related[$viaName]); - } else { - $viaRelation = $relation->via; - $viaTable = reset($relation->via->from); - } - $columns = []; - foreach ($viaRelation->link as $a => $b) { - $columns[$a] = $this->$b; - } - foreach ($relation->link as $a => $b) { - $columns[$b] = $model->$a; - } - if (is_array($relation->via)) { - /** @var $viaClass ActiveRecord */ - if ($delete) { - $viaClass::deleteAll($columns); - } else { - $nulls = []; - foreach (array_keys($columns) as $a) { - $nulls[$a] = null; - } - $viaClass::updateAll($nulls, $columns); - } - } else { - /** @var $viaTable string */ - $command = static::getDb()->createCommand(); - if ($delete) { - $command->delete($viaTable, $columns)->execute(); - } else { - $nulls = []; - foreach (array_keys($columns) as $a) { - $nulls[$a] = null; - } - $command->update($viaTable, $nulls, $columns)->execute(); - } - } - } else { - $p1 = $model->isPrimaryKey(array_keys($relation->link)); - $p2 = $this->isPrimaryKey(array_values($relation->link)); - if ($p1 && $p2 || $p2) { - foreach ($relation->link as $a => $b) { - $model->$a = null; - } - $delete ? $model->delete() : $model->save(false); - } elseif ($p1) { - foreach ($relation->link as $b) { - $this->$b = null; - } - $delete ? $this->delete() : $this->save(false); - } else { - throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.'); - } - } - - if (!$relation->multiple) { - unset($this->_related[$name]); - } elseif (isset($this->_related[$name])) { - /** @var ActiveRecord $b */ - foreach ($this->_related[$name] as $a => $b) { - if ($model->getPrimaryKey() == $b->getPrimaryKey()) { - unset($this->_related[$name][$a]); - } - } - } - } - - /** - * @param array $link - * @param ActiveRecord $foreignModel - * @param ActiveRecord $primaryModel - * @throws InvalidCallException - */ - private function bindModels($link, $foreignModel, $primaryModel) - { - foreach ($link as $fk => $pk) { - $value = $primaryModel->$pk; - if ($value === null) { - throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.'); - } - $foreignModel->$fk = $value; - } - $foreignModel->save(false); - } - - /** - * Returns a value indicating whether the given set of attributes represents the primary key for this model - * @param array $keys the set of attributes to check - * @return boolean whether the given set of attributes represents the primary key for this model - */ - public static function isPrimaryKey($keys) - { - $pks = static::primaryKey(); - foreach ($keys as $key) { - if (!in_array($key, $pks, true)) { - return false; - } - } - return count($keys) === count($pks); - } - - /** * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. * @return boolean whether the specified operation is transactional in the current [[scenario]]. diff --git a/framework/yii/db/ActiveRecordInterface.php b/framework/yii/db/ActiveRecordInterface.php new file mode 100644 index 0000000..4965a26 --- /dev/null +++ b/framework/yii/db/ActiveRecordInterface.php @@ -0,0 +1,279 @@ + + */ + +namespace yii\db; + +/** + * ActiveRecordInterface + * + * @author Qiang Xue + * @author Carsten Brandt + * @since 2.0 + */ +interface ActiveRecordInterface +{ + /** + * Returns the primary key **name(s)** for this AR class. + * + * Note that an array should be returned even when the record only has a single primary key. + * + * For the primary key **value** see [[getPrimaryKey()]] instead. + * + * @return string[] the primary key name(s) for this AR class. + */ + public static function primaryKey(); + + /** + * Returns the list of all attribute names of the record. + * @return array list of attribute names. + */ + public function attributes(); + + /** + * Returns the named attribute value. + * If this record is the result of a query and the attribute is not loaded, + * null will be returned. + * @param string $name the attribute name + * @return mixed the attribute value. Null if the attribute is not set or does not exist. + * @see hasAttribute() + */ + public function getAttribute($name); + + /** + * Sets the named attribute value. + * @param string $name the attribute name. + * @param mixed $value the attribute value. + * @see hasAttribute() + */ + public function setAttribute($name, $value); + + /** + * Returns a value indicating whether the record has an attribute with the specified name. + * @param string $name the name of the attribute + * @return boolean whether the record has an attribute with the specified name. + */ + public function hasAttribute($name); + + /** + * Returns the primary key value(s). + * @param boolean $asArray whether to return the primary key value as an array. If true, + * the return value will be an array with attribute names as keys and attribute values as values. + * Note that for composite primary keys, an array will always be returned regardless of this parameter value. + * @return mixed the primary key value. An array (attribute name => attribute value) is returned if the primary key + * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if + * the key value is null). + */ + public function getPrimaryKey($asArray = false); + + /** + * Creates an [[ActiveQueryInterface|ActiveQuery]] instance for query purpose. + * + * This method is usually ment to be used like this: + * + * ```php + * Customer::find(1); // find one customer by primary key + * Customer::find()->all(); // find all customers + * ``` + * + * @param mixed $q the query parameter. This can be one of the followings: + * + * - a scalar value (integer or string): query by a single primary key value and return the + * corresponding record. + * - an array of name-value pairs: query by a set of attribute values and return a single record matching all of them. + * - null (not specified): return a new [[ActiveQuery]] object for further query purpose. + * + * @return ActiveQueryInterface|static|null When `$q` is null, a new [[ActiveQuery]] instance + * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be + * returned (null will be returned if there is no matching). + */ + public static function find($q = null); + + /** + * Creates an [[ActiveQueryInterface|ActiveQuery]] instance. + * This method is called by [[find()]] to start a SELECT query. + * You may override this method to return a customized query (e.g. `CustomerQuery` specified + * written for querying `Customer` purpose.) + * @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance. + */ + public static function createQuery(); + + /** + * Updates records using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * Customer::updateAll(['status' => 1], ['status' => '2']); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved for the record. + * Unlike [[update()]] these are not going to be validated. + * @param array $condition the condition that matches the records that should get updated. + * Please refer to [[QueryInterface::where()]] on how to specify this parameter. + * An empty condition will match all records. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = null); + + /** + * Deletes records using the provided conditions. + * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. + * + * For example, to delete all customers whose status is 3: + * + * ~~~ + * Customer::deleteAll([status = 3]); + * ~~~ + * + * @param array $condition the condition that matches the records that should get deleted. + * Please refer to [[QueryInterface::where()]] on how to specify this parameter. + * An empty condition will match all records. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = null); + + /** + * Saves the current record. + * + * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]] + * when [[isNewRecord]] is false. + * + * For example, to save a customer record: + * + * ~~~ + * $customer = new Customer; // or $customer = Customer::find($id); + * $customer->name = $name; + * $customer->email = $email; + * $customer->save(); + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be saved to database. `false` will be returned + * in this case. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the saving succeeds + */ + public function save($runValidation = true, $attributes = null); + + /** + * Inserts the record into the database using the attribute values of this record. + * + * Usage example: + * + * ```php + * $customer = new Customer; + * $customer->name = $name; + * $customer->email = $email; + * $customer->insert(); + * ``` + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + */ + public function insert($runValidation = true, $attributes = null); + + /** + * Saves the changes to this active record into the database. + * + * Usage example: + * + * ```php + * $customer = Customer::find($id); + * $customer->name = $name; + * $customer->email = $email; + * $customer->update(); + * ``` + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return integer|boolean the number of rows affected, or false if validation fails + * or updating process is stopped for other reasons. + * Note that it is possible that the number of rows affected is 0, even though the + * update execution is successful. + */ + public function update($runValidation = true, $attributes = null); + + /** + * Deletes the record from the database. + * + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible that the number of rows deleted is 0, even though the deletion execution is successful. + */ + public function delete(); + + /** + * Returns a value indicating whether the current record is new (not saved in the database). + * @return boolean whether the record is new and should be inserted when calling [[save()]]. + */ + public function getIsNewRecord(); + + /** + * Returns a value indicating whether the given active record is the same as the current one. + * Two [[isNewRecord|new]] records are considered to be not equal. + * @param static $record record to compare to + * @return boolean whether the two active records refer to the same row in the same database table. + */ + public function equals($record); + + /** + * Creates an [[ActiveRelationInterface|ActiveRelation]] instance. + * This method is called by [[BaseActiveRecord::hasOne()]] and [[BaseActiveRecord::hasMany()]] to + * create a relation instance. + * You may override this method to return a customized relation. + * @param array $config the configuration passed to the ActiveRelation class. + * @return ActiveRelation the newly created [[ActiveRelation]] instance. + */ + public static function createActiveRelation($config = []); + + /** + * Returns the relation object with the specified name. + * A relation is defined by a getter method which returns an [[ActiveRelationInterface|ActiveRelation]] object. + * It can be declared in either the ActiveRecord class itself or one of its behaviors. + * @param string $name the relation name + * @return ActiveRelation the relation object + */ + public function getRelation($name); + + /** + * Establishes the relationship between two records. + * + * The relationship is established by setting the foreign key value(s) in one record + * to be the corresponding primary key value(s) in the other record. + * The record with the foreign key will be saved into database without performing validation. + * + * If the relationship involves a pivot table, a new row will be inserted into the + * pivot table which contains the primary key values from both records. + * + * This method requires that the primary key value is not null. + * + * @param string $name the case sensitive name of the relationship. + * @param static $model the record to be linked with the current one. + * @param array $extraColumns additional column values to be saved into the pivot table. + * This parameter is only meaningful for a relationship involving a pivot table + * (i.e., a relation set with `[[ActiveRelationInterface::via()]]`.) + */ + public function link($name, $model, $extraColumns = []); + + /** + * Destroys the relationship between two records. + * + * The record with the foreign key of the relationship will be deleted if `$delete` is true. + * Otherwise, the foreign key will be set null and the record will be saved without validation. + * + * @param string $name the case sensitive name of the relationship. + * @param static $model the model to be unlinked from the current one. + * @param boolean $delete whether to delete the model that contains the foreign key. + * If false, the model's foreign key will be set null and saved. + * If true, the model containing the foreign key will be deleted. + */ + public function unlink($name, $model, $delete = false); +} \ No newline at end of file diff --git a/framework/yii/db/ActiveRelationTrait.php b/framework/yii/db/ActiveRelationTrait.php index 2960196..832bb62 100644 --- a/framework/yii/db/ActiveRelationTrait.php +++ b/framework/yii/db/ActiveRelationTrait.php @@ -104,7 +104,7 @@ trait ActiveRelationTrait if (count($primaryModels) === 1 && !$this->multiple) { $model = $this->one(); foreach ($primaryModels as $i => $primaryModel) { - if ($primaryModel instanceof ActiveRecord) { + if ($primaryModel instanceof ActiveRecordInterface) { $primaryModel->populateRelation($name, $model); } else { $primaryModels[$i][$name] = $model; @@ -123,7 +123,7 @@ trait ActiveRelationTrait foreach ($primaryModels as $i => $primaryModel) { $key = $this->getModelKey($primaryModel, $link); $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null); - if ($primaryModel instanceof ActiveRecord) { + if ($primaryModel instanceof ActiveRecordInterface) { $primaryModel->populateRelation($name, $value); } else { $primaryModels[$i][$name] = $value; diff --git a/framework/yii/db/BaseActiveRecord.php b/framework/yii/db/BaseActiveRecord.php new file mode 100644 index 0000000..00360c4 --- /dev/null +++ b/framework/yii/db/BaseActiveRecord.php @@ -0,0 +1,1229 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\db; + +use yii\base\InvalidConfigException; +use yii\base\Model; +use yii\base\InvalidParamException; +use yii\base\ModelEvent; +use yii\base\NotSupportedException; +use yii\base\UnknownMethodException; +use yii\base\InvalidCallException; +use yii\helpers\StringHelper; +use yii\helpers\Inflector; + +/** + * ActiveRecord is the base class for classes representing relational data in terms of objects. + * + * @include @yii/db/ActiveRecord.md + * + * @property array $dirtyAttributes The changed attribute values (name-value pairs). This property is + * read-only. + * @property boolean $isNewRecord Whether the record is new and should be inserted when calling [[save()]]. + * @property array $oldAttributes The old attribute values (name-value pairs). + * @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is + * returned if the primary key is composite. A string is returned otherwise (null will be returned if the key + * value is null). This property is read-only. + * @property array $populatedRelations An array of relation data indexed by relation names. This property is + * read-only. + * @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if + * the primary key is composite. A string is returned otherwise (null will be returned if the key value is null). + * This property is read-only. + * + * @author Qiang Xue + * @author Carsten Brandt + * @since 2.0 + */ +abstract class BaseActiveRecord extends Model implements ActiveRecordInterface +{ + /** + * @event Event an event that is triggered when the record is initialized via [[init()]]. + */ + const EVENT_INIT = 'init'; + /** + * @event Event an event that is triggered after the record is created and populated with query result. + */ + const EVENT_AFTER_FIND = 'afterFind'; + /** + * @event ModelEvent an event that is triggered before inserting a record. + * You may set [[ModelEvent::isValid]] to be false to stop the insertion. + */ + const EVENT_BEFORE_INSERT = 'beforeInsert'; + /** + * @event Event an event that is triggered after a record is inserted. + */ + const EVENT_AFTER_INSERT = 'afterInsert'; + /** + * @event ModelEvent an event that is triggered before updating a record. + * You may set [[ModelEvent::isValid]] to be false to stop the update. + */ + const EVENT_BEFORE_UPDATE = 'beforeUpdate'; + /** + * @event Event an event that is triggered after a record is updated. + */ + const EVENT_AFTER_UPDATE = 'afterUpdate'; + /** + * @event ModelEvent an event that is triggered before deleting a record. + * You may set [[ModelEvent::isValid]] to be false to stop the deletion. + */ + const EVENT_BEFORE_DELETE = 'beforeDelete'; + /** + * @event Event an event that is triggered after a record is deleted. + */ + const EVENT_AFTER_DELETE = 'afterDelete'; + + /** + * @var array attribute values indexed by attribute names + */ + private $_attributes = []; + /** + * @var array old attribute values indexed by attribute names. + */ + private $_oldAttributes; + /** + * @var array related models indexed by the relation names + */ + private $_related = []; + + + /** + * Creates an [[ActiveQuery]] instance for query purpose. + * + * @include @yii/db/ActiveRecord-find.md + * + * @param mixed $q the query parameter. This can be one of the followings: + * + * - a scalar value (integer or string): query by a single primary key value and return the + * corresponding record. + * - an array of name-value pairs: query by a set of column values and return a single record matching all of them. + * - null: return a new [[ActiveQuery]] object for further query purpose. + * + * @return ActiveQuery|ActiveRecord|null When `$q` is null, a new [[ActiveQuery]] instance + * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be + * returned (null will be returned if there is no matching). + * @throws InvalidConfigException if the AR class does not have a primary key + * @see createQuery() + */ + public static function find($q = null) + { + $query = static::createQuery(); + if (is_array($q)) { + return $query->where($q)->one(); + } elseif ($q !== null) { + // query by primary key + $primaryKey = static::primaryKey(); + if (isset($primaryKey[0])) { + return $query->where([$primaryKey[0] => $q])->one(); + } else { + throw new InvalidConfigException(get_called_class() . ' must have a primary key.'); + } + } + return $query; + } + + /** + * Updates the whole table using the provided attribute values and conditions. + * For example, to change the status to be 1 for all customers whose status is 2: + * + * ~~~ + * Customer::updateAll(['status' => 1], 'status = 2'); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved into the table + * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = '') + { + throw new NotSupportedException(__METHOD__ . ' is not supported.'); + } + + /** + * Updates the whole table using the provided counter changes and conditions. + * For example, to increment all customers' age by 1, + * + * ~~~ + * Customer::updateAllCounters(['age' => 1]); + * ~~~ + * + * @param array $counters the counters to be updated (attribute name => increment value). + * Use negative values if you want to decrement the counters. + * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method. + * @return integer the number of rows updated + */ + public static function updateAllCounters($counters, $condition = '') + { + throw new NotSupportedException(__METHOD__ . ' is not supported.'); + } + + /** + * Deletes rows in the table using the provided conditions. + * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. + * + * For example, to delete all customers whose status is 3: + * + * ~~~ + * Customer::deleteAll('status = 3'); + * ~~~ + * + * @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. + * Please refer to [[Query::where()]] on how to specify this parameter. + * @param array $params the parameters (name => value) to be bound to the query. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = '', $params = []) + { + throw new NotSupportedException(__METHOD__ . ' is not supported.'); + } + + /** + * Returns the name of the column that stores the lock version for implementing optimistic locking. + * + * Optimistic locking allows multiple users to access the same record for edits and avoids + * potential conflicts. In case when a user attempts to save the record upon some staled data + * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown, + * and the update or deletion is skipped. + * + * Optimistic locking is only supported by [[update()]] and [[delete()]]. + * + * To use Optimistic locking: + * + * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`. + * Override this method to return the name of this column. + * 2. In the Web form that collects the user input, add a hidden field that stores + * the lock version of the recording being updated. + * 3. In the controller action that does the data updating, try to catch the [[StaleObjectException]] + * and implement necessary business logic (e.g. merging the changes, prompting stated data) + * to resolve the conflict. + * + * @return string the column name that stores the lock version of a table row. + * If null is returned (default implemented), optimistic locking will not be supported. + */ + public function optimisticLock() + { + return null; + } + + /** + * PHP getter magic method. + * This method is overridden so that attributes and related objects can be accessed like properties. + * @param string $name property name + * @return mixed property value + * @see getAttribute() + */ + public function __get($name) + { + if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) { + return $this->_attributes[$name]; + } elseif ($this->hasAttribute($name)) { + return null; + } else { + if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) { + return $this->_related[$name]; + } + $value = parent::__get($name); + if ($value instanceof ActiveRelationInterface) { + return $this->_related[$name] = $value->multiple ? $value->all() : $value->one(); + } else { + return $value; + } + } + } + + /** + * PHP setter magic method. + * This method is overridden so that AR attributes can be accessed like properties. + * @param string $name property name + * @param mixed $value property value + */ + public function __set($name, $value) + { + if ($this->hasAttribute($name)) { + $this->_attributes[$name] = $value; + } else { + parent::__set($name, $value); + } + } + + /** + * Checks if a property value is null. + * This method overrides the parent implementation by checking if the named attribute is null or not. + * @param string $name the property name or the event name + * @return boolean whether the property value is null + */ + public function __isset($name) + { + try { + return $this->__get($name) !== null; + } catch (\Exception $e) { + return false; + } + } + + /** + * Sets a component property to be null. + * This method overrides the parent implementation by clearing + * the specified attribute value. + * @param string $name the property name or the event name + */ + public function __unset($name) + { + if ($this->hasAttribute($name)) { + unset($this->_attributes[$name]); + } else { + if (isset($this->_related[$name])) { + unset($this->_related[$name]); + } else { + parent::__unset($name); + } + } + } + + /** + * Declares a `has-one` relation. + * The declaration is returned in terms of an [[ActiveRelation]] instance + * through which the related record can be queried and retrieved back. + * + * A `has-one` relation means that there is at most one related record matching + * the criteria set by this relation, e.g., a customer has one country. + * + * For example, to declare the `country` relation for `Customer` class, we can write + * the following code in the `Customer` class: + * + * ~~~ + * public function getCountry() + * { + * return $this->hasOne(Country::className(), ['id' => 'country_id']); + * } + * ~~~ + * + * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name + * in the related class `Country`, while the 'country_id' value refers to an attribute name + * in the current AR class. + * + * Call methods declared in [[ActiveRelation]] to further customize the relation. + * + * @param string $class the class name of the related record + * @param array $link the primary-foreign key constraint. The keys of the array refer to + * the attributes of the record associated with the `$class` model, while the values of the + * array refer to the corresponding attributes in **this** AR class. + * @return ActiveRelationInterface the relation object. + */ + public function hasOne($class, $link) + { + /** @var ActiveRecord $class */ + return $class::createActiveRelation([ + 'modelClass' => $class, + 'primaryModel' => $this, + 'link' => $link, + 'multiple' => false, + ]); + } + + /** + * Declares a `has-many` relation. + * The declaration is returned in terms of an [[ActiveRelation]] instance + * through which the related record can be queried and retrieved back. + * + * A `has-many` relation means that there are multiple related records matching + * the criteria set by this relation, e.g., a customer has many orders. + * + * For example, to declare the `orders` relation for `Customer` class, we can write + * the following code in the `Customer` class: + * + * ~~~ + * public function getOrders() + * { + * return $this->hasMany(Order::className(), ['customer_id' => 'id']); + * } + * ~~~ + * + * Note that in the above, the 'customer_id' key in the `$link` parameter refers to + * an attribute name in the related class `Order`, while the 'id' value refers to + * an attribute name in the current AR class. + * + * @param string $class the class name of the related record + * @param array $link the primary-foreign key constraint. The keys of the array refer to + * the attributes of the record associated with the `$class` model, while the values of the + * array refer to the corresponding attributes in **this** AR class. + * @return ActiveRelationInterface the relation object. + */ + public function hasMany($class, $link) + { + /** @var ActiveRecord $class */ + return $class::createActiveRelation([ + 'modelClass' => $class, + 'primaryModel' => $this, + 'link' => $link, + 'multiple' => true, + ]); + } + + /** + * Populates the named relation with the related records. + * Note that this method does not check if the relation exists or not. + * @param string $name the relation name (case-sensitive) + * @param ActiveRecord|array|null the related records to be populated into the relation. + */ + public function populateRelation($name, $records) + { + $this->_related[$name] = $records; + } + + /** + * Check whether the named relation has been populated with records. + * @param string $name the relation name (case-sensitive) + * @return bool whether relation has been populated with records. + */ + public function isRelationPopulated($name) + { + return array_key_exists($name, $this->_related); + } + + /** + * Returns all populated relations. + * @return array an array of relation data indexed by relation names. + */ + public function getPopulatedRelations() + { + return $this->_related; + } + + /** + * Returns a value indicating whether the model has an attribute with the specified name. + * @param string $name the name of the attribute + * @return boolean whether the model has an attribute with the specified name. + */ + public function hasAttribute($name) + { + return isset($this->_attributes[$name]) || in_array($name, $this->attributes()); + } + + /** + * Returns the named attribute value. + * If this record is the result of a query and the attribute is not loaded, + * null will be returned. + * @param string $name the attribute name + * @return mixed the attribute value. Null if the attribute is not set or does not exist. + * @see hasAttribute() + */ + public function getAttribute($name) + { + return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; + } + + /** + * Sets the named attribute value. + * @param string $name the attribute name + * @param mixed $value the attribute value. + * @throws InvalidParamException if the named attribute does not exist. + * @see hasAttribute() + */ + public function setAttribute($name, $value) + { + if ($this->hasAttribute($name)) { + $this->_attributes[$name] = $value; + } else { + throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); + } + } + + /** + * Returns the old attribute values. + * @return array the old attribute values (name-value pairs) + */ + public function getOldAttributes() + { + return $this->_oldAttributes === null ? [] : $this->_oldAttributes; + } + + /** + * Sets the old attribute values. + * All existing old attribute values will be discarded. + * @param array $values old attribute values to be set. + */ + public function setOldAttributes($values) + { + $this->_oldAttributes = $values; + } + + /** + * Returns the old value of the named attribute. + * If this record is the result of a query and the attribute is not loaded, + * null will be returned. + * @param string $name the attribute name + * @return mixed the old attribute value. Null if the attribute is not loaded before + * or does not exist. + * @see hasAttribute() + */ + public function getOldAttribute($name) + { + return isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; + } + + /** + * Sets the old value of the named attribute. + * @param string $name the attribute name + * @param mixed $value the old attribute value. + * @throws InvalidParamException if the named attribute does not exist. + * @see hasAttribute() + */ + public function setOldAttribute($name, $value) + { + if (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name)) { + $this->_oldAttributes[$name] = $value; + } else { + throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); + } + } + + /** + * Returns a value indicating whether the named attribute has been changed. + * @param string $name the name of the attribute + * @return boolean whether the attribute has been changed + */ + public function isAttributeChanged($name) + { + if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) { + return $this->_attributes[$name] !== $this->_oldAttributes[$name]; + } else { + return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]); + } + } + + /** + * Returns the attribute values that have been modified since they are loaded or saved most recently. + * @param string[]|null $names the names of the attributes whose values may be returned if they are + * changed recently. If null, [[attributes()]] will be used. + * @return array the changed attribute values (name-value pairs) + */ + public function getDirtyAttributes($names = null) + { + if ($names === null) { + $names = $this->attributes(); + } + $names = array_flip($names); + $attributes = []; + if ($this->_oldAttributes === null) { + foreach ($this->_attributes as $name => $value) { + if (isset($names[$name])) { + $attributes[$name] = $value; + } + } + } else { + foreach ($this->_attributes as $name => $value) { + if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) { + $attributes[$name] = $value; + } + } + } + return $attributes; + } + + /** + * Saves the current record. + * + * This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]] + * when [[isNewRecord]] is false. + * + * For example, to save a customer record: + * + * ~~~ + * $customer = new Customer; // or $customer = Customer::find($id); + * $customer->name = $name; + * $customer->email = $email; + * $customer->save(); + * ~~~ + * + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be saved to database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the saving succeeds + */ + public function save($runValidation = true, $attributes = null) + { + if ($this->getIsNewRecord()) { + return $this->insert($runValidation, $attributes); + } else { + return $this->update($runValidation, $attributes) !== false; + } + } + + /** + * Saves the changes to this active record into the associated database table. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. save the record into database. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_UPDATE]], [[EVENT_AFTER_UPDATE]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[changedAttributes|changed attribute values]] will be saved into database. + * + * For example, to update a customer record: + * + * ~~~ + * $customer = Customer::find($id); + * $customer->name = $name; + * $customer->email = $email; + * $customer->update(); + * ~~~ + * + * Note that it is possible the update does not affect any row in the table. + * In this case, this method will return 0. For this reason, you should use the following + * code to check if update() is successful or not: + * + * ~~~ + * if ($this->update() !== false) { + * // update successful + * } else { + * // update failed + * } + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return integer|boolean the number of rows affected, or false if validation fails + * or [[beforeSave()]] stops the updating process. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being updated is outdated. + * @throws \Exception in case update failed. + */ + public function update($runValidation = true, $attributes = null) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + return $this->updateInternal($attributes); + } + + /** + * @see CActiveRecord::update() + * @throws StaleObjectException + */ + protected function updateInternal($attributes = null) + { + if (!$this->beforeSave(false)) { + return false; + } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + $this->afterSave(false); + return 0; + } + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + if (!isset($values[$lock])) { + $values[$lock] = $this->$lock + 1; + } + $condition[$lock] = $this->$lock; + } + // We do not check the return value of updateAll() because it's possible + // that the UPDATE statement doesn't change anything and thus returns 0. + $rows = $this->updateAll($values, $condition); + + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } + + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $this->_attributes[$name]; + } + $this->afterSave(false); + return $rows; + } + + /** + * Updates one or several counter columns for the current AR object. + * Note that this method differs from [[updateAllCounters()]] in that it only + * saves counters for the current AR object. + * + * An example usage is as follows: + * + * ~~~ + * $post = Post::find($id); + * $post->updateCounters(['view_count' => 1]); + * ~~~ + * + * @param array $counters the counters to be updated (attribute name => increment value) + * Use negative values if you want to decrement the counters. + * @return boolean whether the saving is successful + * @see updateAllCounters() + */ + public function updateCounters($counters) + { + if ($this->updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) { + foreach ($counters as $name => $value) { + $this->_attributes[$name] += $value; + $this->_oldAttributes[$name] = $this->_attributes[$name]; + } + return true; + } else { + return false; + } + } + + /** + * Deletes the table row corresponding to this active record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeDelete()]]. If the method returns false, it will skip the + * rest of the steps; + * 2. delete the record from the database; + * 3. call [[afterDelete()]]. + * + * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] + * will be raised by the corresponding methods. + * + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being deleted is outdated. + * @throws \Exception in case delete failed. + */ + public function delete() + { + $result = false; + if ($this->beforeDelete()) { + // we do not check the return value of deleteAll() because it's possible + // the record is already deleted in the database and thus the method will return 0 + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + $condition[$lock] = $this->$lock; + } + $result = $this->deleteAll($condition); + if ($lock !== null && !$result) { + throw new StaleObjectException('The object being deleted is outdated.'); + } + $this->_oldAttributes = null; + $this->afterDelete(); + } + return $result; + } + + /** + * Returns a value indicating whether the current record is new. + * @return boolean whether the record is new and should be inserted when calling [[save()]]. + */ + public function getIsNewRecord() + { + return $this->_oldAttributes === null; + } + + /** + * Sets the value indicating whether the record is new. + * @param boolean $value whether the record is new and should be inserted when calling [[save()]]. + * @see getIsNewRecord() + */ + public function setIsNewRecord($value) + { + $this->_oldAttributes = $value ? null : $this->_attributes; + } + + /** + * Initializes the object. + * This method is called at the end of the constructor. + * The default implementation will trigger an [[EVENT_INIT]] event. + * If you override this method, make sure you call the parent implementation at the end + * to ensure triggering of the event. + */ + public function init() + { + parent::init(); + $this->trigger(self::EVENT_INIT); + } + + /** + * This method is called when the AR object is created and populated with the query result. + * The default implementation will trigger an [[EVENT_AFTER_FIND]] event. + * When overriding this method, make sure you call the parent implementation to ensure the + * event is triggered. + */ + public function afterFind() + { + $this->trigger(self::EVENT_AFTER_FIND); + } + + /** + * This method is called at the beginning of inserting or updating a record. + * The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is true, + * or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is false. + * When overriding this method, make sure you call the parent implementation like the following: + * + * ~~~ + * public function beforeSave($insert) + * { + * if (parent::beforeSave($insert)) { + * // ...custom code here... + * return true; + * } else { + * return false; + * } + * } + * ~~~ + * + * @param boolean $insert whether this method called while inserting a record. + * If false, it means the method is called while updating a record. + * @return boolean whether the insertion or updating should continue. + * If false, the insertion or updating will be cancelled. + */ + public function beforeSave($insert) + { + $event = new ModelEvent; + $this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event); + return $event->isValid; + } + + /** + * This method is called at the end of inserting or updating a record. + * The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is true, + * or an [[EVENT_AFTER_UPDATE]] event if `$insert` is false. + * When overriding this method, make sure you call the parent implementation so that + * the event is triggered. + * @param boolean $insert whether this method called while inserting a record. + * If false, it means the method is called while updating a record. + */ + public function afterSave($insert) + { + $this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE); + } + + /** + * This method is invoked before deleting a record. + * The default implementation raises the [[EVENT_BEFORE_DELETE]] event. + * When overriding this method, make sure you call the parent implementation like the following: + * + * ~~~ + * public function beforeDelete() + * { + * if (parent::beforeDelete()) { + * // ...custom code here... + * return true; + * } else { + * return false; + * } + * } + * ~~~ + * + * @return boolean whether the record should be deleted. Defaults to true. + */ + public function beforeDelete() + { + $event = new ModelEvent; + $this->trigger(self::EVENT_BEFORE_DELETE, $event); + return $event->isValid; + } + + /** + * This method is invoked after deleting a record. + * The default implementation raises the [[EVENT_AFTER_DELETE]] event. + * You may override this method to do postprocessing after the record is deleted. + * Make sure you call the parent implementation so that the event is raised properly. + */ + public function afterDelete() + { + $this->trigger(self::EVENT_AFTER_DELETE); + } + + /** + * Repopulates this active record with the latest data. + * @return boolean whether the row still exists in the database. If true, the latest data + * will be populated to this active record. Otherwise, this record will remain unchanged. + */ + public function refresh() + { + $record = $this->find($this->getPrimaryKey(true)); + if ($record === null) { + return false; + } + foreach ($this->attributes() as $name) { + $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null; + } + $this->_oldAttributes = $this->_attributes; + $this->_related = []; + return true; + } + + /** + * Returns a value indicating whether the given active record is the same as the current one. + * The comparison is made by comparing the table names and the primary key values of the two active records. + * If one of the records [[isNewRecord|is new]] they are also considered not equal. + * @param ActiveRecord $record record to compare to + * @return boolean whether the two active records refer to the same row in the same database table. + */ + public function equals($record) + { + if ($this->isNewRecord || $record->isNewRecord) { + return false; + } + return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey(); + } + + /** + * Returns the primary key value(s). + * @param boolean $asArray whether to return the primary key value as an array. If true, + * the return value will be an array with column names as keys and column values as values. + * Note that for composite primary keys, an array will always be returned regardless of this parameter value. + * @property mixed The primary key value. An array (column name => column value) is returned if + * the primary key is composite. A string is returned otherwise (null will be returned if + * the key value is null). + * @return mixed the primary key value. An array (column name => column value) is returned if the primary key + * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if + * the key value is null). + */ + public function getPrimaryKey($asArray = false) + { + $keys = $this->primaryKey(); + if (count($keys) === 1 && !$asArray) { + return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null; + } else { + $values = []; + foreach ($keys as $name) { + $values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; + } + return $values; + } + } + + /** + * Returns the old primary key value(s). + * This refers to the primary key value that is populated into the record + * after executing a find method (e.g. find(), findAll()). + * The value remains unchanged even if the primary key attribute is manually assigned with a different value. + * @param boolean $asArray whether to return the primary key value as an array. If true, + * the return value will be an array with column name as key and column value as value. + * If this is false (default), a scalar value will be returned for non-composite primary key. + * @property mixed The old primary key value. An array (column name => column value) is + * returned if the primary key is composite. A string is returned otherwise (null will be + * returned if the key value is null). + * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key + * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if + * the key value is null). + */ + public function getOldPrimaryKey($asArray = false) + { + $keys = $this->primaryKey(); + if (count($keys) === 1 && !$asArray) { + return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null; + } else { + $values = []; + foreach ($keys as $name) { + $values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; + } + return $values; + } + } + + /** + * Creates an active record object using a row of data. + * This method is called by [[ActiveQuery]] to populate the query results + * into Active Records. It is not meant to be used to create new records. + * @param array $row attribute values (name => value) + * @return ActiveRecord the newly created active record. + */ + public static function create($row) + { + $record = static::instantiate($row); + $columns = array_flip($record->attributes()); + foreach ($row as $name => $value) { + if (isset($columns[$name])) { + $record->_attributes[$name] = $value; + } else { + $record->$name = $value; + } + } + $record->_oldAttributes = $record->_attributes; + $record->afterFind(); + return $record; + } + + /** + * Creates an active record instance. + * This method is called by [[create()]]. + * You may override this method if the instance being created + * depends on the row data to be populated into the record. + * For example, by creating a record based on the value of a column, + * you may implement the so-called single-table inheritance mapping. + * @param array $row row data to be populated into the record. + * @return ActiveRecord the newly created active record + */ + public static function instantiate($row) + { + return new static; + } + + /** + * Returns whether there is an element at the specified offset. + * This method is required by the interface ArrayAccess. + * @param mixed $offset the offset to check on + * @return boolean whether there is an element at the specified offset. + */ + public function offsetExists($offset) + { + return $this->__isset($offset); + } + + /** + * Returns the relation object with the specified name. + * A relation is defined by a getter method which returns an [[ActiveRelation]] object. + * It can be declared in either the Active Record class itself or one of its behaviors. + * @param string $name the relation name + * @return ActiveRelation the relation object + * @throws InvalidParamException if the named relation does not exist. + */ + public function getRelation($name) + { + $getter = 'get' . $name; + try { + $relation = $this->$getter(); + if ($relation instanceof ActiveRelationInterface) { + return $relation; + } else { + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); + } + } catch (UnknownMethodException $e) { + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e); + } + } + + /** + * Establishes the relationship between two models. + * + * The relationship is established by setting the foreign key value(s) in one model + * to be the corresponding primary key value(s) in the other model. + * The model with the foreign key will be saved into database without performing validation. + * + * If the relationship involves a pivot table, a new row will be inserted into the + * pivot table which contains the primary key values from both models. + * + * Note that this method requires that the primary key value is not null. + * + * @param string $name the case sensitive name of the relationship + * @param ActiveRecord $model the model to be linked with the current one. + * @param array $extraColumns additional column values to be saved into the pivot table. + * This parameter is only meaningful for a relationship involving a pivot table + * (i.e., a relation set with `[[ActiveRelation::via()]]` or `[[ActiveRelation::viaTable()]]`.) + * @throws InvalidCallException if the method is unable to link two models. + */ + public function link($name, $model, $extraColumns = []) + { + $relation = $this->getRelation($name); + + if ($relation->via !== null) { + if ($this->getIsNewRecord() || $model->getIsNewRecord()) { + throw new InvalidCallException('Unable to link models: both models must NOT be newly created.'); + } + if (is_array($relation->via)) { + /** @var ActiveRelation $viaRelation */ + list($viaName, $viaRelation) = $relation->via; + $viaClass = $viaRelation->modelClass; + // unset $viaName so that it can be reloaded to reflect the change + unset($this->_related[$viaName]); + } else { + $viaRelation = $relation->via; + $viaTable = reset($relation->via->from); + } + $columns = []; + foreach ($viaRelation->link as $a => $b) { + $columns[$a] = $this->$b; + } + foreach ($relation->link as $a => $b) { + $columns[$b] = $model->$a; + } + foreach ($extraColumns as $k => $v) { + $columns[$k] = $v; + } + if (is_array($relation->via)) { + /** @var $viaClass ActiveRecord */ + /** @var $record ActiveRecord */ + $record = new $viaClass(); + foreach($columns as $column => $value) { + $record->$column = $value; + } + $record->insert(false); + } else { + /** @var $viaTable string */ + static::getDb()->createCommand() + ->insert($viaTable, $columns)->execute(); + } + } else { + $p1 = $model->isPrimaryKey(array_keys($relation->link)); + $p2 = $this->isPrimaryKey(array_values($relation->link)); + if ($p1 && $p2) { + if ($this->getIsNewRecord() && $model->getIsNewRecord()) { + throw new InvalidCallException('Unable to link models: both models are newly created.'); + } elseif ($this->getIsNewRecord()) { + $this->bindModels(array_flip($relation->link), $this, $model); + } else { + $this->bindModels($relation->link, $model, $this); + } + } elseif ($p1) { + $this->bindModels(array_flip($relation->link), $this, $model); + } elseif ($p2) { + $this->bindModels($relation->link, $model, $this); + } else { + throw new InvalidCallException('Unable to link models: the link does not involve any primary key.'); + } + } + + // update lazily loaded related objects + if (!$relation->multiple) { + $this->_related[$name] = $model; + } elseif (isset($this->_related[$name])) { + if ($relation->indexBy !== null) { + $indexBy = $relation->indexBy; + $this->_related[$name][$model->$indexBy] = $model; + } else { + $this->_related[$name][] = $model; + } + } + } + + /** + * Destroys the relationship between two models. + * + * The model with the foreign key of the relationship will be deleted if `$delete` is true. + * Otherwise, the foreign key will be set null and the model will be saved without validation. + * + * @param string $name the case sensitive name of the relationship. + * @param ActiveRecord $model the model to be unlinked from the current one. + * @param boolean $delete whether to delete the model that contains the foreign key. + * If false, the model's foreign key will be set null and saved. + * If true, the model containing the foreign key will be deleted. + * @throws InvalidCallException if the models cannot be unlinked + */ + public function unlink($name, $model, $delete = false) + { + $relation = $this->getRelation($name); + + if ($relation->via !== null) { + if (is_array($relation->via)) { + /** @var ActiveRelation $viaRelation */ + list($viaName, $viaRelation) = $relation->via; + $viaClass = $viaRelation->modelClass; + unset($this->_related[$viaName]); + } else { + $viaRelation = $relation->via; + $viaTable = reset($relation->via->from); + } + $columns = []; + foreach ($viaRelation->link as $a => $b) { + $columns[$a] = $this->$b; + } + foreach ($relation->link as $a => $b) { + $columns[$b] = $model->$a; + } + if (is_array($relation->via)) { + /** @var $viaClass ActiveRecord */ + if ($delete) { + $viaClass::deleteAll($columns); + } else { + $nulls = []; + foreach (array_keys($columns) as $a) { + $nulls[$a] = null; + } + $viaClass::updateAll($nulls, $columns); + } + } else { + /** @var $viaTable string */ + $command = static::getDb()->createCommand(); + if ($delete) { + $command->delete($viaTable, $columns)->execute(); + } else { + $nulls = []; + foreach (array_keys($columns) as $a) { + $nulls[$a] = null; + } + $command->update($viaTable, $nulls, $columns)->execute(); + } + } + } else { + $p1 = $model->isPrimaryKey(array_keys($relation->link)); + $p2 = $this->isPrimaryKey(array_values($relation->link)); + if ($p1 && $p2 || $p2) { + foreach ($relation->link as $a => $b) { + $model->$a = null; + } + $delete ? $model->delete() : $model->save(false); + } elseif ($p1) { + foreach ($relation->link as $b) { + $this->$b = null; + } + $delete ? $this->delete() : $this->save(false); + } else { + throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.'); + } + } + + if (!$relation->multiple) { + unset($this->_related[$name]); + } elseif (isset($this->_related[$name])) { + /** @var ActiveRecord $b */ + foreach ($this->_related[$name] as $a => $b) { + if ($model->getPrimaryKey() == $b->getPrimaryKey()) { + unset($this->_related[$name][$a]); + } + } + } + } + + /** + * @param array $link + * @param ActiveRecord $foreignModel + * @param ActiveRecord $primaryModel + * @throws InvalidCallException + */ + private function bindModels($link, $foreignModel, $primaryModel) + { + foreach ($link as $fk => $pk) { + $value = $primaryModel->$pk; + if ($value === null) { + throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.'); + } + $foreignModel->$fk = $value; + } + $foreignModel->save(false); + } + + /** + * Returns a value indicating whether the given set of attributes represents the primary key for this model + * @param array $keys the set of attributes to check + * @return boolean whether the given set of attributes represents the primary key for this model + */ + public static function isPrimaryKey($keys) + { + $pks = static::primaryKey(); + foreach ($keys as $key) { + if (!in_array($key, $pks, true)) { + return false; + } + } + return count($keys) === count($pks); + } +} diff --git a/framework/yii/test/DbFixtureManager.php b/framework/yii/test/DbFixtureManager.php index ed90284..23d25d4 100644 --- a/framework/yii/test/DbFixtureManager.php +++ b/framework/yii/test/DbFixtureManager.php @@ -11,6 +11,7 @@ use Yii; use yii\base\Component; use yii\base\InvalidConfigException; use yii\db\ActiveRecord; +use yii\db\ActiveRecordInterface; use yii\db\Connection; /** @@ -92,7 +93,7 @@ class DbFixtureManager extends Component foreach ($fixtures as $name => $fixture) { if (strpos($fixture, '\\') !== false) { $model = new $fixture; - if ($model instanceof ActiveRecord) { + if ($model instanceof ActiveRecordInterface) { $this->_modelClasses[$name] = $fixture; $fixtures[$name] = $model->getTableSchema()->name; } else { diff --git a/framework/yii/validators/UniqueValidator.php b/framework/yii/validators/UniqueValidator.php index 053f795..d123aad 100644 --- a/framework/yii/validators/UniqueValidator.php +++ b/framework/yii/validators/UniqueValidator.php @@ -10,6 +10,7 @@ namespace yii\validators; use Yii; use yii\base\InvalidConfigException; use yii\db\ActiveRecord; +use yii\db\ActiveRecordInterface; /** * UniqueValidator validates that the attribute value is unique in the corresponding database table. @@ -67,7 +68,7 @@ class UniqueValidator extends Validator $query = $className::find(); $query->where([$attributeName => $value]); - if (!$object instanceof ActiveRecord || $object->getIsNewRecord()) { + if (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) { // if current $object isn't in the database yet then it's OK just to call exists() $exists = $query->exists(); } else {