diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index baa9db49f7..db13375e27 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -5,6 +5,7 @@ Yii Framework 2 Change Log ------------------------ - Bug #13920: Fixed erroneous validation for specific cases (tim-fischer-maschinensucher) +- Bug #19911: Resolved inconsistency in `ActiveRecord::getAttributeLabel()` with regard of overriding in primary model labels for attributes of related model in favor of allowing such overriding for all levels of relation nesting (PowerGamer1) - Bug #19872: Fixed the definition of dirty attributes in AR properties for a non-associative array in case of changing the order of elements (eegusakov) - Bug #19899: Fixed `GridView` in some cases calling `Model::generateAttributeLabel()` to generate label values that are never used (PowerGamer1) - Bug #9899: Fix caching a MSSQL query with BLOB data type (terabytesoftw) @@ -17,6 +18,7 @@ Yii Framework 2 Change Log - Enh #19884: Added support Enums in Query Builder (sk1t0n) - Bug #19908: Fix associative array cell content rendering in Table widget (rhertogh) - Bug #19906: Fixed multiline strings in the `\yii\console\widgets\Table` widget (rhertogh) +- Bug #19924: Fix `yii\i18n\Formatter` to not throw error `Unknown named parameter` under PHP 8 (arollmann) - Bug #19914: Fixed `ArrayHelper::keyExists()` and `::remove()` functions when the key is a float and the value is `null` (rhertogh) - Enh #19920: Broadened the accepted type of `Cookie::$expire` from `int` to `int|string|\DateTimeInterface|null` (rhertogh) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index 7d2ca03eb9..3a9569a982 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -100,6 +100,11 @@ Upgrade from Yii 2.0.45 2.0.45 behavior, [introduce your own method](https://github.com/yiisoft/yii2/pull/19495/files). * `yii\log\FileTarget::$rotateByCopy` is now deprecated and setting it to `false` has no effect since rotating of the files is done only by copy. +* `yii\validators\UniqueValidator` and `yii\validators\ExistValidator`, when used on multiple attributes, now only + generate an error on a single attribute. Previously, they would report a separate error on each attribute. + Old behavior can be achieved by setting `'skipOnError' => false`, but this might have undesired side effects with + additional validators on one of the target attributes. + See [issue #19407](https://github.com/yiisoft/yii2/issues/19407) Upgrade from Yii 2.0.44 ----------------------- diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index 60f621eb44..9b00cb430f 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -1610,40 +1610,46 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface /** * Returns the text label for the specified attribute. - * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model. + * 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. * @param string $attribute the attribute name * @return string the attribute label - * @see generateAttributeLabel() * @see attributeLabels() + * @see generateAttributeLabel() */ public function getAttributeLabel($attribute) { - $labels = $this->attributeLabels(); - if (isset($labels[$attribute])) { - return $labels[$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 $this->generateAttributeLabel($attribute); - } - /* @var $modelClass ActiveRecordInterface */ - $modelClass = $relation->modelClass; - $relatedModel = $modelClass::instance(); - } + $model = $this; + $modelAttribute = $attribute; + for (;;) { + $labels = $model->attributeLabels(); + if (isset($labels[$modelAttribute])) { + return $labels[$modelAttribute]; } - $labels = $relatedModel->attributeLabels(); - if (isset($labels[$neededAttribute])) { - return $labels[$neededAttribute]; + $parts = explode('.', $modelAttribute, 2); + if (count($parts) < 2) { + break; + } + + 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 $modelClass ActiveRecordInterface */ + $modelClass = $relation->modelClass; + $model = $modelClass::instance(); } } diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index 3236a433be..001b81bfa9 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -460,7 +460,7 @@ class Formatter extends Component } $method = 'as' . $format; if ($this->hasMethod($method)) { - return call_user_func_array([$this, $method], $params); + return call_user_func_array([$this, $method], array_values($params)); } throw new InvalidArgumentException("Unknown format type: $format"); diff --git a/tests/framework/db/ActiveRecordTest.php b/tests/framework/db/ActiveRecordTest.php index f1370f4728..b74803b94d 100644 --- a/tests/framework/db/ActiveRecordTest.php +++ b/tests/framework/db/ActiveRecordTest.php @@ -2192,4 +2192,87 @@ abstract class ActiveRecordTest extends DatabaseTestCase $this->assertNotNull($order->virtualCustomer); } + public function labelTestModelProvider() + { + $data = []; + + // Model 2 and 3 are represented by objects. + $model1 = new LabelTestModel1(); + $model2 = new LabelTestModel2(); + $model3 = new LabelTestModel3(); + $model2->populateRelation('model3', $model3); + $model1->populateRelation('model2', $model2); + $data[] = [$model1]; + + // Model 2 and 3 are represented by arrays instead of objects. + $model1 = new LabelTestModel1(); + $model2 = ['model3' => []]; + $model1->populateRelation('model2', $model2); + $data[] = [$model1]; + + return $data; + } + + /** + * @dataProvider labelTestModelProvider + * @param \yii\db\ActiveRecord $model + */ + public function testGetAttributeLabel($model) + { + $this->assertEquals('model3.attr1 from model2', $model->getAttributeLabel('model2.model3.attr1')); + $this->assertEquals('attr2 from model3', $model->getAttributeLabel('model2.model3.attr2')); + $this->assertEquals('model3.attr3 from model2', $model->getAttributeLabel('model2.model3.attr3')); + $attr = 'model2.doesNotExist.attr1'; + $this->assertEquals($model->generateAttributeLabel($attr), $model->getAttributeLabel($attr)); + } +} + +class LabelTestModel1 extends \yii\db\ActiveRecord +{ + public function attributes() + { + return []; + } + + public function getModel2() + { + return $this->hasOne(LabelTestModel2::className(), []); + } +} + +class LabelTestModel2 extends \yii\db\ActiveRecord +{ + public function attributes() + { + return []; + } + + public function getModel3() + { + return $this->hasOne(LabelTestModel3::className(), []); + } + + public function attributeLabels() + { + return [ + 'model3.attr1' => 'model3.attr1 from model2', // Override label defined in model3. + 'model3.attr3' => 'model3.attr3 from model2', // Define label not defined in model3. + ]; + } +} + +class LabelTestModel3 extends \yii\db\ActiveRecord +{ + public function attributes() + { + return ['attr1', 'attr2', 'attr3']; + } + + public function attributeLabels() + { + return [ + 'attr1' => 'attr1 from model3', + 'attr2' => 'attr2 from model3', + ]; + } }