From 548ee8df058a6867dd029a686af3f9503d151e30 Mon Sep 17 00:00:00 2001 From: PowerGamer1 Date: Tue, 25 Jul 2023 18:49:09 +0300 Subject: [PATCH] Allow ActiveRecord::getAttributeLabel() to override labels of deeply nested relations. --- framework/CHANGELOG.md | 1 + framework/db/BaseActiveRecord.php | 57 +++++++++-------- tests/framework/db/ActiveRecordTest.php | 83 +++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 26 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 9833be22ac4..44809155184 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -4,6 +4,7 @@ Yii Framework 2 Change Log 2.0.49 under development ------------------------ +- 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 #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) - Bug #16208: Fix `yii\log\FileTarget` to not export empty messages (terabytesoftw) diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index 88fb2f11d7c..260d459f47c 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -1610,40 +1610,45 @@ public static function isPrimaryKey($keys) /** * 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/tests/framework/db/ActiveRecordTest.php b/tests/framework/db/ActiveRecordTest.php index f1370f47282..b74803b94dd 100644 --- a/tests/framework/db/ActiveRecordTest.php +++ b/tests/framework/db/ActiveRecordTest.php @@ -2192,4 +2192,87 @@ public function testVirtualRelation() $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', + ]; + } }