[Fix] Laravel 4.2. Отваливаются события Eloquent моделей в Unit тестах

Категория: Laravel

Этому багу уже больше года (был зафиксирован 5 мая 2013)! Каждый разработчик, который натыкался на эти грабли, терял несколько часов времени при локализации проблемы и поиске решения. И даже сейчас (Laravel 4.2) баг не исправлен, в документации по Unit тестам об этом ни слова!

Проблема

При запуске phpunit (без параметров) у моделей отваливается обработка событий! Обсуждение бага-фичи идет здесь - https://github.com/laravel/framework/issues/1181. Проблема заключается в том, что метод boot() вызывается только один раз - для первого теста. Соответственно, события Eloquent моделей также регистрируются только в первом Unit-тесте. После этого, класс модели уже загружен и event dispatchers больше не регистрирует события.

Решение №1 (EloquentEventsMechanic)

Это самое практичное решение, основанное на перезагрузке событий всех Eloquent моделей приложения (в каталоге models/) перед каждым тестом. Вызывайте метод EloquentEventsMechanic::reload() в TestCase::setUp():

public function setUp()
{
  parent::setUp();
  EloquentEventsMechanic::reload();
}

Класс EloquentEventsMechanic:

class EloquentEventsMechanic
{
  public static function reload()
  {
      // Get all models in the Model directory
    $models_dir = dirname(__DIR__) . '/models';
    $files = \File::files($models_dir);

    // Exclude non *.php files
    foreach ($files as $i => $file) if (!strpos($file, '.php')) {
        unset($files[$i]);
    }

    // Remove the directory name and the .php from the filename
    $files = str_replace($models_dir.'/', '', $files);
    $files = str_replace('.php', '', $files);

    // Remove "BaseModel" as we dont want to boot that moodel
    $key = array_search('BaseModel', $files);
    if ($key !== false) {
        unset($files[$key]);
    }

    // Reset each model event listeners
    foreach ($files as $model)
    {
        if (!method_exists($model, 'flushEventListeners')) {
            continue;
        }

        // Flush any existing listeners
        call_user_func(array($model, 'flushEventListeners'));

        // Re-register them
        call_user_func(array($model, 'boot'));
    }
  }
}

Решение №2

Следующий вариант решения проблемы - это явно вызывать метод Model::boot(), для используемых моделей (которые имеют зарегистрированные события):

public function setUp()
{
  parent::setUp();
  
  YourModel::boot();
  // OR
  YourModel::observe(new YourModelObserver);
}

Решение №3

Выгружать (unboot) классы моделей после каждого теста, в котором они используются (источник: https://github.com/laravel/framework/issues/1181#issuecomment-54392670). Вкратце, создаем трейт с методом unbootIfBooted(), добавляем трейт в модель и дергаем метод YouModel::unbootIfBooted() после выполнения теста в методе TestCase::tearDown().

Трейт UnbootTrait:

trait UnbootTrait
{
    public static function unbootIfBooted()
    {
        $class = get_called_class();

        if (isset(static::$booted[$class])) {
            static::$booted[$class] = null;

            // fireModelEvent('unbooting', false);

            static::unboot();

            // fireModelEvent('unbooted', false);
        }
    }

    protected static function unboot() {}
}

Заключение

Где-то говорили, что может помочь запуск phpunit с параметром --process-isolation. Однако это не работает, т.к. phpunit выдает ошибку: "PHP Notice: Constant LARAVEL_START already defined in ..."

Позднее статическое связывание (Late Static Binding, LSB) тоже не дает результатов:

public static function boot()
{
  parent::boot();
  static::saving(function($content) {
    // логика обработчика события
  });
}

#Laravel 4.2, #Unit-тесты, #Eloquent Events

категория: Laravel