LaravelMiddleCoding

Как реализовать soft deletes в Laravel?

Soft deletes реализуются через трейт SoftDeletes в модели и метод softDeletes() в миграции. Удалённые записи помечаются в поле deleted_at и фильтруются автоматически; восстановление — через restore(), включение в запрос — через withTrashed().

Soft Deletes в Laravel

Soft delete (мягкое удаление) — это подход, при котором запись не удаляется из базы данных физически, а помечается временной меткой в поле deleted_at. Все стандартные запросы Eloquent автоматически фильтруют такие записи через WHERE deleted_at IS NULL.

Настройка модели

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Article extends Model
{
    use SoftDeletes;

    protected $fillable = ['title', 'body', 'user_id'];
}

Добавление колонки в миграции

Schema::create('articles', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body')->nullable();
    $table->timestamps();
    $table->softDeletes(); // добавляет deleted_at TIMESTAMP NULL
});

// Для существующей таблицы
Schema::table('articles', function (Blueprint $table) {
    $table->softDeletes();
});

Базовые операции

// Мягкое удаление — устанавливает deleted_at = now()
$article = Article::find(1);
$article->delete();

// Проверить, удалена ли запись
$article->trashed(); // true

// Восстановить запись
$article->restore();

// Физически удалить из БД
$article->forceDelete();

Запросы с учётом удалённых записей

// Обычный запрос — удалённые записи НЕ включаются
$articles = Article::all(); // WHERE deleted_at IS NULL

// Включить удалённые
$allArticles = Article::withTrashed()->get();

// Только удалённые
$deletedArticles = Article::onlyTrashed()->get();

// Восстановить пачку по условию
Article::onlyTrashed()
    ->where('user_id', 42)
    ->restore();

Soft deletes и связи

По умолчанию Eloquent отношения тоже фильтруют удалённые записи. Чтобы включить их явно:

// В определении отношения
public function comments()
{
    return $this->hasMany(Comment::class)->withTrashed();
}

// Каскадное мягкое удаление связей через Observer
class ArticleObserver
{
    public function deleted(Article $article): void
    {
        $article->comments()->delete(); // тоже soft delete
    }

    public function restored(Article $article): void
    {
        $article->comments()->onlyTrashed()->restore();
    }
}

Уникальные индексы и soft deletes

Если есть уникальный индекс (например, на slug), мягко удалённые записи с таким же slug заблокируют создание новых. Решение — составной уникальный индекс с deleted_at:

$table->unique(['slug', 'deleted_at']);

Или использовать whereNull('deleted_at') в validation rule вручную.

Подводные камни

  • Физический внешний ключ FK не знает о soft deletes — запись с deleted_at != null всё равно блокирует удаление родительской таблицы. Используйте ->cascadeOnDelete() аккуратно или управляйте каскадом через Observer.
  • Индексы по полям фильтрации (WHERE status = 'active') должны включать deleted_at для эффективной работы — иначе БД сканирует все строки включая удалённые.
  • withTrashed() в production-запросах нужно использовать осознанно: легко случайно отдать удалённые данные пользователям.
  • При использовании Route Model Binding Laravel автоматически применяет WHERE deleted_at IS NULL — мягко удалённая запись вернёт 404. Если нужно показать её, используйте явный запрос вместо биндинга.
  • forceDelete() не вызывает событие deleting если модель уже soft-deleted — будьте осторожны с Observers, которые рассчитывают на это событие.
  • Таблицы с большим количеством soft-deleted записей требуют периодической очистки (Article::onlyTrashed()->where('deleted_at', '<', now()->subMonths(6))->forceDelete()), иначе производительность деградирует.
  • Если используете Scout (full-text search), soft-deleted записи по умолчанию остаются в индексе — нужно явно слушать событие deleted и удалять из индекса.

Common mistakes

  • Сводить soft deletes к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: Laravel 13 поверх PHP: request проходит через HTTP kernel, middleware, routing, controller/action и response pipeline.
  • Не отделять validation, authorization, transaction boundary и business logic.

What the interviewer is testing

  • Объясняет soft deletes через конкретную точку lifecycle в Laravel.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics