{"id":300,"date":"2025-08-27T19:28:25","date_gmt":"2025-08-27T19:28:25","guid":{"rendered":"https:\/\/1v0.net\/blog\/?p=300"},"modified":"2025-08-27T19:28:29","modified_gmt":"2025-08-27T19:28:29","slug":"soft-deletes-in-laravel-restore-force-delete-and-prune-data","status":"publish","type":"post","link":"https:\/\/1v0.net\/blog\/soft-deletes-in-laravel-restore-force-delete-and-prune-data\/","title":{"rendered":"Soft Deletes in Laravel: Restore, Force Delete, and Prune Data"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\"><strong>Soft Deletes in Laravel: Restore, Force Delete, and Prune Data<\/strong><\/h2>\n\n\n\n<p>Soft deletes let you \u201cdelete\u201d rows without losing them immediately. Instead of removing data, Eloquent sets a <code>deleted_at<\/code> timestamp and excludes those rows from normal queries. You can later <em>restore<\/em> or <em>permanently remove<\/em> them, and even <em>prune<\/em> old soft-deleted data on a schedule. In this guide, you\u2019ll enable soft deletes, build a Recycle Bin UI, and automate cleanup safely.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1 &#8211; Add <code>deleted_at<\/code> to Your Table<\/strong><\/h2>\n\n\n\n<p>Add a soft delete column using the schema builder. Use <code>softDeletes()<\/code> (or <code>softDeletesTz()<\/code> if you prefer timezone-aware timestamps).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ database\/migrations\/2025_08_27_000000_add_soft_deletes_to_posts_table.php<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Migrations<\/span>\\<span class=\"hljs-title\">Migration<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Schema<\/span>\\<span class=\"hljs-title\">Blueprint<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">Schema<\/span>;\n\n<span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Migration<\/span> <\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">up<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        Schema::table(<span class=\"hljs-string\">'posts'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">(Blueprint $table)<\/span> <\/span>{\n            $table-&gt;softDeletes(); <span class=\"hljs-comment\">\/\/ adds nullable deleted_at TIMESTAMP<\/span>\n        });\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">down<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        Schema::table(<span class=\"hljs-string\">'posts'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">(Blueprint $table)<\/span> <\/span>{\n            $table-&gt;dropSoftDeletes(); <span class=\"hljs-comment\">\/\/ drops deleted_at<\/span>\n        });\n    }\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This migration adds a <code>deleted_at<\/code> column that Eloquent uses to hide \u201ctrashed\u201d rows. The <code>down()<\/code> method makes the change reversible.<\/p>\n\n\n\n<p>Run the migration:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">php artisan migrate<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>After running, the table is ready to support soft deletes without breaking existing queries.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2 &#8211; Enable Soft Deletes on the Model<\/strong><\/h2>\n\n\n\n<p>Add the <code>SoftDeletes<\/code> trait to your Eloquent model. This automatically excludes trashed rows from default queries such as <code>Model::all()<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Models\/Post.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">Model<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">SoftDeletes<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">Post<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Model<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">SoftDeletes<\/span>;\n\n    <span class=\"hljs-keyword\">protected<\/span> $fillable = &#91;<span class=\"hljs-string\">'user_id'<\/span>,<span class=\"hljs-string\">'title'<\/span>,<span class=\"hljs-string\">'body'<\/span>,<span class=\"hljs-string\">'status'<\/span>];\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>With the trait, calling <code>Post::query()<\/code> ignores rows where <code>deleted_at<\/code> is not null. You\u2019ll use special helpers to include or filter trashed rows when needed.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3 &#8211; Soft Delete, Restore, and Force Delete<\/strong><\/h2>\n\n\n\n<p>Soft delete marks the row; restore brings it back; force delete removes it permanently from the database.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ Soft delete a post<\/span>\n$post = Post::findOrFail($id);\n$post-&gt;delete(); <span class=\"hljs-comment\">\/\/ sets deleted_at<\/span>\n\n<span class=\"hljs-comment\">\/\/ Restore a soft-deleted post<\/span>\n$post = Post::withTrashed()-&gt;findOrFail($id);\n$post-&gt;restore(); <span class=\"hljs-comment\">\/\/ clears deleted_at<\/span>\n\n<span class=\"hljs-comment\">\/\/ Permanently delete<\/span>\n$post = Post::withTrashed()-&gt;findOrFail($id);\n$post-&gt;forceDelete(); <span class=\"hljs-comment\">\/\/ removes row from DB<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Use <code>withTrashed()<\/code> to access items regardless of deletion state, then call <code>restore()<\/code> or <code>forceDelete()<\/code> as appropriate.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4 &#8211; Query Helpers for Trashed Rows<\/strong><\/h2>\n\n\n\n<p>These helpers give you fine control over which rows are returned in queries.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ Include trashed + non-trashed<\/span>\n$all = Post::withTrashed()-&gt;latest()-&gt;paginate(<span class=\"hljs-number\">10<\/span>);\n\n<span class=\"hljs-comment\">\/\/ Only trashed<\/span>\n$trashed = Post::onlyTrashed()-&gt;orderBy(<span class=\"hljs-string\">'deleted_at'<\/span>,<span class=\"hljs-string\">'desc'<\/span>)-&gt;get();\n\n<span class=\"hljs-comment\">\/\/ Explicitly exclude trashed (same as default)<\/span>\n$active = Post::withoutTrashed()-&gt;get();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>withTrashed()<\/code> is useful for admin reports; <code>onlyTrashed()<\/code> powers a Recycle Bin; <code>withoutTrashed()<\/code> matches the default behavior when the trait is enabled.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5 &#8211; Routes &amp; Controller for a Recycle Bin UI<\/strong><\/h2>\n\n\n\n<p>Expose routes to view trashed items, restore them, or permanently delete them. Authorize these actions to admin roles only.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/web.php (snippet)<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>\\<span class=\"hljs-title\">PostTrashController<\/span>;\n\nRoute::middleware(&#91;<span class=\"hljs-string\">'auth'<\/span>])-&gt;group(<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">()<\/span> <\/span>{\n    Route::get(<span class=\"hljs-string\">'\/posts\/trash'<\/span>, &#91;PostTrashController::class, <span class=\"hljs-string\">'index'<\/span>])-&gt;name(<span class=\"hljs-string\">'posts.trash.index'<\/span>);\n    Route::patch(<span class=\"hljs-string\">'\/posts\/{id}\/restore'<\/span>, &#91;PostTrashController::class, <span class=\"hljs-string\">'restore'<\/span>])-&gt;name(<span class=\"hljs-string\">'posts.trash.restore'<\/span>);\n    Route::delete(<span class=\"hljs-string\">'\/posts\/{id}\/force'<\/span>, &#91;PostTrashController::class, <span class=\"hljs-string\">'force'<\/span>])-&gt;name(<span class=\"hljs-string\">'posts.trash.force'<\/span>);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>These routes provide a list view plus endpoints to restore or force delete a specific post. Use policies\/middleware to ensure only privileged users can perform destructive actions.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Controllers\/PostTrashController.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Post<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Request<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">PostTrashController<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Controller<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">index<\/span><span class=\"hljs-params\">()<\/span>\n    <\/span>{\n        $posts = Post::onlyTrashed()\n            -&gt;orderBy(<span class=\"hljs-string\">'deleted_at'<\/span>,<span class=\"hljs-string\">'desc'<\/span>)\n            -&gt;paginate(<span class=\"hljs-number\">10<\/span>);\n\n        <span class=\"hljs-keyword\">return<\/span> view(<span class=\"hljs-string\">'posts.trash'<\/span>, compact(<span class=\"hljs-string\">'posts'<\/span>));\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">restore<\/span><span class=\"hljs-params\">($id)<\/span>\n    <\/span>{\n        $post = Post::withTrashed()-&gt;findOrFail($id);\n        <span class=\"hljs-comment\">\/\/ $this-&gt;authorize('restore', $post); \/\/ optional policy<\/span>\n        $post-&gt;restore();\n\n        <span class=\"hljs-keyword\">return<\/span> back()-&gt;with(<span class=\"hljs-string\">'status'<\/span>,<span class=\"hljs-string\">'Post restored.'<\/span>);\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">force<\/span><span class=\"hljs-params\">($id)<\/span>\n    <\/span>{\n        $post = Post::withTrashed()-&gt;findOrFail($id);\n        <span class=\"hljs-comment\">\/\/ $this-&gt;authorize('forceDelete', $post); \/\/ optional policy<\/span>\n        $post-&gt;forceDelete();\n\n        <span class=\"hljs-keyword\">return<\/span> back()-&gt;with(<span class=\"hljs-string\">'status'<\/span>,<span class=\"hljs-string\">'Post permanently deleted.'<\/span>);\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The controller paginates trashed posts and provides RESTful handlers to restore or permanently delete items. Optionally enforce policies for extra safety.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>6 &#8211; UI: Recycle Bin Blade View<\/strong><\/h2>\n\n\n\n<p>Here\u2019s a simple Recycle Bin with Restore and Delete buttons. Use CSRF and method spoofing to protect the actions.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;!-- resources\/views\/posts\/trash.blade.php --&gt;\n@extends(<span class=\"hljs-string\">'layouts.app'<\/span>)\n\n@section(<span class=\"hljs-string\">'content'<\/span>)\n&lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">container<\/span>\"&gt;\n  &lt;<span class=\"hljs-title\">h1<\/span> <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">mb<\/span>-4\"&gt;<span class=\"hljs-title\">Recycle<\/span> <span class=\"hljs-title\">Bin<\/span>&lt;\/<span class=\"hljs-title\">h1<\/span>&gt;\n\n  @<span class=\"hljs-title\">if<\/span>(<span class=\"hljs-title\">session<\/span>('<span class=\"hljs-title\">status<\/span>'))\n    &lt;<span class=\"hljs-title\">div<\/span> <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">alert<\/span> <span class=\"hljs-title\">alert<\/span>-<span class=\"hljs-title\">success<\/span>\"&gt;<\/span>{{ session(<span class=\"hljs-string\">'status'<\/span>) }}&lt;\/div&gt;\n  @<span class=\"hljs-keyword\">endif<\/span>\n\n  @forelse($posts <span class=\"hljs-keyword\">as<\/span> $post)\n    &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">card<\/span> <span class=\"hljs-title\">mb<\/span>-3\"&gt;\n      &lt;<span class=\"hljs-title\">div<\/span> <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">card<\/span>-<span class=\"hljs-title\">body<\/span> <span class=\"hljs-title\">d<\/span>-<span class=\"hljs-title\">flex<\/span> <span class=\"hljs-title\">justify<\/span>-<span class=\"hljs-title\">content<\/span>-<span class=\"hljs-title\">between<\/span> <span class=\"hljs-title\">align<\/span>-<span class=\"hljs-title\">items<\/span>-<span class=\"hljs-title\">center<\/span>\"&gt;\n        &lt;<span class=\"hljs-title\">div<\/span>&gt;\n          &lt;<span class=\"hljs-title\">h5<\/span> <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">card<\/span>-<span class=\"hljs-title\">title<\/span> <span class=\"hljs-title\">mb<\/span>-1\"&gt;<\/span>{{ $post-&gt;title }}&lt;\/h5&gt;\n          &lt;small <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">text<\/span>-<span class=\"hljs-title\">muted<\/span>\"&gt;<span class=\"hljs-title\">Deleted<\/span> <span class=\"hljs-title\">at<\/span>: <\/span>{{ $post-&gt;deleted_at }}&lt;\/small&gt;\n        &lt;\/div&gt;\n\n        &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">d<\/span>-<span class=\"hljs-title\">flex<\/span> <span class=\"hljs-title\">gap<\/span>-2\"&gt;\n          &lt;<span class=\"hljs-title\">form<\/span> <span class=\"hljs-title\">method<\/span>=\"<span class=\"hljs-title\">POST<\/span>\" <span class=\"hljs-title\">action<\/span>=\"<\/span>{{ route(<span class=\"hljs-string\">'posts.trash.restore'<\/span>, $post-&gt;id) }}<span class=\"hljs-string\">\"&gt;\n            @csrf @method('PATCH')\n            &lt;button class=\"<\/span>btn btn-outline-secondary<span class=\"hljs-string\">\"&gt;Restore&lt;\/button&gt;\n          &lt;\/form&gt;\n\n          &lt;form method=\"<\/span>POST<span class=\"hljs-string\">\" action=\"<\/span>{{ route(<span class=\"hljs-string\">'posts.trash.force'<\/span>, $post-&gt;id) }}<span class=\"hljs-string\">\"\n                onsubmit=\"<\/span><span class=\"hljs-keyword\">return<\/span> confirm(<span class=\"hljs-string\">'Permanently delete this post?'<\/span>);<span class=\"hljs-string\">\"&gt;\n            @csrf @method('DELETE')\n            &lt;button class=\"<\/span>btn btn-danger<span class=\"hljs-string\">\"&gt;Delete Forever&lt;\/button&gt;\n          &lt;\/form&gt;\n        &lt;\/div&gt;\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  @empty\n    &lt;p class=\"<\/span>text-muted<span class=\"hljs-string\">\"&gt;No trashed posts.&lt;\/p&gt;\n  @endforelse\n\n  {{ $posts-&gt;links() }}\n&lt;\/div&gt;\n@endsection<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This UI lists trashed posts with the deletion timestamp and action buttons. Restore unsets <code>deleted_at<\/code>, while \u201cDelete Forever\u201d removes the row from the database.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>7 &#8211; Scheduled Pruning of Old Soft-Deleted Rows<\/strong><\/h2>\n\n\n\n<p>Use model pruning to automatically purge items that have been soft-deleted for a while (e.g., 30 days).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Models\/Post.php (add trait &amp; prunable() if you like)<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">Model<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">SoftDeletes<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">Prunable<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">Post<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Model<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">SoftDeletes<\/span>, <span class=\"hljs-title\">Prunable<\/span>;\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">prunable<\/span><span class=\"hljs-params\">()<\/span>\n    <\/span>{\n        <span class=\"hljs-comment\">\/\/ prune items soft-deleted more than 30 days ago<\/span>\n        <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">static<\/span>::onlyTrashed()\n            -&gt;where(<span class=\"hljs-string\">'deleted_at'<\/span>, <span class=\"hljs-string\">'&lt;'<\/span>, now()-&gt;subDays(<span class=\"hljs-number\">30<\/span>));\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The <code>Prunable<\/code> trait defines a query selecting candidates for permanent removal. Here we keep trashed posts for 30 days before pruning them.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Console\/Kernel.php (schedule pruning)<\/span>\n<span class=\"hljs-keyword\">protected<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">schedule<\/span><span class=\"hljs-params\">(\\Illuminate\\Console\\Scheduling\\Schedule $schedule)<\/span>: <span class=\"hljs-title\">void<\/span>\n<\/span>{\n    <span class=\"hljs-comment\">\/\/ Run daily at 02:00; add --pretend in staging to preview deletions<\/span>\n    $schedule-&gt;command(<span class=\"hljs-string\">'model:prune'<\/span>, &#91;\n        <span class=\"hljs-string\">'--model'<\/span> =&gt; &#91;\\App\\Models\\Post::class],\n    ])-&gt;dailyAt(<span class=\"hljs-string\">'02:00'<\/span>);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This schedules the built-in <code>model:prune<\/code> command to run daily. In non-production environments, consider <code>--pretend<\/code> to preview what would be deleted.<\/p>\n\n\n\n<p>Remember to set up your cron to run the Laravel scheduler: <code>* * * * * php \/path\/to\/artisan schedule:run &gt;&gt; \/dev\/null 2&gt;&amp;1<\/code>.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping Up<\/h2>\n\n\n\n<p>Soft deletes provide a safe middle ground between \u201cactive\u201d and \u201cgone.\u201d You added a <code>deleted_at<\/code> column, enabled the <code>SoftDeletes<\/code> trait, implemented restore and force delete flows, built a Recycle Bin UI, and configured pruning to keep the database lean. This pattern reduces accidental loss and gives you clean, auditable lifecycle management for records.<\/p>\n\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\">What\u2019s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/handling-large-data-sets-in-laravel-with-chunking-cursors\">Handling Large Data Sets with Chunking &amp; Cursors<\/a><\/li>\n<li><a href=\"\/blog\/how-to-use-eloquent-api-resources-for-clean-apis\">How to Use Eloquent API Resources for Clean APIs<\/a><\/li>\n<li><a href=\"\/blog\/filtering-and-searching-with-laravel-eloquent-query-builder\">Filtering and Searching with Eloquent Query Builder<\/a><\/li>\n<\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>Soft Deletes in Laravel: Restore, Force Delete, and Prune Data Soft deletes let you \u201cdelete\u201d rows without losing them immediately. Instead of removing data, Eloquent sets a deleted_at timestamp and excludes those rows from normal queries. You can later restore or permanently remove them, and even prune old soft-deleted data on a schedule. In this [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":304,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[43,38,36,42],"class_list":["post-300","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-laravel","tag-data-retention","tag-database","tag-eloquent","tag-soft-deletes"],"_links":{"self":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/300","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/comments?post=300"}],"version-history":[{"count":1,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/300\/revisions"}],"predecessor-version":[{"id":303,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/300\/revisions\/303"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media\/304"}],"wp:attachment":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media?parent=300"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/categories?post=300"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/tags?post=300"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}