Tutorial Portal Berita - Level 2: Struktur Data
Selamat datang di Level 2! Setelah kita berhasil menyiapkan proyek Laravel dan Laravolt di Level 1, kini saatnya membangun fondasi data untuk aplikasi portal berita kita. Di level ini, kita akan fokus pada pembuatan model, factory, dan migrasi database untuk entitas-entitas utama, serta mendefinisikan relasi antar mereka.
Tujuan Level 2:
- Membuat model, factory, dan migrasi database untuk
Topic
,Post
, danComment
. - Menggunakan
traits
penting sepertiHasFactory
,SoftDeletes
, danHasUlids
pada model-model baru. - Mendefinisikan relasi Eloquent (seperti
belongsTo
,hasMany
) di dalam model. - Menjalankan migrasi untuk membuat tabel-tabel baru di database.
- Memverifikasi struktur database yang telah dibuat.
Mari kita mulai merancang struktur data kita!
1. Membuat Model, Factory, dan Migrasi
Kita akan membuat tiga model utama baru: Topic
, Post
, dan Comment
. Untuk setiap model, kita juga akan membuat file migrasinya (untuk skema tabel) dan factory-nya (untuk menghasilkan data dummy). Laravel menyediakan perintah Artisan yang sangat membantu untuk ini.
Kita akan menggunakan ULID (Universally Unique Lexicographically Sortable Identifier) sebagai primary key untuk model-model baru kita. Kita juga akan mengimplementasikan SoftDeletes
agar data tidak benar-benar hilang saat dihapus.
a. Topic
(Model, Factory, Migrasi)
Topic
akan digunakan untuk mengkategorikan artikel berita.
-
Buat Model, Factory, dan File Migrasi: Jalankan perintah Artisan berikut di terminal Anda:
php artisan make:model Topic -mf
Perintah ini akan membuat:
- File model:
app/Models/Topic.php
- File factory:
database/factories/TopicFactory.php
- File migrasi: di
database/migrations/
(contoh:YYYY_MM_DD_HHMMSS_create_topics_table.php
)
- File model:
-
Definisikan Skema Migrasi
Topic
: Buka file migrasi yang baru saja dibuat untuktopics
. Modifikasi methodup()
sebagai berikut:// database/migrations/YYYY_MM_DD_HHMMSS_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 { public function up(): void { Schema::create('topics', function (Blueprint $table) { $table->ulid('id')->primary(); $table->string('name'); $table->string('slug')->unique(); $table->text('description')->nullable(); $table->timestamps(); $table->softDeletes(); }); } public function down(): void { Schema::dropIfExists('topics'); } };
-
Modifikasi Model
Topic
: Buka fileapp/Models/Topic.php
. Tambahkan traitsHasFactory
,SoftDeletes
,HasUlids
, dan definisikan properti$fillable
sertagetRouteKeyName()
.// app/Models/Topic.php <?php namespace App\Models; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Relations\HasMany; class Topic extends Model { use HasFactory, SoftDeletes, HasUlids; protected $fillable = ['name', 'slug', 'description']; public function getRouteKeyName(): string { return 'slug'; } public function posts(): HasMany { return $this->hasMany(Post::class); } }
-
Definisikan Factory
Topic
: Buka filedatabase/factories/TopicFactory.php
dan modifikasi methoddefinition()
:// database/factories/TopicFactory.php <?php namespace Database\Factories; use App\Models\Topic; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; class TopicFactory extends Factory { protected $model = Topic::class; public function definition(): array { $name = $this->faker->unique()->words(asText: true); // Menghasilkan 1-3 kata unik return [ 'name' => Str::title($name), // Mengkapitalkan setiap kata 'slug' => Str::slug($name), ]; } }
b. Post
(Model, Factory, Migrasi)
Post
akan merepresentasikan artikel berita.
-
Buat Model, Factory, dan File Migrasi:
php artisan make:model Post -mf
-
Definisikan Skema Migrasi
Post
: Buka file migrasi untukposts
dan modifikasi methodup()
:// database/migrations/YYYY_MM_DD_HHMMSS_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 { public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->ulid('id')->primary(); $table->foreignUlid('topic_id')->constrained('topics')->cascadeOnDelete(); $table->foreignId('created_by')->constrained('users')->cascadeOnDelete(); // Penulis $table->string('title'); $table->string('slug')->unique(); $table->text('body'); $table->string('featured_image')->nullable(); $table->enum('status', ['draft', 'published', 'archived'])->default('draft'); $table->timestamp('published_at')->nullable(); $table->timestamps(); $table->softDeletes(); }); } public function down(): void { Schema::dropIfExists('posts'); } };
-
Modifikasi Model
Post
: Bukaapp/Models/Post.php
:// app/Models/Post.php <?php namespace App\Models; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; class Post extends Model { use HasFactory, SoftDeletes, HasUlids; protected $fillable = [ 'topic_id', 'created_by', 'title', 'slug', 'body', 'featured_image', 'status', 'published_at', ]; protected $casts = [ 'published_at' => 'datetime', ]; public function getRouteKeyName(): string { return 'slug'; } public function topic(): BelongsTo { return $this->belongsTo(Topic::class); } public function writer(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); } public function comments(): HasMany { return $this->hasMany(Comment::class); } }
-
Definisikan Factory
Post
: Bukadatabase/factories/PostFactory.php
:// database/factories/PostFactory.php <?php namespace Database\Factories; use App\Models\Post; use App\Models\Topic; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; class PostFactory extends Factory { protected $model = Post::class; public function definition(): array { $title = $this->faker->unique()->sentence(nbWords: 6, variableNbWords: true); // Judul unik $isPublished = $this->faker->boolean(75); // 75% kemungkinan published return [ 'topic_id' => Topic::factory(), // Membuat atau mengambil Topic yang ada 'created_by' => User::factory(), // Membuat atau mengambil User yang ada (Writer) 'title' => Str::title($title), 'slug' => Str::slug($title), 'body' => $this->faker->paragraphs(asText: true, nb: $this->faker->numberBetween(5, 15)), // 5-15 paragraf 'featured_image' => $this->faker->imageUrl(1200, 800, 'news', true, 'cats'), // Contoh gambar berita 'status' => $isPublished ? 'published' : 'draft', 'published_at' => $isPublished ? $this->faker->dateTimeThisMonth() : null, ]; } /** * Indicate that the post is in draft status. */ public function draft(): Factory { return $this->state(function (array $attributes) { return [ 'status' => 'draft', 'published_at' => null, ]; }); } /** * Indicate that the post is published. */ public function published(): Factory { return $this->state(function (array $attributes) { return [ 'status' => 'published', 'published_at' => $this->faker->dateTimeThisMonth(), ]; }); } }
Di sini,
Topic::factory()
danUser::factory()
akan membuat entitas terkait jika belum ada, atau Anda bisa melewatkan ID jika Anda mengelolanya secara manual saat seeding.
c. Comment
(Model, Factory, Migrasi)
Comment
akan menyimpan komentar dari pengguna pada artikel.
-
Buat Model, Factory, dan File Migrasi:
php artisan make:model Comment -mf
-
Definisikan Skema Migrasi
Comment
: Buka file migrasi untukcomments
dan modifikasi methodup()
:// database/migrations/YYYY_MM_DD_HHMMSS_create_comments_table.php <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('comments', function (Blueprint $table) { $table->ulid('id')->primary(); $table->foreignUlid('post_id')->constrained('posts')->cascadeOnDelete(); $table->foreignId('created_by')->constrained('users')->cascadeOnDelete(); // Member $table->text('body'); $table->timestamps(); $table->softDeletes(); }); } public function down(): void { Schema::dropIfExists('comments'); } };
-
Modifikasi Model
Comment
: Bukaapp/Models/Comment.php
:// app/Models/Comment.php <?php namespace App\Models; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Comment extends Model { use HasFactory, SoftDeletes, HasUlids; protected $fillable = [ 'post_id', 'created_by', 'body', ]; public function post(): BelongsTo { return $this->belongsTo(Post::class); } public function member(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); } }
-
Definisikan Factory
Comment
: Bukadatabase/factories/CommentFactory.php
:// database/factories/CommentFactory.php <?php namespace Database\Factories; use App\Models\Comment; use App\Models\Post; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; class CommentFactory extends Factory { protected $model = Comment::class; public function definition(): array { return [ 'post_id' => Post::factory(), // Membuat atau mengambil Post yang ada 'user_id' => User::factory(), // Membuat atau mengambil User yang ada (Member) 'body' => $this->faker->paragraph(nbSentences: $this->faker->numberBetween(1, 5)), ]; } }
2. Mendefinisikan Relasi Antar Model (Tambahan pada User)
Relasi pada model Topic
, Post
, dan Comment
sudah didefinisikan di atas. Sekarang kita pastikan model User
juga memiliki relasi yang relevan.
Model User
(app/Models/User.php)
Tambahkan relasi ke Post
(sebagai penulis) dan Comment
(sebagai member).
// app/Models/User.php
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory; // Pastikan HasFactory ada untuk User jika ingin User::factory() bekerja
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravolt\Platform\Traits\UserHasRole;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Authenticatable // Mungkin implement MustVerifyEmail jika diperlukan
{
use HasFactory, Notifiable, UserHasRole; // Tambahkan HasFactory di sini
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'status', // Laravolt mungkin menambahkan ini
'timezone', // Laravolt mungkin menambahkan ini
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}
Penting: Pastikan trait HasFactory
juga digunakan pada model User
(biasanya sudah ada secara default di instalasi Laravel baru) agar User::factory()
bisa digunakan di dalam factory lain.
3. Menjalankan Migrasi
Setelah semua file migrasi, model, dan factory telah didefinisikan, saatnya untuk membuat tabel-tabel ini di database Anda.
Jalankan perintah Artisan berikut:
php artisan migrate
Laravel akan menjalankan semua migrasi yang belum pernah dijalankan. Jika berhasil, Anda akan melihat output di terminal yang mengindikasikan tabel topics
, posts
, dan comments
telah dibuat.
4. Membuat Seeder Konten (ContentSeeder)
Sekarang kita memiliki struktur database dan factory, mari kita buat seeder untuk mengisi database kita dengan data konten awal (topics, posts, dan comments).
-
Buat File
ContentSeeder
: Jalankan perintah Artisan berikut di terminal Anda untuk membuat file seeder baru:php artisan make:seeder ContentSeeder
Perintah ini akan membuat file
database/seeders/ContentSeeder.php
. -
Definisikan Logika Seeder: Buka file
database/seeders/ContentSeeder.php
dan modifikasi methodrun()
sebagai berikut:<?php namespace Database\Seeders; use App\Models\Topic; use App\Models\Post; // Pastikan Post di-import jika menggunakan type hint use App\Models\Comment; // Pastikan Comment di-import jika menggunakan type hint use Illuminate\Database\Seeder; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; // Bisa di-uncomment jika perlu class ContentSeeder extends Seeder { /** * Run the database seeds. */ public function run(): void { $this->command->info('Memulai ContentSeeder...'); // Membuat 10 Topik // Setiap topik akan memiliki 25 post Topic::factory()->count(10)->create()->each(function (Topic $topic) { $posts = \App\Models\Post::factory()->count(25)->make([ // 'topic_id' tidak perlu diset di sini karena saveMany akan menanganinya // 'created_by' akan di-generate oleh PostFactory menggunakan User::factory() ]); $topic->posts()->saveMany($posts); // Untuk setiap post yang baru dibuat untuk topik ini, tambahkan komentar $posts->each(function (\App\Models\Post $post) { $comments = \App\Models\Comment::factory()->count(13)->make([ // 'post_id' tidak perlu diset di sini karena saveMany akan menanganinya // 'created_by' atau 'user_id' akan di-generate oleh CommentFactory menggunakan User::factory() ]); $post->comments()->saveMany($comments); }); }); $this->command->info('ContentSeeder selesai.'); } }
Penjelasan Seeder:
Topic::factory()->count(10)->create()
: Membuat 10 objekTopic
menggunakanTopicFactory
dan menyimpannya ke database.->each(function (Topic $topic) { ... })
: Untuk setiapTopic
yang baru dibuat:\App\Models\Post::factory()->count(25)->make()
: Membuat 25 instans objekPost
menggunakanPostFactory
di memori (belum disimpan ke DB).topic_id
tidak perlu di-set di sini secara eksplisit karena...$topic->posts()->saveMany(...)
: Menyimpan 25 instansPost
tersebut ke database dan secara otomatis mengaitkannya dengan$topic
saat ini (mengisitopic_id
).created_by
(untuk penulis post) akan di-generate olehPostFactory
karena kita menggunakanUser::factory()
di dalamnya.$posts->each(function (\App\Models\Post $post) { ... })
: Untuk setiapPost
yang baru dibuat (yang terkait dengan topik di atas):\App\Models\Comment::factory()->count(13)->make()
: Membuat 13 instans objekComment
di memori.$post->comments()->saveMany(...)
: Menyimpan 13 instansComment
tersebut ke database dan secara otomatis mengaitkannya dengan$post
saat ini.created_by
atauuser_id
(untuk member yang berkomentar) akan di-generate olehCommentFactory
.
-
Tambahkan
ContentSeeder
keDatabaseSeeder.php
: AgarContentSeeder
dapat dijalankan bersama dengan seeder lainnya (atau melalui perintahdb:seed
utama), daftarkan didatabase/seeders/DatabaseSeeder.php
:// database/seeders/DatabaseSeeder.php <?php namespace Database\Seeders; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. */ public function run(): void { // Panggil seeder lain yang mungkin sudah ada atau akan dibuat // $this->call([ // RolePermissionSeeder::class, // Akan dibuat di Level 3 // AdminUserSeeder::class, // Akan dibuat di Level 3 // ]); // Panggil ContentSeeder $this->call(ContentSeeder::class); // Anda juga bisa memanggil factory secara langsung di sini jika tidak ingin membuat seeder terpisah // \App\Models\User::factory(10)->create(); // \App\Models\User::factory()->create([ // 'name' => 'Test User', // 'email' => '[email protected]', // ]); } }
Catatan: Kita akan membuat
RolePermissionSeeder
danAdminUserSeeder
di Level 3. Untuk saat ini, Anda bisa mengomentarinya jika belum ada. -
Jalankan Seeder: Setelah migrasi dijalankan, Anda bisa menjalankan semua seeder (yang terdaftar di
DatabaseSeeder.php
) dengan perintah:php artisan db:seed
Atau, jika Anda ingin menjalankan
ContentSeeder
secara spesifik:php artisan db:seed --class=ContentSeeder
Jika Anda ingin melakukan migrasi ulang dan seeding dari awal (PERHATIAN: ini akan menghapus semua data di database):
php artisan migrate:fresh --seed
Proses seeding ini mungkin membutuhkan beberapa saat karena membuat banyak data (10 topik, 250 post, dan 3250 komentar).
5. Memverifikasi Struktur dan Data Database
Setelah menjalankan migrasi dan seeder, Anda bisa memverifikasi hasilnya:
-
Database Client: Gunakan database client seperti DB Browser for SQLite, DataGrip, TablePlus, atau ekstensi VS Code untuk SQLite. Buka file
database/database.sqlite
Anda.- Periksa skema tabel
users
,topics
,posts
, dancomments
. - Periksa apakah tabel-tabel tersebut sudah terisi data sesuai dengan yang dijalankan oleh
ContentSeeder
. Anda akan melihat banyak record di sana.
- Periksa skema tabel
-
php artisan tinker
: Gunakan Tinker untuk berinteraksi dengan data Anda:php artisan tinker
Kemudian di dalam Tinker:
// Menghitung jumlah Topik \App\Models\Topic::count(); // Harusnya 10 // Menghitung jumlah Post \App\Models\Post::count(); // Harusnya 250 (10 topik * 25 post) // Menghitung jumlah Komentar \App\Models\Comment::count(); // Harusnya 3250 (250 post * 13 komentar) // Mengambil satu post beserta topik dan penulisnya // $post = \App\Models\Post::with(['topic', 'writer'])->first(); // $post->topic->name; // $post->writer->name; // Mengambil satu post beserta komentarnya // $postWithComments = \App\Models\Post::with('comments')->first(); // $postWithComments->comments;
Selamat! 🎉 Anda telah berhasil menyelesaikan Level 2. Fondasi data untuk aplikasi portal berita kita kini telah terbentuk dengan model, factory, tabel, dan relasi yang jelas. Dengan ContentSeeder
, database kita juga sudah terisi dengan data sampel yang representatif, yang akan sangat berguna untuk pengembangan dan pengujian di tahap selanjutnya.
Di Level 3, kita akan fokus pada sistem Autentikasi & Otorisasi. Kita akan mengkonfigurasi Laravolt untuk menangani autentikasi, mengatur peran pengguna (Admin, Writer, Member), mendefinisikan permissions, dan mengimplementasikan policies untuk model Post
dan Comment
. Sampai jumpa di level berikutnya!