Home » Errors » How to Fix Indirect Modification of Overloaded Property Has no Effect in Laravel

How to Fix Indirect Modification of Overloaded Property Has no Effect in Laravel

In the previous article, I showed how to write an array to a model field with automatic conversion using $casts. This is quite convenient because you don’t need to think about how to convert it to a string and then how to make an array from a string. However, there is one issue. You cannot directly change or add elements to this array in the model field.

If you try to do this, you will get a fatal error with the message “Indirect modification of overloaded property has no effect”. In this article, I will explain why this happens and show how to solve this issue.


Table of Contents

Why does this error occur?

Let’s imagine that you have a Project model that has a settings field as well as in the previous article, but with configured $casts variable.

app/Models/Project.php<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Project extends Model { protected $casts = [ "settings" => "array", ]; protected $attributes = [ "settings" => "[]" ]; }

If you try to add a new element to the settings array or change the value of an existing one like this, you will get an error:

app/Http/Controllers/ModifyProjectController.php$project = \App\Models\Project::findOrFail(1); $project->settings["new_option"] = 42;

The PHP magic methods __get() and __set() are used to access model fields in Laravel. These methods can be used to add virtual properties to classes. The first is called when trying to access non-existent properties, and the second is when trying to write data to a non-existent field.

However, there is no method for getting a value of the field as a reference. If the field is an array, you cannot modify it. You can either read it in full or write the value of this field in full. This happens because in PHP, all scalar types, as well as arrays, are passed by value. Thus, when the function __get() returns an array for modification, it is a copy of the array that is in the model field, which was created at the time of the method- call. And in fact, your changes will not be saved.

There are two ways around this. First, you can get an array, perform the necessary actions, and then rewrite it. Or you can use an object and store your array as its field. Since objects in PHP are always passed by reference, everything will work here.

How to fix the error?

Let’s take a closer look at the first approach. Everything is straightforward here. We save the field value to a local variable, modify it and write it back:

app/Http/Controllers/ModifyProjectController.php$project = \App\Models\Project::findOrFail(1); //... $settings = $project->settings; $settings["new_option"] = 42; $project->settings = $settings;

You can even write this in one line:

app/Http/Controllers/ModifyProjectController.php$project = \App\Models\Project::findOrFail(1); //... $project->settings = array_merge($project->settings, ["new_option" => 42]);

But this is not very convenient. If you also don’t like this approach, let’s look at how to implement another, more complicated solution. Firstly, let’s create an object which will contain your array:

app/DataTransfers/ProjectSettings.php<?php namespace App\DataTransfers; class ProjectSettings { public array $list; public function __construct(array $list) { $this->list = $list; } public function toArray() { return $this->list; } }

Eloquent does not know how to convert such an object to a string nor how to restore it from the database, so you need to create a custom cast class with the following code:

app/Casts/ProjectSettingsCast.php<?php namespace App\Casts; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; use App\DataTransfers\ProjectSettings; class ProjectSettingsCast implements CastsAttributes { public function get($model, $key, $value, $attributes) { return new ProjectSettings(json_decode($attributes["settings"])) } public function set($model, $key, $value, $attributes) { if (!$value instanceof ProjectSettings) { throw new \InvalidArgumentException("Unsupported value!"); } return [ "settings" => $value->toArray(), ]; } }

After that, update the $casts field in the model:

app/Models/Project.phpprotected $casts = [ "settings" => \App\Casts\ProjectSettingsCast::class, ];

After this, you can modify the settings list directly in the model property. For example:

app/Http/Controllers/ModifyProjectController.php$project = \App\Models\Project::findOrFail(1); //... $project->settings->list["new_option"] = 42;

This code is much easier to read and understand. However, the value in the database has the same structure, so it is completely compatible with the previous approach.

Additionally, you can add methods to your DTO object to add and remove values. Then everything will look even better:

app/Http/Controllers/ModifyProjectController.php$project->settings->add("new_option", 42);

Wrapping Up

In this short article, I have shown you how to fix the error indirect modification of overloaded property has no effect in Laravel. It’s a simple approach, but it can make your code more readable.

Leave a Comment