How to Build a Blog with Laravel - Part 2 - Database Stuff

#laravel #php

Welcome back! We're going to continue building our blog in Laravel. Let's create the components needed in order to store our blog in a database.

If you missed out on part one, where we installed Laravel and got our server running, you can find it here. Or, if you want to skip part one, you can clone the git repo to get started:

git clone https://github.com/fstrube/blog-app.git
cd blog-app
npm install
composer install
composer run dev

Data Modeling

First, let's design a data model that makes sense for our blog. The blog will consist of posts that have a title, content, and published date. Posts can have one or many topics, so we will need to store topics in the database with a many-to-many relationship between posts and topics. Lastly, we will need a database of users. A post can be authored by a user, so we will need a relationship between posts and users as well. Below is a representation of our data model:

erDiagram
    posts {
        bigint id
        string title
        string slug
        text content
        bigint author_id
        timestamp published_at
    }
    users {
        bigint id
        string name
        string email
        string password
        text bio
    }
    topics {
        bigint id
        string name
    }
    posts }o--|| users: author
    posts }o--o{ topics: has

In order to create our database structure, we need to write a migration script and a model class for each database table. This can be done with the commandline utility, called artiasan, that comes with Laravel.

Laravel's commandline utility allows you to interact with your application in all sorts of ways. If you run php artisan you will see the list of commands that it supports, and there are many. Many of these commands are useful for creating the PHP code that makes up our app. The first artisan command that we will run creates our Post model and migration script.

php artisan make:model -m -f Post

make model post.png

The extra options -m and -f create a migration and a factory, respectively. The migration is essential for changing the database schema. The factory is a non-essential, but useful, class that popuplates the database with fake information during testing and local development.

Let's do the same for our Topic model:

php artisan make:model -m -f Topic

make model topic.png

You'll notice that we didn't make a model for users. This is because one is created for you when you set up a new Laravel project. The only thing we need to do is add a column for the user's bio. For that, we turn to another Artisan command:

php artisan make:migration add_bio_column_to_users_table

make migration add_bio_column_to_users_table.png

Writing Database Migrations

Alright, with these files created we can now get down to the dirty work. We have three new migrations, and each one must add the appropriate database tables and columns.

database/migrations/2025_01_26_214302_create_posts_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('author_id')->nullable()
                ->references('id')->on('users')->nullOnDelete();
            $table->string('title')->default('');
            $table->string('slug')->unique();
            $table->text('content')->default('');
            $table->datetime('published_at')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

database/migrations/2025_01_26_214355_create_topics_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('topics', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('topics');
    }
};

database/migrations/2025_01_26_214553_add_bio_column_to_users_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->text('bio')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('bio');
        });
    }
};

But, wait...we're missing something. We have Posts and Topics, but we don't have a way to relate them. We need to create a join table for the many-to-many relationship between Posts and Topics.

php artisan make:migration create_post_topic_table

make migration create_posts_topics_table.png

database/migrations/2025_01_26_221114_create_post_topic_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts_topics', function (Blueprint $table) {
            $table->foreignId('post_id')->references('id')->on('posts')->cascadeOnDelete();
            $table->foreignId('topic_id')->references('id')->on('topics')->cascadeOnDelete();
            $table->primary(['post_id', 'topic_id']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts_topics');
    }
};

All that's left now is to commit these changes to our database. Running the following command in your terminal will execute the pending database migrations:

php artisan migrate

migrate.png

Relating the Models

Laravel ships with a great database abstraction layer called Eloquent. Eloquent makes working with the database very straightforward. It supports basic operations, like creating, querying, and updating data, and it also supports all sorts of advanced techniques, like query scopes, relationships, and eager-loading.

At this point, everything is configured at our database layer, but our application still needs to understand how each model should behave. Laravel can guess which models correlate to which tables in the database by analyzing the classname of the model. However, we have to declare some things on each model to get the most out of Eloquent.

In our Post model, we will tell Eloquent that it belongs to many Topics. This is one side of our many-to-many relationship between topics and posts.

app/Models/Post.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /** @use HasFactory<\Database\Factories\PostFactory> */
    use HasFactory;

    public function topics()
    {
        return $this->belongsToMany(Topic::class);
    }
}

We reciprocate this relationship in the Topic model:

app/Models/Topic.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Topic extends Model
{
    /** @use HasFactory<\Database\Factories\TopicFactory> */
    use HasFactory;

    public function posts()
    {
        return $this->belongsToMany(Post::class);
    }
}

Making use of attribute accessors

One thing we'll want to do eventually on our blog is to show readers an excerpt of each post on the home page. In preparation for this, we will add an attribute accessor on our Post model so we can easily reference the value as $post->excerpt. Here is what I mean.

app/Models/Post.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /** @use HasFactory<\Database\Factories\PostFactory> */
    use HasFactory;

    public function topics()
    {
        return $this->belongsToMany(Topic::class);
    }

    /**
     * Show the first 150 characters of the content with an
     * ellipsis, if necessary.
     * 
     * @return string
     */
    public function getExcerptAttribute()
    {
        $excerpt = strip_tags($this->content);

        if (strlen($excerpt) > 150) {
            $excerpt = substr($excerpt, 0, 150).'...';
        }

        return $excerpt;
    }
}

The getExcerptAttribute() function strips HTML tags from the content, and truncates the text to 150 characters or less.

Populating the database with factories

Factories are a great way to simulate live data. I highly recommend reading the factories page in the Laravel docs to get an idea of their capabilities. Factories are very useful for seeding your database during testing or for creating large amounts of fake data for benchmarking your application. They make heavy use of the FakerPHP package to generate random data values.

We're going to create two factories: one for Topics and one for Posts.

database/factories/PostFactory.php

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
 */
class PostFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'slug' => $this->faker->unique()->slug(),
            'title' => $this->faker->words(4, true),
            'content' => $this->faker->paragraphs(5, true),
        ];
    }

    /**
     * A published post
     *
     * @return static
     */
    public function published($at = null)
    {
        return $this->state([
            'published_at' => $at === null ? now() : $at,
        ]);
    }
}

Our PostFactory uses FakerPHP to generate random data when creating a Post, and it exposes a factory "state" for creating published posts. Let's go ahead and create 10 fake posts in our database. We can do so by starting up a Laravel Tinker prompt.

php artisan tinker

Tinker lets you call PHP code directly in your application through a PHP prompt. We can execute the following code to create our posts:

App\Models\Post::factory()->count(10)->create();

tinker posts.png

Let's also make a few published posts while we're at it:

App\Models\Post::factory()->count(5)->published()->create();

tinker posts published.png

We also need a TopicFactory. This one is short, as a topic simply needs a unique name.

database/factories/TopicFactory.php

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Topic>
 */
class TopicFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => $this->faker->unique()->words(3, true),
        ];
    }
}

We can now create posts that have topics, like so:

App\Models\Post::factory()->has(App\Models\Topic::factory())->create();

tinker posts topics.png

Up next

In the next article, we will start working on the frontend of our blog application.