В попередній статті я розповідав про те, як правильно записати масив в поле моделі. Це доволі зручно, тому що вам не потрібно думати про те як конвертувати його в стрічку, а потім як зі стрічки зробити масив. Але є одна проблема. Ви не можете міняти або додавати поля в цей масив прямо в полі моделі.
Якщо ви спробуєте це зробити то отримаєте помилку з повідомленням “Indirect Modification of Overloaded Property Has no Effect”. У цій статті я покажу чому так відбувається, а також як вирішити цю проблему.
Чому виникає ця проблема?
Припустимо у вас є модель Project, в якій є поле settings.
app/Models/Project.php<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
protected $casts = [
"settings" => "array",
];
protected $attributes = [
"settings" => "[]",
];
}
Якщо ви спробуєте додати нову опцію в масив settings, або ж змінити значення вже існуючої ось так, то отримаєте помилку:
app/Http/Controllers/ModifyProjectController.php$project = \App\Models\Project::findOrFail(1);
$project->settings["new_option"] = 42;

Для доступу до полів моделей в Laravel використовуються магічні методи PHP __get() та __set(). За допомогою них можна додавати віртуальні поля до класів. Перший викликається при спробі отримати доступ до неіснуючого поля, а другий при спробі запису данних в неіснуюче поле.
Але якщо в такому полі знаходиться масив, то модифікувати його не можна. Його можна або прочитати повністю, або записати значення вього поля повністю. Так стається тому що в PHP всі скалярні типи, а також масиви передаються по значенню. Таким чином, коли функція __get() повертає вам масив для модифікації, то він є копією масиву, який знаходиться в полі моделі на момент виклику функції. І фактично ваші зміни не будуть збережені.
Це можна обійти двома способами. По перше, ви можете отримати масив, виконати необхідні дії, а потім записати його заново. Або ж використовувати для зберігання опцій в полі замість масиву обєкт. Оскільки обєкти в PHP завжди передаються за посиланням, тут все буде працювати.
Як виправити помилку?
Давайте розглянемо детальніше перший спосіб. Тут все дуже просто, зберігаємо значення поля в локальну змінну, модифікуємо і повертаємо назад:
app/Http/Controllers/ModifyProjectController.php$project = \App\Models\Project::findOrFail(1);
//...
$settings = $project->settings;
$settings["new_option"] = 42;
$project->settings = $settings;
Можна навіть записати все в одну стрічку:
app/Http/Controllers/ModifyProjectController.php$project = \App\Models\Project::findOrFail(1);
//...
$project->settings = array_merge($project->settings, ["new_option" => 42]);
Але мені здається що це не дуже зручно. Якщо вам також не подобається це спосіб, то є ще одне, більш складне вирішення. Якщо налаштування будуть зберігатись в обєкті, або ж просто загорнути ваш масив в обєкт, то ви зможете модифікувати їх без будь-яких проблем. Наприклад, давайте створимо такий обєкт з налаштуваннями:
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 не знає як конвертувати такий обєкт в масив, а також як його відновити з бази данних тому потрібно створити cast клас:
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(),
];
}
}
Після цього обновить замінну $casts в моделі:
app/Models/Project.phpprotected $casts = [
"settings" => \App\Casts\ProjectSettingsCast::class,
];
А тепер ви можете модифікувати список налаштувань прямо в полі моделі. Наприклад:
app/Http/Controllers/ModifyProjectController.php$project = \App\Models\Project::findOrFail(1);
//...
$project->settings->list["new_option"] = 42;
Такий код значно легше читати та розуміти. Додатково ви можете додати в ваш обєкт DTO методи для додавання видалення значень. Тоді все буде виглядати ще краще:
app/Http/Controllers/ModifyProjectController.php$project->settings->add("new_option", 42);
Висновки
В цьому короткому матеріалі я показав як виправити помилку indirect modification of overloaded property has no effect в Laravel. Це доволі простий підхід, але він може зробити ваш код більш читабельним.