View All Posts By An Author in Laravel

Ep#29@Laracasts: View All Posts By An Author

This post is a part of the Week X of 100DaysOfCode Laravel Challenge series. We are building the Blog project while following the Laravel 8 from Scratch series on Laracasts with the 100DaysOfCode challenge.

In previous episodes, we added the feature to view all posts by a category. We already have set up relations between our Post and User models. So, we are all set to view all posts by an author too. All we need now is to add the corresponding route to our app.

  • routes/web.php
Route::get('/authors/{author}', function (User $author) {
    return view('posts', [
        'posts' => $author->posts
    ]);
});

I hope you remember from the previous lessons that the wildcard and the callback variable names should match up, author in this case. You should also remember that the wildcard by default looks for the id attribute of the model. So we should now get all posts by the user with id equal to 1 using the URL http://127.0.0.1:8000/authors/1 (local).

Let's update our views to make the author clickable.

  • posts.blade.php
  • post.blade.php
<p>
By <a href="/authors/{{ $post->user->id}}">
{{ $post->user->name }}
</a> in <a href="/categories/{{ $post->category->slug }}">
{{ $post->category->title }}
</a>
</p>

Your code should reflect what you speak

Here you should notice one problem with our code. The problem is we call the writer of the post the author but when we fetch it from the Post model as user. This is a contradiction between what we speak and what we code. So, we should rename our relation from user to author in the Post model, and update the views accordingly.

  • \App\Models\Post
public function author() {
    return $this->belongsTo(User::class);
}
  • posts.blade.php
  • post.blade.php
<p>
By <a href="/authors/{{ $post->author->id}}">
{{ $post->author->name }}
</a> in <a href="/categories/{{ $post->category->slug }}">
{{ $post->category->title }}
</a>
</p>

But would it work now just by renaming the relation? The answer is no.

Because Laravel makes some assumptions based on the relation name. Now Laravel will look for the author_id as the foreign key of the relation which is not present in the users table. The solution is to specify the foreign key name as the second optional parameter of the belongsTo() relationship.

  • \App\Models\Post
public function author() {
    return $this->belongsTo(User::class, "user_id");
}

Now click on the author name and it should work now, listing all the posts by that author.

Filter posts by author username, not id

The author id in the URL http://127.0.0.1:8000/authors/1 is neither looking very prettier nor is it very useful. We may want to manually write the name or username of an author in the URL and filter posts by that author. So, what User attribute should we replace the id with? The full name doesn't look very suitable attribute. We can update it to username instead. So update the route definition to specify the username in the route wildcard.

  • routes/web.php
Route::get('/authors/{author:username}', function (User $author) {
    return view('posts', [
        'posts' => $author->posts
    ]);
});

But the problem is that we don't have a username key in our users table. So, add the line $table->string('username')->unique(); to your users table migration file.

  • database/migrations/2014_10_12_000000_create_users_table.php
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('username')->unique();
    ...
});

And add the line 'username' => $this->faker->unique()->username() to the UserFactory.

  • database/factories/UserFactory.php
public function definition()
{
    return [
        'name' => $this->faker->name(),
        'username' => $this->faker->unique()->username(),
       ...
    ];
}

Run the Artisan command

php artisan migrate:fresh --seed

Now a username column is added to your users table and you can filter posts by username as http://127.0.0.1:8000/authors/JohnDoe. Update views for blog overview and individual blog post pages to replace the id with username in the link.

  • posts.blade.php
  • post.blade.php
<p>
By <a href="/authors/{{ $post->author->username}}">
{{ $post->author->name }}
</a> in <a href="/categories/{{ $post->category->slug }}">
{{ $post->category->title }}
</a>
</p>

Sort posts by latest-first order

On the Blog overview page, the latest posts are currently appearing at the bottom. We want them to be on the top of the list. Let's review our Blog overview page route definition.

  • routes/web.php
Route::get('/', function () {
    return view('posts', [
        'posts' => Post::with("category")->get()
    ]);
});

We can use the latest() method on the Post model to fetch posts in the latest-first order.

  • routes/web.php
Route::get('/', function () {
    return view('posts', [
        'posts' => Post::latest()->with("category")->get()
    ]);
});

Test now and your latest posts should appear on the top. You can create a new post in tinker \App\Models\Post::factory()->create(); to verify it appears on the top.

Again in the N+1 problem

In a previous episode we fixed the N+1 problem when we referenced the related Category model on the blog overview page. Now we are again in the N+1 problem as we showed authors on the blog overview page.

We are looping over an array of posts and for each post, we are Lazy Loading its author. The solution is to Eager Load or include the author with the posts in our route definition.

  • routes/web.php
Route::get('/', function () {
    return view('posts', [
        'posts' => Post::latest()->with("category", "author")->get()
    ]);
});

We already had added the "category" parameter in the with() method, now we added the "author" too to eager load it with the posts. Visit blog overview page http://127.0.0.1:8000/ again and notice the number of queries in the Clockwork tab this time.

Yeah, we fixed it again.

Thank you for following along with me. Stay tuned for the next article. Comments and suggestions are always appreciated and welcome.

47