When we talk about a resource with regard to the RESTful approach to development, we are making use of generic overviews of how to manage a collection of similar items. So far we have really focused on Games as our main resource in our little application. If we want to show all games, we use the index() method. If we want to display a form to insert a new game into the system, we use the create() method. To store a game submission, we make use of the store() method. Finally to show a particular resource, or game in this case, we use the show() method. As of yet, we do not give that same treatment to the Reviews resource in our application. Let’s build that out in this tutorial.
Listing All Reviews with index()
We can start with our routes file. If we want to visit a particular route to display all game reviews in the system, we can use the following route.
routes/web.php
<?php
//-- Games Resource --//
Route::get('/games', 'GamesController@index');
Route::get('/games/create', 'GamesController@create');
Route::post('/games', 'GamesController@store');
Route::get('/games/{game}', 'GamesController@show');
//-- Reviews Resource --//
Route::get('/reviews', 'ReviewsController@index');
Route::get('/reviews/{game}/create', 'ReviewsController@create');
Route::post('/games/{game}/reviews', 'ReviewsController@store');
Route::get('/reviews/{review}', 'ReviewsController@show');
//-- User Authentication and Session --//
Route::get('/register', 'RegistrationController@create');
Route::post('/register', 'RegistrationController@store');
Route::get('/login', 'SessionsController@create');
Route::post('/login', 'SessionsController@store');
Route::get('/logout', 'SessionsController@destroy');
With our route in place, we can now define the index() method on the ReviewsController as we see here. Now you may be wondering why we do not simply use $reviews = Review::all(); to get all of the reviews to view. This is because it might be nice to display the most recently added reviews. To do this, we can use $reviews = Review::latest()->get(); like we see.
app/Http/Controllers/ReviewsController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Game;
use App\Review;
class ReviewsController extends Controller
{
public function index()
{
$reviews = Review::latest()->get();
return view('reviews.index', ['reviews' => $reviews]);
}
public function create(Game $game)
{
return view('reviews.create', ['game' => $game]);
}
public function store(Game $game)
{
$this->validate(request(), [
'body' => 'required|min:3'
]);
$game->addReview(request('body'), auth()->id());
return redirect()->to('/games/' . request()->route()->game->id);
}
public function show(Review $review)
{
return view('reviews.show', ['review' => $review]);
}
}
In the snippet above, it looks like we are loading an index.blade.php file from the reviews directory. We do not yet have this, so let’s create that directory and file now. Do you see the pattern here? We are looking at Reviews as their own resource, so we can dedicate a views directory to it, then we can follow the convention of adding index.blade.php, create.blade.php, and show.blade.php, just like we might with any other resource.
resources/views/reviews/index.blade.php
@extends('layouts.master')
@section('content')
@foreach($reviews as $review)
<div class="col-12 mb-3">
<div class="card">
<div class="card-block">
<p class="card-text">{{ $review->user->name }} left a <a href="/reviews/{{$review->id}}">review</a>
for <a
href="/games/{{ $review->game->id }}">{{ $review->game->title }}</a> {{$review->created_at->diffForHumans()}}
</p>
</div>
</div>
</div>
@endforeach
@endsection
There are a few things to notice here. We can see that we take the reviews and loop over them in a foreach loop, just like we did when we were listing all games. Since we have set up our eloquent relationships already, we can use $review->user->name to display the user who submitted a review. We’d like to link to a particular review, so we do that by setting the href attribute value to /reviews/{{$review->id}. We’ll set up the routes, controller function, and view file to handle this in a bit. We can also link to the game a review was left on by using the href value of /games/{{ $review->game->id }} and fetching the game title as the anchor text using $review->game->title. Finally, we make use of that convenient Carbon instance to show a human readable form of how long ago the review was submitted with $review->created_at->diffForHumans().
Adding a List Reviews link to the Navigation
In the screenshot above we can see that we are now listing out all of the reviews in the database. Each review shows the user that submitted it, the game it is associated with, and a link to the review itself. We can also see a link in the navigation area similar to how we have a link to view all games. Here is how we added that link.
resources/views/partials/navbar.blade.php
<nav class="navbar navbar-toggleable-md navbar-light bg-faded">
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse"
data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="/games">We Like Games</a>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/games">List Games</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/reviews">List Reviews</a>
</li>
@if( auth()->check() )
<li class="nav-item">
<a class="nav-link font-weight-bold" href="#">Hi {{ auth()->user()->name }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/games/create">Submit Game</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">Log Out</a>
</li>
@else
<li class="nav-item">
<a class="nav-link" href="/login">Log In</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/register">Register</a>
</li>
@endif
</ul>
</div>
</nav>
Displaying a form to add a review for a game with create()
The next RESTful method we want to implement is the create() method. We actually already have a way to add reviews to games, as we included a form right on the resources/views/games/show.blade.php view of the games resource. We are going to remove that form, and give game reviews their own dedicated route to display a form. This way, we can protect that route from non authenticated users when we add middleware to our application. First, we observe the route we need to display a form for creating a review.
routes/web.php
<?php
//-- Games Resource --//
Route::get('/games', 'GamesController@index');
Route::get('/games/create', 'GamesController@create');
Route::post('/games', 'GamesController@store');
Route::get('/games/{game}', 'GamesController@show');
//-- Reviews Resource --//
Route::get('/reviews', 'ReviewsController@index');
Route::get('/reviews/{game}/create', 'ReviewsController@create');
Route::post('/games/{game}/reviews', 'ReviewsController@store');
Route::get('/reviews/{review}', 'ReviewsController@show');
//-- User Authentication and Session --//
Route::get('/register', 'RegistrationController@create');
Route::post('/register', 'RegistrationController@store');
Route::get('/login', 'SessionsController@create');
Route::post('/login', 'SessionsController@store');
Route::get('/logout', 'SessionsController@destroy');
Now check it out. With the Games resource, all we needed was a route of ‘/games/create’. So why do we have that wildcard in the Reviews resource like this ‘/reviews/{game}/create’? This is because when we display a form, we are going to need to display it for a specific game. We can use Route Model Binding to fetch the game that we want to leave a review for, and display the form as such. With this in mind, we can now set up the create() method on our ReviewsController and note how we again make use of Route Model Binding which I have come to fall in love with.
app/Http/Controllers/ReviewsController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Game;
use App\Review;
class ReviewsController extends Controller
{
public function index()
{
$reviews = Review::latest()->get();
return view('reviews.index', ['reviews' => $reviews]);
}
public function create(Game $game)
{
return view('reviews.create', ['game' => $game]);
}
public function store(Game $game)
{
$this->validate(request(), [
'body' => 'required|min:3'
]);
$game->addReview(request('body'), auth()->id());
return redirect()->to('/games/' . request()->route()->game->id);
}
public function show(Review $review)
{
return view('reviews.show', ['review' => $review]);
}
}
Let us now add a view to display a form. Since we have an instance of the game in question, we are able to display in the form exactly which game we are leaving a review for. This means we can remove the form from resources/views/games/show.blade.php and replace it with a link to create a review for the given form like so: <a href=”/reviews/{{$game->id}}/create”>Add A Review!</a>
resources/views/reviews/create.blade.php
@extends('layouts.master')
@section('content')
<h2>Add a Review for {{$game->title}}</h2>
<div class="addreview">
<div class="card-block">
<form method="POST" action="/games/{{ $game->id }}/reviews">
{{ csrf_field() }}
<div class="form-group">
<textarea name="body" class="form-control" placeholder="Add a review!"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Add a review!</button>
</div>
@include('partials.formerrors')
</form>
</div>
</div>
@endsection
We now have a dedicated route and view in place to render a form to submit a game review.
Updating the store() method in ReviewsController.php
Mostly, the store() method can stay the same as it was before. We do make a small update however. In the last iteration, we used return back(); to simply redirect the user right back to the page they submitted the review on. In this case, whey were on the show.blade.php view of the Games resource. In other words, they were viewing a specific game at the time of a review submission. By redirecting back, they see their review immediately on that page. Now, we have a review submission form that lives on a completely separate route. This means we would redirect right back to an empty form to submit a review upon submitting our review. It would be better if we could redirect to the specific games page for that game we just left a review for. We can do this with return redirect()->to(‘/games/’ . request()->route()->game->id);
app/Http/Controllers/ReviewsController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Game;
use App\Review;
class ReviewsController extends Controller
{
public function index()
{
$reviews = Review::latest()->get();
return view('reviews.index', ['reviews' => $reviews]);
}
public function create(Game $game)
{
return view('reviews.create', ['game' => $game]);
}
public function store(Game $game)
{
$this->validate(request(), [
'body' => 'required|min:3'
]);
$game->addReview(request('body'), auth()->id());
return redirect()->to('/games/' . request()->route()->game->id);
}
public function show(Review $review)
{
return view('reviews.show', ['review' => $review]);
}
}
We should be good now. We should be able to visit a specific game, click the Add a Review Link, fill out our review, submit, and then be redirected right back to that game so we can see the game review we just submitted.
Customizing the sort order of an Eloquent Relationship
We can see that submitting reviews for a specific game is now working. Also note that earlier, our new reviews were showing up last in the list of reviews. In the image above, we show that new reviews are now being displayed first in the list. How do we do this? We did this by extending the hasMany() relationship on the Game model. Here is how we do that.
app/Game.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Game extends Model
{
public function reviews()
{
return $this->hasMany(Review::class)->orderBy('created_at', 'desc');
}
public function user()
{
return $this->belongsTo(User::class);
}
public function scopeNintendo($query)
{
return $query->where('publisher', '=', 'Nintendo');
}
public function addReview($body, $userid)
{
$this->reviews()->create(['body' => $body, 'user_id' => $userid]);
}
}
Displaying a single game review with show()
The final step in our journey of applying the RESTful treatment to our game reviews is to set up a route, controller method, and view file to display individual reviews from the database. This way, just as we can visit a specific route to view the details of one specific game, we will now also be able to visit a specific route to view one specific review.
web/routes.php
<?php
//-- Games Resource --//
Route::get('/games', 'GamesController@index');
Route::get('/games/create', 'GamesController@create');
Route::post('/games', 'GamesController@store');
Route::get('/games/{game}', 'GamesController@show');
//-- Reviews Resource --//
Route::get('/reviews', 'ReviewsController@index');
Route::get('/reviews/{game}/create', 'ReviewsController@create');
Route::post('/games/{game}/reviews', 'ReviewsController@store');
Route::get('/reviews/{review}', 'ReviewsController@show');
//-- User Authentication and Session --//
Route::get('/register', 'RegistrationController@create');
Route::post('/register', 'RegistrationController@store');
Route::get('/login', 'SessionsController@create');
Route::post('/login', 'SessionsController@store');
Route::get('/logout', 'SessionsController@destroy');
app/Http/Controllers/ReviewsController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Game;
use App\Review;
class ReviewsController extends Controller
{
public function index()
{
$reviews = Review::latest()->get();
return view('reviews.index', ['reviews' => $reviews]);
}
public function create(Game $game)
{
return view('reviews.create', ['game' => $game]);
}
public function store(Game $game)
{
$this->validate(request(), [
'body' => 'required|min:3'
]);
$game->addReview(request('body'), auth()->id());
return redirect()->to('/games/' . request()->route()->game->id);
}
public function show(Review $review)
{
return view('reviews.show', ['review' => $review]);
}
}
resources/views/reviews/show.blade.php
@extends('layouts.master')
@section('content')
<div class="col-12 mb-3">
<div class="card">
<div class="card-block">
<h3 class="card-title">{{ $review->body }}</h3>
<p class="small">a review of <a href="/games/{{ $review->game->id }}">{{ $review->game->title }}</a>
submitted by {{ $review->user->name }} {{$review->created_at->diffForHumans()}}</p>
<a href="/reviews" class="btn btn-primary">List all Reviews</a>
</div>
</div>
</div>
@endsection
Updating the resources/views/games/show.blade.php
We’re also going to want to display a link for each review when we visit a specific game page. Each review will have something like posted 2 hours ago by user Mario associated with it. Here we just wrap that test with an anchor tag to link to the specific review in question.
resources/views/games/show.blade.php
@extends('layouts.master')
@section('content')
<div class="card" style="width: 270px;margin: 5px">
<img class="card-img-top" src="{{ Storage::url($game->image) }}" alt="Card image cap">
<div class="card-block">
<h3 class="card-title">{{ $game->title }}</h3>
<p class="card-text">{{ $game->title }} is published by {{ $game->publisher }}</p>
<p class="small">Game submitted by user {{ $game->user->name }}</p>
<a href="/games" class="btn btn-primary">List All Games</a>
</div>
</div>
<hr>
<div class="reviews">
<h4>What Gamers Are Saying</h4>
<ul class="list-group">
@foreach($game->reviews as $review)
<li class="list-group-item">{{ $review->body }}
<hr>
<a href="/reviews/{{$review->id}}"><small class="text-primary">posted {{$review->created_at->diffForHumans()}} by
user {{ $review->user->name }}</small></a>
</li>
@endforeach
</ul>
</div>
<div class="mb-3">
<h4 class="mt-3"><a href="/reviews/{{$game->id}}/create">Add A Review!</a></h4>
</div>
@endsection
Applying RESTful Methods to the Reviews Resource Summary
In this tutorial we gave the RESTful treatment to our Reviews resource. We can look at game reviews as a first class citizen in our little application now, just like the Games themselves. As we know, when adhering to the RESTful approach, we can choose from the methods of index(), create(), store(), show(), edit(), update(), and destroy() to do our work for us.