It has been more than 8 years now since I have been using Laravel. Today, I will talk about how my coding practices have evolved from complex to simple — from unnecessarily complex to clean and concise — especially when it comes to CRUD operations.
Laravel CRUD operations are the foundation of the application, and Laravel has introduced many simple methods to do the job well. Here is my implementation.
Generating the ResourceController
We will start with Resource Controller. Let's generate one.
We will start with Resource Controller. Let's generate one.
php artisan make:controller PostController --resource
This will generate PostController with default methods.
class PostController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}
Defining the Routes
Next we will define routes for this controller.
use App\Http\Controllers\PostController;
Route::resource('posts', PostController::class);
Creating Form Request Classes
Finally, we will execute some commands to generate FormRequest classes.
php artisan make:request Post\GetAllPostsRequest
php artisan make:request Post\GetPostRequest
php artisan make:request Post\CreatePostRequest
php artisan make:request Post\UpdatePostRequest
php artisan make:request Post\DeletePostRequest
That completes our code generation step. Next we will modify our PostController to have these FormRequest classes and call its handle method, which we will define later.
The Updated Controller
use App\Http\Requests\Post\GetAllPostsRequest;
use App\Http\Requests\Post\GetPostRequest;
use App\Http\Requests\Post\CreatePostRequest;
use App\Http\Requests\Post\UpdatePostRequest;
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 create(GetPostRequest $request)
{
$response = $request->handle();
return view('post.create', ['post' => $response]);
}
/**
* Store a newly created resource in storage.
*/
public function store(CreatePostRequest $request)
{
$request->handle();
session()->flash('message', 'Post has been saved 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]);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(int $id, GetPostRequest $request)
{
$response = $request->handle($id);
return view('post.edit', ['post' => $response]);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdatePostRequest $request, int $id)
{
$request->handle();
session()->flash('message', 'Post has been updated successfully.');
return redirect()->route('post.index');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(int $id, DeletePostRequest $request)
{
$request->handle();
session()->flash('message', 'Post has been deleted successfully.');
return redirect()->route('post.index');
}
}
You can see how simple and lean our controller is.
- We have set flash messages when our operation is done.
- We have defined redirects when our operation is successful.
- Every method of our controller has identical implementation.
Diving into Form Request Classes
Now let's move to our FormRequest classes. As you can see, I have defined the handle method in each class we will talk about here. Let's start with GetAllPostsRequest.
class GetAllPostsRequest extends FormRequest
{
public function handle()
{
$params = $this->all();
$postObj = Post::latest('id');
if (isset($params['status'])) {
$postObj->where('status', $params['status']);
}
return $postObj->paginate(Post::PAGINATE);
}
}
The handle method is pretty simple. It defines pagination logic as well as filtering records if required.
Tip: You can move the filtering logic to scopes.
Next, let’s see GetPostRequest.
class GetPostRequest extends FormRequest
{
public function handle($id)
{
return Post::findOrNew($id);
}
}
GetPostRequest is even simpler. It defines a very simple method: just grab a record by ID. Since it is called in 3 methods in the controller—create, edit, and show—consider the following:
- For create method where id is 0, it will fetch a new record with empty or default values
- For edit and show methods it will query a record based on ID.
- By using the findOrNew() method, our logic is simplified and reusable.
In the blade template create.blade.php or edit.blade.php, you do this.
<input type="text" name="title" value="{{ old('title', $post->title) }}" />
This not only shows the previously entered input if validation failed but also populates data from the database if we are in edit mode. In the add/create mode, since we are using the findOrNew() method, it will give us a post title with an empty string.
At this point our record-fetching logic is complete. Let’s move to record-saving logic.
class CreatePostRequest 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()
{
$data = $this->validated();
Post::create($data);
return true;
}
}
We call the validated() method before proceeding. This will give us the following benefits:
- If validation logic fails it will redirect user to form with validation errors
- If validation passes, the record will be saved
Tip: Use $fillable along with the create() method.
The UpdatePostRequest has identical implementation to the CreatePostRequest.
class UpdatePostRequest extends FormRequest
{
/**
* 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();
Post::where('id', $id)->update($data);
return true;
}
}
And here is the DeletePostRequest.
class DeletePostRequest extends FormRequest
{
public function handle($id)
{
Post::where(['id' => $id])->delete();
return true;
}
}
Why use FormRequests?
Our CRUD implementation logic is complete now. You might be wondering why we have chosen to use FormRequests instead of defining our logic directly in the controller. Consider the following benefits:
Authorization
FormRequest gives us a simple authorize method; if used effectively, you can control action authorization. Let's say you are using Bouncer. In GetPostRequest, define this to control access.
public function authorize(): bool
{
return Bouncer::can('view-post');
}
Handle Method for Business Logic
This is rather a custom implementation to define business logic at some relevant place. For further insights, read this post.
Lean Controller
You can see our controller has as little code as possible, with all the methods having identical structure that is easy to understand and remember.
Tip: You can replace the logic of returning model data in HTML or AJAX format. For example, in the index method, use this.
return ResponseHelper::handle('index', $data);
The ResponseHelper Class
class ResponseHelper
{
public static function handle($view, $data)
{
if (request()->is('api/*')) {
return response()->json($data);
}
return view($view,$data);
}
}
This will return the response in AJAX format if called from an API or in HTML format if called from the web.
Identical Pattern
Since all our ForRequest classes follow the same pattern, if we strictly stick to this, our application will have clean and concise code no matter how big the application grows.
One Liner Code
This one is the most satisfying point. The actual reason why I have written this post. Have you noticed? If not, let me repeat by indicating some code.
$postObj = Post::latest('id');
if (isset($params['status'])) {
$postObj->where('status', $params['status']);
}
return $postObj->paginate(Post::PAGINATE);
/* --------------------------- */
return Post::findOrNew($id);
/* --------------------------- */
Post::create($data);
/* --------------------------- */
Post::where('id', $id)->update($data);
/* --------------------------- */
Post::where(['id' => $id])->delete();
This is the power of Laravel that gives us control to write as little code as possible but do as much work as we want.
Final Thoughts
Laravel gives us elegant tools to simplify CRUD operations. By moving logic to FormRequest classes and keeping controllers clean, we write less and achieve more.
This post summarizes how my coding practices have evolved from complex to simple. I will write yet another post on this topic, cutting down some unnecessary parts from this structure. If you are interested, you can subscribe to my profile.