На работе поступила очередная задача: разобраться и устранить странную проблему в работе давно и надёжно работающего сервиса. Проблема заключалась в том, что часть объектов двух видов перестала работать. Причём именно часть объектов.
Сам сервис написан на PHP с использованием фреймворка Laravel и служит для общения с внешней системой.
Поскольку есть внешняя система, то в первую очередь проверил её. Но с ней всё было в порядке. Данные уходили и приходили. И в БД сервиса всё заносилось как надо.
Но при обращении к ресурсам определённых объектов по API не возвращалась часть полей, которые хранятся в связанной таблице, связь типа полиморфное отношение «один-к-одному» («MorphOne»).
class FirstModel extends Model
{
...
public function childModel(): MorphOne
{
return $this->morphOne(ChildModel::class, 'entity');
}
...
}
class SecondModel extends Model
{
...
public function childModel(): MorphOne
{
return $this->morphOne(ChildModel::class, 'entity');
}
...
}
class ChildModel extends Model
{
...
public function entity(): MorphTo
{
return $this->morphTo();
}
...
}
Анализ проблемных объектов показал, что неполные данные возвращались по тем из них, которые были созданы до определенного момента времени вчера. Значит, дело возможно в изменениях в коде. Посмотрел логи git’а.
И в них увидел, что как раз вчера, добавляя новые модели, коллега изменил пространство имён у ряда уже существующих. Именно у тех, в которых и появились проблемы. Само изменение было правомерным и давно напрашивалось. И сделано правильно: пространство имён было изменено во всех нужных местах кода. PHPStorm за этим проследил.
Было:
namespace App\Models\Name1;
...
class FirstModel extends Model
{
...
}
namespace App\Models\Name1;
...
class SecondModel extends Model
{
...
}
Стало:
namespace App\Models\Name2;
...
class FirstModel extends Model
{
...
}
namespace App\Models\Name2;
...
class SecondModel extends Model
{
...
}
Значит, предположил я, где-то закэшировалось старое namespace. Были очищены все кэши Laravel. Но проблема осталась.
Снова внимательно изучил записи в БД. В них всё было записано правильно.
Обратился за помощью к коллегам. Созвонились, описал проблему, показал сделанные шаги. Бурное обсуждение и дискуссия не дали результата. В конце концов, один из нас спросил у DeepSeek. И в ответе ИИ, глаз выхватил строку про то, что в таблице, связанной связью типа “MorphOne”, хранится полное квалифицированное имя объекта.
Снова глянул в таблицу БД. Ну, конечно. Для объектов, созданных до переноса классов в новое пространство, в таблице было записано старое. Я же смотрел на эти записи и не заметил очевидного. Вот что значит «глаз замылился».
Написал и залил простенькую миграцию.
public function up()
{
ChildModel::where('entity_type', 'App\Models\Name1\FirstModel')
->update(['entity_type' => 'App\Models\Name2\FirstModel']);
ChildModel::where('entity_type', 'App\Models\Name1\SecondModel')
->update(['entity_type' => 'App\Models\Name2\SecondModel']);
}
Дождался окончания тестирования и развёртывания. И вуаля, всё заработало правильно.
Вывод простой: в Laravel при любых изменения в полном квалифицированном имени класса модели, имеющей полиморфные отношения, не забывать изменять эти имена в соответствующих связанных таблицах в БД.
И остаётся проблема: как не забыть про этот нюанс в следующий раз.
Дополнение от 22.07.2025
Как справедливо указал в комментариях @Dekmabot, для полиморфных связей лучшим решением было бы использование псевдонимов, а не полных квалифицированных имён классов.
Чтение документации вывело на статический метод Relation::enforceMorphMap
, который:
Устанавливает обязательность использования псевдонимов для полиморфных связей в приложении;
И одновременно, определяет эти псевдонимы для всех нужных классов.
Документация рекомендует вызывать этот метод в AppServiceProvider::boot
или в методе boot
специально зарегистрированного сервис-провайдера.
Назначение псевдонимов таким способом решает все указанные выше проблемы:
Если разработчик забудет назначить псевдоним для какого-либо класса, будет выкинуто исключение
ClassMorphViolationException
;Можно абсолютно свободно менять имена классов, не переживая про потерю связи между объектами.
Поэтому был произведён рефакторинг приложения:
Установлены псевдонимы для классов
class AppServiceProvider extends ServiceProvider { ... /** * Bootstrap any application services. * * @return void */ public function boot() { Relation::enforceMorphMap([ 'FirstModel' => FirstModel::class, 'SecondModel' => SecondModel::class, ... ]); } ... }
Добавлена небольшая миграция:
public function up() { foreach (Relation::$morphMap as $morphName => $fullyQualifiedClassName) { ChildModel::where('entity_type', $fullyQualifiedClassName) ->update(['entity_type' => $morphName]); } }
В ряде мест кода было учтено, что entity_type теперь хранит псевдоним, а не полное имя класса.