2026-02-25 06:59:34 +00:00
< ? php
/**
2026-02-27 00:03:00 +00:00
* @ link https :// www . yiiframework . com /
2026-02-25 06:59:34 +00:00
* @ copyright Copyright ( c ) 2008 Yii Software LLC
2026-02-27 00:03:00 +00:00
* @ license https :// www . yiiframework . com / license /
2026-02-25 06:59:34 +00:00
*/
namespace yii\db ;
use Yii ;
use yii\base\InvalidArgumentException ;
use yii\base\InvalidCallException ;
use yii\base\InvalidConfigException ;
use yii\base\InvalidParamException ;
use yii\base\Model ;
use yii\base\ModelEvent ;
use yii\base\NotSupportedException ;
use yii\base\UnknownMethodException ;
use yii\helpers\ArrayHelper ;
/**
* ActiveRecord is the base class for classes representing relational data in terms of objects .
*
* See [[ \yii\db\ActiveRecord ]] for a concrete implementation .
*
* @ property - read array $dirtyAttributes The changed attribute values ( name - value pairs ) .
* @ property bool $isNewRecord Whether the record is new and should be inserted when calling [[ save ()]] .
* @ property array $oldAttributes The old attribute values ( name - value pairs ) . Note that the type of this
* property differs in getter and setter . See [[ getOldAttributes ()]] and [[ setOldAttributes ()]] for details .
* @ property - read mixed $oldPrimaryKey The old primary key value . An array ( column name => column value ) is
2026-02-27 00:03:00 +00:00
* 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 ) .
2026-02-25 06:59:34 +00:00
* @ property - read mixed $primaryKey The primary key value . An array ( column name => column value ) is returned
2026-02-27 00:03:00 +00:00
* 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 ) .
2026-02-25 06:59:34 +00:00
* @ property - read array $relatedRecords An array of related records indexed by relation names .
*
* @ author Qiang Xue < qiang . xue @ gmail . com >
* @ author Carsten Brandt < mail @ cebe . cc >
* @ since 2.0
*/
abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
{
/**
* @ event Event an event that is triggered when the record is initialized via [[ init ()]] .
*/
2026-02-27 00:03:00 +00:00
public const EVENT_INIT = 'init' ;
2026-02-25 06:59:34 +00:00
/**
* @ event Event an event that is triggered after the record is created and populated with query result .
*/
2026-02-27 00:03:00 +00:00
public const EVENT_AFTER_FIND = 'afterFind' ;
2026-02-25 06:59:34 +00:00
/**
* @ event ModelEvent an event that is triggered before inserting a record .
* You may set [[ ModelEvent :: isValid ]] to be `false` to stop the insertion .
*/
2026-02-27 00:03:00 +00:00
public const EVENT_BEFORE_INSERT = 'beforeInsert' ;
2026-02-25 06:59:34 +00:00
/**
* @ event AfterSaveEvent an event that is triggered after a record is inserted .
*/
2026-02-27 00:03:00 +00:00
public const EVENT_AFTER_INSERT = 'afterInsert' ;
2026-02-25 06:59:34 +00:00
/**
* @ event ModelEvent an event that is triggered before updating a record .
* You may set [[ ModelEvent :: isValid ]] to be `false` to stop the update .
*/
2026-02-27 00:03:00 +00:00
public const EVENT_BEFORE_UPDATE = 'beforeUpdate' ;
2026-02-25 06:59:34 +00:00
/**
* @ event AfterSaveEvent an event that is triggered after a record is updated .
*/
2026-02-27 00:03:00 +00:00
public const EVENT_AFTER_UPDATE = 'afterUpdate' ;
2026-02-25 06:59:34 +00:00
/**
* @ event ModelEvent an event that is triggered before deleting a record .
* You may set [[ ModelEvent :: isValid ]] to be `false` to stop the deletion .
*/
2026-02-27 00:03:00 +00:00
public const EVENT_BEFORE_DELETE = 'beforeDelete' ;
2026-02-25 06:59:34 +00:00
/**
* @ event Event an event that is triggered after a record is deleted .
*/
2026-02-27 00:03:00 +00:00
public const EVENT_AFTER_DELETE = 'afterDelete' ;
2026-02-25 06:59:34 +00:00
/**
* @ event Event an event that is triggered after a record is refreshed .
* @ since 2.0 . 8
*/
2026-02-27 00:03:00 +00:00
public const EVENT_AFTER_REFRESH = 'afterRefresh' ;
2026-02-25 06:59:34 +00:00
/**
* @ var array attribute values indexed by attribute names
*/
private $_attributes = [];
/**
* @ var array | null old attribute values indexed by attribute names .
* This is `null` if the record [[ isNewRecord | is new ]] .
*/
private $_oldAttributes ;
/**
* @ var array related models indexed by the relation names
*/
private $_related = [];
/**
* @ var array relation names indexed by their link attributes
*/
private $_relationsDependencies = [];
/**
* { @ inheritdoc }
* @ return static | null ActiveRecord instance matching the condition , or `null` if nothing matches .
*/
public static function findOne ( $condition )
{
return static :: findByCondition ( $condition ) -> one ();
}
/**
* { @ inheritdoc }
* @ return static [] an array of ActiveRecord instances , or an empty array if nothing matches .
*/
public static function findAll ( $condition )
{
return static :: findByCondition ( $condition ) -> all ();
}
/**
* Finds ActiveRecord instance ( s ) by the given condition .
* This method is internally called by [[ findOne ()]] and [[ findAll ()]] .
* @ param mixed $condition please refer to [[ findOne ()]] for the explanation of this parameter
* @ return ActiveQueryInterface the newly created [[ ActiveQueryInterface | ActiveQuery ]] instance .
* @ throws InvalidConfigException if there is no primary key defined
* @ internal
*/
protected static function findByCondition ( $condition )
{
$query = static :: find ();
if ( ! ArrayHelper :: isAssociative ( $condition ) && ! $condition instanceof ExpressionInterface ) {
// query by primary key
$primaryKey = static :: primaryKey ();
if ( isset ( $primaryKey [ 0 ])) {
// if condition is scalar, search for a single primary key, if it is array, search for multiple primary key values
$condition = [ $primaryKey [ 0 ] => is_array ( $condition ) ? array_values ( $condition ) : $condition ];
} else {
throw new InvalidConfigException ( '"' . get_called_class () . '" must have a primary key.' );
}
}
return $query -> andWhere ( $condition );
}
/**
* 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 :
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* 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 .
* @ return int the number of rows updated
* @ throws NotSupportedException if not overridden
*/
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 ,
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* 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 .
* @ return int the number of rows updated
* @ throws NotSupportedException if not overrided
*/
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 :
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* Customer :: deleteAll ( 'status = 3' );
* `` `
*
2026-02-27 00:03:00 +00:00
* @ param string | array | null $condition the conditions that will be put in the WHERE part of the DELETE SQL .
2026-02-25 06:59:34 +00:00
* Please refer to [[ Query :: where ()]] on how to specify this parameter .
* @ return int the number of rows deleted
* @ throws NotSupportedException if not overridden .
*/
public static function deleteAll ( $condition = null )
{
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. Ensure the version value is submitted and loaded to your model before any update or delete .
* Or add [[ \yii\behaviors\OptimisticLockBehavior | OptimisticLockBehavior ]] to your model
* class in order to automate the process .
* 3. In the Web form that collects the user input , add a hidden field that stores
* the lock version of the record being updated .
* 4. 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 .
*
2026-02-27 00:03:00 +00:00
* @ return string | null the column name that stores the lock version of a table row .
2026-02-25 06:59:34 +00:00
* If `null` is returned ( default implemented ), optimistic locking will not be supported .
*/
public function optimisticLock ()
{
return null ;
}
/**
* { @ inheritdoc }
*/
public function canGetProperty ( $name , $checkVars = true , $checkBehaviors = true )
{
if ( parent :: canGetProperty ( $name , $checkVars , $checkBehaviors )) {
return true ;
}
try {
return $this -> hasAttribute ( $name );
} catch ( \Exception $e ) {
// `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used
return false ;
}
}
/**
* { @ inheritdoc }
*/
public function canSetProperty ( $name , $checkVars = true , $checkBehaviors = true )
{
if ( parent :: canSetProperty ( $name , $checkVars , $checkBehaviors )) {
return true ;
}
try {
return $this -> hasAttribute ( $name );
} catch ( \Exception $e ) {
// `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used
return false ;
}
}
/**
* PHP getter magic method .
* This method is overridden so that attributes and related objects can be accessed like properties .
*
* @ param string $name property name
* @ throws InvalidArgumentException if relation name is wrong
* @ return mixed property value
* @ see getAttribute ()
*/
public function __get ( $name )
{
2026-02-27 00:03:00 +00:00
if ( array_key_exists ( $name , $this -> _attributes )) {
2026-02-25 06:59:34 +00:00
return $this -> _attributes [ $name ];
}
if ( $this -> hasAttribute ( $name )) {
return null ;
}
2026-02-27 00:03:00 +00:00
if ( array_key_exists ( $name , $this -> _related )) {
2026-02-25 06:59:34 +00:00
return $this -> _related [ $name ];
}
$value = parent :: __get ( $name );
if ( $value instanceof ActiveQueryInterface ) {
$this -> setRelationDependencies ( $name , $value );
return $this -> _related [ $name ] = $value -> findFor ( $name , $this );
}
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 )) {
if (
! empty ( $this -> _relationsDependencies [ $name ])
&& ( ! array_key_exists ( $name , $this -> _attributes ) || $this -> _attributes [ $name ] !== $value )
) {
$this -> resetDependentRelations ( $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 bool whether the property value is null
*/
public function __isset ( $name )
{
try {
return $this -> __get ( $name ) !== null ;
} catch ( \Exception $t ) {
return false ;
} catch ( \Throwable $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 ]);
if ( ! empty ( $this -> _relationsDependencies [ $name ])) {
$this -> resetDependentRelations ( $name );
}
} elseif ( array_key_exists ( $name , $this -> _related )) {
unset ( $this -> _related [ $name ]);
} elseif ( $this -> getRelation ( $name , false ) === null ) {
parent :: __unset ( $name );
}
}
/**
* Declares a `has-one` relation .
* The declaration is returned in terms of a relational [[ ActiveQuery ]] 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 :
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* public function getCountry ()
* {
* return $this -> hasOne ( Country :: class , [ '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 [[ ActiveQuery ]] 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 ActiveQueryInterface the relational query object .
2026-02-27 00:03:00 +00:00
*
* @ phpstan - param class - string $class
* @ psalm - param class - string $class
*
* @ phpstan - param array < string , string > $link
* @ psalm - param array < string , string > $link
2026-02-25 06:59:34 +00:00
*/
public function hasOne ( $class , $link )
{
return $this -> createRelationQuery ( $class , $link , false );
}
/**
* Declares a `has-many` relation .
* The declaration is returned in terms of a relational [[ ActiveQuery ]] 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 :
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* public function getOrders ()
* {
* return $this -> hasMany ( Order :: class , [ '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 .
*
* Call methods declared in [[ ActiveQuery ]] 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 ActiveQueryInterface the relational query object .
2026-02-27 00:03:00 +00:00
*
* @ phpstan - param class - string $class
* @ psalm - param class - string $class
*
* @ phpstan - param array < string , string > $link
* @ psalm - param array < string , string > $link
2026-02-25 06:59:34 +00:00
*/
public function hasMany ( $class , $link )
{
return $this -> createRelationQuery ( $class , $link , true );
}
/**
* Creates a query instance for `has-one` or `has-many` relation .
* @ param string $class the class name of the related record .
* @ param array $link the primary - foreign key constraint .
* @ param bool $multiple whether this query represents a relation to more than one record .
* @ return ActiveQueryInterface the relational query object .
* @ since 2.0 . 12
* @ see hasOne ()
* @ see hasMany ()
*/
protected function createRelationQuery ( $class , $link , $multiple )
{
2026-02-27 00:03:00 +00:00
/**
* @ var ActiveRecordInterface $class
* @ var ActiveQuery $query
*
* @ phpstan - var ActiveQuery < ActiveRecord > $query
*/
2026-02-25 06:59:34 +00:00
$query = $class :: find ();
$query -> primaryModel = $this ;
$query -> link = $link ;
$query -> multiple = $multiple ;
return $query ;
}
/**
* 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 , e . g . `orders` for a relation defined via `getOrders()` method ( case - sensitive ) .
* @ param ActiveRecordInterface | array | null $records the related records to be populated into the relation .
* @ see getRelation ()
*/
public function populateRelation ( $name , $records )
{
foreach ( $this -> _relationsDependencies as & $relationNames ) {
unset ( $relationNames [ $name ]);
}
$this -> _related [ $name ] = $records ;
}
/**
* Check whether the named relation has been populated with records .
* @ param string $name the relation name , e . g . `orders` for a relation defined via `getOrders()` method ( case - sensitive ) .
* @ return bool whether relation has been populated with records .
* @ see getRelation ()
*/
public function isRelationPopulated ( $name )
{
return array_key_exists ( $name , $this -> _related );
}
/**
* Returns all populated related records .
* @ return array an array of related records indexed by relation names .
* @ see getRelation ()
*/
public function getRelatedRecords ()
{
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 bool whether the model has an attribute with the specified name .
*/
public function hasAttribute ( $name )
{
2026-02-27 00:03:00 +00:00
// using null as an array offset is deprecated in PHP 8.5
if ( $name === null || $name === '' ) {
return false ;
}
2026-02-25 06:59:34 +00:00
return isset ( $this -> _attributes [ $name ]) || in_array ( $name , $this -> attributes (), true );
}
/**
* 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 InvalidArgumentException if the named attribute does not exist .
* @ see hasAttribute ()
*/
public function setAttribute ( $name , $value )
{
if ( $this -> hasAttribute ( $name )) {
if (
! empty ( $this -> _relationsDependencies [ $name ])
&& ( ! array_key_exists ( $name , $this -> _attributes ) || $this -> _attributes [ $name ] !== $value )
) {
$this -> resetDependentRelations ( $name );
}
$this -> _attributes [ $name ] = $value ;
} else {
throw new InvalidArgumentException ( 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 | null $values old attribute values to be set .
* If set to `null` this record is considered to be [[ isNewRecord | new ]] .
*/
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 InvalidArgumentException if the named attribute does not exist .
* @ see hasAttribute ()
*/
public function setOldAttribute ( $name , $value )
{
2026-02-27 00:03:00 +00:00
if ( $this -> canSetOldAttribute ( $name )) {
2026-02-25 06:59:34 +00:00
$this -> _oldAttributes [ $name ] = $value ;
} else {
throw new InvalidArgumentException ( get_class ( $this ) . ' has no attribute named "' . $name . '".' );
}
}
2026-02-27 00:03:00 +00:00
/**
* Returns if the old named attribute can be set .
* @ param string $name the attribute name
* @ return bool whether the old attribute can be set
* @ see setOldAttribute ()
*/
public function canSetOldAttribute ( $name )
{
return ( isset ( $this -> _oldAttributes [ $name ]) || $this -> hasAttribute ( $name ));
}
2026-02-25 06:59:34 +00:00
/**
* Marks an attribute dirty .
* This method may be called to force updating a record when calling [[ update ()]],
* even if there is no change being made to the record .
* @ param string $name the attribute name
*/
public function markAttributeDirty ( $name )
{
unset ( $this -> _oldAttributes [ $name ]);
}
/**
* Returns a value indicating whether the named attribute has been changed .
* @ param string $name the name of the attribute .
* @ param bool $identical whether the comparison of new and old value is made for
* identical values using `===` , defaults to `true` . Otherwise `==` is used for comparison .
* This parameter is available since version 2.0 . 4.
* @ return bool whether the attribute has been changed
*/
public function isAttributeChanged ( $name , $identical = true )
{
if ( isset ( $this -> _attributes [ $name ], $this -> _oldAttributes [ $name ])) {
if ( $identical ) {
return $this -> _attributes [ $name ] !== $this -> _oldAttributes [ $name ];
}
return $this -> _attributes [ $name ] != $this -> _oldAttributes [ $name ];
}
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 .
*
* The comparison of new and old values is made for identical values using `===` .
*
* @ 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 ) {
2026-02-27 00:03:00 +00:00
if ( isset ( $names [ $name ]) && ( ! array_key_exists ( $name , $this -> _oldAttributes ) || $this -> isValueDifferent ( $value , $this -> _oldAttributes [ $name ]))) {
2026-02-25 06:59:34 +00:00
$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 :
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* $customer = new Customer ; // or $customer = Customer::findOne($id);
* $customer -> name = $name ;
* $customer -> email = $email ;
* $customer -> save ();
* `` `
*
* @ param bool $runValidation whether to perform validation ( calling [[ validate ()]])
* before saving the record . Defaults to `true` . If the validation fails , the record
* will not be saved to the database and this method will return `false` .
2026-02-27 00:03:00 +00:00
* @ param array | null $attributeNames list of attribute names that need to be saved . Defaults to null ,
2026-02-25 06:59:34 +00:00
* meaning all attributes that are loaded from DB will be saved .
* @ return bool whether the saving succeeded ( i . e . no validation errors occurred ) .
2026-02-27 00:03:00 +00:00
* @ throws Exception in case update or insert failed .
2026-02-25 06:59:34 +00:00
*/
public function save ( $runValidation = true , $attributeNames = null )
{
if ( $this -> getIsNewRecord ()) {
return $this -> insert ( $runValidation , $attributeNames );
}
return $this -> update ( $runValidation , $attributeNames ) !== 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 [[ beforeValidate ()]]
* returns `false` , the rest of the steps will be skipped ;
* 2. call [[ afterValidate ()]] when `$runValidation` is `true` . If validation
* failed , the rest of the steps will be skipped ;
* 3. call [[ beforeSave ()]] . If [[ beforeSave ()]] returns `false` ,
* the rest of the steps will be skipped ;
* 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_AFTER_VALIDATE ]], [[ EVENT_BEFORE_UPDATE ]], and [[ EVENT_AFTER_UPDATE ]]
* will be raised by the corresponding methods .
*
* Only the [[ dirtyAttributes | changed attribute values ]] will be saved into database .
*
* For example , to update a customer record :
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* $customer = Customer :: findOne ( $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 :
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* if ( $customer -> update () !== false ) {
* // update successful
* } else {
* // update failed
* }
* `` `
*
* @ param bool $runValidation whether to perform validation ( calling [[ validate ()]])
* before saving the record . Defaults to `true` . If the validation fails , the record
* will not be saved to the database and this method will return `false` .
2026-02-27 00:03:00 +00:00
* @ param array | null $attributeNames list of attribute names that need to be saved . Defaults to null ,
2026-02-25 06:59:34 +00:00
* meaning all attributes that are loaded from DB will be saved .
* @ return int | false 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 , $attributeNames = null )
{
if ( $runValidation && ! $this -> validate ( $attributeNames )) {
return false ;
}
return $this -> updateInternal ( $attributeNames );
}
/**
* Updates the specified attributes .
*
* This method is a shortcut to [[ update ()]] when data validation is not needed
* and only a small set attributes need to be updated .
*
* You may specify the attributes to be updated as name list or name - value pairs .
* If the latter , the corresponding attribute values will be modified accordingly .
* The method will then save the specified attributes into database .
*
* Note that this method will ** not ** perform data validation and will ** not ** trigger events .
*
* @ param array $attributes the attributes ( names or name - value pairs ) to be updated
* @ return int the number of rows affected .
*/
public function updateAttributes ( $attributes )
{
$attrs = [];
foreach ( $attributes as $name => $value ) {
if ( is_int ( $name )) {
$attrs [] = $value ;
} else {
$this -> $name = $value ;
$attrs [] = $name ;
}
}
$values = $this -> getDirtyAttributes ( $attrs );
if ( empty ( $values ) || $this -> getIsNewRecord ()) {
return 0 ;
}
$rows = static :: updateAll ( $values , $this -> getOldPrimaryKey ( true ));
foreach ( $values as $name => $value ) {
$this -> _oldAttributes [ $name ] = $this -> _attributes [ $name ];
}
return $rows ;
}
/**
* @ see update ()
2026-02-27 00:03:00 +00:00
* @ param array | null $attributes attributes to update
2026-02-25 06:59:34 +00:00
* @ return int | false the number of rows affected , or false if [[ beforeSave ()]] stops the updating process .
* @ throws StaleObjectException
*/
protected function updateInternal ( $attributes = null )
{
if ( ! $this -> beforeSave ( false )) {
return false ;
}
$values = $this -> getDirtyAttributes ( $attributes );
if ( empty ( $values )) {
$this -> afterSave ( false , $values );
return 0 ;
}
$condition = $this -> getOldPrimaryKey ( true );
$lock = $this -> optimisticLock ();
if ( $lock !== null ) {
$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 = static :: updateAll ( $values , $condition );
if ( $lock !== null && ! $rows ) {
throw new StaleObjectException ( 'The object being updated is outdated.' );
}
2026-02-27 00:03:00 +00:00
// using null as an array offset is deprecated in PHP `8.5`
if ( $lock !== null && isset ( $values [ $lock ])) {
2026-02-25 06:59:34 +00:00
$this -> $lock = $values [ $lock ];
}
$changedAttributes = [];
foreach ( $values as $name => $value ) {
$changedAttributes [ $name ] = isset ( $this -> _oldAttributes [ $name ]) ? $this -> _oldAttributes [ $name ] : null ;
$this -> _oldAttributes [ $name ] = $value ;
}
$this -> afterSave ( false , $changedAttributes );
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 :
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* $post = Post :: findOne ( $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 bool whether the saving is successful
* @ see updateAllCounters ()
*/
public function updateCounters ( $counters )
{
if ( static :: updateAllCounters ( $counters , $this -> getOldPrimaryKey ( true )) > 0 ) {
foreach ( $counters as $name => $value ) {
if ( ! isset ( $this -> _attributes [ $name ])) {
$this -> _attributes [ $name ] = $value ;
} else {
$this -> _attributes [ $name ] += $value ;
}
$this -> _oldAttributes [ $name ] = $this -> _attributes [ $name ];
}
return true ;
}
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 int | false 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 = static :: 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 bool 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 bool $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 .
*/
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 :
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* public function beforeSave ( $insert )
* {
* if ( ! parent :: beforeSave ( $insert )) {
* return false ;
* }
*
* // ...custom code here...
* return true ;
* }
* `` `
*
* @ param bool $insert whether this method called while inserting a record .
* If `false` , it means the method is called while updating a record .
* @ return bool 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` . The event class used is [[ AfterSaveEvent ]] .
* When overriding this method , make sure you call the parent implementation so that
* the event is triggered .
* @ param bool $insert whether this method called while inserting a record .
* If `false` , it means the method is called while updating a record .
* @ param array $changedAttributes The old values of attributes that had changed and were saved .
* You can use this parameter to take action based on the changes made for example send an email
* when the password had changed or implement audit trail that tracks all the changes .
* `$changedAttributes` gives you the old attribute values while the active record ( `$this` ) has
* already the new , updated values .
*
* Note that no automatic type conversion performed by default . You may use
* [[ \yii\behaviors\AttributeTypecastBehavior ]] to facilitate attribute typecasting .
2026-02-27 00:03:00 +00:00
* See https :// www . yiiframework . com / doc - 2.0 / guide - db - active - record . html #attributes-typecasting.
2026-02-25 06:59:34 +00:00
*/
public function afterSave ( $insert , $changedAttributes )
{
$this -> trigger ( $insert ? self :: EVENT_AFTER_INSERT : self :: EVENT_AFTER_UPDATE , new AfterSaveEvent ([
'changedAttributes' => $changedAttributes ,
]));
}
/**
* 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 :
*
2026-02-27 00:03:00 +00:00
* `` `
2026-02-25 06:59:34 +00:00
* public function beforeDelete ()
* {
* if ( ! parent :: beforeDelete ()) {
* return false ;
* }
*
* // ...custom code here...
* return true ;
* }
* `` `
*
* @ return bool 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 .
*
* If the refresh is successful , an [[ EVENT_AFTER_REFRESH ]] event will be triggered .
* This event is available since version 2.0 . 8.
*
* @ return bool 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 ()
{
2026-02-27 00:03:00 +00:00
/** @var self $record */
2026-02-25 06:59:34 +00:00
$record = static :: findOne ( $this -> getPrimaryKey ( true ));
return $this -> refreshInternal ( $record );
}
/**
* Repopulates this active record with the latest data from a newly fetched instance .
* @ param BaseActiveRecord $record the record to take attributes from .
* @ return bool whether refresh was successful .
* @ see refresh ()
* @ since 2.0 . 13
*/
protected function refreshInternal ( $record )
{
if ( $record === null ) {
return false ;
}
foreach ( $this -> attributes () as $name ) {
$this -> _attributes [ $name ] = isset ( $record -> _attributes [ $name ]) ? $record -> _attributes [ $name ] : null ;
}
$this -> _oldAttributes = $record -> _oldAttributes ;
$this -> _related = [];
$this -> _relationsDependencies = [];
$this -> afterRefresh ();
return true ;
}
/**
* This method is called when the AR object is refreshed .
* The default implementation will trigger an [[ EVENT_AFTER_REFRESH ]] event .
* When overriding this method , make sure you call the parent implementation to ensure the
* event is triggered .
* @ since 2.0 . 8
*/
public function afterRefresh ()
{
$this -> trigger ( self :: EVENT_AFTER_REFRESH );
}
/**
* 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 ActiveRecordInterface $record record to compare to
* @ return bool whether the two active records refer to the same row in the same database table .
*/
public function equals ( $record )
{
if ( $this -> getIsNewRecord () || $record -> getIsNewRecord ()) {
return false ;
}
return get_class ( $this ) === get_class ( $record ) && $this -> getPrimaryKey () === $record -> getPrimaryKey ();
}
/**
* Returns the primary key value ( s ) .
* @ param bool $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 .
* @ 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 = static :: primaryKey ();
if ( ! $asArray && count ( $keys ) === 1 ) {
return isset ( $this -> _attributes [ $keys [ 0 ]]) ? $this -> _attributes [ $keys [ 0 ]] : null ;
}
$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 (), findOne ()) .
* The value remains unchanged even if the primary key attribute is manually assigned with a different value .
* @ param bool $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 .
* @ 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 ) .
* @ throws Exception if the AR model does not have a primary key
*/
public function getOldPrimaryKey ( $asArray = false )
{
$keys = static :: primaryKey ();
if ( empty ( $keys )) {
throw new Exception ( get_class ( $this ) . ' does not have a primary key. You should either define a primary key for the corresponding table or override the primaryKey() method.' );
}
if ( ! $asArray && count ( $keys ) === 1 ) {
return isset ( $this -> _oldAttributes [ $keys [ 0 ]]) ? $this -> _oldAttributes [ $keys [ 0 ]] : null ;
}
$values = [];
foreach ( $keys as $name ) {
$values [ $name ] = isset ( $this -> _oldAttributes [ $name ]) ? $this -> _oldAttributes [ $name ] : null ;
}
return $values ;
}
/**
* Populates an active record object using a row of data from the database / storage .
*
* This is an internal method meant to be called to create active record objects after
* fetching data from the database . It is mainly used by [[ ActiveQuery ]] to populate
* the query results into active records .
*
* When calling this method manually you should call [[ afterFind ()]] on the created
* record to trigger the [[ EVENT_AFTER_FIND | afterFind Event ]] .
*
* @ param BaseActiveRecord $record the record to be populated . In most cases this will be an instance
* created by [[ instantiate ()]] beforehand .
* @ param array $row attribute values ( name => value )
*/
public static function populateRecord ( $record , $row )
{
$columns = array_flip ( $record -> attributes ());
foreach ( $row as $name => $value ) {
if ( isset ( $columns [ $name ])) {
$record -> _attributes [ $name ] = $value ;
} elseif ( $record -> canSetProperty ( $name )) {
$record -> $name = $value ;
}
}
$record -> _oldAttributes = $record -> _attributes ;
$record -> _related = [];
$record -> _relationsDependencies = [];
}
/**
* Creates an active record instance .
*
* This method is called together with [[ populateRecord ()]] by [[ ActiveQuery ]] .
* It is not meant to be used for creating new records directly .
*
* 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 static 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 bool whether there is an element at the specified offset .
*/
#[\ReturnTypeWillChange]
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 [[ ActiveQueryInterface ]] object .
* It can be declared in either the Active Record class itself or one of its behaviors .
* @ param string $name the relation name , e . g . `orders` for a relation defined via `getOrders()` method ( case - sensitive ) .
* @ param bool $throwException whether to throw exception if the relation does not exist .
2026-02-27 00:03:00 +00:00
* @ return ActiveQueryInterface | ActiveQuery | null the relational query object . If the relation does not exist
2026-02-25 06:59:34 +00:00
* and `$throwException` is `false` , `null` will be returned .
* @ throws InvalidArgumentException if the named relation does not exist .
*/
public function getRelation ( $name , $throwException = true )
{
$getter = 'get' . $name ;
try {
// the relation could be defined in a behavior
$relation = $this -> $getter ();
} catch ( UnknownMethodException $e ) {
if ( $throwException ) {
throw new InvalidArgumentException ( get_class ( $this ) . ' has no relation named "' . $name . '".' , 0 , $e );
}
return null ;
}
if ( ! $relation instanceof ActiveQueryInterface ) {
if ( $throwException ) {
throw new InvalidArgumentException ( get_class ( $this ) . ' has no relation named "' . $name . '".' );
}
return null ;
}
if ( method_exists ( $this , $getter )) {
// relation name is case sensitive, trying to validate it when the relation is defined within this class
$method = new \ReflectionMethod ( $this , $getter );
$realName = lcfirst ( substr ( $method -> getName (), 3 ));
if ( $realName !== $name ) {
if ( $throwException ) {
throw new InvalidArgumentException ( 'Relation names are case sensitive. ' . get_class ( $this ) . " has a relation named \" $realName\ " instead of \ " $name\ " . " );
}
return null ;
}
}
return $relation ;
}
/**
* 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
* and ** without ** events / behaviors .
*
* If the relationship involves a junction table , a new row will be inserted into the
* junction 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 , e . g . `orders` for a relation defined via `getOrders()` method .
* @ param ActiveRecordInterface $model the model to be linked with the current one .
* @ param array $extraColumns additional column values to be saved into the junction table .
* This parameter is only meaningful for a relationship involving a junction table
* ( i . e . , a relation set with [[ ActiveRelationTrait :: via ()]] or [[ ActiveQuery :: viaTable ()]] . )
* @ throws InvalidCallException if the method is unable to link two models .
*/
public function link ( $name , $model , $extraColumns = [])
{
2026-02-27 00:03:00 +00:00
/** @var ActiveQueryInterface|ActiveQuery $relation */
2026-02-25 06:59:34 +00:00
$relation = $this -> getRelation ( $name );
if ( $relation -> via !== null ) {
if ( $this -> getIsNewRecord () || $model -> getIsNewRecord ()) {
throw new InvalidCallException ( 'Unable to link models: the models being linked cannot be newly created.' );
}
if ( is_array ( $relation -> via )) {
2026-02-27 00:03:00 +00:00
/**
* @ var ActiveQuery $viaRelation
* @ phpstan - var ActiveQuery < ActiveRecord | array < string , mixed >> $viaRelation
*/
2026-02-25 06:59:34 +00:00
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 )) {
2026-02-27 00:03:00 +00:00
/** @var ActiveRecordInterface $viaClass */
/** @var ActiveRecordInterface $record */
2026-02-25 06:59:34 +00:00
$record = Yii :: createObject ( $viaClass );
foreach ( $columns as $column => $value ) {
$record -> $column = $value ;
}
$record -> insert ( false );
} else {
2026-02-27 00:03:00 +00:00
/** @var string $viaTable */
2026-02-25 06:59:34 +00:00
static :: getDb () -> createCommand () -> insert ( $viaTable , $columns ) -> execute ();
}
} else {
$p1 = $model -> isPrimaryKey ( array_keys ( $relation -> link ));
$p2 = static :: isPrimaryKey ( array_values ( $relation -> link ));
if ( $p1 && $p2 ) {
if ( $this -> getIsNewRecord ()) {
if ( $model -> getIsNewRecord ()) {
throw new InvalidCallException ( 'Unable to link models: at most one model can be newly created.' );
}
$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 defining the relation 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 ) {
if ( $relation -> indexBy instanceof \Closure ) {
$index = call_user_func ( $relation -> indexBy , $model );
} else {
$index = $model -> { $relation -> indexBy };
}
$this -> _related [ $name ][ $index ] = $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 , e . g . `orders` for a relation defined via `getOrders()` method .
* @ param ActiveRecordInterface $model the model to be unlinked from the current one .
* You have to make sure that the model is really related with the current model as this method
* does not check this .
* @ param bool $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
* @ throws Exception
* @ throws StaleObjectException
*/
public function unlink ( $name , $model , $delete = false )
{
2026-02-27 00:03:00 +00:00
/** @var ActiveQueryInterface|ActiveQuery $relation */
2026-02-25 06:59:34 +00:00
$relation = $this -> getRelation ( $name );
if ( $relation -> via !== null ) {
if ( is_array ( $relation -> via )) {
2026-02-27 00:03:00 +00:00
/**
* @ var ActiveQuery $viaRelation
* @ phpstan - var ActiveQuery < ActiveRecord > $viaRelation
*/
2026-02-25 06:59:34 +00:00
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 ;
}
$nulls = [];
foreach ( array_keys ( $columns ) as $a ) {
$nulls [ $a ] = null ;
}
if ( property_exists ( $viaRelation , 'on' ) && $viaRelation -> on !== null ) {
$columns = [ 'and' , $columns , $viaRelation -> on ];
}
if ( is_array ( $relation -> via )) {
2026-02-27 00:03:00 +00:00
/** @var ActiveRecordInterface $viaClass */
2026-02-25 06:59:34 +00:00
if ( $delete ) {
$viaClass :: deleteAll ( $columns );
} else {
$viaClass :: updateAll ( $nulls , $columns );
}
} else {
2026-02-27 00:03:00 +00:00
/** @var string $viaTable */
/** @var Command $command */
2026-02-25 06:59:34 +00:00
$command = static :: getDb () -> createCommand ();
if ( $delete ) {
$command -> delete ( $viaTable , $columns ) -> execute ();
} else {
$command -> update ( $viaTable , $nulls , $columns ) -> execute ();
}
}
} else {
$p1 = $model -> isPrimaryKey ( array_keys ( $relation -> link ));
$p2 = static :: isPrimaryKey ( array_values ( $relation -> link ));
if ( $p2 ) {
if ( $delete ) {
$model -> delete ();
} else {
foreach ( $relation -> link as $a => $b ) {
$model -> $a = null ;
}
$model -> save ( false );
}
} elseif ( $p1 ) {
foreach ( $relation -> link as $a => $b ) {
if ( is_array ( $this -> $b )) { // relation via array valued attribute
if (( $key = array_search ( $model -> $a , $this -> $b , false )) !== false ) {
$values = $this -> $b ;
unset ( $values [ $key ]);
$this -> $b = array_values ( $values );
}
} else {
$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 ])) {
2026-02-27 00:03:00 +00:00
/** @var ActiveRecordInterface $b */
2026-02-25 06:59:34 +00:00
foreach ( $this -> _related [ $name ] as $a => $b ) {
if ( $model -> getPrimaryKey () === $b -> getPrimaryKey ()) {
unset ( $this -> _related [ $name ][ $a ]);
}
}
}
}
/**
* Destroys the relationship in current model .
*
* 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 .
*
* Note that to destroy the relationship without removing records make sure your keys can be set to null
*
* @ param string $name the case sensitive name of the relationship , e . g . `orders` for a relation defined via `getOrders()` method .
* @ param bool $delete whether to delete the model that contains the foreign key .
*
* Note that the deletion will be performed using [[ deleteAll ()]], which will not trigger any events on the related models .
* If you need [[ EVENT_BEFORE_DELETE ]] or [[ EVENT_AFTER_DELETE ]] to be triggered , you need to [[ find () | find ]] the models first
* and then call [[ delete ()]] on each of them .
*/
public function unlinkAll ( $name , $delete = false )
{
2026-02-27 00:03:00 +00:00
/** @var ActiveQueryInterface|ActiveQuery $relation */
2026-02-25 06:59:34 +00:00
$relation = $this -> getRelation ( $name );
if ( $relation -> via !== null ) {
if ( is_array ( $relation -> via )) {
2026-02-27 00:03:00 +00:00
/**
* @ var ActiveQuery $viaRelation
* @ phpstan - var ActiveQuery < ActiveRecord | array < string , mixed >> $viaRelation
*/
2026-02-25 06:59:34 +00:00
list ( $viaName , $viaRelation ) = $relation -> via ;
$viaClass = $viaRelation -> modelClass ;
unset ( $this -> _related [ $viaName ]);
} else {
$viaRelation = $relation -> via ;
$viaTable = reset ( $relation -> via -> from );
}
$condition = [];
$nulls = [];
foreach ( $viaRelation -> link as $a => $b ) {
$nulls [ $a ] = null ;
$condition [ $a ] = $this -> $b ;
}
if ( ! empty ( $viaRelation -> where )) {
$condition = [ 'and' , $condition , $viaRelation -> where ];
}
if ( property_exists ( $viaRelation , 'on' ) && ! empty ( $viaRelation -> on )) {
$condition = [ 'and' , $condition , $viaRelation -> on ];
}
if ( is_array ( $relation -> via )) {
2026-02-27 00:03:00 +00:00
/** @var ActiveRecordInterface $viaClass */
2026-02-25 06:59:34 +00:00
if ( $delete ) {
$viaClass :: deleteAll ( $condition );
} else {
$viaClass :: updateAll ( $nulls , $condition );
}
} else {
2026-02-27 00:03:00 +00:00
/** @var string $viaTable */
/** @var Command $command */
2026-02-25 06:59:34 +00:00
$command = static :: getDb () -> createCommand ();
if ( $delete ) {
$command -> delete ( $viaTable , $condition ) -> execute ();
} else {
$command -> update ( $viaTable , $nulls , $condition ) -> execute ();
}
}
} else {
2026-02-27 00:03:00 +00:00
/** @var ActiveRecordInterface $relatedModel */
2026-02-25 06:59:34 +00:00
$relatedModel = $relation -> modelClass ;
if ( ! $delete && count ( $relation -> link ) === 1 && is_array ( $this -> { $b = reset ( $relation -> link )})) {
// relation via array valued attribute
$this -> $b = [];
$this -> save ( false );
} else {
$nulls = [];
$condition = [];
foreach ( $relation -> link as $a => $b ) {
$nulls [ $a ] = null ;
$condition [ $a ] = $this -> $b ;
}
if ( ! empty ( $relation -> where )) {
$condition = [ 'and' , $condition , $relation -> where ];
}
if ( property_exists ( $relation , 'on' ) && ! empty ( $relation -> on )) {
$condition = [ 'and' , $condition , $relation -> on ];
}
if ( $delete ) {
$relatedModel :: deleteAll ( $condition );
} else {
$relatedModel :: updateAll ( $nulls , $condition );
}
}
}
unset ( $this -> _related [ $name ]);
}
/**
* @ param array $link
* @ param ActiveRecordInterface $foreignModel
* @ param ActiveRecordInterface $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.' );
}
if ( is_array ( $foreignModel -> $fk )) { // relation via array valued attribute
$foreignModel -> { $fk }[] = $value ;
} else {
$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 bool whether the given set of attributes represents the primary key for this model
*/
public static function isPrimaryKey ( $keys )
{
$pks = static :: primaryKey ();
if ( count ( $keys ) === count ( $pks )) {
return count ( array_intersect ( $keys , $pks )) === count ( $pks );
}
return false ;
}
/**
* Returns the text label for the specified attribute .
2026-02-27 00:03:00 +00:00
* The attribute may be specified in a dot format to retrieve the label from related model or allow this model to override the label defined in related model .
* For example , if the attribute is specified as 'relatedModel1.relatedModel2.attr' the function will return the first label definition it can find
* in the following order :
* - the label for 'relatedModel1.relatedModel2.attr' defined in [[ attributeLabels ()]] of this model ;
* - the label for 'relatedModel2.attr' defined in related model represented by relation 'relatedModel1' of this model ;
* - the label for 'attr' defined in related model represented by relation 'relatedModel2' of relation 'relatedModel1' .
* If no label definition was found then the value of $this -> generateAttributeLabel ( 'relatedModel1.relatedModel2.attr' ) will be returned .
2026-02-25 06:59:34 +00:00
* @ param string $attribute the attribute name
* @ return string the attribute label
* @ see attributeLabels ()
2026-02-27 00:03:00 +00:00
* @ see generateAttributeLabel ()
2026-02-25 06:59:34 +00:00
*/
public function getAttributeLabel ( $attribute )
{
2026-02-27 00:03:00 +00:00
$model = $this ;
$modelAttribute = $attribute ;
for (;;) {
$labels = $model -> attributeLabels ();
if ( isset ( $labels [ $modelAttribute ])) {
return $labels [ $modelAttribute ];
}
2026-02-25 06:59:34 +00:00
2026-02-27 00:03:00 +00:00
$parts = explode ( '.' , $modelAttribute , 2 );
if ( count ( $parts ) < 2 ) {
break ;
2026-02-25 06:59:34 +00:00
}
2026-02-27 00:03:00 +00:00
list ( $relationName , $modelAttribute ) = $parts ;
if ( $model -> isRelationPopulated ( $relationName ) && $model -> $relationName instanceof self ) {
$model = $model -> $relationName ;
} else {
try {
$relation = $model -> getRelation ( $relationName );
} catch ( InvalidArgumentException $e ) {
break ;
}
/** @var ActiveRecordInterface $modelClass */
$modelClass = $relation -> modelClass ;
$model = $modelClass :: instance ();
2026-02-25 06:59:34 +00:00
}
}
return $this -> generateAttributeLabel ( $attribute );
}
/**
* Returns the text hint for the specified attribute .
* If the attribute looks like `relatedModel.attribute` , then the attribute will be received from the related model .
* @ param string $attribute the attribute name
* @ return string the attribute hint
* @ see attributeHints ()
* @ since 2.0 . 4
*/
public function getAttributeHint ( $attribute )
{
$hints = $this -> attributeHints ();
if ( isset ( $hints [ $attribute ])) {
return $hints [ $attribute ];
} elseif ( strpos ( $attribute , '.' )) {
$attributeParts = explode ( '.' , $attribute );
$neededAttribute = array_pop ( $attributeParts );
$relatedModel = $this ;
foreach ( $attributeParts as $relationName ) {
if ( $relatedModel -> isRelationPopulated ( $relationName ) && $relatedModel -> $relationName instanceof self ) {
$relatedModel = $relatedModel -> $relationName ;
} else {
try {
$relation = $relatedModel -> getRelation ( $relationName );
} catch ( InvalidParamException $e ) {
return '' ;
}
2026-02-27 00:03:00 +00:00
/** @var ActiveRecordInterface $modelClass */
2026-02-25 06:59:34 +00:00
$modelClass = $relation -> modelClass ;
$relatedModel = $modelClass :: instance ();
}
}
$hints = $relatedModel -> attributeHints ();
if ( isset ( $hints [ $neededAttribute ])) {
return $hints [ $neededAttribute ];
}
}
return '' ;
}
/**
* { @ inheritdoc }
*
* The default implementation returns the names of the columns whose values have been populated into this record .
*/
public function fields ()
{
$fields = array_keys ( $this -> _attributes );
return array_combine ( $fields , $fields );
}
/**
* { @ inheritdoc }
*
* The default implementation returns the names of the relations that have been populated into this record .
*/
public function extraFields ()
{
$fields = array_keys ( $this -> getRelatedRecords ());
return array_combine ( $fields , $fields );
}
/**
* Sets the element value at the specified offset to null .
* This method is required by the SPL interface [[ \ArrayAccess ]] .
* It is implicitly called when you use something like `unset($model[$offset])` .
* @ param mixed $offset the offset to unset element
*/
public function offsetUnset ( $offset )
{
if ( property_exists ( $this , $offset )) {
$this -> $offset = null ;
} else {
unset ( $this -> $offset );
}
}
/**
* Resets dependent related models checking if their links contain specific attribute .
* @ param string $attribute The changed attribute name .
*/
private function resetDependentRelations ( $attribute )
{
foreach ( $this -> _relationsDependencies [ $attribute ] as $relation ) {
unset ( $this -> _related [ $relation ]);
}
unset ( $this -> _relationsDependencies [ $attribute ]);
}
/**
* Sets relation dependencies for a property
* @ param string $name property name
* @ param ActiveQueryInterface $relation relation instance
* @ param string | null $viaRelationName intermediate relation
*/
private function setRelationDependencies ( $name , $relation , $viaRelationName = null )
{
if ( empty ( $relation -> via ) && $relation -> link ) {
foreach ( $relation -> link as $attribute ) {
$this -> _relationsDependencies [ $attribute ][ $name ] = $name ;
if ( $viaRelationName !== null ) {
$this -> _relationsDependencies [ $attribute ][] = $viaRelationName ;
}
}
} elseif ( $relation -> via instanceof ActiveQueryInterface ) {
$this -> setRelationDependencies ( $name , $relation -> via );
} elseif ( is_array ( $relation -> via )) {
list ( $viaRelationName , $viaQuery ) = $relation -> via ;
$this -> setRelationDependencies ( $name , $viaQuery , $viaRelationName );
}
}
2026-02-27 00:03:00 +00:00
/**
* @ param mixed $newValue
* @ param mixed $oldValue
* @ return bool
* @ since 2.0 . 48
*/
private function isValueDifferent ( $newValue , $oldValue )
{
if ( is_array ( $newValue ) && is_array ( $oldValue )) {
// Only sort associative arrays
$sorter = function ( & $array ) {
if ( ArrayHelper :: isAssociative ( $array )) {
ksort ( $array );
}
};
$newValue = ArrayHelper :: recursiveSort ( $newValue , $sorter );
$oldValue = ArrayHelper :: recursiveSort ( $oldValue , $sorter );
}
return $newValue !== $oldValue ;
}
/**
* Eager loads related models for the already loaded primary models .
*
* Helps to reduce the number of queries performed against database if some related models are only used
* when a specific condition is met . For example :
*
* `` `
* $customers = Customer :: find () -> where ([ 'country_id' => 123 ]) -> all ();
* if ( Yii : app () -> getUser () -> getIdentity () -> canAccessOrders ()) {
* Customer :: loadRelationsFor ( $customers , 'orders.items' );
* }
* `` `
*
* @ param array | ActiveRecordInterface [] $models array of primary models . Each model should have the same type and can be :
* - an active record instance ;
* - active record instance represented by array ( i . e . active record was loaded using [[ ActiveQuery :: asArray ()]]) .
* @ param string | array $relationNames the names of the relations of primary models to be loaded from database . See [[ ActiveQueryInterface :: with ()]] on how to specify this argument .
* @ param bool $asArray whether to load each related model as an array or an object ( if the relation itself does not specify that ) .
* @ since 2.0 . 50
*/
public static function loadRelationsFor ( & $models , $relationNames , $asArray = false )
{
// ActiveQueryTrait::findWith() called below assumes $models array is non-empty.
if ( empty ( $models )) {
return ;
}
static :: find () -> asArray ( $asArray ) -> findWith (( array ) $relationNames , $models );
}
/**
* Eager loads related models for the already loaded primary model .
*
* Helps to reduce the number of queries performed against database if some related models are only used
* when a specific condition is met . For example :
*
* `` `
* $customer = Customer :: find () -> where ([ 'id' => 123 ]) -> one ();
* if ( Yii : app () -> getUser () -> getIdentity () -> canAccessOrders ()) {
* $customer -> loadRelations ( 'orders.items' );
* }
* `` `
*
* @ param string | array $relationNames the names of the relations of this model to be loaded from database . See [[ ActiveQueryInterface :: with ()]] on how to specify this argument .
* @ param bool $asArray whether to load each relation as an array or an object ( if the relation itself does not specify that ) .
* @ since 2.0 . 50
*/
public function loadRelations ( $relationNames , $asArray = false )
{
$models = [ $this ];
static :: loadRelationsFor ( $models , $relationNames , $asArray );
}
2026-02-25 06:59:34 +00:00
}