Admin Panel — Posts CRUD
LaravelNow we'll build the full posts management system in the admin panel — list all posts, create new ones, edit existing ones, and delete them.
Admin Post Controller
Terminal
php artisan make:controller Admin/PostController --resourceapp/Http/Controllers/Admin/PostController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Post;
use App\Models\Category;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
class PostController extends Controller
{
public function index()
{
$posts = Post::with(['user', 'category'])
->latest()
->paginate(15);
return view('admin.posts.index', compact('posts'));
}
public function create()
{
$categories = Category::all();
$tags = Tag::all();
return view('admin.posts.create', compact('categories', 'tags'));
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
'category_id' => 'nullable|exists:categories,id',
'tags' => 'array',
'tags.*' => 'exists:tags,id',
'image' => 'nullable|image|max:2048',
'published' => 'boolean',
'excerpt' => 'nullable|string|max:500',
]);
// Handle image upload
if ($request->hasFile('image')) {
$validated['image'] = $request->file('image')
->store('posts', 'public');
}
$validated['user_id'] = auth()->id();
$validated['slug'] = Str::slug($validated['title']);
$validated['published_at'] = $request->published ? now() : null;
$post = Post::create($validated);
// Sync tags
if ($request->tags) {
$post->tags()->sync($request->tags);
}
return redirect()
->route('admin.posts.index')
->with('success', 'Post created successfully!');
}
public function edit(Post $post)
{
$categories = Category::all();
$tags = Tag::all();
return view('admin.posts.edit', compact('post', 'categories', 'tags'));
}
public function update(Request $request, Post $post)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
'category_id' => 'nullable|exists:categories,id',
'tags' => 'array',
'image' => 'nullable|image|max:2048',
'published' => 'boolean',
'excerpt' => 'nullable|string|max:500',
]);
// Handle new image upload
if ($request->hasFile('image')) {
// Delete old image
if ($post->image) {
Storage::disk('public')->delete($post->image);
}
$validated['image'] = $request->file('image')
->store('posts', 'public');
}
// Set published_at when publishing for first time
if ($request->published && !$post->published_at) {
$validated['published_at'] = now();
}
$post->update($validated);
$post->tags()->sync($request->tags ?? []);
return redirect()
->route('admin.posts.index')
->with('success', 'Post updated successfully!');
}
public function destroy(Post $post)
{
if ($post->image) {
Storage::disk('public')->delete($post->image);
}
$post->delete();
return redirect()
->route('admin.posts.index')
->with('success', 'Post deleted successfully!');
}
}Posts List View
resources/views/admin/posts/index.blade.php
@extends('layouts.admin')
@section('title', 'All Posts')
@section('content')
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-semibold">All Posts ({{ $posts->total() }})</h2>
<a href="{{ route('admin.posts.create') }}"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
+ New Post
</a>
</div>
<div class="bg-white rounded shadow overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b">
<tr>
<th class="p-4 text-left">Title</th>
<th class="p-4 text-left">Category</th>
<th class="p-4 text-left">Status</th>
<th class="p-4 text-left">Views</th>
<th class="p-4 text-left">Date</th>
<th class="p-4 text-left">Actions</th>
</tr>
</thead>
<tbody class="divide-y">
@forelse($posts as $post)
<tr class="hover:bg-gray-50">
<td class="p-4">
<div class="font-medium">{{ $post->title }}</div>
<div class="text-gray-400 text-xs">{{ $post->slug }}</div>
</td>
<td class="p-4">{{ $post->category?->name ?? '—' }}</td>
<td class="p-4">
@if($post->published)
<span class="bg-green-100 text-green-700 px-2 py-1 rounded text-xs">Published</span>
@else
<span class="bg-yellow-100 text-yellow-700 px-2 py-1 rounded text-xs">Draft</span>
@endif
</td>
<td class="p-4">{{ number_format($post->views) }}</td>
<td class="p-4 text-gray-500">{{ $post->created_at->format('M d, Y') }}</td>
<td class="p-4">
<div class="flex gap-2">
<a href="{{ route('admin.posts.edit', $post) }}"
class="text-blue-600 hover:underline">Edit</a>
<form method="POST" action="{{ route('admin.posts.destroy', $post) }}"
onsubmit="return confirm('Delete this post?')">
@csrf @method('DELETE')
<button class="text-red-600 hover:underline">Delete</button>
</form>
</div>
</td>
</tr>
@empty
<tr><td colspan="6" class="p-8 text-center text-gray-400">No posts yet.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">{{ $posts->links() }}</div>
@endsection💡
Always use
Storage::disk('public')->delete() when deleting a post with an image. If you don't, the image file stays on disk forever wasting storage space.💡
@forelse is better than @foreach for lists! It has a built-in
@empty block for when there's no data — no need for a separate @if(count($posts) > 0) check.Stuck? Need help?
Review the previous lessons or check the Laravel documentation.