Skip to content

BPM Migration

Laravel Migration extension dengan custom Blueprint untuk kolom ULID, actor tracking (creator/editor), dan soft deletes otomatis

Versi: 0.4.1 Changelog

Laravel


TL;DR

BPM Migration extends Laravel migrations dengan custom Blueprint yang menyediakan ULID primary keys by default, automatic actor tracking (siapa yang create/edit), dan soft deletes otomatis. Mengurangi boilerplate dan standardisasi struktur database dengan konvensi yang konsisten.

Namespace:

php
use Bpmlib\BpmMigration\BpmSchema;
use Bpmlib\BpmMigration\Migration;
use Bpmlib\BpmMigration\Blueprint\BpmBlueprint;
use Bpmlib\BpmMigration\Model\Trait\HasCreator;
use Bpmlib\BpmMigration\Model\Trait\HasEditor;

Artisan Commands: NEW v0.2.0

bash
php artisan bpm:make:migration  # Generate BPM migration
php artisan bpm:make:model      # Generate BPM model + migration

Konfigurasi:


Instalasi

Install melalui Composer:

bash
composer require bpmlib/bpm-migration

Library ini menggunakan auto-discovery untuk Laravel 5.5+. Service provider akan otomatis terdaftar.

Publish konfigurasi (opsional):

bash
php artisan vendor:publish --tag=bpm-migration-config

Requirements

Library ini memerlukan:

PHP Version:

bash
PHP ^8.1

Composer Dependencies:

PackageVersionDescription
illuminate/console^10.0|^11.0|^12.0Laravel Console
illuminate/container^10.0|^11.0|^12.0Laravel Container
illuminate/database^10.0|^11.0|^12.0Laravel Database
illuminate/support^10.0|^11.0|^12.0Laravel Support

Framework Requirements:

FrameworkVersion
Laravel^10.0 || ^11.0 || ^12.0

Quick Start

Basic Migration dengan BpmSchema

Contoh paling sederhana menggunakan BpmSchema:

php
<?php

use Bpmlib\BpmMigration\BpmSchema;
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        BpmSchema::create('products', function ($table) {
            $table->id();              // ULID primary key
            $table->string('name');
            $table->actor();           // creator_id + latest_editor_id
            $table->timestamps();      // created_at, updated_at, deleted_at
        });
    }

    public function down(): void
    {
        BpmSchema::dropIfExists('products');
    }
};

Key Points:

  • id() → ULID (bukan auto-increment)
  • timestamps() → includes soft deletes
  • actor() → creator + editor tracking

Comprehensive Example

Full-featured migration menggunakan semua BpmBlueprint methods:

php
<?php

use Bpmlib\BpmMigration\BpmSchema;
use App\Models\{User, Category};
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        BpmSchema::create('orders', function ($table) {
            $table->id();
            
            // Foreign keys dengan auto-detection
            $table->foreignTable(User::class);
            $table->foreignTableNullable(Category::class);
            
            $table->string('order_number')->unique();
            $table->decimal('total', 12, 2);
            $table->string('status')->default('pending');
            
            // Actor tracking dengan custom names
            $table->actor(
                creatorColumn: 'created_by',
                editorColumn: 'updated_by'
            );
            
            // Timestamps options
            $table->timestamps();              // Default: with soft deletes
            // $table->timestamps(withoutSoftDelete: true);  // Without soft deletes
            // $table->timestamps(withBackdoor: true);       // With backdoor_updated_at
            
            $table->index('status');
        });
    }

    public function down(): void
    {
        BpmSchema::dropIfExists('orders');
    }
};

Alternative: Using Migration Class

Extend Bpmlib\BpmMigration\Migration untuk cleaner syntax tanpa import BpmSchema:

php
<?php

use Bpmlib\BpmMigration\Migration;

class CreateProductsTable extends Migration
{
    public function up(): void
    {
        $this->schema()->create('products', function ($table) {
            $table->id();
            $table->string('name');
            $table->actor();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        $this->schema()->dropIfExists('products');
    }
}

Key Points:

  • Tidak perlu import BpmSchema
  • Method $this->schema() otomatis menggunakan BpmBlueprint
  • Lebih clean untuk class-based migrations

Core Concepts

BPM Migration dibangun dengan beberapa opinionated defaults yang meningkatkan produktivitas. Pahami konsep-konsep ini untuk menggunakan library secara efektif.

BpmSchema vs Laravel Schema

PENTING

BpmBlueprint hanya bekerja ketika menggunakan BpmSchema atau class Migration dari library ini.

TIDAK akan bekerja jika menggunakan Laravel's default Schema facade:

php
// ❌ SALAH - BpmBlueprint methods tidak tersedia
use Illuminate\Support\Facades\Schema;

Schema::create('users', function (Blueprint $table) {
    $table->creator(); // ❌ Method tidak ada!
});

// ✅ BENAR - Menggunakan BpmSchema
use Bpmlib\BpmMigration\BpmSchema;

BpmSchema::create('users', function ($table) {
    $table->creator(); // ✅ Bekerja!
});

// ✅ BENAR - Extend Migration class
use Bpmlib\BpmMigration\Migration;

class CreateUsersTable extends Migration
{
    public function up()
    {
        $this->schema()->create('users', function ($table) {
            $table->creator(); // ✅ Bekerja!
        });
    }
}

Custom Blueprint System

Cara kerja:

  1. BpmSchema mendapatkan schema builder dari Laravel
  2. Mengganti blueprint resolver untuk menggunakan BpmBlueprint
  3. Semua method create(), table(), dll menggunakan blueprint custom ini
  4. Compatible dengan Laravel 11 dan Laravel 12 (berbeda signature)

Implikasi:

php
// BpmSchema mengembalikan Builder dengan custom resolver
$schema = BpmSchema::extend();

// Setiap operasi schema akan menggunakan BpmBlueprint
$schema->create('table', function ($table) {
    // $table adalah instance BpmBlueprint
    $table->creator(); // Method custom tersedia
});

Key points:

  • Compatible dengan Laravel 11 dan 12
  • Tidak mengubah global Schema behavior
  • Setiap connection bisa memiliki resolver sendiri

ULID Primary Keys

Default behavior:

Method id() di BpmBlueprint menggunakan ULID, bukan auto-increment integer.

php
$table->id(); // Membuat kolom 'id' dengan type ULID

Equivalent dengan:

php
$table->ulid('id')->primary();

Benefits:

  • Globally unique
  • Sortable by creation time
  • Tidak sequential (lebih aman)
  • Cocok untuk distributed systems

Jika masih perlu integer:

php
$table->intId(); // Menggunakan bigIncrements

Actor Tracking System

Automatic tracking:

Library ini otomatis tracking user yang membuat (creator) dan terakhir edit (editor) data melalui Model Traits.

Tiga mode operasi:

ModeDeskripsiUse Case
localUser table ada di service iniMonolith app
remoteUser data di service lainMicroservice
hybridMixed, model tentukan sendiriTransisi/mixed architecture

Behavior:

  1. Creating: Trait HasCreator set creator_id otomatis saat model dibuat
  2. Updating: Trait HasEditor set latest_editor_id otomatis saat model diupdate
  3. Relationship: Opsional, tergantung mode (local/hybrid support relationship)
  4. Denormalization: Support denormalized fields (override method fillCreatorDenormalized)

Konfigurasi default di config/bpm.migration.php:

php
'actor_mode' => env('BPM_MIGRATION_ACTOR_MODE', 'hybrid'),

Model bisa override:

php
class Product extends Model
{
    use HasCreator, HasEditor;
    
    protected function hasCreatorRelation(): bool
    {
        return false; // Disable relationship (microservice mode)
    }
}

Soft Deletes by Default

Default behavior:

Method timestamps() di BpmBlueprint otomatis include soft deletes:

php
$table->timestamps(); 
// Membuat: created_at, updated_at, deleted_at (nullable)

Jika tidak ingin soft deletes:

php
$table->timestamps(withoutSoftDelete: true);
// Membuat: created_at, updated_at saja

Backdoor timestamp (opsional):

php
$table->timestamps(withBackdoor: true);
// Membuat: created_at, updated_at, deleted_at, backdoor_updated_at

Field backdoor_updated_at menggunakan useCurrent() dan useCurrentOnUpdate(), berguna untuk tracking perubahan yang bypass Eloquent events.


Artisan Commands

PRODUCTIVITY BOOST

Sekarang kamu sudah memahami konsep ULID, actor tracking, dan soft deletes, gunakan Artisan commands untuk generate migrations dan models secara otomatis dengan conventions ini.

Artisan commands mempercepat development dengan auto-generate code yang sudah mengikuti BPM conventions.

Generate migration:

bash
php artisan bpm:make:migration create_products_table
# → Auto-includes: id() (ULID), timestamps() (soft deletes)

Generate model + migration:

bash
php artisan bpm:make:model Product -m
# → Auto-includes: HasUlids, SoftDeletes traits

bpm:make:migration

FITUR BARU - v0.2.0

Generate migration file menggunakan BpmSchema dan BpmBlueprint.

UPDATE - v0.4.1

Command sekarang auto-detect operasi CREATE vs ALTER dan menggunakan stub yang sesuai.

Signature:

bash
php artisan bpm:make:migration {name} [options]

Options:

OptionShortcutDescription
--create=table-Buat migration untuk create table
--table=table-Buat migration untuk alter table
--path=path-Custom path untuk migration file
--realpath-Path adalah absolute path
--int-iGunakan intId() (BIGINT) instead of ULID
--disable-soft-delete-dDisable soft deletes di timestamps()

Auto-Detection:

Command secara otomatis mendeteksi operasi CREATE vs ALTER:

bash
# Auto-detect CREATE (menggunakan create stub)
php artisan bpm:make:migration create_products_table

# Auto-detect ALTER (menggunakan alter stub)
php artisan bpm:make:migration add_status_to_products

# Explicit CREATE
php artisan bpm:make:migration anything --create=products

# Explicit ALTER
php artisan bpm:make:migration anything --table=products

Stub Selection:

OperasiOptionsStub File
CREATEDefaultcreate/migration.ulid-soft.stub
CREATE--intcreate/migration.int-soft.stub
CREATE--disable-soft-deletecreate/migration.ulid-nosoft.stub
CREATE--int --disable-soft-deletecreate/migration.int-nosoft.stub
ALTERAnyalter/migration.alter.stub

CATATAN

Options --int dan --disable-soft-delete diabaikan untuk ALTER migrations (dengan warning).

Contoh Penggunaan:

bash
# Default: ULID + soft deletes
php artisan bpm:make:migration create_products_table

# Integer ID + soft deletes
php artisan bpm:make:migration create_users_table --int

# ULID tanpa soft deletes
php artisan bpm:make:migration create_logs_table --disable-soft-delete

# Integer ID tanpa soft deletes
php artisan bpm:make:migration create_sessions_table --int --disable-soft-delete

# ALTER table (auto-detect dari nama)
php artisan bpm:make:migration add_status_to_products

# ALTER table (explicit)
php artisan bpm:make:migration update_products --table=products

Generated File (CREATE):

php
<?php

use Bpmlib\BpmMigration\BpmSchema;
use Bpmlib\BpmMigration\Blueprint\BpmBlueprint;
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        BpmSchema::create('products', function ($table) {
            /** @var BpmBlueprint $table */

            $table->id();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        BpmSchema::dropIfExists('products');
    }
};

Generated File (ALTER):

php
<?php

use Bpmlib\BpmMigration\BpmSchema;
use Bpmlib\BpmMigration\Blueprint\BpmBlueprint;
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        BpmSchema::table('products', function ($table) {
            /** @var BpmBlueprint $table */

            //
        });
    }

    public function down(): void
    {
        BpmSchema::table('products', function ($table) {
            /** @var BpmBlueprint $table */

            //
        });
    }
};

bpm:make:model

Generate Eloquent model dengan traits BPM (HasUlids, SoftDeletes) dan optional migration.

Signature:

bash
php artisan bpm:make:model {name} [options]

Options:

OptionShortcutDescription
--migration-mBuat migration file juga
--force-fOverwrite jika file sudah ada
--int-iGunakan integer ID (skip HasUlids trait)
--disable-soft-delete-dSkip SoftDeletes trait

Contoh Penggunaan:

bash
# Basic model (ULID + SoftDeletes)
php artisan bpm:make:model Product

# Model + migration
php artisan bpm:make:model Product -m

# Integer ID model
php artisan bpm:make:model LegacyItem --int

# Tanpa soft deletes
php artisan bpm:make:model Log --disable-soft-delete

# Custom kombinasi
php artisan bpm:make:model Session --int --disable-soft-delete -m

Generated Model (Default):

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Concerns\HasUlids;

class Product extends Model
{
    use SoftDeletes, HasUlids;

    protected $fillable = [
        // Isi nama kolom yang bisa di mass-assign :D
    ];
}

Generated Model (Laravel 12 dengan Observer):

LARAVEL 12

Jika ada observer file, akan menggunakan modern #[ObservedBy] attribute.

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use App\Observers\ProductObserver;

#[ObservedBy([ProductObserver::class])]
class Product extends Model
{
    use SoftDeletes, HasUlids;

    protected $fillable = [
        // Isi nama kolom yang bisa di mass-assign :D
    ];
}

API Reference

Dokumentasi lengkap API tersedia di halaman terpisah untuk kemudahan navigasi dan performa halaman.

📖 Buka API Reference Lengkap →

Daftar Isi API:

Classes

Traits

  • HasCreator - Auto-fill creator_id

    • Automatic behavior saat model create
    • Relationships: creator(), creatorFull()
    • Scopes & utilities
  • HasEditor - Auto-fill latest_editor_id

    • Automatic behavior saat model update
    • Relationships: editor(), editorFull()
    • Scopes & utilities

Global Macros

  • constrainedFor() - ColumnDefinition macro
    • Model-aware constraint shortcut
    • Alternative untuk .constrained() dengan Model support

Examples

Advanced usage patterns dan integration scenarios untuk BPM Migration.

Contains:

1. Migration Class Pattern

Extend Migration class untuk cleaner syntax tanpa perlu import BpmSchema di setiap migration.

php
<?php

use Bpmlib\BpmMigration\Migration;

class CreateCategoriesTable extends Migration
{
    public function up(): void
    {
        $this->schema()->create('categories', function ($table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->actor();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        $this->schema()->dropIfExists('categories');
    }
}

Key Takeaways:

  • Tidak perlu import BpmSchema
  • Method $this->schema() otomatis menggunakan BpmBlueprint
  • Lebih clean untuk named migrations

2. Actor Tracking dengan Model Traits

Kombinasi migration + Model traits untuk auto-tracking creator dan editor.

Migration:

php
<?php

use Bpmlib\BpmMigration\BpmSchema;
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        BpmSchema::create('articles', function ($table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            
            // Actor tracking columns
            $table->actor();
            
            $table->timestamps();
        });
    }

    public function down(): void
    {
        BpmSchema::dropIfExists('articles');
    }
};

Model:

php
<?php

namespace App\Models;

use Bpmlib\BpmMigration\Model\Trait\{HasCreator, HasEditor};
use Illuminate\Database\Eloquent\{Model, SoftDeletes};
use Illuminate\Database\Eloquent\Concerns\HasUlids;

class Article extends Model
{
    use HasUlids, SoftDeletes, HasCreator, HasEditor;
    
    protected $fillable = ['title', 'content'];
}

Usage:

php
// Create - creator_id auto-filled
$article = Article::create([
    'title' => 'My Article',
    'content' => 'Content here...'
]);

// creator_id otomatis terisi dengan Auth::id()
echo $article->creator->name; // "John Doe"

// Update - latest_editor_id auto-filled
$article->update(['title' => 'Updated Title']);

// latest_editor_id otomatis terisi dengan Auth::id()
echo $article->editor->name; // "Jane Doe"

// Query by creator
$myArticles = Article::createdBy(Auth::id())->get();

// Query by editor
$editedByMe = Article::editedBy(Auth::id())->get();

Key Takeaways:

  • Trait auto-fill saat creating dan updating events
  • Relationship otomatis tersedia (creator, editor)
  • Query scopes untuk filtering (createdBy, editedBy)

3. Denormalized Actor Data

Denormalisasi data actor untuk menghindari joins atau remote calls di microservices.

Migration:

php
BpmSchema::create('products', function ($table) {
    $table->id();
    $table->string('name');
    
    // Actor IDs
    $table->creator(withForeignKey: false); // No FK untuk microservice
    $table->editor(withForeignKey: false);
    
    // Denormalized actor data
    $table->string('creator_name')->nullable();
    $table->string('creator_email')->nullable();
    $table->string('editor_name')->nullable();
    $table->string('editor_email')->nullable();
    
    $table->timestamps();
});

Model:

php
<?php

namespace App\Models;

use Bpmlib\BpmMigration\Model\Trait\{HasCreator, HasEditor};
use Illuminate\Database\Eloquent\{Model, SoftDeletes};
use Illuminate\Database\Eloquent\Concerns\HasUlids;

class Product extends Model
{
    use HasUlids, SoftDeletes, HasCreator, HasEditor;
    
    protected $fillable = ['name'];
    
    // Disable relationships (microservice mode)
    protected function hasCreatorRelation(): bool
    {
        return false;
    }
    
    protected function hasEditorRelation(): bool
    {
        return false;
    }
    
    // Fill denormalized creator data
    protected function fillCreatorDenormalized($user): void
    {
        $this->creator_name = $user->name;
        $this->creator_email = $user->email;
    }
    
    // Fill denormalized editor data
    protected function fillEditorDenormalized($user): void
    {
        $this->editor_name = $user->name;
        $this->editor_email = $user->email;
    }
}

Usage:

php
$product = Product::create(['name' => 'Product A']);

// Data otomatis terisi
echo $product->creator_name;  // "John Doe"
echo $product->creator_email; // "john@example.com"

$product->update(['name' => 'Product B']);

echo $product->editor_name;  // "Jane Doe"
echo $product->editor_email; // "jane@example.com"

Key Takeaways:

  • Cocok untuk microservices (no foreign keys)
  • Data user tersimpan langsung di table
  • Override fillCreatorDenormalized() dan fillEditorDenormalized()

4. Microservice Mode (Remote Users)

Konfigurasi untuk microservice architecture dimana user data ada di service lain.

Configuration (config/bpm.migration.php):

php
return [
    // Set mode ke 'remote' untuk microservice
    'actor_mode' => env('BPM_MIGRATION_ACTOR_MODE', 'remote'),
    
    'default_actor_select' => ['id', 'name', 'email'],
];

Migration:

php
BpmSchema::create('orders', function ($table) {
    $table->id();
    $table->string('order_number');
    
    // Tanpa foreign key constraint
    $table->actor(withForeignKey: false);
    
    // Denormalized user data
    $table->string('creator_name')->nullable();
    $table->string('creator_service')->nullable(); // Identify source service
    
    $table->timestamps();
});

Model:

php
<?php

namespace App\Models;

use Bpmlib\BpmMigration\Model\Trait\{HasCreator, HasEditor};
use Illuminate\Database\Eloquent\{Model, SoftDeletes};
use Illuminate\Database\Eloquent\Concerns\HasUlids;

class Order extends Model
{
    use HasUlids, SoftDeletes, HasCreator, HasEditor;
    
    // Disable relationships globally untuk semua models
    protected function hasCreatorRelation(): bool
    {
        return config('bpm.migration.actor_mode') !== 'remote';
    }
    
    protected function hasEditorRelation(): bool
    {
        return config('bpm.migration.actor_mode') !== 'remote';
    }
    
    // Store minimal user data
    protected function fillCreatorDenormalized($user): void
    {
        $this->creator_name = $user->name;
        $this->creator_service = config('app.name'); // Track source
    }
}

Key Takeaways:

  • Mode remote disable relationships by default
  • Denormalisasi wajib untuk menghindari cross-service joins
  • Track source service untuk debugging

5. BpmSchema dengan Model Support

FITUR BARU - v0.3.0

BpmSchema methods sekarang menerima Model instances atau class names untuk type-safe operations.

php
<?php

use App\Models\Product;
use Bpmlib\BpmMigration\BpmSchema;
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        // String table name (traditional)
        BpmSchema::create('products', function ($table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
        
        // Model class (v0.3.0+) - Type-safe
        BpmSchema::table(Product::class, function ($table) {
            $table->string('sku')->after('id');
        });
        
        // Model instance (v0.3.0+)
        $product = new Product();
        BpmSchema::table($product, function ($table) {
            $table->decimal('price', 10, 2);
        });
        
        // Introspection dengan Model
        if (BpmSchema::hasTable(Product::class)) {
            BpmSchema::table(Product::class, function ($table) {
                // Add index
            });
        }
        
        // Drop dengan Model
        // BpmSchema::drop(Product::class);
    }
};

Benefits:

  • Type-safe: IDE autocomplete
  • Refactoring-friendly: Rename class = rename table references
  • Dynamic table names: $product->getTable() runtime resolution

Key Takeaways:

  • Semua BpmSchema methods (except create()) support Model
  • Works dengan dynamic table names
  • Best practice untuk large projects

6. Foreign Key Auto-Detection

FITUR BARU - v0.4.0

Foreign key methods dengan auto-detection tipe ID dari Model traits.

php
<?php

use Bpmlib\BpmMigration\BpmSchema;
use App\Models\{Product, User, Category};
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        BpmSchema::create('order_items', function ($table) {
            $table->id();
            
            // Product uses HasUlids → Auto-creates ULID column
            $table->foreignTable(Product::class);
            // Equivalent: foreignUlid('product_id')->constrained('products')
            
            // User uses HasUuids → Auto-creates UUID column
            $table->foreignTable(User::class);
            // Equivalent: foreignUuid('user_id')->constrained('users')
            
            // Category uses default ID → Auto-creates BIGINT column
            $table->foreignTable(Category::class);
            // Equivalent: foreignId('category_id')->constrained('categories')
            
            // Nullable relationship
            $table->foreignTableNullable(Product::class, 'related_product_id');
            
            $table->integer('quantity');
            $table->decimal('unit_price', 10, 2);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        BpmSchema::dropIfExists('order_items');
    }
};

How Auto-Detection Works:

  1. Inspects Model's traits (class_uses_recursive())
  2. Checks for HasUlids → creates ULID column
  3. Checks for HasUuids → creates UUID column
  4. Default → creates BIGINT column

Key Takeaways:

  • Zero configuration needed
  • Type automatically matches Model's primary key
  • Prevents type mismatch errors
  • Column name auto-derived: singular(table_name)_id

7. Foreign Key Primitive vs Preset

FITUR BARU - v0.4.0

Dua kategori foreign key methods: Primitive (chainable) dan Preset (opinionated).

Primitive Methods (Chainable):

php
BpmSchema::create('posts', function ($table) {
    $table->id();
    
    // Primitive: Kolom saja, bisa dichain
    $table->foreignFor(User::class)           // Auto-detect type
        ->nullable()                          // ← Chainable
        ->constrained()                       // ← Chainable
        ->cascadeOnDelete();                  // ← Chainable
    
    $table->foreignForUlid(Category::class)   // Force ULID
        ->nullable()
        ->constrained('categories')
        ->cascadeOnUpdate();
    
    $table->timestamps();
});

Preset Methods (Opinionated):

php
BpmSchema::create('comments', function ($table) {
    $table->id();
    
    // Preset: Kolom + constraint langsung diterapkan
    $table->foreignTable(Post::class);
    // ✅ Kolom + constraint sekaligus
    // ❌ TIDAK bisa dichain dengan modifier
    
    $table->foreignTableNullable(User::class);
    // ✅ Kolom + constraint + nullable
    
    // Explicit type + custom column
    $table->foreignTableUlid('products', 'main_product_id');
    
    $table->timestamps();
});

When to Use:

Use CaseMethodExample
Need full controlPrimitiveforeignFor()->nullable()->onUpdate('cascade')
Standard relationshipPresetforeignTable(Product::class)
Quick prototypePresetforeignTableNullable(User::class)
Custom constraintPrimitiveforeignForId()->constrained()->restrictOnDelete()

Key Takeaways:

  • Primitive: Flexibility, chainable, no constraint by default
  • Preset: Speed, opinionated, constraint included
  • Use preset for 80% cases, primitive untuk edge cases

8. Complete Real-World Migration

Contoh comprehensive e-commerce migration menggunakan semua fitur BPM Migration.

php
<?php

use Bpmlib\BpmMigration\BpmSchema;
use App\Models\{User, Category, Brand};
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        // ========================================
        // Products Table
        // ========================================
        BpmSchema::create('products', function ($table) {
            // Primary key (ULID)
            $table->id();
            
            // Foreign keys dengan auto-detection
            $table->foreignTable(Category::class);            // ULID/UUID/BIGINT auto
            $table->foreignTableNullable(Brand::class);       // Optional brand
            $table->foreignTableNullable(User::class, 'vendor_id'); // Optional vendor
            
            // Product details
            $table->string('sku', 50)->unique();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table->text('specifications')->nullable();
            
            // Pricing
            $table->decimal('cost_price', 12, 2)->default(0);
            $table->decimal('selling_price', 12, 2);
            $table->decimal('discount_price', 12, 2)->nullable();
            
            // Inventory
            $table->integer('stock')->default(0);
            $table->integer('low_stock_threshold')->default(10);
            $table->string('stock_status')->default('in_stock');
            
            // Dimensions (nullable for digital products)
            $table->decimal('weight', 8, 2)->nullable();
            $table->decimal('length', 8, 2)->nullable();
            $table->decimal('width', 8, 2)->nullable();
            $table->decimal('height', 8, 2)->nullable();
            
            // SEO
            $table->string('meta_title')->nullable();
            $table->text('meta_description')->nullable();
            $table->json('meta_keywords')->nullable();
            
            // Status
            $table->string('status')->default('draft'); // draft, published, archived
            $table->boolean('is_featured')->default(false);
            $table->timestamp('published_at')->nullable();
            
            // Actor tracking
            $table->actor();
            
            // Timestamps dengan soft deletes
            $table->timestamps();
            
            // Indexes untuk performance
            $table->index(['status', 'published_at']);
            $table->index('stock_status');
            $table->index('slug');
            $table->index('is_featured');
        });
        
        // ========================================
        // Order Items (Integer ID untuk legacy compatibility)
        // ========================================
        BpmSchema::create('order_items', function ($table) {
            // Legacy system uses auto-increment
            $table->intId();
            
            // Foreign keys
            $table->foreignId('order_id')->constrained()->cascadeOnDelete();
            $table->foreignFor(Product::class)->constrained();  // Product uses ULID
            
            // Order item details
            $table->string('product_snapshot');  // Cached product name
            $table->integer('quantity');
            $table->decimal('unit_price', 10, 2);
            $table->decimal('discount', 10, 2)->default(0);
            $table->decimal('tax', 10, 2)->default(0);
            $table->decimal('total', 10, 2);
            
            // No soft deletes untuk line items
            $table->timestamps(withoutSoftDelete: true);
            
            $table->index('order_id');
        });
        
        // ========================================
        // Audit Logs (dengan backdoor timestamp)
        // ========================================
        BpmSchema::create('product_audits', function ($table) {
            $table->id();
            
            $table->foreignFor(Product::class)->constrained()->cascadeOnDelete();
            $table->foreignFor(User::class, 'actor_id')->nullable()->constrained('users');
            
            $table->string('action'); // created, updated, deleted, restored
            $table->json('old_values')->nullable();
            $table->json('new_values')->nullable();
            $table->string('ip_address')->nullable();
            $table->text('user_agent')->nullable();
            
            // Backdoor timestamp untuk bypass Eloquent events
            $table->timestamps(
                withoutSoftDelete: true,
                withBackdoor: true
            );
            
            $table->index(['product_id', 'created_at']);
            $table->index('action');
        });
    }
    
    public function down(): void
    {
        BpmSchema::dropIfExists('product_audits');
        BpmSchema::dropIfExists('order_items');
        BpmSchema::dropIfExists('products');
    }
};

Key Takeaways:

  • Combines multiple features: ULID, foreign keys, actor tracking
  • Shows real-world patterns: SKU, slug, SEO fields, pricing
  • Demonstrates when to use intId() vs id()
  • Shows timestamps() variations
  • Proper indexes untuk performance
  • Mix of chainable and preset foreign keys

Configuration

Configuration File

Publish configuration file (opsional):

bash
php artisan vendor:publish --tag=bpm-migration-config

Configuration file akan dibuat di config/bpm.migration.php:

php
<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Actor Resolution Mode
    |--------------------------------------------------------------------------
    |
    | Defines the DEFAULT behavior for creator/editor relationships.
    | Models may override this behavior explicitly.
    |
    | Supported modes:
    | - local  : User table exists in this service (monolith)
    | - remote : User data lives in another service (microservice)
    | - hybrid : Mixed usage; models decide individually
    |
    */

    'actor_mode' => env('BPM_MIGRATION_ACTOR_MODE', 'hybrid'),

    /*
    |--------------------------------------------------------------------------
    | Default Actor Columns
    |--------------------------------------------------------------------------
    |
    | Fallback columns used when selecting creator/editor relationships,
    | if the User model does not define its own defaults.
    |
    */

    'default_actor_select' => ['id', 'name', 'email'],

];

Available Options

OptionTypeDefaultDescription
actor_modestring'hybrid'Mode untuk actor relationships (local, remote, hybrid)
default_actor_selectarray['id', 'name', 'email']Kolom default untuk select relationships

Environment Variables

bash
# .env
BPM_MIGRATION_ACTOR_MODE=local

Runtime Configuration

Configuration dapat diakses di runtime:

php
$mode = config('bpm.migration.actor_mode');

// Model dapat check mode
if (config('bpm.migration.actor_mode') === 'remote') {
    // Disable relationships
}

Laravel Integration

Service Provider

Service provider otomatis terdaftar melalui package auto-discovery.

Manual Registration (jika diperlukan):

php
// config/app.php

'providers' => [
    // ...
    Bpmlib\BpmMigration\BpmMigrationServiceProvider::class,
],

Published Assets

Configuration:

bash
php artisan vendor:publish --tag=bpm-migration-config

File akan dipublish ke: config/bpm.migration.php

Global Macro

Service provider register global macro constrainedFor() pada ColumnDefinition:

php
// Registered automatically
ColumnDefinition::macro('constrainedFor', function (Model|string $related) {
    // Implementation...
});

// Usage
$table->foreignUlid('product_id')->constrainedFor(Product::class);

Lihat API Reference - constrainedFor() untuk detail lengkap.