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:

  1. Membuat model, factory, dan migrasi database untuk Topic, Post, dan Comment.
  2. Menggunakan traits penting seperti HasFactory, SoftDeletes, dan HasUlids pada model-model baru.
  3. Mendefinisikan relasi Eloquent (seperti belongsTo, hasMany) di dalam model.
  4. Menjalankan migrasi untuk membuat tabel-tabel baru di database.
  5. 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.

  1. 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)
  2. Definisikan Skema Migrasi Topic: Buka file migrasi yang baru saja dibuat untuk topics. Modifikasi method up() 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');
        }
    };
    
  3. Modifikasi Model Topic: Buka file app/Models/Topic.php. Tambahkan traits HasFactory, SoftDeletes, HasUlids, dan definisikan properti $fillable serta getRouteKeyName().

    // 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);
        }
    }
    
  4. Definisikan Factory Topic: Buka file database/factories/TopicFactory.php dan modifikasi method definition():

    // 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.

  1. Buat Model, Factory, dan File Migrasi:

    php artisan make:model Post -mf
    
  2. Definisikan Skema Migrasi Post: Buka file migrasi untuk posts dan modifikasi method up():

    // 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');
        }
    };
    
  3. Modifikasi Model Post: Buka app/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);
        }
    }
    
  4. Definisikan Factory Post: Buka database/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() dan User::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.

  1. Buat Model, Factory, dan File Migrasi:

    php artisan make:model Comment -mf
    
  2. Definisikan Skema Migrasi Comment: Buka file migrasi untuk comments dan modifikasi method up():

    // 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');
        }
    };
    
  3. Modifikasi Model Comment: Buka app/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');
        }
    }
    
  4. Definisikan Factory Comment: Buka database/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).

  1. 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.

  2. Definisikan Logika Seeder: Buka file database/seeders/ContentSeeder.php dan modifikasi method run() 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 objek Topic menggunakan TopicFactory dan menyimpannya ke database.
    • ->each(function (Topic $topic) { ... }): Untuk setiap Topic yang baru dibuat:
      • \App\Models\Post::factory()->count(25)->make(): Membuat 25 instans objek Post menggunakan PostFactory di memori (belum disimpan ke DB). topic_id tidak perlu di-set di sini secara eksplisit karena...
      • $topic->posts()->saveMany(...): Menyimpan 25 instans Post tersebut ke database dan secara otomatis mengaitkannya dengan $topic saat ini (mengisi topic_id). created_by (untuk penulis post) akan di-generate oleh PostFactory karena kita menggunakan User::factory() di dalamnya.
      • $posts->each(function (\App\Models\Post $post) { ... }): Untuk setiap Post yang baru dibuat (yang terkait dengan topik di atas):
        • \App\Models\Comment::factory()->count(13)->make(): Membuat 13 instans objek Comment di memori.
        • $post->comments()->saveMany(...): Menyimpan 13 instans Comment tersebut ke database dan secara otomatis mengaitkannya dengan $post saat ini. created_by atau user_id (untuk member yang berkomentar) akan di-generate oleh CommentFactory.
  3. Tambahkan ContentSeeder ke DatabaseSeeder.php: Agar ContentSeeder dapat dijalankan bersama dengan seeder lainnya (atau melalui perintah db:seed utama), daftarkan di database/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 dan AdminUserSeeder di Level 3. Untuk saat ini, Anda bisa mengomentarinya jika belum ada.

  4. 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:

  1. 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, dan comments.
    • Periksa apakah tabel-tabel tersebut sudah terisi data sesuai dengan yang dijalankan oleh ContentSeeder. Anda akan melihat banyak record di sana.
  2. 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!