In this lesson, let's show a column from the relationship in the posts table. We will add a category for every post.
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.
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.
It also needs to have BelongsTo in the
use
section on top.Povillas question:
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?
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 :)
In this case project continues and we add a feature. Imagine if project is in production how then migration would run?
Thank you for the clarification!
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.
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 :)
Is the category seeder and posts.catetgory_id population functions left out? The screenshots has it populated
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)
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...
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...
Then once you rerun
php migrate:fresh --seed
you'll have all you need.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...
Thanks, I did something of that nature👍