Courses

Vue.js 3 + Laravel 11 API + Vite: SPA CRUD

Category: API Resource with Relationship

Summary of this lesson:
- Setting up category model and relationships
- Creating category API resource
- Managing eager loading for categories
- Displaying related data in table view

In this lesson, let's show a column from the relationship in the posts table. We will add a category for every post.

finished category relation


First, we need to create a Category model and migration.

php artisan make:model Category -m

database/migrations/xxxx_create_categories_table.php:

public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}

app/Models/Category.php:

class Category extends Model
{
protected $fillable = [
'name',
];
}

Next, we need to create a migration to add a category_id column to the Posts table.

php artisan make:migration "add category to posts table"

database/migrations/xxxx_add_category_to_posts_table.php:

public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->foreignId('category_id')->after('content')->constrained();
});
}

In the model, we also need a relationship.

app/Models/Post.php:

use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Post extends Model
{
protected $fillable = [
'title',
'content',
'category_id',
];
 
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}

Now we don't want to make a N+1 issue. So in the PostController we need to eager load categories.

app/Http/Controllers/Api/PostController.php:

class PostController extends Controller
{
public function index()
{
$posts = Post::with('category')->paginate(10);
 
return PostResource::collection($posts);
}
}

We are using API Resources. So it means we need to category to it, otherwise, API won't return the category.

app/Http/Resources/PostResource.php:

class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => substr($this->content, 0, 50) . '...',
'category' => $this->category->name,
'created_at' => $this->created_at->toDateString()
];
}
}

All that is left to show the category in the frontend. Let's show it near the title.

resources/js/components/Posts/Index.vue:

<template>
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-gray-200">
<div class="min-w-full align-middle">
<table class="min-w-full divide-y divide-gray-200 border">
<thead>
<tr>
// ...
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Title</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Category</span>
</th>
// ...
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
<tr v-for="post in posts.data">
// ...
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ post.title }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ post.category }}
</td>
// ...
</tr>
</tbody>
</table>
 
<TailwindPagination :data="posts" @pagination-change-page="getPosts" class="mt-4" />
</div>
</div>
</template>
 
// ...

That's it. We now show a category in the posts table.

finished category relation

Previous: Pagination with External Vue Package
avatar

while i use public function category(): BelongsTo in app/Models/Post.php i get this error App\Models\Post::category(): Return value must be of type App\Models\BelongsTo, Illuminate\Database\Eloquent\Relations\BelongsTo returned i removed : BelongsTo from function category and now work.

avatar

It also needs to have BelongsTo in the use section on top.

use Illuminate\Database\Eloquent\Relations\BelongsTo;
avatar
You can use Markdown
avatar

Povillas question:

    $table->foreignId('category_id')->after('content')->constrained();

why would we need this to be in a seperate migration (add category to posts table), couldnt we just add thiis to create_categories_table migration?

avatar

You technically can add that, but it hides the intention under another name. Imagine, you are looking where category_id is defined and you don't find it in migrations list. It's there in the database, but there is no migration. So you just have to guess which file contains in... Takes more time :)

avatar

In this case project continues and we add a feature. Imagine if project is in production how then migration would run?

avatar

Thank you for the clarification!

avatar

IMHO, generarily you need categories migration to go BEFORE your posts migration in order to use this foreign key in the posts migration itself. Which is not true in this case, if you follow along: categories migration has been created after the posts one. To avoid errors when running migrations, it's wise to create all the tables and add foreign keys afterwards when they are all present in the database.

avatar

Sorry, what are you talking about?

We have created posts, then created a categories table. Once both of them are up - we then create another migration that adds the connection between them.

This is by far, the most common and realistic approach that you will encounter when creating a product. Unless of course, you don't write any code until you have a full database layout already figured out and stationary :)

avatar
You can use Markdown
avatar

Is the category seeder and posts.catetgory_id population functions left out? The screenshots has it populated

avatar

Also, when migrating with constrained() I get the error

Cannot add a NOT NULL column with default value NULL (Connection: sqlite, SQL: alter table "posts" add column "category_id" integer not null)

avatar

It looks like those steps have been overlooked from this page.

Add the a category_id to the PostFactory 'category_id' => 1, so that the posts seeder can default to an id.

Next, create a CategorySeeder with php artisan make:seeder CategorySeeder

Then add the following to the run method...

public function run(): void
    {
        Category::factory()->count(1)->create();
    }

Then create a new factory for the category with php artisan make:factory CategoryFactory

Then add this to the returned array the definition() function... 'name' => $this->faker->words(3, true),

Lastly, add the CategorySeeder to the DatabaseSeeder before calling the PostsSeeder like so...

$this->call([CategorySeeder::class]);
$this->call([PostSeeder::class]);

Then once you rerun php migrate:fresh --seed you'll have all you need.

I added the "default" category_Id to the factory as It's probably best not set in the migration as this might have ugly consequenses in real world situations.

You could go further by change 1 to 10 categories in the CategorySeeder then setting PostFactory (or even in the PostSeeder) to pick a random id for use as category_id like this...

public function definition(): array
    {
        $categoryId = $this->getRandomCategoryId();
        return [
            'title'       => $this->faker->word(),
            'content'     => $this->faker->paragraphs(asText: true),
            'category_id' => $categoryId,
        ];
    }

    private function getRandomCategoryId(): int
    {
        $category = Category::inRandomOrder()->first();

        return $category->id ?? 1;
    }
avatar

Thanks, I did something of that nature👍

avatar
You can use Markdown
avatar
You can use Markdown