1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @link http://www.yiiframework.com/ |
4
|
|
|
* @copyright Copyright (c) 2008 Yii Software LLC |
5
|
|
|
* @license http://www.yiiframework.com/license/ |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace yii\db; |
9
|
|
|
|
10
|
|
|
use yii\base\InvalidConfigException; |
11
|
|
|
use yii\base\Event; |
12
|
|
|
use yii\base\Model; |
13
|
|
|
use yii\base\InvalidParamException; |
14
|
|
|
use yii\base\ModelEvent; |
15
|
|
|
use yii\base\NotSupportedException; |
16
|
|
|
use yii\base\UnknownMethodException; |
17
|
|
|
use yii\base\InvalidCallException; |
18
|
|
|
use yii\helpers\ArrayHelper; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* ActiveRecord is the base class for classes representing relational data in terms of objects. |
22
|
|
|
* |
23
|
|
|
* See [[\yii\db\ActiveRecord]] for a concrete implementation. |
24
|
|
|
* |
25
|
|
|
* @property array $dirtyAttributes The changed attribute values (name-value pairs). This property is |
26
|
|
|
* read-only. |
27
|
|
|
* @property boolean $isNewRecord Whether the record is new and should be inserted when calling [[save()]]. |
28
|
|
|
* @property array $oldAttributes The old attribute values (name-value pairs). Note that the type of this |
29
|
|
|
* property differs in getter and setter. See [[getOldAttributes()]] and [[setOldAttributes()]] for details. |
30
|
|
|
* @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is |
31
|
|
|
* returned if the primary key is composite. A string is returned otherwise (null will be returned if the key |
32
|
|
|
* value is null). This property is read-only. |
33
|
|
|
* @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if |
34
|
|
|
* the primary key is composite. A string is returned otherwise (null will be returned if the key value is null). |
35
|
|
|
* This property is read-only. |
36
|
|
|
* @property array $relatedRecords An array of related records indexed by relation names. This property is |
37
|
|
|
* read-only. |
38
|
|
|
* |
39
|
|
|
* @author Qiang Xue <[email protected]> |
40
|
|
|
* @author Carsten Brandt <[email protected]> |
41
|
|
|
* @since 2.0 |
42
|
|
|
*/ |
43
|
|
|
abstract class BaseActiveRecord extends Model implements ActiveRecordInterface |
44
|
|
|
{ |
45
|
|
|
/** |
46
|
|
|
* @event Event an event that is triggered when the record is initialized via [[init()]]. |
47
|
|
|
*/ |
48
|
|
|
const EVENT_INIT = 'init'; |
49
|
|
|
/** |
50
|
|
|
* @event Event an event that is triggered after the record is created and populated with query result. |
51
|
|
|
*/ |
52
|
|
|
const EVENT_AFTER_FIND = 'afterFind'; |
53
|
|
|
/** |
54
|
|
|
* @event ModelEvent an event that is triggered before inserting a record. |
55
|
|
|
* You may set [[ModelEvent::isValid]] to be false to stop the insertion. |
56
|
|
|
*/ |
57
|
|
|
const EVENT_BEFORE_INSERT = 'beforeInsert'; |
58
|
|
|
/** |
59
|
|
|
* @event AfterSaveEvent an event that is triggered after a record is inserted. |
60
|
|
|
*/ |
61
|
|
|
const EVENT_AFTER_INSERT = 'afterInsert'; |
62
|
|
|
/** |
63
|
|
|
* @event ModelEvent an event that is triggered before updating a record. |
64
|
|
|
* You may set [[ModelEvent::isValid]] to be false to stop the update. |
65
|
|
|
*/ |
66
|
|
|
const EVENT_BEFORE_UPDATE = 'beforeUpdate'; |
67
|
|
|
/** |
68
|
|
|
* @event AfterSaveEvent an event that is triggered after a record is updated. |
69
|
|
|
*/ |
70
|
|
|
const EVENT_AFTER_UPDATE = 'afterUpdate'; |
71
|
|
|
/** |
72
|
|
|
* @event ModelEvent an event that is triggered before deleting a record. |
73
|
|
|
* You may set [[ModelEvent::isValid]] to be false to stop the deletion. |
74
|
|
|
*/ |
75
|
|
|
const EVENT_BEFORE_DELETE = 'beforeDelete'; |
76
|
|
|
/** |
77
|
|
|
* @event Event an event that is triggered after a record is deleted. |
78
|
|
|
*/ |
79
|
|
|
const EVENT_AFTER_DELETE = 'afterDelete'; |
80
|
|
|
/** |
81
|
|
|
* @event Event an event that is triggered after a record is refreshed. |
82
|
|
|
* @since 2.0.8 |
83
|
|
|
*/ |
84
|
|
|
const EVENT_AFTER_REFRESH = 'afterRefresh'; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @var array attribute values indexed by attribute names |
88
|
|
|
*/ |
89
|
|
|
private $_attributes = []; |
90
|
|
|
/** |
91
|
|
|
* @var array|null old attribute values indexed by attribute names. |
92
|
|
|
* This is `null` if the record [[isNewRecord|is new]]. |
93
|
|
|
*/ |
94
|
|
|
private $_oldAttributes; |
95
|
|
|
/** |
96
|
|
|
* @var array related models indexed by the relation names |
97
|
|
|
*/ |
98
|
|
|
private $_related = []; |
99
|
|
|
|
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* @inheritdoc |
103
|
|
|
* @return static|null ActiveRecord instance matching the condition, or `null` if nothing matches. |
104
|
|
|
*/ |
105
|
148 |
|
public static function findOne($condition) |
106
|
|
|
{ |
107
|
148 |
|
return static::findByCondition($condition)->one(); |
|
|
|
|
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* @inheritdoc |
112
|
|
|
* @return static[] an array of ActiveRecord instances, or an empty array if nothing matches. |
113
|
|
|
*/ |
114
|
|
|
public static function findAll($condition) |
115
|
|
|
{ |
116
|
|
|
return static::findByCondition($condition)->all(); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Finds ActiveRecord instance(s) by the given condition. |
121
|
|
|
* This method is internally called by [[findOne()]] and [[findAll()]]. |
122
|
|
|
* @param mixed $condition please refer to [[findOne()]] for the explanation of this parameter |
123
|
|
|
* @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance. |
124
|
|
|
* @throws InvalidConfigException if there is no primary key defined |
125
|
|
|
* @internal |
126
|
|
|
*/ |
127
|
|
|
protected static function findByCondition($condition) |
128
|
|
|
{ |
129
|
|
|
$query = static::find(); |
130
|
|
|
|
131
|
|
|
if (!ArrayHelper::isAssociative($condition)) { |
132
|
|
|
// query by primary key |
133
|
|
|
$primaryKey = static::primaryKey(); |
134
|
|
|
if (isset($primaryKey[0])) { |
135
|
|
|
$condition = [$primaryKey[0] => $condition]; |
136
|
|
|
} else { |
137
|
|
|
throw new InvalidConfigException('"' . get_called_class() . '" must have a primary key.'); |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return $query->andWhere($condition); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Updates the whole table using the provided attribute values and conditions. |
146
|
|
|
* For example, to change the status to be 1 for all customers whose status is 2: |
147
|
|
|
* |
148
|
|
|
* ```php |
149
|
|
|
* Customer::updateAll(['status' => 1], 'status = 2'); |
150
|
|
|
* ``` |
151
|
|
|
* |
152
|
|
|
* @param array $attributes attribute values (name-value pairs) to be saved into the table |
153
|
|
|
* @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. |
154
|
|
|
* Please refer to [[Query::where()]] on how to specify this parameter. |
155
|
|
|
* @return integer the number of rows updated |
156
|
|
|
* @throws NotSupportedException if not overridden |
157
|
|
|
*/ |
158
|
|
|
public static function updateAll($attributes, $condition = '') |
159
|
|
|
{ |
160
|
|
|
throw new NotSupportedException(__METHOD__ . ' is not supported.'); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Updates the whole table using the provided counter changes and conditions. |
165
|
|
|
* For example, to increment all customers' age by 1, |
166
|
|
|
* |
167
|
|
|
* ```php |
168
|
|
|
* Customer::updateAllCounters(['age' => 1]); |
169
|
|
|
* ``` |
170
|
|
|
* |
171
|
|
|
* @param array $counters the counters to be updated (attribute name => increment value). |
172
|
|
|
* Use negative values if you want to decrement the counters. |
173
|
|
|
* @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. |
174
|
|
|
* Please refer to [[Query::where()]] on how to specify this parameter. |
175
|
|
|
* @return integer the number of rows updated |
176
|
|
|
* @throws NotSupportedException if not overrided |
177
|
|
|
*/ |
178
|
|
|
public static function updateAllCounters($counters, $condition = '') |
179
|
|
|
{ |
180
|
|
|
throw new NotSupportedException(__METHOD__ . ' is not supported.'); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Deletes rows in the table using the provided conditions. |
185
|
|
|
* WARNING: If you do not specify any condition, this method will delete ALL rows in the table. |
186
|
|
|
* |
187
|
|
|
* For example, to delete all customers whose status is 3: |
188
|
|
|
* |
189
|
|
|
* ```php |
190
|
|
|
* Customer::deleteAll('status = 3'); |
191
|
|
|
* ``` |
192
|
|
|
* |
193
|
|
|
* @param string|array $condition the conditions that will be put in the WHERE part of the DELETE SQL. |
194
|
|
|
* Please refer to [[Query::where()]] on how to specify this parameter. |
195
|
|
|
* @param array $params the parameters (name => value) to be bound to the query. |
196
|
|
|
* @return integer the number of rows deleted |
197
|
|
|
* @throws NotSupportedException if not overrided |
198
|
|
|
*/ |
199
|
|
|
public static function deleteAll($condition = '', $params = []) |
200
|
|
|
{ |
201
|
|
|
throw new NotSupportedException(__METHOD__ . ' is not supported.'); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Returns the name of the column that stores the lock version for implementing optimistic locking. |
206
|
|
|
* |
207
|
|
|
* Optimistic locking allows multiple users to access the same record for edits and avoids |
208
|
|
|
* potential conflicts. In case when a user attempts to save the record upon some staled data |
209
|
|
|
* (because another user has modified the data), a [[StaleObjectException]] exception will be thrown, |
210
|
|
|
* and the update or deletion is skipped. |
211
|
|
|
* |
212
|
|
|
* Optimistic locking is only supported by [[update()]] and [[delete()]]. |
213
|
|
|
* |
214
|
|
|
* To use Optimistic locking: |
215
|
|
|
* |
216
|
|
|
* 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`. |
217
|
|
|
* Override this method to return the name of this column. |
218
|
|
|
* 2. Add a `required` validation rule for the version column to ensure the version value is submitted. |
219
|
|
|
* 3. In the Web form that collects the user input, add a hidden field that stores |
220
|
|
|
* the lock version of the recording being updated. |
221
|
|
|
* 4. In the controller action that does the data updating, try to catch the [[StaleObjectException]] |
222
|
|
|
* and implement necessary business logic (e.g. merging the changes, prompting stated data) |
223
|
|
|
* to resolve the conflict. |
224
|
|
|
* |
225
|
|
|
* @return string the column name that stores the lock version of a table row. |
226
|
|
|
* If null is returned (default implemented), optimistic locking will not be supported. |
227
|
|
|
*/ |
228
|
22 |
|
public function optimisticLock() |
229
|
|
|
{ |
230
|
22 |
|
return null; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* PHP getter magic method. |
235
|
|
|
* This method is overridden so that attributes and related objects can be accessed like properties. |
236
|
|
|
* |
237
|
|
|
* @param string $name property name |
238
|
|
|
* @throws \yii\base\InvalidParamException if relation name is wrong |
239
|
|
|
* @return mixed property value |
240
|
|
|
* @see getAttribute() |
241
|
|
|
*/ |
242
|
239 |
|
public function __get($name) |
243
|
|
|
{ |
244
|
239 |
|
if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) { |
245
|
229 |
|
return $this->_attributes[$name]; |
246
|
131 |
|
} elseif ($this->hasAttribute($name)) { |
247
|
23 |
|
return null; |
248
|
|
|
} else { |
249
|
116 |
|
if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) { |
250
|
64 |
|
return $this->_related[$name]; |
251
|
|
|
} |
252
|
82 |
|
$value = parent::__get($name); |
253
|
82 |
|
if ($value instanceof ActiveQueryInterface) { |
254
|
48 |
|
return $this->_related[$name] = $value->findFor($name, $this); |
255
|
|
|
} else { |
256
|
40 |
|
return $value; |
257
|
|
|
} |
258
|
|
|
} |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* PHP setter magic method. |
263
|
|
|
* This method is overridden so that AR attributes can be accessed like properties. |
264
|
|
|
* @param string $name property name |
265
|
|
|
* @param mixed $value property value |
266
|
|
|
*/ |
267
|
103 |
|
public function __set($name, $value) |
268
|
|
|
{ |
269
|
103 |
|
if ($this->hasAttribute($name)) { |
270
|
103 |
|
$this->_attributes[$name] = $value; |
271
|
103 |
|
} else { |
272
|
|
|
parent::__set($name, $value); |
273
|
|
|
} |
274
|
103 |
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Checks if a property value is null. |
278
|
|
|
* This method overrides the parent implementation by checking if the named attribute is null or not. |
279
|
|
|
* @param string $name the property name or the event name |
280
|
|
|
* @return boolean whether the property value is null |
281
|
|
|
*/ |
282
|
35 |
|
public function __isset($name) |
283
|
|
|
{ |
284
|
|
|
try { |
285
|
35 |
|
return $this->__get($name) !== null; |
286
|
|
|
} catch (\Exception $e) { |
287
|
|
|
return false; |
288
|
|
|
} |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Sets a component property to be null. |
293
|
|
|
* This method overrides the parent implementation by clearing |
294
|
|
|
* the specified attribute value. |
295
|
|
|
* @param string $name the property name or the event name |
296
|
|
|
*/ |
297
|
6 |
|
public function __unset($name) |
298
|
|
|
{ |
299
|
6 |
|
if ($this->hasAttribute($name)) { |
300
|
|
|
unset($this->_attributes[$name]); |
301
|
6 |
|
} elseif (array_key_exists($name, $this->_related)) { |
302
|
6 |
|
unset($this->_related[$name]); |
303
|
6 |
|
} elseif ($this->getRelation($name, false) === null) { |
304
|
|
|
parent::__unset($name); |
305
|
|
|
} |
306
|
6 |
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Declares a `has-one` relation. |
310
|
|
|
* The declaration is returned in terms of a relational [[ActiveQuery]] instance |
311
|
|
|
* through which the related record can be queried and retrieved back. |
312
|
|
|
* |
313
|
|
|
* A `has-one` relation means that there is at most one related record matching |
314
|
|
|
* the criteria set by this relation, e.g., a customer has one country. |
315
|
|
|
* |
316
|
|
|
* For example, to declare the `country` relation for `Customer` class, we can write |
317
|
|
|
* the following code in the `Customer` class: |
318
|
|
|
* |
319
|
|
|
* ```php |
320
|
|
|
* public function getCountry() |
321
|
|
|
* { |
322
|
|
|
* return $this->hasOne(Country::class, ['id' => 'country_id']); |
323
|
|
|
* } |
324
|
|
|
* ``` |
325
|
|
|
* |
326
|
|
|
* Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name |
327
|
|
|
* in the related class `Country`, while the 'country_id' value refers to an attribute name |
328
|
|
|
* in the current AR class. |
329
|
|
|
* |
330
|
|
|
* Call methods declared in [[ActiveQuery]] to further customize the relation. |
331
|
|
|
* |
332
|
|
|
* @param string $class the class name of the related record |
333
|
|
|
* @param array $link the primary-foreign key constraint. The keys of the array refer to |
334
|
|
|
* the attributes of the record associated with the `$class` model, while the values of the |
335
|
|
|
* array refer to the corresponding attributes in **this** AR class. |
336
|
|
|
* @return ActiveQueryInterface the relational query object. |
337
|
|
|
*/ |
338
|
39 |
|
public function hasOne($class, $link) |
339
|
|
|
{ |
340
|
|
|
/* @var $class ActiveRecordInterface */ |
341
|
|
|
/* @var $query ActiveQuery */ |
342
|
39 |
|
$query = $class::find(); |
343
|
39 |
|
$query->primaryModel = $this; |
|
|
|
|
344
|
39 |
|
$query->link = $link; |
345
|
39 |
|
$query->multiple = false; |
346
|
39 |
|
return $query; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* Declares a `has-many` relation. |
351
|
|
|
* The declaration is returned in terms of a relational [[ActiveQuery]] instance |
352
|
|
|
* through which the related record can be queried and retrieved back. |
353
|
|
|
* |
354
|
|
|
* A `has-many` relation means that there are multiple related records matching |
355
|
|
|
* the criteria set by this relation, e.g., a customer has many orders. |
356
|
|
|
* |
357
|
|
|
* For example, to declare the `orders` relation for `Customer` class, we can write |
358
|
|
|
* the following code in the `Customer` class: |
359
|
|
|
* |
360
|
|
|
* ```php |
361
|
|
|
* public function getOrders() |
362
|
|
|
* { |
363
|
|
|
* return $this->hasMany(Order::class, ['customer_id' => 'id']); |
364
|
|
|
* } |
365
|
|
|
* ``` |
366
|
|
|
* |
367
|
|
|
* Note that in the above, the 'customer_id' key in the `$link` parameter refers to |
368
|
|
|
* an attribute name in the related class `Order`, while the 'id' value refers to |
369
|
|
|
* an attribute name in the current AR class. |
370
|
|
|
* |
371
|
|
|
* Call methods declared in [[ActiveQuery]] to further customize the relation. |
372
|
|
|
* |
373
|
|
|
* @param string $class the class name of the related record |
374
|
|
|
* @param array $link the primary-foreign key constraint. The keys of the array refer to |
375
|
|
|
* the attributes of the record associated with the `$class` model, while the values of the |
376
|
|
|
* array refer to the corresponding attributes in **this** AR class. |
377
|
|
|
* @return ActiveQueryInterface the relational query object. |
378
|
|
|
*/ |
379
|
115 |
|
public function hasMany($class, $link) |
380
|
|
|
{ |
381
|
|
|
/* @var $class ActiveRecordInterface */ |
382
|
|
|
/* @var $query ActiveQuery */ |
383
|
115 |
|
$query = $class::find(); |
384
|
115 |
|
$query->primaryModel = $this; |
|
|
|
|
385
|
115 |
|
$query->link = $link; |
386
|
115 |
|
$query->multiple = true; |
387
|
115 |
|
return $query; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* Populates the named relation with the related records. |
392
|
|
|
* Note that this method does not check if the relation exists or not. |
393
|
|
|
* @param string $name the relation name (case-sensitive) |
394
|
|
|
* @param ActiveRecordInterface|array|null $records the related records to be populated into the relation. |
395
|
|
|
*/ |
396
|
85 |
|
public function populateRelation($name, $records) |
397
|
|
|
{ |
398
|
85 |
|
$this->_related[$name] = $records; |
399
|
85 |
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* Check whether the named relation has been populated with records. |
403
|
|
|
* @param string $name the relation name (case-sensitive) |
404
|
|
|
* @return boolean whether relation has been populated with records. |
405
|
|
|
*/ |
406
|
40 |
|
public function isRelationPopulated($name) |
407
|
|
|
{ |
408
|
40 |
|
return array_key_exists($name, $this->_related); |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
/** |
412
|
|
|
* Returns all populated related records. |
413
|
|
|
* @return array an array of related records indexed by relation names. |
414
|
|
|
*/ |
415
|
6 |
|
public function getRelatedRecords() |
416
|
|
|
{ |
417
|
6 |
|
return $this->_related; |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
/** |
421
|
|
|
* Returns a value indicating whether the model has an attribute with the specified name. |
422
|
|
|
* @param string $name the name of the attribute |
423
|
|
|
* @return boolean whether the model has an attribute with the specified name. |
424
|
|
|
*/ |
425
|
184 |
|
public function hasAttribute($name) |
426
|
|
|
{ |
427
|
184 |
|
return isset($this->_attributes[$name]) || in_array($name, $this->attributes()); |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* Returns the named attribute value. |
432
|
|
|
* If this record is the result of a query and the attribute is not loaded, |
433
|
|
|
* null will be returned. |
434
|
|
|
* @param string $name the attribute name |
435
|
|
|
* @return mixed the attribute value. Null if the attribute is not set or does not exist. |
436
|
|
|
* @see hasAttribute() |
437
|
|
|
*/ |
438
|
|
|
public function getAttribute($name) |
439
|
|
|
{ |
440
|
|
|
return isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* Sets the named attribute value. |
445
|
|
|
* @param string $name the attribute name |
446
|
|
|
* @param mixed $value the attribute value. |
447
|
|
|
* @throws InvalidParamException if the named attribute does not exist. |
448
|
|
|
* @see hasAttribute() |
449
|
|
|
*/ |
450
|
51 |
|
public function setAttribute($name, $value) |
451
|
|
|
{ |
452
|
51 |
|
if ($this->hasAttribute($name)) { |
453
|
51 |
|
$this->_attributes[$name] = $value; |
454
|
51 |
|
} else { |
455
|
|
|
throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); |
456
|
|
|
} |
457
|
51 |
|
} |
458
|
|
|
|
459
|
|
|
/** |
460
|
|
|
* Returns the old attribute values. |
461
|
|
|
* @return array the old attribute values (name-value pairs) |
462
|
|
|
*/ |
463
|
|
|
public function getOldAttributes() |
464
|
|
|
{ |
465
|
|
|
return $this->_oldAttributes === null ? [] : $this->_oldAttributes; |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
/** |
469
|
|
|
* Sets the old attribute values. |
470
|
|
|
* All existing old attribute values will be discarded. |
471
|
|
|
* @param array|null $values old attribute values to be set. |
472
|
|
|
* If set to `null` this record is considered to be [[isNewRecord|new]]. |
473
|
|
|
*/ |
474
|
67 |
|
public function setOldAttributes($values) |
475
|
|
|
{ |
476
|
67 |
|
$this->_oldAttributes = $values; |
477
|
67 |
|
} |
478
|
|
|
|
479
|
|
|
/** |
480
|
|
|
* Returns the old value of the named attribute. |
481
|
|
|
* If this record is the result of a query and the attribute is not loaded, |
482
|
|
|
* null will be returned. |
483
|
|
|
* @param string $name the attribute name |
484
|
|
|
* @return mixed the old attribute value. Null if the attribute is not loaded before |
485
|
|
|
* or does not exist. |
486
|
|
|
* @see hasAttribute() |
487
|
|
|
*/ |
488
|
|
|
public function getOldAttribute($name) |
489
|
|
|
{ |
490
|
|
|
return isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
/** |
494
|
|
|
* Sets the old value of the named attribute. |
495
|
|
|
* @param string $name the attribute name |
496
|
|
|
* @param mixed $value the old attribute value. |
497
|
|
|
* @throws InvalidParamException if the named attribute does not exist. |
498
|
|
|
* @see hasAttribute() |
499
|
|
|
*/ |
500
|
|
|
public function setOldAttribute($name, $value) |
501
|
|
|
{ |
502
|
|
|
if (isset($this->_oldAttributes[$name]) || $this->hasAttribute($name)) { |
503
|
|
|
$this->_oldAttributes[$name] = $value; |
504
|
|
|
} else { |
505
|
|
|
throw new InvalidParamException(get_class($this) . ' has no attribute named "' . $name . '".'); |
506
|
|
|
} |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
/** |
510
|
|
|
* Marks an attribute dirty. |
511
|
|
|
* This method may be called to force updating a record when calling [[update()]], |
512
|
|
|
* even if there is no change being made to the record. |
513
|
|
|
* @param string $name the attribute name |
514
|
|
|
*/ |
515
|
|
|
public function markAttributeDirty($name) |
516
|
|
|
{ |
517
|
|
|
unset($this->_oldAttributes[$name]); |
518
|
|
|
} |
519
|
|
|
|
520
|
|
|
/** |
521
|
|
|
* Returns a value indicating whether the named attribute has been changed. |
522
|
|
|
* @param string $name the name of the attribute. |
523
|
|
|
* @param boolean $identical whether the comparison of new and old value is made for |
524
|
|
|
* identical values using `===`, defaults to `true`. Otherwise `==` is used for comparison. |
525
|
|
|
* This parameter is available since version 2.0.4. |
526
|
|
|
* @return boolean whether the attribute has been changed |
527
|
|
|
*/ |
528
|
1 |
|
public function isAttributeChanged($name, $identical = true) |
529
|
|
|
{ |
530
|
1 |
|
if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) { |
531
|
1 |
|
if ($identical) { |
532
|
1 |
|
return $this->_attributes[$name] !== $this->_oldAttributes[$name]; |
533
|
|
|
} else { |
534
|
|
|
return $this->_attributes[$name] != $this->_oldAttributes[$name]; |
535
|
|
|
} |
536
|
|
|
} else { |
537
|
|
|
return isset($this->_attributes[$name]) || isset($this->_oldAttributes[$name]); |
538
|
|
|
} |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* Returns the attribute values that have been modified since they are loaded or saved most recently. |
543
|
|
|
* |
544
|
|
|
* The comparison of new and old values is made for identical values using `===`. |
545
|
|
|
* |
546
|
|
|
* @param string[]|null $names the names of the attributes whose values may be returned if they are |
547
|
|
|
* changed recently. If null, [[attributes()]] will be used. |
548
|
|
|
* @return array the changed attribute values (name-value pairs) |
549
|
|
|
*/ |
550
|
77 |
|
public function getDirtyAttributes($names = null) |
551
|
|
|
{ |
552
|
77 |
|
if ($names === null) { |
553
|
74 |
|
$names = $this->attributes(); |
554
|
74 |
|
} |
555
|
77 |
|
$names = array_flip($names); |
556
|
77 |
|
$attributes = []; |
557
|
77 |
|
if ($this->_oldAttributes === null) { |
558
|
61 |
|
foreach ($this->_attributes as $name => $value) { |
559
|
58 |
|
if (isset($names[$name])) { |
560
|
58 |
|
$attributes[$name] = $value; |
561
|
58 |
|
} |
562
|
61 |
|
} |
563
|
61 |
|
} else { |
564
|
26 |
|
foreach ($this->_attributes as $name => $value) { |
565
|
26 |
|
if (isset($names[$name]) && (!array_key_exists($name, $this->_oldAttributes) || $value !== $this->_oldAttributes[$name])) { |
566
|
25 |
|
$attributes[$name] = $value; |
567
|
25 |
|
} |
568
|
26 |
|
} |
569
|
|
|
} |
570
|
77 |
|
return $attributes; |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
/** |
574
|
|
|
* Saves the current record. |
575
|
|
|
* |
576
|
|
|
* This method will call [[insert()]] when [[isNewRecord]] is true, or [[update()]] |
577
|
|
|
* when [[isNewRecord]] is false. |
578
|
|
|
* |
579
|
|
|
* For example, to save a customer record: |
580
|
|
|
* |
581
|
|
|
* ```php |
582
|
|
|
* $customer = new Customer; // or $customer = Customer::findOne($id); |
583
|
|
|
* $customer->name = $name; |
584
|
|
|
* $customer->email = $email; |
585
|
|
|
* $customer->save(); |
586
|
|
|
* ``` |
587
|
|
|
* |
588
|
|
|
* @param boolean $runValidation whether to perform validation (calling [[validate()]]) |
589
|
|
|
* before saving the record. Defaults to `true`. If the validation fails, the record |
590
|
|
|
* will not be saved to the database and this method will return `false`. |
591
|
|
|
* @param array $attributeNames list of attribute names that need to be saved. Defaults to null, |
592
|
|
|
* meaning all attributes that are loaded from DB will be saved. |
593
|
|
|
* @return boolean whether the saving succeeded (i.e. no validation errors occurred). |
594
|
|
|
*/ |
595
|
74 |
|
public function save($runValidation = true, $attributeNames = null) |
596
|
|
|
{ |
597
|
74 |
|
if ($this->getIsNewRecord()) { |
598
|
61 |
|
return $this->insert($runValidation, $attributeNames); |
599
|
|
|
} else { |
600
|
23 |
|
return $this->update($runValidation, $attributeNames) !== false; |
601
|
|
|
} |
602
|
|
|
} |
603
|
|
|
|
604
|
|
|
/** |
605
|
|
|
* Saves the changes to this active record into the associated database table. |
606
|
|
|
* |
607
|
|
|
* This method performs the following steps in order: |
608
|
|
|
* |
609
|
|
|
* 1. call [[beforeValidate()]] when `$runValidation` is true. If [[beforeValidate()]] |
610
|
|
|
* returns `false`, the rest of the steps will be skipped; |
611
|
|
|
* 2. call [[afterValidate()]] when `$runValidation` is true. If validation |
612
|
|
|
* failed, the rest of the steps will be skipped; |
613
|
|
|
* 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`, |
614
|
|
|
* the rest of the steps will be skipped; |
615
|
|
|
* 4. save the record into database. If this fails, it will skip the rest of the steps; |
616
|
|
|
* 5. call [[afterSave()]]; |
617
|
|
|
* |
618
|
|
|
* In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], |
619
|
|
|
* [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]] |
620
|
|
|
* will be raised by the corresponding methods. |
621
|
|
|
* |
622
|
|
|
* Only the [[dirtyAttributes|changed attribute values]] will be saved into database. |
623
|
|
|
* |
624
|
|
|
* For example, to update a customer record: |
625
|
|
|
* |
626
|
|
|
* ```php |
627
|
|
|
* $customer = Customer::findOne($id); |
628
|
|
|
* $customer->name = $name; |
629
|
|
|
* $customer->email = $email; |
630
|
|
|
* $customer->update(); |
631
|
|
|
* ``` |
632
|
|
|
* |
633
|
|
|
* Note that it is possible the update does not affect any row in the table. |
634
|
|
|
* In this case, this method will return 0. For this reason, you should use the following |
635
|
|
|
* code to check if update() is successful or not: |
636
|
|
|
* |
637
|
|
|
* ```php |
638
|
|
|
* if ($customer->update() !== false) { |
639
|
|
|
* // update successful |
640
|
|
|
* } else { |
641
|
|
|
* // update failed |
642
|
|
|
* } |
643
|
|
|
* ``` |
644
|
|
|
* |
645
|
|
|
* @param boolean $runValidation whether to perform validation (calling [[validate()]]) |
646
|
|
|
* before saving the record. Defaults to `true`. If the validation fails, the record |
647
|
|
|
* will not be saved to the database and this method will return `false`. |
648
|
|
|
* @param array $attributeNames list of attribute names that need to be saved. Defaults to null, |
649
|
|
|
* meaning all attributes that are loaded from DB will be saved. |
650
|
|
|
* @return integer|boolean the number of rows affected, or false if validation fails |
651
|
|
|
* or [[beforeSave()]] stops the updating process. |
652
|
|
|
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data |
653
|
|
|
* being updated is outdated. |
654
|
|
|
* @throws Exception in case update failed. |
655
|
|
|
*/ |
656
|
|
|
public function update($runValidation = true, $attributeNames = null) |
657
|
|
|
{ |
658
|
|
|
if ($runValidation && !$this->validate($attributeNames)) { |
659
|
|
|
return false; |
660
|
|
|
} |
661
|
|
|
return $this->updateInternal($attributeNames); |
662
|
|
|
} |
663
|
|
|
|
664
|
|
|
/** |
665
|
|
|
* Updates the specified attributes. |
666
|
|
|
* |
667
|
|
|
* This method is a shortcut to [[update()]] when data validation is not needed |
668
|
|
|
* and only a small set attributes need to be updated. |
669
|
|
|
* |
670
|
|
|
* You may specify the attributes to be updated as name list or name-value pairs. |
671
|
|
|
* If the latter, the corresponding attribute values will be modified accordingly. |
672
|
|
|
* The method will then save the specified attributes into database. |
673
|
|
|
* |
674
|
|
|
* Note that this method will **not** perform data validation and will **not** trigger events. |
675
|
|
|
* |
676
|
|
|
* @param array $attributes the attributes (names or name-value pairs) to be updated |
677
|
|
|
* @return integer the number of rows affected. |
678
|
|
|
*/ |
679
|
3 |
|
public function updateAttributes($attributes) |
680
|
|
|
{ |
681
|
3 |
|
$attrs = []; |
682
|
3 |
|
foreach ($attributes as $name => $value) { |
683
|
3 |
|
if (is_int($name)) { |
684
|
3 |
|
$attrs[] = $value; |
685
|
3 |
|
} else { |
686
|
3 |
|
$this->$name = $value; |
687
|
3 |
|
$attrs[] = $name; |
688
|
|
|
} |
689
|
3 |
|
} |
690
|
|
|
|
691
|
3 |
|
$values = $this->getDirtyAttributes($attrs); |
692
|
3 |
|
if (empty($values)) { |
693
|
|
|
return 0; |
694
|
|
|
} |
695
|
|
|
|
696
|
3 |
|
$rows = static::updateAll($values, $this->getOldPrimaryKey(true)); |
697
|
|
|
|
698
|
3 |
|
foreach ($values as $name => $value) { |
699
|
3 |
|
$this->_oldAttributes[$name] = $this->_attributes[$name]; |
700
|
3 |
|
} |
701
|
|
|
|
702
|
3 |
|
return $rows; |
703
|
|
|
} |
704
|
|
|
|
705
|
|
|
/** |
706
|
|
|
* @see update() |
707
|
|
|
* @param array $attributes attributes to update |
708
|
|
|
* @return integer number of rows updated |
709
|
|
|
* @throws StaleObjectException |
710
|
|
|
*/ |
711
|
23 |
|
protected function updateInternal($attributes = null) |
712
|
|
|
{ |
713
|
23 |
|
if (!$this->beforeSave(false)) { |
714
|
|
|
return false; |
715
|
|
|
} |
716
|
23 |
|
$values = $this->getDirtyAttributes($attributes); |
|
|
|
|
717
|
23 |
|
if (empty($values)) { |
718
|
2 |
|
$this->afterSave(false, $values); |
719
|
2 |
|
return 0; |
720
|
|
|
} |
721
|
22 |
|
$condition = $this->getOldPrimaryKey(true); |
722
|
22 |
|
$lock = $this->optimisticLock(); |
723
|
22 |
|
if ($lock !== null) { |
724
|
3 |
|
$values[$lock] = $this->$lock + 1; |
725
|
3 |
|
$condition[$lock] = $this->$lock; |
726
|
3 |
|
} |
727
|
|
|
// We do not check the return value of updateAll() because it's possible |
728
|
|
|
// that the UPDATE statement doesn't change anything and thus returns 0. |
729
|
22 |
|
$rows = static::updateAll($values, $condition); |
730
|
|
|
|
731
|
22 |
|
if ($lock !== null && !$rows) { |
732
|
3 |
|
throw new StaleObjectException('The object being updated is outdated.'); |
733
|
|
|
} |
734
|
|
|
|
735
|
22 |
|
if (isset($values[$lock])) { |
736
|
3 |
|
$this->$lock = $values[$lock]; |
737
|
3 |
|
} |
738
|
|
|
|
739
|
22 |
|
$changedAttributes = []; |
740
|
22 |
|
foreach ($values as $name => $value) { |
741
|
22 |
|
$changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; |
742
|
22 |
|
$this->_oldAttributes[$name] = $value; |
743
|
22 |
|
} |
744
|
22 |
|
$this->afterSave(false, $changedAttributes); |
745
|
|
|
|
746
|
22 |
|
return $rows; |
747
|
|
|
} |
748
|
|
|
|
749
|
|
|
/** |
750
|
|
|
* Updates one or several counter columns for the current AR object. |
751
|
|
|
* Note that this method differs from [[updateAllCounters()]] in that it only |
752
|
|
|
* saves counters for the current AR object. |
753
|
|
|
* |
754
|
|
|
* An example usage is as follows: |
755
|
|
|
* |
756
|
|
|
* ```php |
757
|
|
|
* $post = Post::findOne($id); |
758
|
|
|
* $post->updateCounters(['view_count' => 1]); |
759
|
|
|
* ``` |
760
|
|
|
* |
761
|
|
|
* @param array $counters the counters to be updated (attribute name => increment value) |
762
|
|
|
* Use negative values if you want to decrement the counters. |
763
|
|
|
* @return boolean whether the saving is successful |
764
|
|
|
* @see updateAllCounters() |
765
|
|
|
*/ |
766
|
6 |
|
public function updateCounters($counters) |
767
|
|
|
{ |
768
|
6 |
|
if (static::updateAllCounters($counters, $this->getOldPrimaryKey(true)) > 0) { |
769
|
6 |
|
foreach ($counters as $name => $value) { |
770
|
6 |
|
if (!isset($this->_attributes[$name])) { |
771
|
3 |
|
$this->_attributes[$name] = $value; |
772
|
3 |
|
} else { |
773
|
3 |
|
$this->_attributes[$name] += $value; |
774
|
|
|
} |
775
|
6 |
|
$this->_oldAttributes[$name] = $this->_attributes[$name]; |
776
|
6 |
|
} |
777
|
6 |
|
return true; |
778
|
|
|
} else { |
779
|
|
|
return false; |
780
|
|
|
} |
781
|
|
|
} |
782
|
|
|
|
783
|
|
|
/** |
784
|
|
|
* Deletes the table row corresponding to this active record. |
785
|
|
|
* |
786
|
|
|
* This method performs the following steps in order: |
787
|
|
|
* |
788
|
|
|
* 1. call [[beforeDelete()]]. If the method returns false, it will skip the |
789
|
|
|
* rest of the steps; |
790
|
|
|
* 2. delete the record from the database; |
791
|
|
|
* 3. call [[afterDelete()]]. |
792
|
|
|
* |
793
|
|
|
* In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] |
794
|
|
|
* will be raised by the corresponding methods. |
795
|
|
|
* |
796
|
|
|
* @return integer|false the number of rows deleted, or false if the deletion is unsuccessful for some reason. |
797
|
|
|
* Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. |
798
|
|
|
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data |
799
|
|
|
* being deleted is outdated. |
800
|
|
|
* @throws Exception in case delete failed. |
801
|
|
|
*/ |
802
|
|
|
public function delete() |
803
|
|
|
{ |
804
|
|
|
$result = false; |
805
|
|
|
if ($this->beforeDelete()) { |
806
|
|
|
// we do not check the return value of deleteAll() because it's possible |
807
|
|
|
// the record is already deleted in the database and thus the method will return 0 |
808
|
|
|
$condition = $this->getOldPrimaryKey(true); |
809
|
|
|
$lock = $this->optimisticLock(); |
810
|
|
|
if ($lock !== null) { |
811
|
|
|
$condition[$lock] = $this->$lock; |
812
|
|
|
} |
813
|
|
|
$result = static::deleteAll($condition); |
814
|
|
|
if ($lock !== null && !$result) { |
815
|
|
|
throw new StaleObjectException('The object being deleted is outdated.'); |
816
|
|
|
} |
817
|
|
|
$this->_oldAttributes = null; |
818
|
|
|
$this->afterDelete(); |
819
|
|
|
} |
820
|
|
|
|
821
|
|
|
return $result; |
822
|
|
|
} |
823
|
|
|
|
824
|
|
|
/** |
825
|
|
|
* Returns a value indicating whether the current record is new. |
826
|
|
|
* @return boolean whether the record is new and should be inserted when calling [[save()]]. |
827
|
|
|
*/ |
828
|
104 |
|
public function getIsNewRecord() |
829
|
|
|
{ |
830
|
104 |
|
return $this->_oldAttributes === null; |
831
|
|
|
} |
832
|
|
|
|
833
|
|
|
/** |
834
|
|
|
* Sets the value indicating whether the record is new. |
835
|
|
|
* @param boolean $value whether the record is new and should be inserted when calling [[save()]]. |
836
|
|
|
* @see getIsNewRecord() |
837
|
|
|
*/ |
838
|
|
|
public function setIsNewRecord($value) |
839
|
|
|
{ |
840
|
|
|
$this->_oldAttributes = $value ? null : $this->_attributes; |
841
|
|
|
} |
842
|
|
|
|
843
|
|
|
/** |
844
|
|
|
* Initializes the object. |
845
|
|
|
* This method is called at the end of the constructor. |
846
|
|
|
* The default implementation will trigger an [[EVENT_INIT]] event. |
847
|
|
|
* If you override this method, make sure you call the parent implementation at the end |
848
|
|
|
* to ensure triggering of the event. |
849
|
|
|
*/ |
850
|
260 |
|
public function init() |
851
|
|
|
{ |
852
|
260 |
|
parent::init(); |
853
|
260 |
|
$this->trigger(self::EVENT_INIT); |
854
|
260 |
|
} |
855
|
|
|
|
856
|
|
|
/** |
857
|
|
|
* This method is called when the AR object is created and populated with the query result. |
858
|
|
|
* The default implementation will trigger an [[EVENT_AFTER_FIND]] event. |
859
|
|
|
* When overriding this method, make sure you call the parent implementation to ensure the |
860
|
|
|
* event is triggered. |
861
|
|
|
*/ |
862
|
225 |
|
public function afterFind() |
863
|
|
|
{ |
864
|
225 |
|
$this->trigger(self::EVENT_AFTER_FIND); |
865
|
225 |
|
} |
866
|
|
|
|
867
|
|
|
/** |
868
|
|
|
* This method is called at the beginning of inserting or updating a record. |
869
|
|
|
* The default implementation will trigger an [[EVENT_BEFORE_INSERT]] event when `$insert` is true, |
870
|
|
|
* or an [[EVENT_BEFORE_UPDATE]] event if `$insert` is false. |
871
|
|
|
* When overriding this method, make sure you call the parent implementation like the following: |
872
|
|
|
* |
873
|
|
|
* ```php |
874
|
|
|
* public function beforeSave($insert) |
875
|
|
|
* { |
876
|
|
|
* if (parent::beforeSave($insert)) { |
877
|
|
|
* // ...custom code here... |
878
|
|
|
* return true; |
879
|
|
|
* } else { |
880
|
|
|
* return false; |
881
|
|
|
* } |
882
|
|
|
* } |
883
|
|
|
* ``` |
884
|
|
|
* |
885
|
|
|
* @param boolean $insert whether this method called while inserting a record. |
886
|
|
|
* If false, it means the method is called while updating a record. |
887
|
|
|
* @return boolean whether the insertion or updating should continue. |
888
|
|
|
* If false, the insertion or updating will be cancelled. |
889
|
|
|
*/ |
890
|
74 |
|
public function beforeSave($insert) |
891
|
|
|
{ |
892
|
74 |
|
$event = new ModelEvent; |
893
|
74 |
|
$this->trigger($insert ? self::EVENT_BEFORE_INSERT : self::EVENT_BEFORE_UPDATE, $event); |
894
|
|
|
|
895
|
74 |
|
return $event->isValid; |
896
|
|
|
} |
897
|
|
|
|
898
|
|
|
/** |
899
|
|
|
* This method is called at the end of inserting or updating a record. |
900
|
|
|
* The default implementation will trigger an [[EVENT_AFTER_INSERT]] event when `$insert` is true, |
901
|
|
|
* or an [[EVENT_AFTER_UPDATE]] event if `$insert` is false. The event class used is [[AfterSaveEvent]]. |
902
|
|
|
* When overriding this method, make sure you call the parent implementation so that |
903
|
|
|
* the event is triggered. |
904
|
|
|
* @param boolean $insert whether this method called while inserting a record. |
905
|
|
|
* If false, it means the method is called while updating a record. |
906
|
|
|
* @param array $changedAttributes The old values of attributes that had changed and were saved. |
907
|
|
|
* You can use this parameter to take action based on the changes made for example send an email |
908
|
|
|
* when the password had changed or implement audit trail that tracks all the changes. |
909
|
|
|
* `$changedAttributes` gives you the old attribute values while the active record (`$this`) has |
910
|
|
|
* already the new, updated values. |
911
|
|
|
*/ |
912
|
74 |
|
public function afterSave($insert, $changedAttributes) |
913
|
|
|
{ |
914
|
74 |
|
$this->trigger($insert ? self::EVENT_AFTER_INSERT : self::EVENT_AFTER_UPDATE, new AfterSaveEvent([ |
915
|
74 |
|
'changedAttributes' => $changedAttributes, |
916
|
74 |
|
])); |
917
|
74 |
|
} |
918
|
|
|
|
919
|
|
|
/** |
920
|
|
|
* This method is invoked before deleting a record. |
921
|
|
|
* The default implementation raises the [[EVENT_BEFORE_DELETE]] event. |
922
|
|
|
* When overriding this method, make sure you call the parent implementation like the following: |
923
|
|
|
* |
924
|
|
|
* ```php |
925
|
|
|
* public function beforeDelete() |
926
|
|
|
* { |
927
|
|
|
* if (parent::beforeDelete()) { |
928
|
|
|
* // ...custom code here... |
929
|
|
|
* return true; |
930
|
|
|
* } else { |
931
|
|
|
* return false; |
932
|
|
|
* } |
933
|
|
|
* } |
934
|
|
|
* ``` |
935
|
|
|
* |
936
|
|
|
* @return boolean whether the record should be deleted. Defaults to true. |
937
|
|
|
*/ |
938
|
6 |
|
public function beforeDelete() |
939
|
|
|
{ |
940
|
6 |
|
$event = new ModelEvent; |
941
|
6 |
|
$this->trigger(self::EVENT_BEFORE_DELETE, $event); |
942
|
|
|
|
943
|
6 |
|
return $event->isValid; |
944
|
|
|
} |
945
|
|
|
|
946
|
|
|
/** |
947
|
|
|
* This method is invoked after deleting a record. |
948
|
|
|
* The default implementation raises the [[EVENT_AFTER_DELETE]] event. |
949
|
|
|
* You may override this method to do postprocessing after the record is deleted. |
950
|
|
|
* Make sure you call the parent implementation so that the event is raised properly. |
951
|
|
|
*/ |
952
|
6 |
|
public function afterDelete() |
953
|
|
|
{ |
954
|
6 |
|
$this->trigger(self::EVENT_AFTER_DELETE); |
955
|
6 |
|
} |
956
|
|
|
|
957
|
|
|
/** |
958
|
|
|
* Repopulates this active record with the latest data. |
959
|
|
|
* |
960
|
|
|
* If the refresh is successful, an [[EVENT_AFTER_REFRESH]] event will be triggered. |
961
|
|
|
* This event is available since version 2.0.8. |
962
|
|
|
* |
963
|
|
|
* @return boolean whether the row still exists in the database. If true, the latest data |
964
|
|
|
* will be populated to this active record. Otherwise, this record will remain unchanged. |
965
|
|
|
*/ |
966
|
17 |
|
public function refresh() |
967
|
|
|
{ |
968
|
|
|
/* @var $record BaseActiveRecord */ |
969
|
17 |
|
$record = static::findOne($this->getPrimaryKey(true)); |
970
|
17 |
|
if ($record === null) { |
971
|
3 |
|
return false; |
972
|
|
|
} |
973
|
17 |
|
foreach ($this->attributes() as $name) { |
974
|
17 |
|
$this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null; |
975
|
17 |
|
} |
976
|
17 |
|
$this->_oldAttributes = $this->_attributes; |
977
|
17 |
|
$this->_related = []; |
978
|
17 |
|
$this->afterRefresh(); |
979
|
|
|
|
980
|
17 |
|
return true; |
981
|
|
|
} |
982
|
|
|
|
983
|
|
|
/** |
984
|
|
|
* This method is called when the AR object is refreshed. |
985
|
|
|
* The default implementation will trigger an [[EVENT_AFTER_REFRESH]] event. |
986
|
|
|
* When overriding this method, make sure you call the parent implementation to ensure the |
987
|
|
|
* event is triggered. |
988
|
|
|
* @since 2.0.8 |
989
|
|
|
*/ |
990
|
17 |
|
public function afterRefresh() |
991
|
|
|
{ |
992
|
17 |
|
$this->trigger(self::EVENT_AFTER_REFRESH); |
993
|
17 |
|
} |
994
|
|
|
|
995
|
|
|
/** |
996
|
|
|
* Returns a value indicating whether the given active record is the same as the current one. |
997
|
|
|
* The comparison is made by comparing the table names and the primary key values of the two active records. |
998
|
|
|
* If one of the records [[isNewRecord|is new]] they are also considered not equal. |
999
|
|
|
* @param ActiveRecordInterface $record record to compare to |
1000
|
|
|
* @return boolean whether the two active records refer to the same row in the same database table. |
1001
|
|
|
*/ |
1002
|
|
|
public function equals($record) |
1003
|
|
|
{ |
1004
|
|
|
if ($this->getIsNewRecord() || $record->getIsNewRecord()) { |
1005
|
|
|
return false; |
1006
|
|
|
} |
1007
|
|
|
|
1008
|
|
|
return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey(); |
1009
|
|
|
} |
1010
|
|
|
|
1011
|
|
|
/** |
1012
|
|
|
* Returns the primary key value(s). |
1013
|
|
|
* @param boolean $asArray whether to return the primary key value as an array. If true, |
1014
|
|
|
* the return value will be an array with column names as keys and column values as values. |
1015
|
|
|
* Note that for composite primary keys, an array will always be returned regardless of this parameter value. |
1016
|
|
|
* @property mixed The primary key value. An array (column name => column value) is returned if |
1017
|
|
|
* the primary key is composite. A string is returned otherwise (null will be returned if |
1018
|
|
|
* the key value is null). |
1019
|
|
|
* @return mixed the primary key value. An array (column name => column value) is returned if the primary key |
1020
|
|
|
* is composite or `$asArray` is true. A string is returned otherwise (null will be returned if |
1021
|
|
|
* the key value is null). |
1022
|
|
|
*/ |
1023
|
43 |
|
public function getPrimaryKey($asArray = false) |
1024
|
|
|
{ |
1025
|
43 |
|
$keys = $this->primaryKey(); |
1026
|
43 |
|
if (!$asArray && count($keys) === 1) { |
1027
|
26 |
|
return isset($this->_attributes[$keys[0]]) ? $this->_attributes[$keys[0]] : null; |
1028
|
|
|
} else { |
1029
|
20 |
|
$values = []; |
1030
|
20 |
|
foreach ($keys as $name) { |
1031
|
20 |
|
$values[$name] = isset($this->_attributes[$name]) ? $this->_attributes[$name] : null; |
1032
|
20 |
|
} |
1033
|
|
|
|
1034
|
20 |
|
return $values; |
1035
|
|
|
} |
1036
|
|
|
} |
1037
|
|
|
|
1038
|
|
|
/** |
1039
|
|
|
* Returns the old primary key value(s). |
1040
|
|
|
* This refers to the primary key value that is populated into the record |
1041
|
|
|
* after executing a find method (e.g. find(), findOne()). |
1042
|
|
|
* The value remains unchanged even if the primary key attribute is manually assigned with a different value. |
1043
|
|
|
* @param boolean $asArray whether to return the primary key value as an array. If true, |
1044
|
|
|
* the return value will be an array with column name as key and column value as value. |
1045
|
|
|
* If this is false (default), a scalar value will be returned for non-composite primary key. |
1046
|
|
|
* @property mixed The old primary key value. An array (column name => column value) is |
1047
|
|
|
* returned if the primary key is composite. A string is returned otherwise (null will be |
1048
|
|
|
* returned if the key value is null). |
1049
|
|
|
* @return mixed the old primary key value. An array (column name => column value) is returned if the primary key |
1050
|
|
|
* is composite or `$asArray` is true. A string is returned otherwise (null will be returned if |
1051
|
|
|
* the key value is null). |
1052
|
|
|
* @throws Exception if the AR model does not have a primary key |
1053
|
|
|
*/ |
1054
|
43 |
|
public function getOldPrimaryKey($asArray = false) |
1055
|
|
|
{ |
1056
|
43 |
|
$keys = $this->primaryKey(); |
1057
|
43 |
|
if (empty($keys)) { |
1058
|
|
|
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.'); |
1059
|
|
|
} |
1060
|
43 |
|
if (!$asArray && count($keys) === 1) { |
1061
|
7 |
|
return isset($this->_oldAttributes[$keys[0]]) ? $this->_oldAttributes[$keys[0]] : null; |
1062
|
|
|
} else { |
1063
|
37 |
|
$values = []; |
1064
|
37 |
|
foreach ($keys as $name) { |
1065
|
37 |
|
$values[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null; |
1066
|
37 |
|
} |
1067
|
|
|
|
1068
|
37 |
|
return $values; |
1069
|
|
|
} |
1070
|
|
|
} |
1071
|
|
|
|
1072
|
|
|
/** |
1073
|
|
|
* Populates an active record object using a row of data from the database/storage. |
1074
|
|
|
* |
1075
|
|
|
* This is an internal method meant to be called to create active record objects after |
1076
|
|
|
* fetching data from the database. It is mainly used by [[ActiveQuery]] to populate |
1077
|
|
|
* the query results into active records. |
1078
|
|
|
* |
1079
|
|
|
* When calling this method manually you should call [[afterFind()]] on the created |
1080
|
|
|
* record to trigger the [[EVENT_AFTER_FIND|afterFind Event]]. |
1081
|
|
|
* |
1082
|
|
|
* @param BaseActiveRecord $record the record to be populated. In most cases this will be an instance |
1083
|
|
|
* created by [[instantiate()]] beforehand. |
1084
|
|
|
* @param array $row attribute values (name => value) |
1085
|
|
|
*/ |
1086
|
225 |
|
public static function populateRecord($record, $row) |
1087
|
|
|
{ |
1088
|
225 |
|
$columns = array_flip($record->attributes()); |
1089
|
225 |
|
foreach ($row as $name => $value) { |
1090
|
225 |
|
if (isset($columns[$name])) { |
1091
|
225 |
|
$record->_attributes[$name] = $value; |
1092
|
225 |
|
} elseif ($record->canSetProperty($name)) { |
1093
|
6 |
|
$record->$name = $value; |
1094
|
6 |
|
} |
1095
|
225 |
|
} |
1096
|
225 |
|
$record->_oldAttributes = $record->_attributes; |
1097
|
225 |
|
} |
1098
|
|
|
|
1099
|
|
|
/** |
1100
|
|
|
* Creates an active record instance. |
1101
|
|
|
* |
1102
|
|
|
* This method is called together with [[populateRecord()]] by [[ActiveQuery]]. |
1103
|
|
|
* It is not meant to be used for creating new records directly. |
1104
|
|
|
* |
1105
|
|
|
* You may override this method if the instance being created |
1106
|
|
|
* depends on the row data to be populated into the record. |
1107
|
|
|
* For example, by creating a record based on the value of a column, |
1108
|
|
|
* you may implement the so-called single-table inheritance mapping. |
1109
|
|
|
* @param array $row row data to be populated into the record. |
1110
|
|
|
* @return static the newly created active record |
1111
|
|
|
*/ |
1112
|
222 |
|
public static function instantiate($row) |
|
|
|
|
1113
|
|
|
{ |
1114
|
222 |
|
return new static; |
1115
|
|
|
} |
1116
|
|
|
|
1117
|
|
|
/** |
1118
|
|
|
* Returns whether there is an element at the specified offset. |
1119
|
|
|
* This method is required by the interface [[\ArrayAccess]]. |
1120
|
|
|
* @param mixed $offset the offset to check on |
1121
|
|
|
* @return boolean whether there is an element at the specified offset. |
1122
|
|
|
*/ |
1123
|
24 |
|
public function offsetExists($offset) |
1124
|
|
|
{ |
1125
|
24 |
|
return $this->__isset($offset); |
1126
|
|
|
} |
1127
|
|
|
|
1128
|
|
|
/** |
1129
|
|
|
* Returns the relation object with the specified name. |
1130
|
|
|
* A relation is defined by a getter method which returns an [[ActiveQueryInterface]] object. |
1131
|
|
|
* It can be declared in either the Active Record class itself or one of its behaviors. |
1132
|
|
|
* @param string $name the relation name |
1133
|
|
|
* @param boolean $throwException whether to throw exception if the relation does not exist. |
1134
|
|
|
* @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist |
1135
|
|
|
* and `$throwException` is false, null will be returned. |
1136
|
|
|
* @throws InvalidParamException if the named relation does not exist. |
1137
|
|
|
*/ |
1138
|
109 |
|
public function getRelation($name, $throwException = true) |
1139
|
|
|
{ |
1140
|
109 |
|
$getter = 'get' . $name; |
1141
|
|
|
try { |
1142
|
|
|
// the relation could be defined in a behavior |
1143
|
109 |
|
$relation = $this->$getter(); |
1144
|
109 |
|
} catch (UnknownMethodException $e) { |
1145
|
|
|
if ($throwException) { |
1146
|
|
|
throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e); |
1147
|
|
|
} else { |
1148
|
|
|
return null; |
1149
|
|
|
} |
1150
|
|
|
} |
1151
|
109 |
|
if (!$relation instanceof ActiveQueryInterface) { |
1152
|
|
|
if ($throwException) { |
1153
|
|
|
throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); |
1154
|
|
|
} else { |
1155
|
|
|
return null; |
1156
|
|
|
} |
1157
|
|
|
} |
1158
|
|
|
|
1159
|
109 |
|
if (method_exists($this, $getter)) { |
1160
|
|
|
// relation name is case sensitive, trying to validate it when the relation is defined within this class |
1161
|
109 |
|
$method = new \ReflectionMethod($this, $getter); |
1162
|
109 |
|
$realName = lcfirst(substr($method->getName(), 3)); |
|
|
|
|
1163
|
109 |
|
if ($realName !== $name) { |
1164
|
|
|
if ($throwException) { |
1165
|
|
|
throw new InvalidParamException('Relation names are case sensitive. ' . get_class($this) . " has a relation named \"$realName\" instead of \"$name\"."); |
1166
|
|
|
} else { |
1167
|
|
|
return null; |
1168
|
|
|
} |
1169
|
|
|
} |
1170
|
109 |
|
} |
1171
|
|
|
|
1172
|
109 |
|
return $relation; |
1173
|
|
|
} |
1174
|
|
|
|
1175
|
|
|
/** |
1176
|
|
|
* Establishes the relationship between two models. |
1177
|
|
|
* |
1178
|
|
|
* The relationship is established by setting the foreign key value(s) in one model |
1179
|
|
|
* to be the corresponding primary key value(s) in the other model. |
1180
|
|
|
* The model with the foreign key will be saved into database without performing validation. |
1181
|
|
|
* |
1182
|
|
|
* If the relationship involves a junction table, a new row will be inserted into the |
1183
|
|
|
* junction table which contains the primary key values from both models. |
1184
|
|
|
* |
1185
|
|
|
* Note that this method requires that the primary key value is not null. |
1186
|
|
|
* |
1187
|
|
|
* @param string $name the case sensitive name of the relationship. |
1188
|
|
|
* @param ActiveRecordInterface $model the model to be linked with the current one. |
1189
|
|
|
* @param array $extraColumns additional column values to be saved into the junction table. |
1190
|
|
|
* This parameter is only meaningful for a relationship involving a junction table |
1191
|
|
|
* (i.e., a relation set with [[ActiveRelationTrait::via()]] or [[ActiveQuery::viaTable()]].) |
1192
|
|
|
* @throws InvalidCallException if the method is unable to link two models. |
1193
|
|
|
*/ |
1194
|
9 |
|
public function link($name, $model, $extraColumns = []) |
1195
|
|
|
{ |
1196
|
9 |
|
$relation = $this->getRelation($name); |
1197
|
|
|
|
1198
|
9 |
|
if ($relation->via !== null) { |
|
|
|
|
1199
|
3 |
|
if ($this->getIsNewRecord() || $model->getIsNewRecord()) { |
1200
|
|
|
throw new InvalidCallException('Unable to link models: the models being linked cannot be newly created.'); |
1201
|
|
|
} |
1202
|
3 |
|
if (is_array($relation->via)) { |
|
|
|
|
1203
|
|
|
/* @var $viaRelation ActiveQuery */ |
1204
|
3 |
|
list($viaName, $viaRelation) = $relation->via; |
|
|
|
|
1205
|
3 |
|
$viaClass = $viaRelation->modelClass; |
1206
|
|
|
// unset $viaName so that it can be reloaded to reflect the change |
1207
|
3 |
|
unset($this->_related[$viaName]); |
1208
|
3 |
|
} else { |
1209
|
|
|
$viaRelation = $relation->via; |
|
|
|
|
1210
|
|
|
$viaTable = reset($relation->via->from); |
|
|
|
|
1211
|
|
|
} |
1212
|
3 |
|
$columns = []; |
1213
|
3 |
|
foreach ($viaRelation->link as $a => $b) { |
1214
|
3 |
|
$columns[$a] = $this->$b; |
1215
|
3 |
|
} |
1216
|
3 |
|
foreach ($relation->link as $a => $b) { |
|
|
|
|
1217
|
3 |
|
$columns[$b] = $model->$a; |
1218
|
3 |
|
} |
1219
|
3 |
|
foreach ($extraColumns as $k => $v) { |
1220
|
3 |
|
$columns[$k] = $v; |
1221
|
3 |
|
} |
1222
|
3 |
|
if (is_array($relation->via)) { |
|
|
|
|
1223
|
|
|
/* @var $viaClass ActiveRecordInterface */ |
1224
|
|
|
/* @var $record ActiveRecordInterface */ |
1225
|
3 |
|
$record = new $viaClass(); |
|
|
|
|
1226
|
3 |
|
foreach ($columns as $column => $value) { |
1227
|
3 |
|
$record->$column = $value; |
1228
|
3 |
|
} |
1229
|
3 |
|
$record->insert(false); |
1230
|
3 |
|
} else { |
1231
|
|
|
/* @var $viaTable string */ |
1232
|
|
|
static::getDb()->createCommand() |
1233
|
|
|
->insert($viaTable, $columns)->execute(); |
|
|
|
|
1234
|
|
|
} |
1235
|
3 |
|
} else { |
1236
|
9 |
|
$p1 = $model->isPrimaryKey(array_keys($relation->link)); |
|
|
|
|
1237
|
9 |
|
$p2 = static::isPrimaryKey(array_values($relation->link)); |
|
|
|
|
1238
|
9 |
|
if ($p1 && $p2) { |
1239
|
|
|
if ($this->getIsNewRecord() && $model->getIsNewRecord()) { |
1240
|
|
|
throw new InvalidCallException('Unable to link models: at most one model can be newly created.'); |
1241
|
|
|
} elseif ($this->getIsNewRecord()) { |
1242
|
|
|
$this->bindModels(array_flip($relation->link), $this, $model); |
|
|
|
|
1243
|
|
|
} else { |
1244
|
|
|
$this->bindModels($relation->link, $model, $this); |
|
|
|
|
1245
|
|
|
} |
1246
|
9 |
|
} elseif ($p1) { |
1247
|
3 |
|
$this->bindModels(array_flip($relation->link), $this, $model); |
|
|
|
|
1248
|
9 |
|
} elseif ($p2) { |
1249
|
9 |
|
$this->bindModels($relation->link, $model, $this); |
|
|
|
|
1250
|
9 |
|
} else { |
1251
|
|
|
throw new InvalidCallException('Unable to link models: the link defining the relation does not involve any primary key.'); |
1252
|
|
|
} |
1253
|
|
|
} |
1254
|
|
|
|
1255
|
|
|
// update lazily loaded related objects |
1256
|
9 |
|
if (!$relation->multiple) { |
|
|
|
|
1257
|
3 |
|
$this->_related[$name] = $model; |
1258
|
9 |
|
} elseif (isset($this->_related[$name])) { |
1259
|
9 |
|
if ($relation->indexBy !== null) { |
|
|
|
|
1260
|
6 |
|
if ($relation->indexBy instanceof \Closure) { |
|
|
|
|
1261
|
3 |
|
$index = call_user_func($relation->indexBy, $model); |
|
|
|
|
1262
|
3 |
|
} else { |
1263
|
3 |
|
$index = $model->{$relation->indexBy}; |
|
|
|
|
1264
|
|
|
} |
1265
|
6 |
|
$this->_related[$name][$index] = $model; |
1266
|
6 |
|
} else { |
1267
|
3 |
|
$this->_related[$name][] = $model; |
1268
|
|
|
} |
1269
|
9 |
|
} |
1270
|
9 |
|
} |
1271
|
|
|
|
1272
|
|
|
/** |
1273
|
|
|
* Destroys the relationship between two models. |
1274
|
|
|
* |
1275
|
|
|
* The model with the foreign key of the relationship will be deleted if `$delete` is true. |
1276
|
|
|
* Otherwise, the foreign key will be set null and the model will be saved without validation. |
1277
|
|
|
* |
1278
|
|
|
* @param string $name the case sensitive name of the relationship. |
1279
|
|
|
* @param ActiveRecordInterface $model the model to be unlinked from the current one. |
1280
|
|
|
* You have to make sure that the model is really related with the current model as this method |
1281
|
|
|
* does not check this. |
1282
|
|
|
* @param boolean $delete whether to delete the model that contains the foreign key. |
1283
|
|
|
* If false, the model's foreign key will be set null and saved. |
1284
|
|
|
* If true, the model containing the foreign key will be deleted. |
1285
|
|
|
* @throws InvalidCallException if the models cannot be unlinked |
1286
|
|
|
*/ |
1287
|
3 |
|
public function unlink($name, $model, $delete = false) |
1288
|
|
|
{ |
1289
|
3 |
|
$relation = $this->getRelation($name); |
1290
|
|
|
|
1291
|
3 |
|
if ($relation->via !== null) { |
|
|
|
|
1292
|
3 |
|
if (is_array($relation->via)) { |
|
|
|
|
1293
|
|
|
/* @var $viaRelation ActiveQuery */ |
1294
|
3 |
|
list($viaName, $viaRelation) = $relation->via; |
|
|
|
|
1295
|
3 |
|
$viaClass = $viaRelation->modelClass; |
1296
|
3 |
|
unset($this->_related[$viaName]); |
1297
|
3 |
|
} else { |
1298
|
3 |
|
$viaRelation = $relation->via; |
|
|
|
|
1299
|
3 |
|
$viaTable = reset($relation->via->from); |
|
|
|
|
1300
|
|
|
} |
1301
|
3 |
|
$columns = []; |
1302
|
3 |
|
foreach ($viaRelation->link as $a => $b) { |
1303
|
3 |
|
$columns[$a] = $this->$b; |
1304
|
3 |
|
} |
1305
|
3 |
|
foreach ($relation->link as $a => $b) { |
|
|
|
|
1306
|
3 |
|
$columns[$b] = $model->$a; |
1307
|
3 |
|
} |
1308
|
3 |
|
$nulls = []; |
1309
|
3 |
|
foreach (array_keys($columns) as $a) { |
1310
|
3 |
|
$nulls[$a] = null; |
1311
|
3 |
|
} |
1312
|
3 |
|
if (is_array($relation->via)) { |
|
|
|
|
1313
|
|
|
/* @var $viaClass ActiveRecordInterface */ |
1314
|
3 |
|
if ($delete) { |
1315
|
3 |
|
$viaClass::deleteAll($columns); |
|
|
|
|
1316
|
3 |
|
} else { |
1317
|
|
|
$viaClass::updateAll($nulls, $columns); |
1318
|
|
|
} |
1319
|
3 |
|
} else { |
1320
|
|
|
/* @var $viaTable string */ |
1321
|
|
|
/* @var $command Command */ |
1322
|
3 |
|
$command = static::getDb()->createCommand(); |
1323
|
3 |
|
if ($delete) { |
1324
|
|
|
$command->delete($viaTable, $columns)->execute(); |
|
|
|
|
1325
|
|
|
} else { |
1326
|
3 |
|
$command->update($viaTable, $nulls, $columns)->execute(); |
1327
|
|
|
} |
1328
|
|
|
} |
1329
|
3 |
|
} else { |
1330
|
3 |
|
$p1 = $model->isPrimaryKey(array_keys($relation->link)); |
|
|
|
|
1331
|
3 |
|
$p2 = static::isPrimaryKey(array_values($relation->link)); |
|
|
|
|
1332
|
3 |
|
if ($p2) { |
1333
|
3 |
|
if ($delete) { |
1334
|
3 |
|
$model->delete(); |
1335
|
3 |
|
} else { |
1336
|
3 |
|
foreach ($relation->link as $a => $b) { |
|
|
|
|
1337
|
3 |
|
$model->$a = null; |
1338
|
3 |
|
} |
1339
|
3 |
|
$model->save(false); |
1340
|
|
|
} |
1341
|
3 |
|
} elseif ($p1) { |
1342
|
|
|
foreach ($relation->link as $a => $b) { |
|
|
|
|
1343
|
|
|
if (is_array($this->$b)) { // relation via array valued attribute |
1344
|
|
|
if (($key = array_search($model->$a, $this->$b, false)) !== false) { |
1345
|
|
|
$values = $this->$b; |
1346
|
|
|
unset($values[$key]); |
1347
|
|
|
$this->$b = array_values($values); |
1348
|
|
|
} |
1349
|
|
|
} else { |
1350
|
|
|
$this->$b = null; |
1351
|
|
|
} |
1352
|
|
|
} |
1353
|
|
|
$delete ? $this->delete() : $this->save(false); |
1354
|
|
|
} else { |
1355
|
|
|
throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.'); |
1356
|
|
|
} |
1357
|
|
|
} |
1358
|
|
|
|
1359
|
3 |
|
if (!$relation->multiple) { |
|
|
|
|
1360
|
|
|
unset($this->_related[$name]); |
1361
|
3 |
|
} elseif (isset($this->_related[$name])) { |
1362
|
|
|
/* @var $b ActiveRecordInterface */ |
1363
|
3 |
|
foreach ($this->_related[$name] as $a => $b) { |
1364
|
3 |
|
if ($model->getPrimaryKey() === $b->getPrimaryKey()) { |
1365
|
3 |
|
unset($this->_related[$name][$a]); |
1366
|
3 |
|
} |
1367
|
3 |
|
} |
1368
|
3 |
|
} |
1369
|
3 |
|
} |
1370
|
|
|
|
1371
|
|
|
/** |
1372
|
|
|
* Destroys the relationship in current model. |
1373
|
|
|
* |
1374
|
|
|
* The model with the foreign key of the relationship will be deleted if `$delete` is true. |
1375
|
|
|
* Otherwise, the foreign key will be set null and the model will be saved without validation. |
1376
|
|
|
* |
1377
|
|
|
* Note that to destroy the relationship without removing records make sure your keys can be set to null |
1378
|
|
|
* |
1379
|
|
|
* @param string $name the case sensitive name of the relationship. |
1380
|
|
|
* @param boolean $delete whether to delete the model that contains the foreign key. |
1381
|
|
|
*/ |
1382
|
12 |
|
public function unlinkAll($name, $delete = false) |
1383
|
|
|
{ |
1384
|
12 |
|
$relation = $this->getRelation($name); |
1385
|
|
|
|
1386
|
12 |
|
if ($relation->via !== null) { |
|
|
|
|
1387
|
6 |
|
if (is_array($relation->via)) { |
|
|
|
|
1388
|
|
|
/* @var $viaRelation ActiveQuery */ |
1389
|
3 |
|
list($viaName, $viaRelation) = $relation->via; |
|
|
|
|
1390
|
3 |
|
$viaClass = $viaRelation->modelClass; |
1391
|
3 |
|
unset($this->_related[$viaName]); |
1392
|
3 |
|
} else { |
1393
|
3 |
|
$viaRelation = $relation->via; |
|
|
|
|
1394
|
3 |
|
$viaTable = reset($relation->via->from); |
|
|
|
|
1395
|
|
|
} |
1396
|
6 |
|
$condition = []; |
1397
|
6 |
|
$nulls = []; |
1398
|
6 |
|
foreach ($viaRelation->link as $a => $b) { |
1399
|
6 |
|
$nulls[$a] = null; |
1400
|
6 |
|
$condition[$a] = $this->$b; |
1401
|
6 |
|
} |
1402
|
6 |
|
if (!empty($viaRelation->where)) { |
1403
|
|
|
$condition = ['and', $condition, $viaRelation->where]; |
1404
|
|
|
} |
1405
|
6 |
|
if (is_array($relation->via)) { |
|
|
|
|
1406
|
|
|
/* @var $viaClass ActiveRecordInterface */ |
1407
|
3 |
|
if ($delete) { |
1408
|
3 |
|
$viaClass::deleteAll($condition); |
|
|
|
|
1409
|
3 |
|
} else { |
1410
|
3 |
|
$viaClass::updateAll($nulls, $condition); |
1411
|
|
|
} |
1412
|
3 |
|
} else { |
1413
|
|
|
/* @var $viaTable string */ |
1414
|
|
|
/* @var $command Command */ |
1415
|
3 |
|
$command = static::getDb()->createCommand(); |
1416
|
3 |
|
if ($delete) { |
1417
|
3 |
|
$command->delete($viaTable, $condition)->execute(); |
|
|
|
|
1418
|
3 |
|
} else { |
1419
|
3 |
|
$command->update($viaTable, $nulls, $condition)->execute(); |
1420
|
|
|
} |
1421
|
|
|
} |
1422
|
6 |
|
} else { |
1423
|
|
|
/* @var $relatedModel ActiveRecordInterface */ |
1424
|
9 |
|
$relatedModel = $relation->modelClass; |
|
|
|
|
1425
|
9 |
|
if (!$delete && count($relation->link) === 1 && is_array($this->{$b = reset($relation->link)})) { |
|
|
|
|
1426
|
|
|
// relation via array valued attribute |
1427
|
|
|
$this->$b = []; |
1428
|
|
|
$this->save(false); |
1429
|
|
|
} else { |
1430
|
9 |
|
$nulls = []; |
1431
|
9 |
|
$condition = []; |
1432
|
9 |
|
foreach ($relation->link as $a => $b) { |
|
|
|
|
1433
|
9 |
|
$nulls[$a] = null; |
1434
|
9 |
|
$condition[$a] = $this->$b; |
1435
|
9 |
|
} |
1436
|
9 |
|
if (!empty($relation->where)) { |
|
|
|
|
1437
|
6 |
|
$condition = ['and', $condition, $relation->where]; |
|
|
|
|
1438
|
6 |
|
} |
1439
|
9 |
|
if ($delete) { |
1440
|
6 |
|
$relatedModel::deleteAll($condition); |
1441
|
6 |
|
} else { |
1442
|
6 |
|
$relatedModel::updateAll($nulls, $condition); |
1443
|
|
|
} |
1444
|
|
|
} |
1445
|
|
|
} |
1446
|
|
|
|
1447
|
12 |
|
unset($this->_related[$name]); |
1448
|
12 |
|
} |
1449
|
|
|
|
1450
|
|
|
/** |
1451
|
|
|
* @param array $link |
1452
|
|
|
* @param ActiveRecordInterface $foreignModel |
1453
|
|
|
* @param ActiveRecordInterface $primaryModel |
1454
|
|
|
* @throws InvalidCallException |
1455
|
|
|
*/ |
1456
|
9 |
|
private function bindModels($link, $foreignModel, $primaryModel) |
1457
|
|
|
{ |
1458
|
9 |
|
foreach ($link as $fk => $pk) { |
1459
|
9 |
|
$value = $primaryModel->$pk; |
1460
|
9 |
|
if ($value === null) { |
1461
|
|
|
throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.'); |
1462
|
|
|
} |
1463
|
9 |
|
if (is_array($foreignModel->$fk)) { // relation via array valued attribute |
1464
|
|
|
$foreignModel->$fk = array_merge($foreignModel->$fk, [$value]); |
1465
|
|
|
} else { |
1466
|
9 |
|
$foreignModel->$fk = $value; |
1467
|
|
|
} |
1468
|
9 |
|
} |
1469
|
9 |
|
$foreignModel->save(false); |
1470
|
9 |
|
} |
1471
|
|
|
|
1472
|
|
|
/** |
1473
|
|
|
* Returns a value indicating whether the given set of attributes represents the primary key for this model |
1474
|
|
|
* @param array $keys the set of attributes to check |
1475
|
|
|
* @return boolean whether the given set of attributes represents the primary key for this model |
1476
|
|
|
*/ |
1477
|
15 |
|
public static function isPrimaryKey($keys) |
1478
|
|
|
{ |
1479
|
15 |
|
$pks = static::primaryKey(); |
1480
|
15 |
|
if (count($keys) === count($pks)) { |
1481
|
15 |
|
return count(array_intersect($keys, $pks)) === count($pks); |
1482
|
|
|
} else { |
1483
|
9 |
|
return false; |
1484
|
|
|
} |
1485
|
|
|
} |
1486
|
|
|
|
1487
|
|
|
/** |
1488
|
|
|
* Returns the text label for the specified attribute. |
1489
|
|
|
* If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model. |
1490
|
|
|
* @param string $attribute the attribute name |
1491
|
|
|
* @return string the attribute label |
1492
|
|
|
* @see generateAttributeLabel() |
1493
|
|
|
* @see attributeLabels() |
1494
|
|
|
*/ |
1495
|
42 |
|
public function getAttributeLabel($attribute) |
1496
|
|
|
{ |
1497
|
42 |
|
$labels = $this->attributeLabels(); |
1498
|
42 |
|
if (isset($labels[$attribute])) { |
1499
|
10 |
|
return $labels[$attribute]; |
1500
|
39 |
|
} elseif (strpos($attribute, '.')) { |
1501
|
|
|
$attributeParts = explode('.', $attribute); |
1502
|
|
|
$neededAttribute = array_pop($attributeParts); |
1503
|
|
|
|
1504
|
|
|
$relatedModel = $this; |
1505
|
|
|
foreach ($attributeParts as $relationName) { |
1506
|
|
|
if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) { |
1507
|
|
|
$relatedModel = $relatedModel->$relationName; |
1508
|
|
|
} else { |
1509
|
|
|
try { |
1510
|
|
|
$relation = $relatedModel->getRelation($relationName); |
1511
|
|
|
} catch (InvalidParamException $e) { |
1512
|
|
|
return $this->generateAttributeLabel($attribute); |
1513
|
|
|
} |
1514
|
|
|
$relatedModel = new $relation->modelClass; |
1515
|
|
|
} |
1516
|
|
|
} |
1517
|
|
|
|
1518
|
|
|
$labels = $relatedModel->attributeLabels(); |
1519
|
|
|
if (isset($labels[$neededAttribute])) { |
1520
|
|
|
return $labels[$neededAttribute]; |
1521
|
|
|
} |
1522
|
|
|
} |
1523
|
|
|
|
1524
|
39 |
|
return $this->generateAttributeLabel($attribute); |
1525
|
|
|
} |
1526
|
|
|
|
1527
|
|
|
/** |
1528
|
|
|
* Returns the text hint for the specified attribute. |
1529
|
|
|
* If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model. |
1530
|
|
|
* @param string $attribute the attribute name |
1531
|
|
|
* @return string the attribute hint |
1532
|
|
|
* @see attributeHints() |
1533
|
|
|
* @since 2.0.4 |
1534
|
|
|
*/ |
1535
|
|
|
public function getAttributeHint($attribute) |
1536
|
|
|
{ |
1537
|
|
|
$hints = $this->attributeHints(); |
1538
|
|
|
if (isset($hints[$attribute])) { |
1539
|
|
|
return $hints[$attribute]; |
1540
|
|
|
} elseif (strpos($attribute, '.')) { |
1541
|
|
|
$attributeParts = explode('.', $attribute); |
1542
|
|
|
$neededAttribute = array_pop($attributeParts); |
1543
|
|
|
|
1544
|
|
|
$relatedModel = $this; |
1545
|
|
|
foreach ($attributeParts as $relationName) { |
1546
|
|
|
if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) { |
1547
|
|
|
$relatedModel = $relatedModel->$relationName; |
1548
|
|
|
} else { |
1549
|
|
|
try { |
1550
|
|
|
$relation = $relatedModel->getRelation($relationName); |
1551
|
|
|
} catch (InvalidParamException $e) { |
1552
|
|
|
return ''; |
1553
|
|
|
} |
1554
|
|
|
$relatedModel = new $relation->modelClass; |
1555
|
|
|
} |
1556
|
|
|
} |
1557
|
|
|
|
1558
|
|
|
$hints = $relatedModel->attributeHints(); |
1559
|
|
|
if (isset($hints[$neededAttribute])) { |
1560
|
|
|
return $hints[$neededAttribute]; |
1561
|
|
|
} |
1562
|
|
|
} |
1563
|
|
|
return ''; |
1564
|
|
|
} |
1565
|
|
|
|
1566
|
|
|
/** |
1567
|
|
|
* @inheritdoc |
1568
|
|
|
* |
1569
|
|
|
* The default implementation returns the names of the columns whose values have been populated into this record. |
1570
|
|
|
*/ |
1571
|
|
|
public function fields() |
1572
|
|
|
{ |
1573
|
|
|
$fields = array_keys($this->_attributes); |
1574
|
|
|
|
1575
|
|
|
return array_combine($fields, $fields); |
1576
|
|
|
} |
1577
|
|
|
|
1578
|
|
|
/** |
1579
|
|
|
* @inheritdoc |
1580
|
|
|
* |
1581
|
|
|
* The default implementation returns the names of the relations that have been populated into this record. |
1582
|
|
|
*/ |
1583
|
|
|
public function extraFields() |
1584
|
|
|
{ |
1585
|
|
|
$fields = array_keys($this->getRelatedRecords()); |
1586
|
|
|
|
1587
|
|
|
return array_combine($fields, $fields); |
1588
|
|
|
} |
1589
|
|
|
|
1590
|
|
|
/** |
1591
|
|
|
* Sets the element value at the specified offset to null. |
1592
|
|
|
* This method is required by the SPL interface [[\ArrayAccess]]. |
1593
|
|
|
* It is implicitly called when you use something like `unset($model[$offset])`. |
1594
|
|
|
* @param mixed $offset the offset to unset element |
1595
|
|
|
*/ |
1596
|
3 |
|
public function offsetUnset($offset) |
1597
|
|
|
{ |
1598
|
3 |
|
if (property_exists($this, $offset)) { |
1599
|
|
|
$this->$offset = null; |
1600
|
|
|
} else { |
1601
|
3 |
|
unset($this->$offset); |
1602
|
|
|
} |
1603
|
3 |
|
} |
1604
|
|
|
} |
1605
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.