In one sentence

The "U" and "D" of CRUD are three actions (edit, update, destroy) that show a pre-filled form, save the changes, and remove a record, using hidden fields to send PUT and DELETE from plain HTML forms.

โœ๏ธEditing is like correcting a document with tracked changes: you start from what's already there, not a blank page. Deleting is shredding it, so you want a "are you sure?" before the shredder runs.

Why it matters

Close the loop on your first full feature

๐Ÿ”„

Data changes over time

Typos, new categories, outdated posts: users need to fix and remove what they created.

๐Ÿงช

HTML has limits

Forms only support GET and POST. Laravel's method spoofing lets them trigger PUT and DELETE routes.

๐Ÿงฏ

Deletes must be safe

A delete should never be a plain link that a crawler or a stray click can trigger.

The big picture

How a form sends PUT and DELETE

๐Ÿ“<form method="POST">the browser sends POST
๐Ÿ•ต๏ธ_method=PUThidden field from @method
๐ŸงญLaravel routertreats it as PUT
๐Ÿ’พupdate()saves, then redirects
@method('PUT') renders <input type="hidden" name="_method" value="PUT">. Laravel reads it and routes the request as PUT.
ActionVerbURIForm needs
editGET/posts/{post}/editJust a link
updatePUT/PATCH/posts/{post}method="POST" + @csrf + @method('PUT')
destroyDELETE/posts/{post}method="POST" + @csrf + @method('DELETE')
Step by step

Finish the PostController

We continue from Create & Read: the layout already shows session('status').

  1. edit: load the post into a form

    Route model binding gives us the post. We also pass the categories for the dropdown.

    
              public function edit(Post $post)
              {
                  $categories = Category::orderBy('name')->get();
    
                  return view('posts.edit', compact('post', 'categories'));
              }
            

    Add an edit link on the show page: <a href="{{ route('posts.edit', $post) }}">Edit</a>.

  2. The edit form: @method('PUT') and pre-filled values

    old('title', $post->title) means "use what the user just typed if validation failed, otherwise the saved value". The form points at posts.update with the post.

    
              @extends('layouts.app')
    
              @section('content')
                  <form method="POST" action="{{ route('posts.update', $post) }}">
                      @csrf
                      @method('PUT')
    
                      <label>Title <input name="title" value="{{ old('title', $post->title) }}"></label>
                      @error('title') <p class="error">{{ $message }}</p> @enderror
    
                      <label>Slug <input name="slug" value="{{ old('slug', $post->slug) }}"></label>
                      @error('slug') <p class="error">{{ $message }}</p> @enderror
    
                      <select name="category_id">
                          <option value="">None</option>
                          @foreach ($categories as $category)
                              <option value="{{ $category->id }}" @selected(old('category_id', $post->category_id) == $category->id)>
                                  {{ $category->name }}
                              </option>
                          @endforeach
                      </select>
    
                      <textarea name="body">{{ old('body', $post->body) }}</textarea>
                      @error('body') <p class="error">{{ $message }}</p> @enderror
    
                      <button type="submit">Save changes</button>
                  </form>
              @endsection
            
  3. Validate updates with a unique-but-ignore-me rule

    The slug must be unique, but the post's own slug shouldn't count as a clash. Rule::unique(...)->ignore() excludes the current post. Inside a Form Request, $this->post is the bound Post from the URL.

    
              php artisan make:request UpdatePostRequest
            
    
              use Illuminate\Validation\Rule;
    
              public function authorize(): bool
              {
                  return $this->user()?->id === $this->post->user_id; // only the author
              }
    
              public function rules(): array
              {
                  return [
                      'title'       => ['required', 'string', 'max:255'],
                      'slug'        => ['required', 'alpha_dash', Rule::unique('posts', 'slug')->ignore($this->post)],
                      'body'        => ['required', 'string', 'min:20'],
                      'category_id' => ['nullable', 'exists:categories,id'],
                  ];
              }
            
  4. update: save and redirect

    With validation done, update() writes only the validated fields and refreshes updated_at.

    
              use App\Http\Requests\UpdatePostRequest;
    
              public function update(UpdatePostRequest $request, Post $post)
              {
                  $post->update($request->validated());
    
                  return redirect()
                      ->route('posts.show', $post)
                      ->with('status', 'Post updated!');
              }
            
  5. destroy: a delete button, not a link

    Deleting uses a tiny form with @method('DELETE'). A small JavaScript confirm() gives users a last chance to back out.

    
              <form method="POST" action="{{ route('posts.destroy', $post) }}"
                    onsubmit="return confirm('Delete this post for good?')">
                  @csrf
                  @method('DELETE')
                  <button type="submit">Delete</button>
              </form>
            
    
              public function destroy(Post $post)
              {
                  abort_unless(auth()->id() === $post->user_id, 403);
    
                  $post->delete();   // comments go too, thanks to cascadeOnDelete()
    
                  return redirect()
                      ->route('posts.index')
                      ->with('status', 'Post deleted.');
              }
            

    Redirect to the index, not the show page: the post no longer exists, so posts.show would 404.

    โ„น๏ธ Cleaner ownership checks are coming

    The abort_unless and authorize() checks work, but they repeat. A policy puts "who can update or delete a post" in one class. See Policies.

Common mistakes

Avoid this, do that

โŒ Avoid

          <a href="/posts/{{ $post->id }}/delete">Delete</a>
        

A GET link that deletes can be triggered by prefetching, crawlers or a malicious image tag, with no CSRF check.

โœ… Do

          <form method="POST" action="{{ route('posts.destroy', $post) }}">
              @csrf @method('DELETE')
              <button>Delete</button>
          </form>
        

A DELETE request with a CSRF token only happens when a real user clicks the button.

โŒ Avoid

          <form method="PUT" action="{{ route('posts.update', $post) }}">
        

Browsers don't support method="PUT"; they silently fall back to GET and you hit the wrong route.

โœ… Do

          <form method="POST" action="{{ route('posts.update', $post) }}">
              @csrf
              @method('PUT')
        

Use POST in the HTML and let @method tell Laravel the real verb.

๐Ÿ’ก Want an undo? Soft deletes

Add $table->softDeletes(); to the migration and use SoftDeletes; to the model. Now delete() just sets deleted_at, queries hide the post automatically, and $post->restore() brings it back.

๐Ÿ‹๏ธ Try it yourself
  1. Build edit.blade.php and the edit / update actions. Edit a post and see "Post updated!".
  2. Submit the edit form with an empty title and confirm the other fields keep your changes.
  3. Try saving a slug that another post already uses, then save the post's own unchanged slug. Only the first should fail.
  4. Add the delete button and destroy action; check you land on the index with "Post deleted."
  5. Bonus: switch Post to soft deletes and add a "Trash" page listing Post::onlyTrashed()->get().
Recap

Key takeaways

Check yourself

Mini quiz