yii2 icon indicating copy to clipboard operation
yii2 copied to clipboard

Error in yii\db\ActiveQuery::populateInverseRelation() when primary model is an object and relation is an array

Open PowerGamer1 opened this issue 1 year ago • 3 comments

When primary model has a relation that uses inverseOf, "finding" primary model as an object with relation as array produces PHP error. All other combinations of object/array for primary model and relation work fine.

What steps will reproduce the problem?

class Customer extends \yii\db\ActiveRecord
{
	public function getOrders()
	{
		return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
	}
}
class Order extends \yii\db\ActiveRecord
{
	public function getCustomer()
	{
		return $this->hasOne(Customer::class, ['id' => 'customer_id']);
	}
}

$c = Customer::find()->where(['id' => 1])->asArray(false)->with(['orders' => function ($query) { $query->asArray(false); }])->one(); // object->object: OK
$c = Customer::find()->where(['id' => 1])->asArray(true)->with(['orders' => function ($query) { $query->asArray(true); }])->one(); // array->array: OK
$c = Customer::find()->where(['id' => 1])->asArray(true)->with(['orders' => function ($query) { $query->asArray(false); }])->one(); // array->object: OK
$c = Customer::find()->where(['id' => 1])->asArray(false)->with(['orders' => function ($query) { $query->asArray(true); }])->one(); // object->array: ERROR

What is the expected result?

The last line executes without errors.

What do you get instead?

PHP error: "Indirect modification of overloaded element of Customer has no effect" in yii\db\ActiveQuery::populateInverseRelation().

Additional info

Q A
Yii version 2.0.48.1
PHP version 8.2.8

PowerGamer1 avatar Jul 08 '23 13:07 PowerGamer1

The problem arises in yii\db\ActiveQuery::populateInverseRelation():

$primaryModels[$i][$primaryName][$j][$name] = $primaryModel;

If translated to the example above:

$customers[$i]['orders'][$j]['customer'] = $customer;

$customers[$i]['orders'] calls class method returning a simple PHP array (no objects). Modifying that array will create a copy of an array and have no effect on the value stored in $customers[$i] object. This is what PHP complains about. The easy solution would have been:

// Access array directly (not through some hidden class method):
$primaryModels[$i][_related'][$primaryName][$j][$name] = $primaryModel;
// $customers[$i][_related']['orders'][$j]['customer'] = $customer;

But that solution won't work because ActiveRecord::_related is a private property.

The bad solution with lots of array copying would be something like:

if($primaryModels[$i] instanceof ActiveRecordInterface) {
$relatedModels = $primaryModels[$i][$primaryName];
$relatedModels[$j][$name] = $primaryModel;
$primaryModels[$i]->populateRelation($primaryName, $relatedModels);
}

The better solution might be to fill in inverseOf related data BEFORE calling populateRelation() on primary models in ActiveRelationTrait::populateRelation().

PowerGamer1 avatar Jul 08 '23 16:07 PowerGamer1

Or change _related property scope from private to protected.

lubosdz avatar Jul 10 '23 08:07 lubosdz

Or change _related property scope from private to protected.

Won't help because this property needs to be accessed in a different class (_related is a property of \yii\db\BaseActiveRecord and access needs to be in yii\db\ActiveQuery::populateInverseRelation().

PowerGamer1 avatar Jul 10 '23 09:07 PowerGamer1