[Fix] Laravel 4.2. Отваливаются события Eloquent моделей в Unit тестах
Этому багу уже больше года (был зафиксирован 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