← Laravel Menu · ← Prev: Database
// routes/web.php
Route::resource('posts', PostController::class);
// Generates 7 routes automatically:
// GET /posts → index
// GET /posts/create → create
// POST /posts → store
// GET /posts/{post} → show
// GET /posts/{post}/edit → edit
// PUT/PATCH /posts/{post} → update
// DELETE /posts/{post} → destroy
// Nested resources
Route::resource('posts.comments', CommentController::class)
->shallow();
// See all routes:
php artisan route:list
php artisan make:controller PostController --resource
// app/Http/Controllers/PostController.php
class PostController extends Controller
{
public function index()
{
$posts = Post::with('user')
->latest()
->paginate(15);
return view('posts.index', compact('posts'));
}
public function create()
{
return view('posts.create');
}
public function store(Request $request)
{
$data = $request->validate([
'title' => ['required', 'min:3', 'max:255'],
'body' => ['required', 'min:10'],
]);
$data['user_id'] = auth()->id();
$data['slug'] = Str::slug($data['title']);
$post = Post::create($data);
return redirect()
->route('posts.show', $post)
->with('success', 'Post created!');
}
public function show(Post $post) // route model binding
{
return view('posts.show', compact('post'));
}
public function edit(Post $post)
{
$this->authorize('update', $post); // policy check
return view('posts.edit', compact('post'));
}
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
$data = $request->validate([
'title' => ['required', 'min:3', 'max:255'],
'body' => ['required', 'min:10'],
]);
$data['slug'] = Str::slug($data['title']);
$post->update($data);
return redirect()
->route('posts.show', $post)
->with('success', 'Post updated!');
}
public function destroy(Post $post)
{
$this->authorize('delete', $post);
$post->delete();
return redirect()->route('posts.index')
->with('success', 'Post deleted!');
}
}
| Rule | Meaning |
|---|---|
| required | Must be present and non-empty |
| nullable | Can be null/empty (opposite of required) |
| string | Must be a string |
| min:3 / max:255 | String length / numeric min-max |
| Valid email format | |
| unique:users,email | Must not exist in users.email |
| unique:users,email,{$id} | Unique except for this record (update) |
| confirmed | Must have matching _confirmation field |
| numeric / integer | Must be a number |
| in:draft,published | Must be one of these values |
| exists:categories,id | Must exist in categories.id column |
| image | Must be image (jpeg/png/gif/webp) |
| mimes:pdf,doc | Specific file MIME types |
| max:2048 (file) | File max size in KB (2MB) |
| url | Valid URL format |
| date | Valid date string |
| after:today | Date must be after today |
php artisan make:request StorePostRequest
// app/Http/Requests/StorePostRequest.php
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true; // or auth()->check()
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:3', 'max:255'],
'body' => ['required', 'string', 'min:10'],
'category_id' => ['nullable', 'exists:categories,id'],
'image' => ['nullable', 'image', 'max:2048'],
];
}
public function messages(): array
{
return [
'title.required' => 'Post title is required',
'body.min' => 'Body must be at least 10 characters',
];
}
}
// Controller — inject instead of Request
public function store(StorePostRequest $request)
{
$data = $request->validated(); // already validated!
Post::create($data);
return redirect()->route('posts.index');
}
{{-- resources/views/posts/create.blade.php --}}
<form method="POST" action="{{ route('posts.store') }}" enctype="multipart/form-data">
@csrf
<div>
<label>Title</label>
<input type="text" name="title" value="{{ old('title') }}"
class="{{ $errors->has('title') ? 'is-invalid' : '' }}">
@error('title')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div>
<label>Body</label>
<textarea name="body">{{ old('body') }}</textarea>
@error('body')
<span class="error">{{ $message }}</span>
@enderror
</div>
<div>
<label>Image</label>
<input type="file" name="image" accept="image/*">
@error('image')
<span class="error">{{ $message }}</span>
@enderror
</div>
<button type="submit">Create Post</button>
</form>
{{-- Flash success message --}}
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
// Controller — handle image upload
public function store(StorePostRequest $request)
{
$data = $request->validated();
if ($request->hasFile('image')) {
// Store in storage/app/public/images/
$path = $request->file('image')->store('images', 'public');
$data['image'] = $path; // e.g. "images/abc123.jpg"
}
Post::create($data);
return redirect()->route('posts.index');
}
// Don't forget: php artisan storage:link
// Creates public/storage → symbolic link to storage/app/public
// In Blade — display the image
<img src="{{ Storage::url($post->image) }}" alt="Post image">
// or:
<img src="{{ asset('storage/' . $post->image) }}" alt="Post image">
{{-- Blade delete button (HTML form — no JavaScript needed) --}}
<form method="POST" action="{{ route('posts.destroy', $post) }}"
onsubmit="return confirm('Delete this post?')">
@csrf
@method('DELETE')
<button type="submit">Delete</button>
</form>
{{-- Or use Alpine.js for nicer confirmation --}}
<form method="POST" action="{{ route('posts.destroy', $post) }}"
x-data x-on:submit.prevent="if(confirm('Delete?')) $el.submit()">
@csrf
@method('DELETE')
<button>Delete</button>
</form>
@method('DELETE') နဲ့ spoof လုပ်တယ်။ Laravel က _method hidden field ကို check တယ်။
// Controller
$posts = Post::latest()->paginate(15);
return view('posts.index', compact('posts'));
// Blade
@foreach($posts as $post)
<div>{{ $post->title }}</div>
@endforeach
{{ $posts->links() }} {{-- renders pagination links --}}
// With Tailwind styling:
{{ $posts->links('pagination::tailwind') }}
// Append query string to pagination links
{{ $posts->appends(request()->query())->links() }}