How to Build a Blog with Laravel - Part 2 - Database Stuff
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
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
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
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
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
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();
Let's also make a few published posts while we're at it:
App\Models\Post::factory()->count(5)->published()->create();
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();
Up next
In the next article, we will start working on the frontend of our blog application.