Skip to content
Redundant Reusable CRUD Backend Laravel

Laravel CRUD Like a Pro—Part 2: The End of Redundant Forms with findOrNew() + old()

Muhammad Raheel

Muhammad Raheel

April 12th, 2025 · 2 min read

Laravel CRUD Like a Pro—Part 2: The End of Redundant Forms with findOrNew() + old()

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.

 Raheel Shan | Simplify Laravel CRUD with FormRequest and Lean Controllers | Laravel Tips

Raheel Shan | Simplify Laravel CRUD with FormRequest and Lean Controllers | Laravel Tips

Learn how to simplify Laravel CRUD operations using Resource Controllers, FormRequests, and a clean one-liner approach. Ideal for developers who want reusable, scalable, and DRY Laravel code.

raheelshan.com

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 saveForm(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,

There’s no ‘create’ in real CRUD — just editing something that doesn’t exist yet.

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) }}" />
            @error('title')
                <div class="text-red-500 text-sm">{{ $message }}</div>
            @enderror
        </div>
        <div>
            <label>Content </label>
            <textarea name="content"rows="100" cols="80">
                {{ old('content', $post->content) }}
            </textarea>
            @error('content')
                <div class="text-red-500 text-sm">{{ $message }}</div>
            @enderror
        </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.

Planning for Scalability

While centralizing persistence logic inside FormRequests works great for small to medium-sized apps, if the complexity grows you might consider moving heavy operations to service classes or adopting patterns like AQC (Atomic Query Construction).

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.

Found this useful?

Support the work that goes into these posts.

Support
Comments


Comment created and will be displayed once approved.