In the last post I shared how my code has evolved to write clean, succinct, and reusable code. If you haven’t read it, you can check this out below.

In this post I will go a step further to tell how my previous structure can be even a little more concise. If you are ready, here we go.
From Resource Controller to Custom Controller
We will start by writing a controller again, but this time a custom controller with only 4 to 5 methods that will do all the work for us. Here is the sample.
<?php
namespace App\Http\Controllers;
use App\Http\Requests\Post\GetAllPostsRequest;
use App\Http\Requests\Post\GetPostRequest;
use App\Http\Requests\Post\SavePostRequest;
use App\Http\Requests\Post\DeletePostRequest;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(GetAllPostsRequest $request)
{
$response = $request->handle();
return view('post.index', ['posts' => $response]);
}
/**
* Show the form for creating a new resource.
*/
public function form(GetPostRequest $request,$id)
{
$response = $request->handle($id);
return view('post.form', ['post' => $response]);
}
/**
* Update the specified resource in storage.
*/
public function save(SavePostRequest $request, int $id)
{
$request->handle($id);
if($id > 0){
session()->flash('message', 'Post has been updated successfully.');
}else{
session()->flash('message', 'Post has been created successfully.');
}
return redirect()->route('post.index');
}
/**
* Display the specified resource.
*/
public function show(int $id, GetPostRequest $request)
{
$response = $request->handle($id);
return view('post.show', ['post' => $response]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(int $id, DeletePostRequest $request)
{
$request->handle($id);
session()->flash('message', 'Post has been deleted successfully.');
return redirect()->route('post.index');
}
}
Now let’s define routes for them.
Route::prefix('posts')->name('posts.')->group(function () {
Route::get('/', [PostController::class, 'index'])->name('index');
Route::get('/form/{id?}', [PostController::class, 'form'])->name('form');
Route::post('/form/{id?}', [PostController::class, 'saveForm'])->name('save');
Route::delete('/{id}', [PostController::class, 'delete'])->name('delete');
});
Here we have combined create and edit routes to the get-type form route. Similarly, we have combined store and update routes to post-type form routes.Now let’s see GetPostRequest.
class GetPostRequest extends FormRequest
{
public function handle($id)
{
return Post::findOrNew($id);
}
}
It is as intact as it was last time. We have made no changes in it.Before proceeding, I would like to shed some light on two options available here.
Optional Parameter
You can define a route with an optional parameter and set a default parameter in GetPostRequest.
// optional id parameter. Useful for edit route
Route::get('/form/{id?}', [PostController::class, 'form'])->name('form');
class GetPostRequest extends FormRequest
{
public function handle($id)
{
return Post::findOrNew($id);
}
}
And routes for this
<!-- load form in create mode -->
<a href="{{ route('posts.form') }}">Create</a>
<!-- load form in edit mode inside index.blade.php under $posts loop -->
<a href="{{ route('posts.form', [ 'id' => $post->id ]) }}">
Edit
</a>
Required Parameters
You can define a route with a required parameter and pass 0 in create mode.
// optional id parameter. Useful for edit route
Route::get('/form/{id}', [PostController::class, 'form'])->name('form');
And routes for this.
<!-- load form in create mode by passing 0 -->
<a href="{{ route('posts.form', [ 'id' => 0 ]) }}">Create</a>
<!-- load form in edit mode inside index.blade.php under $posts loop -->
<a href="{{ route('posts.form', [ 'id' => $post->id ]) }}">
Edit
</a>
Now that we have deleted create.blade.php and renamed edit.blade.php to form.blade.php, we can say,
If we take a look at form.blade.php, we will have this.
<form action="{{ route('posts.form', [ 'id' => $post->id ]) }}" method="post">
@csrf
<div>
<div>
<label>Title</label>
<input type="text" name="title" value="{{ old('title', $post->title) }}" />
</div>
<div>
<label>Content </label>
<textarea name="content"rows="100" cols="80">
{{ old('content', $post->content) }}
</textarea>
</div>
<div>
<button type="submit">Save</button>
</div>
</div>
</form>
This will suffice to create and edit cases. So combining create and edit forms is worth it. Do pay attention to the second or default parameter in `old()` that will show either the default value in the add case or the populated value in the edit case or the failed-validation-value if form validation fails.
Blade views are now down to two or three.
- index.blade.php to list all posts
- form.blade.php to load new or existing form
- optional show.blade.php to show single post view
Next, we will take a look at SaveFormRequest.
class SavePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title:required,string',
'content:required,string',
];
}
public function handle($id)
{
$data = $this->validated();
if($id > 0){
Post::where('id', $id)->update($data);
}else{
Post::create($data);
}
return true;
}
}
This will add or update a post based on $id.
Summary
- No more resource controller
- Only 4 methods in each controller
- Only 2 blade views for each resource
- Form always loads resource or shows defaults
Final Thoughts
Laravel gives a lot out of the box. If utilized cleverly, you can minimize code as much as possible while still achieving a clean structure with full functionality. I hope you enjoyed this structure — and more importantly, I hope it makes your development smoother and your codebase cleaner.
What’s Next
In the next post, I’ll take this structure even further — making it a little more useful, a little more flexible, without adding any extra noise. Stay tuned. If you want to get notified, do subscribe to my profile.