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
- Run the command
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');
});
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
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, thehref
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')
- 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
<form action="/foo/bar" method="POST">
@method('DELETE')
@csrf
</form>
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()
(andupdate()
) 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);
}
2
3
4
5
6
7
8
9
- Look at the result in JSON format: http://localhost:3000/admin/genres?json
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 classica
tag
- The corresponding route (
- 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 classica
tag again
- The corresponding route (
- 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 withmethod="post"
and add the Blade directive@method('delete')
- The corresponding route (
- A button to edit an existing genre
- 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
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');
});
2
3
4
5
6
7
8
9
10
11
TIP
- Hold down the
Ctrl
key and pressF5
to refresh the browser
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');
}
2
3
4
Edit a genre
Update the edit method
- Update the
edit()
method
public function edit(Genre $genre)
{
return $genre;
}
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 theedit()
method (because we used the--model Genre
flag when we created the controller)
- 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);
}
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 withmethod="post"
and add the Blade directive@method('put')
- The update route (
@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
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');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Refactor the form
- For creating a new genre, we will use almost the same form (only the
action
and themethod
attributes of theform
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
- Move all form elements that will be common to both the edit form and the create form (the
@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>
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
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');
}
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
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 ...
- 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 replaceold('name', $genre->name)
withold('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 ?? '') }}">
2
3
4
5
6
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');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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');
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');
}
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
tobutton
and add a classdeleteGenre
- Add the
data-records
attribute to the delete button
- Change the type of the button from
<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>
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
2
3
4
5
6
7
8
9
10
11
12
13
14
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