Admin: genres

  • Typical for admin pages is that an administrator can fully manage all the tables in the database
  • Take for example the genres table: an administrator can add, change and delete a genre
  • These operations are referred to as CRUD (C for create, R for read, U for Update and D for delete)
  • For a full CRUD you need 7 routes!
  • Luckily Laravel makes it easy for us and we only need to define one resource route ( Route::resource() ) and this route automatically includes 7 individual routes to 7 methods inside a controller

Preparation

Create (an empty) controller

  • Create a new controller class GenreController.php in the folder app/Http/Controllers/Admin
    • Run the command php artisan make:controller Admin/GenreController
    • You don't have to add any methods yet

Add resource routes

  • Open routes/web.php and add, in the admin group, a new resource route


 



Route::middleware(['auth', 'admin'])->prefix('admin')->group(function () {
    Route::redirect('/', '/admin/records');
    Route::resource('genres', 'Admin\GenreController');
    Route::get('records', 'Admin\RecordController@index');
});
1
2
3
4
5

NAMING CONVENTIONS

  • The name of a resource route is always plural and lower-cased (e.g. genres)
  • Run the command php artisan route:list and look at the 7 new routes
    • Actions column: the methods we need to add to our controller
    • URI column: the URI (URL) for every action (there are several URI's with the same path, but with different methods)
    • Method column: which method belongs to which route/URI

'Genres resource routes'

REMARKS

  • Until now, we only used the GET and POST methods, although there exist more/other methods
    • DELETE for deleting a row in a table
    • PUT or PATCH (we always use PUT) for updating a row in a table
  • Within an a tag, the href value is always requested using the GET method
  • GET, POST, PUT, PATCH and DELETE (there are others) are included in the HTTP standard, but in HTML forms, you are limited to GET and POST at this time!
    • If you want to use e.g. a DELETE or PUT method on a form, you have to set the method to POST and add a special (hidden) field to spoof these HTTP verbs using the Blade directive @method('DELETE') or @method('PUT')

 



<form action="/foo/bar" method="POST">
    @method('DELETE')
    @csrf
</form>
1
2
3
4

Create resource controller

  • Do we have to add all 7 methods manually inside the GenreController?
    • You can, but there is a much easier way by creating a resource controller with all the methods built in!
  • First delete Admin/GenreController.php
  • Start with the artisan command for making a normal controller, followed by the --model (or -m) flag and the name of the corresponding model: php artisan make:controller Admin/GenreController --model Genre

Blade naming conventions

  • Collect all comments on the methods inside Admin/GenreController.php and combine with the corresponding routes:
VERB URI Controller Method Comment
GET admin/genres index Display a listing of the resource
GET admin/genres/create create Show the form for creating a new resource
POST admin/genres store Store a newly created resource in storage
GET admin/genres/{genre} show Display the specified resource
GET admin/genres/{genre}/edit edit Show the form for editing the specified resource
PUT admin/genres/{genre} update Update the specified resource in storage
DELETE admin/genres/{genre} destroy Remove the specified resource from storage

NAMING CONVENTIONS

Controller with MORE THAN ONE view

If you have more than one GET route inside a controller (e.g. when using a resource controller), make a folder with (the plural of) the controller (prefix) name (genres). If the controller resides within a subfolder (Admin/GenreController.php), a similar path structure (admin/genres/...) is used for the views.
Give the Blade files the same name as the methods inside the controller.

  • admin/genres/index.blade.php refers to the index() method inside Admin/GenreController.php
  • admin/genres/create.blade.php refers to the create() method inside Admin/GenreController.php
  • admin/genres/show.blade.php refers to the show() method inside Admin/GenreController.php
  • admin/genres/edit.blade.php refers to the edit() method inside Admin/GenreController.php

You probably didn't notice, but we've already used this convention so far!
Take a look at ShopController.php and its Blade files:

  • shop/index.blade.php refers to the index() method inside ShopController.php
  • shop/show.blade.php refers to the show() method inside ShopController.php

Controller with ONLY ONE view

You may use the above logic

  • admin/records/index.blade.php refers to the index() method inside Admin/RecordController.php

OR

You name the only view the same as the controller (prefix)

  • home.blade.php refers to the index() method inside HomeController.php
  • contact.blade.php refers to the show() method inside ContactUsController.php
  • user/profile.blade.php refers to the edit() (and update()) method inside User/ProfileController.php

Master page

Update the index method

  • Update the index() method
    • Select all genres and count the records that belong to a genre
    • Send the result to the view


 
 
 
 
 
 


public function index()
{
    $genres = Genre::orderBy('name')
        ->withCount('records')
        ->get();
    $result = compact('genres');
    Json::dump($result);
    return view('admin.genres.index', $result);
}
1
2
3
4
5
6
7
8
9

Create a view

  • Create a new folder resources/views/admin/genres
  • Add a new file index.blade.php to this genres folder
    • Include the shared alert sub-view
    • Add a button to create a new genre
      • The corresponding route (admin/genres/create) is requested with GET, so we can use a classic a tag
    • Show all the genres in a table
    • Add on every row:
      • A button to edit an existing genre
        • The corresponding route (admin/genres/{genre}/edit) is requested with GET, so we use a classic a tag again
      • A button to delete an existing genre
        • The corresponding route (admin/genres/{genre}) is requested with the DELETE method, so we embed it in a (submit) button in a form with method="post" and add the Blade directive @method('delete')
    • Add a tooltip with extra information to the buttons
@extends('layouts.template')

@section('title', 'Genres')

@section('main')
    <h1>Genres</h1>
    @include('shared.alert')
    <p>
        <a href="/admin/genres/create" class="btn btn-outline-success">
            <i class="fas fa-plus-circle mr-1"></i>Create new genre
        </a>
    </p>
    <div class="table-responsive">
        <table class="table">
            <thead>
            <tr>
                <th>#</th>
                <th>Genre</th>
                <th>Records for this genre</th>
                <th>Actions</th>
            </tr>
            </thead>
            <tbody>
            @foreach($genres as $genre)
                <tr>
                    <td>{{ $genre->id }}</td>
                    <td>{{ $genre->name }}</td>
                    <td>{{ $genre->records_count }}</td>
                    <td>
                        <form action="/admin/genres/{{ $genre->id }}" method="post">
                            @method('delete')
                            @csrf
                            <div class="btn-group btn-group-sm">
                                <a href="/admin/genres/{{ $genre->id }}/edit" class="btn btn-outline-success"
                                   data-toggle="tooltip"
                                   title="Edit {{ $genre->name }}">
                                    <i class="fas fa-edit"></i>
                                </a>
                                <button type="submit" class="btn btn-outline-danger"
                                        data-toggle="tooltip"
                                        title="Delete {{ $genre->name }}">
                                    <i class="fas fa-trash-alt"></i>
                                </button>
                            </div>
                        </form>
                    </td>
                </tr>
            @endforeach
            </tbody>
        </table>
    </div>
@endsection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

Enable the Bootstrap tooltips

  • To enable Bootstrap tooltips on the buttons, you have to enable them globally for the whole site
  • Add this script to the file resources/js/app.js




 
 
 
 
 
 
 

$('[required]').each(function () { ... });

$('nav i.fas').addClass('fa-fw mr-1');

$('body').tooltip({
    selector: '[data-toggle="tooltip"]',
    html : true,
}).on('click', '[data-toggle="tooltip"]', function () {
    // hide tooltip when you click on it
    $(this).tooltip('hide');
});
1
2
3
4
5
6
7
8
9
10
11

TIP

  • Hold down the Ctrl key and press F5 to refresh the browser

'Master page'

Detail page

  • Because all information about a genre is already on the master page, there is no need make a special view for one genre
  • When you navigate to the show route, you'll see an empty page:
    http://localhost:3000/admin/genres/5
  • It's better to redirect this route back to the master page, so update the show() method in the controller:


 


public function show(Genre $genre)
{
    return redirect('admin/genres');
}
1
2
3
4

Edit a genre

Update the edit method

  • Update the edit() method


 


public function edit(Genre $genre)
{
   return $genre;
}
1
2
3
4
  • When you click on the edit link for 'blues' (id = 10), the URL points to:
    http://localhost:3000/admin/genres/10/edit
  • We only send the id with the URL, but the whole genre is sent (as a parameter) to the edit() method (because we used the --model Genre flag when we created the controller)

'Genre'

  • Update the controller to send the data to the view


 
 
 


public function edit(Genre $genre)
{
    $result = compact('genre');
    Json::dump($result);
    return view('admin.genres.edit', $result);
}
1
2
3
4
5
6

Create a view

  • Add a new file edit.blade.php to the resources/views/admin/genres folder
  • Add a form (with a label, a text field and a button) to update the genre
    • The update route (admin/genres/{genre}) is requested with the PUT method, so we embed it in a (submit) button in a form with method="post" and add the Blade directive @method('put')
@extends('layouts.template')

@section('title', 'Edit genre')

@section('main')
    <h1>Edit genre: {{ $genre->name }}</h1>
    <form action="/admin/genres/{{ $genre->id }}" method="post">
        @method('put')
        @csrf
        <div class="form-group">
            <label for="name">Name</label>
            <input type="text" name="name" id="name"
                   class="form-control @error('name') is-invalid @enderror"
                   placeholder="Name"
                   minlength="3"
                   required
                   value="{{ old('name', $genre->name) }}">
            @error('name')
            <div class="invalid-feedback">{{ $message }}</div>
            @enderror
        </div>
        <button type="submit" class="btn btn-success">Save genre</button>
    </form>
@endsection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

Update the update method

  • Update the update() method
    • Check (server side validation) if name is not empty, contains minimal 3 characters and is unique
    • Set $genre->name equal to $request->name
    • Save the (updated) genre
    • Flash a success message
    • Redirect back to the master page


 
 
 
 
 
 
 
 
 
 
 
 
 


public function update(Request $request, Genre $genre)
{
    // Validate $request
    $this->validate($request,[
        'name' => 'required|min:3|unique:genres,name,' . $genre->id
    ]);

    // Update genre
    $genre->name = $request->name;
    $genre->save();

    // Flash a success message to the session
    session()->flash('success', 'The genre has been updated');
    // Redirect to the master page
    return redirect('admin/genres');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

'Update genre'

Refactor the form

  • For creating a new genre, we will use almost the same form (only the action and the method attributes of the form element will differ)
  • To avoid having to write (and maintain) the same code twice, we can separate a part of the form into a separate file (sub-view)
  • Add a new file form.blade.php to the resources/views/admin/genres folder
    • Move all form elements that will be common to both the edit form and the create form (the @csrf token, the text field and the submit button) to this file
@csrf
<div class="form-group">
    <label for="name">Name</label>
    <input type="text" name="name" id="name"
           class="form-control @error('name') is-invalid @enderror"
           placeholder="Name"
           minlength="3"
           required
           value="{{ old('name', $genre->name) }}">
    @error('name')
    <div class="invalid-feedback">{{ $message }}</div>
    @enderror
</div>
<button type="submit" class="btn btn-success">Save genre</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • Include form.blade.php inside the form tag in edit.blade.php




 



@section('main')
    <h1>Edit genre: {{ $genre->name }}</h1>
    <form action="/admin/genres/{{ $genre->id }}" method="post">
        @method('put')
        @include('admin.genres.form')
    </form>
@endsection
1
2
3
4
5
6
7

Create a new genre

Update the create method

  • Update the create() method


 


public function create()
{
    return view('admin.genres.create');
}
1
2
3
4

Create a view

  • Add a new file create.blade.php to the resources/views/admin/genres folder
  • Edit the code
    • The form is based on the sub-view resources/views/admin/genres/form.blade.php
    • By clicking on the submit button, we call the store route (admin/genres) with a POST method
@extends('layouts.template')

@section('title', 'Create new genre')

@section('main')
    <h1>Create new genre</h1>
    <form action="/admin/genres" method="post">
        @include('admin.genres.form')
    </form>
@endsection
1
2
3
4
5
6
7
8
9
10
  • When you open the page (to create a new genre), there is an error: Undefined variable: genre ...

'Error'

  • This is because the value of the text field (in form.blade.php) uses $genre->name which does not exist at this time:
  • You also get a tip on how to solve this with the Null coalescing operator
    Open the form and replace old('name', $genre->name) with old('name', $genre->name ?? '')





 

<input type="text" name="name" id="name"
       class="form-control @error('name') is-invalid @enderror"
       placeholder="Name"
       minlength="3"
       required
       value="{{ old('name', $genre->name ?? '') }}">
1
2
3
4
5
6

'Create new genre'

Update the store method

  • Update the store() method
    • Send the request through the (server side) validator: the genre name cannot be empty, contains minimal 3 characters and is unique
    • Create a new Genre object
    • Set $genre->name equal to $request->name
    • Save the genre
    • Flash a success message
    • Redirect back to the master page


 
 
 
 
 
 
 
 
 
 
 
 
 
 


public function store(Request $request)
{
    // Validate $request
    $this->validate($request,[
        'name' => 'required|min:3|unique:genres,name'
    ]);

    // Create new genre
    $genre = new Genre();
    $genre->name = $request->name;
    $genre->save();

    // Flash a success message to the session
    session()->flash('success', "The genre <b>$genre->name</b> has been added");
    // Redirect to the master page
    return redirect('admin/genres');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

'Create new genre afrobeat'

'Create new genre afrobeat - proof'

Delete a genre

WARNING

  • Remember that we built in some integrity in our database tables
  • If you delete a genre, all related records are deleted as well (as specified in the foreign key relation inside the records migration)
$table->foreign('genre_id')->references('id')->on('genres')->onDelete('cascade')->onUpdate('cascade');
1

Update the destroy method

  • Update the destroy() method
    • Delete the genre
    • Flash a success message
    • Redirect back to the master page


 
 
 


public function destroy(Genre $genre)
{
    $genre->delete();
    session()->flash('success', "The genre <b>$genre->name</b> has been deleted");
    return redirect('admin/genres');
}
1
2
3
4
5
6

WARNING

If you don't want to lose any data, test this functionality with a newly created genre (that is not linked to any record in the database), e.g. 'afrobeat'!

User confirmation

  • At the moment, when you (accidentally) hit the delete button, the genre and all his (associated) records will be removed for good!
  • It's a good practice always to ask the user for a confirmation that he really wants to delete some (database) data
  • You can do this with the JavaScript confirm() function
  • Update the form resources/views/admin/genres/index.blade.php
    • Change the type of the button from submit to button and add a class deleteGenre
    • Add the data-records attribute to the delete button





 

 






<form action="/admin/genres/{{ $genre->id }}" method="post">
    @csrf
    @method('delete')
    <div class="btn-group btn-group-sm">
        ...
        <button type="button" class="btn btn-outline-danger deleteGenre"
                data-toggle="tooltip"
                data-records="{{ $genre->records_count }}"
                title="Delete {{ $genre->name }}">
            <i class="fas fa-trash-alt"></i>
        </button>
    </div>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
  • Add the script_after section to the page and add the confirmation to the page
@section('script_after')
    <script>
        $('.deleteGenre').click(function () {
            const records = $(this).data('records');
            let msg = `Delete this genre?`;
            if (records > 0) {
                msg += `\nThe ${records} records of this genre will also be deleted!`
            }
            if (confirm(msg)) {
                $(this).closest('form').submit();
            }
        })
    </script>
@endsection
1
2
3
4
5
6
7
8
9
10
11
12
13
14

'Delete a genre'

EXERCISE: Adjust the confirmation dialog

  • Add the genre name to the dialog box that pops up when you want to delete a genre with records associated to it

'Delete a genre with genre in dialog box'

Last Updated: 11/20/2021, 6:49:17 PM