BPM Migration
Laravel Migration extension dengan custom Blueprint untuk kolom ULID, actor tracking (creator/editor), dan soft deletes otomatis
Versi: 0.4.1 Changelog
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:
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
php artisan bpm:make:migration # Generate BPM migration
php artisan bpm:make:model # Generate BPM model + migrationKonfigurasi:
config/bpm.migration.php- Lihat konfigurasi lengkap
Instalasi
Install melalui Composer:
composer require bpmlib/bpm-migrationLibrary ini menggunakan auto-discovery untuk Laravel 5.5+. Service provider akan otomatis terdaftar.
Publish konfigurasi (opsional):
php artisan vendor:publish --tag=bpm-migration-configRequirements
Library ini memerlukan:
PHP Version:
PHP ^8.1Composer Dependencies:
| Package | Version | Description |
|---|---|---|
illuminate/console | ^10.0|^11.0|^12.0 | Laravel Console |
illuminate/container | ^10.0|^11.0|^12.0 | Laravel Container |
illuminate/database | ^10.0|^11.0|^12.0 | Laravel Database |
illuminate/support | ^10.0|^11.0|^12.0 | Laravel Support |
Framework Requirements:
| Framework | Version |
|---|---|
| Laravel | ^10.0 || ^11.0 || ^12.0 |
Quick Start
Basic Migration dengan BpmSchema
Contoh paling sederhana menggunakan BpmSchema:
<?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 deletesactor()→ creator + editor tracking
Comprehensive Example
Full-featured migration menggunakan semua BpmBlueprint methods:
<?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
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:
// ❌ 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:
BpmSchemamendapatkan schema builder dari Laravel- Mengganti blueprint resolver untuk menggunakan
BpmBlueprint - Semua method
create(),table(), dll menggunakan blueprint custom ini - Compatible dengan Laravel 11 dan Laravel 12 (berbeda signature)
Implikasi:
// 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.
$table->id(); // Membuat kolom 'id' dengan type ULIDEquivalent dengan:
$table->ulid('id')->primary();Benefits:
- Globally unique
- Sortable by creation time
- Tidak sequential (lebih aman)
- Cocok untuk distributed systems
Jika masih perlu integer:
$table->intId(); // Menggunakan bigIncrementsActor Tracking System
Automatic tracking:
Library ini otomatis tracking user yang membuat (creator) dan terakhir edit (editor) data melalui Model Traits.
Tiga mode operasi:
| Mode | Deskripsi | Use Case |
|---|---|---|
local | User table ada di service ini | Monolith app |
remote | User data di service lain | Microservice |
hybrid | Mixed, model tentukan sendiri | Transisi/mixed architecture |
Behavior:
- Creating: Trait
HasCreatorsetcreator_idotomatis saat model dibuat - Updating: Trait
HasEditorsetlatest_editor_idotomatis saat model diupdate - Relationship: Opsional, tergantung mode (
local/hybridsupport relationship) - Denormalization: Support denormalized fields (override method
fillCreatorDenormalized)
Konfigurasi default di config/bpm.migration.php:
'actor_mode' => env('BPM_MIGRATION_ACTOR_MODE', 'hybrid'),Model bisa override:
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:
$table->timestamps();
// Membuat: created_at, updated_at, deleted_at (nullable)Jika tidak ingin soft deletes:
$table->timestamps(withoutSoftDelete: true);
// Membuat: created_at, updated_at sajaBackdoor timestamp (opsional):
$table->timestamps(withBackdoor: true);
// Membuat: created_at, updated_at, deleted_at, backdoor_updated_atField 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:
php artisan bpm:make:migration create_products_table
# → Auto-includes: id() (ULID), timestamps() (soft deletes)Generate model + migration:
php artisan bpm:make:model Product -m
# → Auto-includes: HasUlids, SoftDeletes traitsbpm: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:
php artisan bpm:make:migration {name} [options]Options:
| Option | Shortcut | Description |
|---|---|---|
--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 | -i | Gunakan intId() (BIGINT) instead of ULID |
--disable-soft-delete | -d | Disable soft deletes di timestamps() |
Auto-Detection:
Command secara otomatis mendeteksi operasi CREATE vs ALTER:
# 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=productsStub Selection:
| Operasi | Options | Stub File |
|---|---|---|
| CREATE | Default | create/migration.ulid-soft.stub |
| CREATE | --int | create/migration.int-soft.stub |
| CREATE | --disable-soft-delete | create/migration.ulid-nosoft.stub |
| CREATE | --int --disable-soft-delete | create/migration.int-nosoft.stub |
| ALTER | Any | alter/migration.alter.stub |
CATATAN
Options --int dan --disable-soft-delete diabaikan untuk ALTER migrations (dengan warning).
Contoh Penggunaan:
# 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=productsGenerated File (CREATE):
<?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
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:
php artisan bpm:make:model {name} [options]Options:
| Option | Shortcut | Description |
|---|---|---|
--migration | -m | Buat migration file juga |
--force | -f | Overwrite jika file sudah ada |
--int | -i | Gunakan integer ID (skip HasUlids trait) |
--disable-soft-delete | -d | Skip SoftDeletes trait |
Contoh Penggunaan:
# 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 -mGenerated Model (Default):
<?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
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
BpmSchema - Schema wrapper untuk BpmBlueprint
- extend() - Get schema builder dengan custom blueprint
- create() - Buat tabel baru
- table() - Modifikasi tabel existing
- drop() - Hapus tabel
- Model support methods - Terima Model instances/classes
Migration - Base migration class
- schema() - Get BpmSchema builder
BpmBlueprint - Custom Blueprint dengan helper methods
- Primary Key Methods -
id(),intId() - Actor Methods -
creator(),editor(),actor() - Timestamp Methods -
timestamps() - Foreign Key Methods ⭐ NEW v0.4.0
- Parameter Support Matrix - Referensi lengkap parameter
- Primitive Methods (Chainable)
- Preset Methods (Opinionated)
- Primary Key Methods -
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
- 2. Actor Tracking dengan Model Traits
- 3. Denormalized Actor Data
- 4. Microservice Mode (Remote Users)
- 5. BpmSchema dengan Model Support ⭐ v0.3.0
- 6. Foreign Key Auto-Detection ⭐ v0.4.0
- 7. Foreign Key Primitive vs Preset ⭐ v0.4.0
- 8. Complete Real-World Migration
1. Migration Class Pattern
Extend Migration class untuk cleaner syntax tanpa perlu import BpmSchema di setiap migration.
<?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
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
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:
// 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
creatingdanupdatingevents - 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:
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
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:
$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()danfillEditorDenormalized()
4. Microservice Mode (Remote Users)
Konfigurasi untuk microservice architecture dimana user data ada di service lain.
Configuration (config/bpm.migration.php):
return [
// Set mode ke 'remote' untuk microservice
'actor_mode' => env('BPM_MIGRATION_ACTOR_MODE', 'remote'),
'default_actor_select' => ['id', 'name', 'email'],
];Migration:
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
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
remotedisable 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
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
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:
- Inspects Model's traits (
class_uses_recursive()) - Checks for
HasUlids→ creates ULID column - Checks for
HasUuids→ creates UUID column - 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):
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):
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 Case | Method | Example |
|---|---|---|
| Need full control | Primitive | foreignFor()->nullable()->onUpdate('cascade') |
| Standard relationship | Preset | foreignTable(Product::class) |
| Quick prototype | Preset | foreignTableNullable(User::class) |
| Custom constraint | Primitive | foreignForId()->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
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()vsid() - Shows
timestamps()variations - Proper indexes untuk performance
- Mix of chainable and preset foreign keys
Configuration
Configuration File
Publish configuration file (opsional):
php artisan vendor:publish --tag=bpm-migration-configConfiguration file akan dibuat di config/bpm.migration.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
| Option | Type | Default | Description |
|---|---|---|---|
actor_mode | string | 'hybrid' | Mode untuk actor relationships (local, remote, hybrid) |
default_actor_select | array | ['id', 'name', 'email'] | Kolom default untuk select relationships |
Environment Variables
# .env
BPM_MIGRATION_ACTOR_MODE=localRuntime Configuration
Configuration dapat diakses di runtime:
$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):
// config/app.php
'providers' => [
// ...
Bpmlib\BpmMigration\BpmMigrationServiceProvider::class,
],Published Assets
Configuration:
php artisan vendor:publish --tag=bpm-migration-configFile akan dipublish ke: config/bpm.migration.php
Global Macro
Service provider register global macro constrainedFor() pada ColumnDefinition:
// 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.