Помилка Indirect Modification of Overloaded Property Has no Effect в Laravel

В попередній статті я розповідав про те, як правильно записати масив в поле моделі. Це доволі зручно, тому що вам не потрібно думати про те як конвертувати його в стрічку, а потім як зі стрічки зробити масив. Але є одна проблема. Ви не можете міняти або додавати поля в цей масив прямо в полі моделі.

Якщо ви спробуєте це зробити то отримаєте помилку з повідомленням “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. Це доволі простий підхід, але він може зробити ваш код більш читабельним.

Залишити коментар