Как стать автором
Поиск
Написать публикацию
Обновить

Неочевидный нюанс при изменении пространства имён моделей в Laravel

Уровень сложностиПростой
Время на прочтение3 мин
Количество просмотров828

На работе поступила очередная задача: разобраться и устранить странную проблему в работе давно и надёжно работающего сервиса. Проблема заключалась в том, что часть объектов двух видов перестала работать. Причём именно часть объектов.

Сам сервис написан на 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, который:

  1. Устанавливает обязательность использования псевдонимов для полиморфных связей в приложении;

  2. И одновременно, определяет эти псевдонимы для всех нужных классов.

Документация рекомендует вызывать этот метод в AppServiceProvider::boot или в методе boot специально зарегистрированного сервис-провайдера.

Назначение псевдонимов таким способом решает все указанные выше проблемы:

  1. Если разработчик забудет назначить псевдоним для какого-либо класса, будет выкинуто исключение ClassMorphViolationException;

  2. Можно абсолютно свободно менять имена классов, не переживая про потерю связи между объектами.

Поэтому был произведён рефакторинг приложения:

  1. Установлены псевдонимы для классов

    class AppServiceProvider extends ServiceProvider
    {
     ...
    
     /**
      * Bootstrap any application services.
      *
      * @return void
      */
     public function boot()
     {
         Relation::enforceMorphMap([
             'FirstModel'        => FirstModel::class,
             'SecondModel'       => SecondModel::class,
             ...
         ]);
     }
     ...
    }
  2. Добавлена небольшая миграция:

     public function up()
     {
         foreach (Relation::$morphMap as $morphName => $fullyQualifiedClassName) {
             ChildModel::where('entity_type', $fullyQualifiedClassName)
                 ->update(['entity_type' => $morphName]);
         }
     }
  3. В ряде мест кода было учтено, что entity_type теперь хранит псевдоним, а не полное имя класса.

Теги:
Хабы:
+3
Комментарии6

Публикации

Ближайшие события