DEV Community

loading...
Cover image for Laravel 8 Tutorial #6: Create Models and Setup Admin Panel

Laravel 8 Tutorial #6: Create Models and Setup Admin Panel

ericnanhu profile image Eric Hu Originally published at techjblog.com Updated on ・11 min read

You can download the source code of this tutorial here: https://www.techjblog.com/index.php/laravel-tutorial-for-beginners/

Design Database Structure

For a simple blogging system, we need at least 4 database tables: Users, Categories, Tags, and Posts. If you want other functions for your blog, comments, for example, you can add other tables yourself. To keep this tutorial short and easy to understand, these four are all we need.

Users Table

id integer auto increment
name string cannot be empty
email string unique, cannot be empty
password string

The users table is already included in Laravel and we don’t need to do anything about it.

Categories Table

name string cannot be empty
slug string unique, cannot be empty
description text can be empty

Tags Table

name string cannot be empty
slug string unique, cannot be empty
description text can be empty

Posts Table

title string cannot be empty
slug string unique, cannot be empty
featured image string or text can be empty
content text cannot be empty
published boolean
featured boolean

Relations

Here, I’d like to introduce some basic relationships. I only picked the ones we need to use to build a simple blogging system since some relationships, like polymorphic relationships, might be too hard to understand for beginners.

One to One

This is the most basic relation. For example, each User is associated with one Phone. To define this relationship, we need to place a phone method on the User model.

class User extends Model
{
    public function phone()
    {
        return $this->hasOne('App\Phone');
    }
}
Enter fullscreen mode Exit fullscreen mode

One to One (Inverse)

The inverse of “has one” would be “belongs to one”. For example, each Phone would belong to one User. In order to define the inverse of the one to one relationship. We place a user method on the Phone model.

class Phone extends Model
{
    public function user()
    {
        return $this->belongsTo('App\User');
    }
}
Enter fullscreen mode Exit fullscreen mode

One to Many

A one-to-many relationship is used to define relationships where a single model owns any amount of other models. For example, one Category could have many Posts. Just like one-to-one relation, it can be defined by putting a posts method in the Category model.

class Category extends Model
{
    public function posts()
    {
        return $this->hasMany('App\Post');
    }
}
Enter fullscreen mode Exit fullscreen mode

One to Many (Inverse)

However, sometimes we need to find the category through the post. The inverse of “has many” would be “belongs to”. In order to define the inverse of the one-to-many relationship. We place a category method on the Post model.

class Post extends Model
{
    public function category()
    {
        return $this->belongsTo('App\Category');
    }
}
Enter fullscreen mode Exit fullscreen mode

Many to Many

Many-to-many relations are slightly more complicated than hasOne and hasMany relationships. An example of such a relationship is that one post has many tags, where each tag is also shared by other posts. We’ll talk about this later.

Design Relationships

For our blog website project. There are six relationships we need to take care of.

  • Each user has multiple posts
  • Each category has many posts
  • Each tag belongs to many posts
  • Each post belongs to one user
  • Each post belongs to one category
  • Each post belongs to many tags

Create Models

Now, it’s time for us to implement that design. The first thing we need to do is to generate the necessary models and migration files for our project with artisan commands.

php artisan make:model Category -m

php artisan make:model Tag -m

php artisan make:model Post -m
Enter fullscreen mode Exit fullscreen mode

Category Model

database/migrations/create_categories_table.php
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->timestamps();
        });
    }
Enter fullscreen mode Exit fullscreen mode

Line 6, unique() means each record in the column slug is unique.

Line 7, nullable() means the record in the column can be empty.

Line 8, timestamps() creates two columns that store the time when the record is created and when it is updated.

app/Models/Category.php
class Category extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'description',
        'slug',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Tag Model

database/migrations/create_tags_table.php
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->timestamps();
        });
    }
app/Models/Tag.php
class Tag extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'description',
        'slug',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Post Model

database/migrations/create_posts_table.php
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->bigInteger('category_id');
            $table->bigInteger('user_id');
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('content');
            $table->string('featured_image')->nullable();
            $table->boolean('is_featured')->default(false);
            $table->boolean('is_published')->default(false);
            $table->timestamps();
        });
    }
app/Models/Post.php
class Post extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'category_id',
        'user_id',
        "title",
        'content',
        'slug',
        'featured_image',
        'is_featured',
        'is_published'
    ];
}
Enter fullscreen mode Exit fullscreen mode

Relationship

Between User and Post (One to Many)

If you followed the previous section, we already added a user_id column in the posts table.

database/migrations/create_posts_table.php
$table->bigInteger('user_id');
Enter fullscreen mode Exit fullscreen mode

This column will store the id of the user that this post belongs to. Now we can define the relationships in the models. Remember the new independent directory for the models!

app/Models/User.php
/**
 * Get the posts for the user.
 */
public function posts()
{
    return $this->hasMany('App\Models\Post');
}
app/Models/Post.php
/**
 * Get the user that owns the post.
 */
public function user()
{
    return $this->belongsTo('App\Models\User');
}
Enter fullscreen mode Exit fullscreen mode

Between Category and Post (One to Many)

Again, we need to have a category_id column in the posts table, which stores the id of the category that has this post.

database/migrations/create_posts_table.php
$table->bigInteger('category_id');
Enter fullscreen mode Exit fullscreen mode

Define the relationships in the models.

app/Models/Category.php
/**
 * Get the posts for the user.
 */
public function posts()
{
    return $this->hasMany('App\Models\Post');
}
Enter fullscreen mode Exit fullscreen mode

app/Models/Post.php:

/**
 * Get the category that owns the post.
 */
public function category()
{
    return $this->belongsTo('App\Models\Category');
}
Enter fullscreen mode Exit fullscreen mode

Between Tag and Post (Many to Many)

This one is a bit more complicated, it requires a Many To Many Relationship and an extra database table post_tag. This table is called a pivot table.

First, create a new migration file:

php artisan make:migration create_post_tag_table
database/migrations/create_post_tag_table.php
Schema::create('post_tag', function (Blueprint $table) {
    $table->bigInteger('tag_id');
    $table->bigInteger('post_id');
});
Enter fullscreen mode Exit fullscreen mode

Now we can define the relationships between tags and posts.

app/Models/Tag.php
public function posts()
{
    return $this->belongsToMany('App\Models\Post');
}
app/Models/Post.php
public function tags()
{
    return $this->belongsToMany('App\Models\Tag');
}
Enter fullscreen mode Exit fullscreen mode

Here Laravel assumes there is a post_tag table and there are two columns post_id and tag_id in it. The name of the table needs to be in alphabetical order. If you named them differently, tag_post, for example, you need to specify them like this.

public function tags()
{
    return $this->belongsToMany('App\Models\Tag', 'tag_post');
}
Enter fullscreen mode Exit fullscreen mode

Remember to apply the migration files using php artisan migrate.

Setup Admin Panel

Laravel Nova

If you are using Laravel Nova, please follow this part.

Generate resources with artisan commands:

php artisan nova:resource Post

php artisan nova:resource Category

php artisan nova:resource Tag
app/Nova/Post.php
<?php

namespace App\Nova;

use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\Text;
use Laravel\Nova\Fields\Trix;
use Laravel\Nova\Fields\Slug;
use Laravel\Nova\Fields\Image;
use Laravel\Nova\Fields\Boolean;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\BelongsToMany;
use Laravel\Nova\Http\Requests\NovaRequest;

class Post extends Resource
{
    /**
     * The model the resource corresponds to.
     *
     * @var string
     */
    public static $model = \App\Models\Post::class;

    /**
     * The single value that should be used to represent the resource when being displayed.
     *
     * @var string
     */
    public static $title = 'title';

    /**
     * The columns that should be searched.
     *
     * @var array
     */
    public static $search = [
        'id', 'title', 'content'
    ];

    /**
     * Get the fields displayed by the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function fields(Request $request)
    {
        return [
            ID::make(__('ID'), 'id')->sortable(),
            Text::make('Title'),
            Slug::make('Slug')->from('Title'),
            Trix::make('Content'),
            Image::make('Featured Image')->path('featured_image'),
            Boolean::make('Is Published'),
            Boolean::make('Is Featured'),

            BelongsTo::make('User'),
            BelongsTo::make('Category'),
            BelongsToMany::make('Tags'),
        ];
    }

    /**
     * Get the cards available for the request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function cards(Request $request)
    {
        return [];
    }

    /**
     * Get the filters available for the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function filters(Request $request)
    {
        return [];
    }

    /**
     * Get the lenses available for the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function lenses(Request $request)
    {
        return [];
    }

    /**
     * Get the actions available for the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function actions(Request $request)
    {
        return [];
    }
}
Enter fullscreen mode Exit fullscreen mode

Line 6 – 13, first import all the necessary packages.

Line 23, remember to change the path to the corresponding model.

Line 52, the from() method will automatically turn the title field into a slug. If you wish to use a different separator, then attach a separator() method like this: Slug::make('Slug')->from('Title')->separator('_'),.

Line 54, the path() method will create a new folder, featured_image, for all the featured images uploaded by the user.

Line 58 – 60, we also need to define the relationships here. The BelongsTo and BelongsToMany fields each corresponds to the belongsTo and belongsToMany Eloquent relationship. Here is a video tutorial on this topic: https://laracasts.com/series/laravel-nova-mastery/episodes/5

app/Nova/Category.php
    public function fields(Request $request)
    {
        return [
            ID::make(__('ID'), 'id')->sortable(),
            Text::make('Name'),
            Slug::make('Slug')->from('Name'),
            Textarea::make('Description'),

            HasMany::make('Posts'),
        ];
    }
app/Nova/Tag.php
    public function fields(Request $request)
    {
        return [
            ID::make(__('ID'), 'id')->sortable(),
            Text::make('Name'),
            Slug::make('Slug')->from('Name'),
            Textarea::make('Description'),

            BelongsToMany::make('Posts'),
        ];
    }
Enter fullscreen mode Exit fullscreen mode

Voyager

If you are using Voyager, please follow this part.

Unlike Nova, to setup Voyager, we don’t need to write any code. All we need to do is to add BREAD (browse, read, edit, add and delete) for each database table (not including post_tag).

Go to Tools->BREAD:

img

Edit the BREAD for categories

First, we need to make sure Voyager can find the corresponding model. In the “Categories BREAD info” section, find the “Model Name”, and change it to App\Models\Category.

After that, scroll down to the next section, and change the input type for “description” to “Text Area”. This will give you a bigger text box for description. Remember to save the changes before proceeding.

img

We also need to add the relationship in Voyager, as we designed, each category has many posts:

img

Edit the BREAD for tags

Do the same thing for tags.

img

Add relationships:

img

Edit the BREAD for posts

img

img

Add relationships:

img

img

img

Edit the BREAD for users

We only need to add one more relationship for users and posts:

img

Now, you should be able to see the menu items “Categories”, “Tags” and “Posts”.

img

img

img

img

Error When Editing Posts

img

This error is because Voyager already has a Post built-in, and the Post we created is conflicting with it.

To solve this problem, we change the URL Slug for posts into something other than posts.

img

Then go to Menu Builder (This step is optional, if it already works for you, there is no need to change anything):

img

Click on Builder, and edit the menu for Posts

img

img

Related Articles

How to Make Your Server More Secure

Laravel Tutorial For Beginners

Django Tutorial For Beginners

Build A Unit Converter with Vue.js

Discussion (14)

pic
Editor guide
Collapse
darwin1501 profile image
darwin1501

now I got an error on adding new tags, I says Route [voyager.posts.store] not defined, but when I change it back to post on post slugs it works, but the post is not working again, how to solve this? the tags can't find the blog-posts, it keeps finding the posts, even if I change it to blog-post.

Collapse
ericnanhu profile image
Eric Hu Author

Did you make changes to the menu builder?

Collapse
darwin1501 profile image
darwin1501

yes, I changed it to blog-post of the URL for menu item, and I also changed the URL slug of post from post to blog-post. and when I create new tag, I got an error Route [voyager.posts.store] not defined,
you can check my screenshot below.

Thread Thread
darwin1501 profile image
Thread Thread
darwin1501 profile image
Thread Thread
darwin1501 profile image
Thread Thread
ericnanhu profile image
Eric Hu Author

I think the voyager you installed is corrupted, try run php artisan voyager:install again. That should solve your problem.

Thread Thread
darwin1501 profile image
darwin1501

How do I solve this I tried to install again but I says The [D:\Code_project\php\laravel\laravel-basics\public\storage] link already exists. Do I need to delete this storage?

Thread Thread
darwin1501 profile image
darwin1501

what folder should I delete?

Thread Thread
darwin1501 profile image
darwin1501

I tried to install the voyager again after deleting the storage, but still the error exists.

Thread Thread
ericnanhu profile image
Eric Hu Author

Try clear the cached routes:
php artisan route:clear

Thread Thread
darwin1501 profile image
darwin1501

Thank you, but still I have the same error, Can I continue to the next topic of your blog having this error? Is it possible?, because I want to learn only the basics of laravel.

Thread Thread
ericnanhu profile image
Eric Hu Author

I think you can. If Voyager is not working for you, you can always choose some other admin panels for Laravel apps

Thread Thread
darwin1501 profile image
darwin1501 • Edited

I think I fixed the the error that I was having for days. Instead of changing the post slug url to blog-post. I changed the Post Model name, and database name posts to blog_post. and I also changed some code on the database migrations "post_tags" inside this php file I changed the post_id to blog_post_id, and I also change the column name post_id to blog_post_id into the database to make it work. Now I can continue to your tutorials, without existing errors. Thank you for providing me some possible solutions. It made me think outside the box to do this solution on my head. Thank you, now I can continue to your next tutorial on the blog.