In many real-world Laravel applications (especially SaaS, multi-tenant systems, or reusable packages), you may want to achieve the following:
- All tables to be prefixed dynamically.
- The prefix to come from a configuration file.
- The same package to be installed in multiple apps with different prefixes.
- Continue using the standard Artisan command:
php artisan make:migration.
The Problem
By default, Laravel migrations are static. When you run the generation command, it creates a file with hardcoded table names:
Schema::create('users', ...)
The Motivation: Drop-in Installation
When you ship a professional package, you want it to be a true drop-in install. You want to avoid forcing your users to manually rename tables or edit vendor migrations, which inevitably leads to broken upgrades. The goal is a package that adapts itself perfectly based on configuration only.
The Goal
We want to implement a system where:
- Add a CLI flag like
--pkg. - When present:
- Laravel uses custom migration stubs.
- Those stubs generate migrations using
config('pkg_config.db_prefix').
- No breaking changes: Standard Laravel behavior remains untouched.
- No core rewrites: Avoid overriding base Artisan commands.
The Strategy
Our approach involves:
- Adding a custom CLI argument (
--pkg). - Stripping the argument before Symfony parses it in
artisan. - Setting a runtime flag.
- Overriding Laravel’s
MigrationCreatorin a Service Provider. - Pointing the creator to custom stub files.
This works seamlessly in Laravel 11/12 and follows framework internals correctly.
Step 1: Create Custom Migration Stubs
First, publish all Laravel stubs to your project’s base path:
php artisan stub:publish
This creates a /stubs folder. For our use case, we will modify migration.create.stub.
The Migration Create Stub
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Resolve the table name with a runtime prefix.
*/
protected function table(string $name): string
{
$prefix = config('pkg_config.db_prefix', '');
return $prefix ? "{$prefix}_{$name}" : $name;
}
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create($this->table('{{ table }}'), function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists($this->table('{{ table }}'));
}
};
You can also update migration.update.stub and migration.stub. Laravel will select the appropriate one automatically.
Step 2: Add CLI Flag Handling in artisan
Open the root artisan file and add the following logic before the application boots:
if (PHP_SAPI === 'cli') {
$argv = $_SERVER['argv'] ?? [];
$key = array_search('--pkg', $argv, true);
if ($key !== false) {
// Remove the flag so Symfony Console doesn't error on unknown flags
unset($argv[$key]);
$_SERVER['argv'] = array_values($argv);
// Pass flag into Laravel runtime environment
$_SERVER['PKG_MIGRATION_STUBS'] = '1';
}
}
Automating the Injection
You can use the following bash script to inject this logic automatically. It will ask for the flag name and update your artisan file safely.
# See setup-artisan-flag.sh in this folder
Step 3: Override MigrationCreator in Service Provider
In your AppServiceProvider or a dedicated package provider:
use Illuminate\Database\Migrations\MigrationCreator;
public function register(): void
{
if (! $this->app->runningInConsole()) {
return;
}
if (($_SERVER['PKG_MIGRATION_STUBS'] ?? null) !== '1') {
return;
}
$this->app->extend(MigrationCreator::class, function ($creator, $app) {
return new MigrationCreator(
$app['files'],
base_path('stubs') // Use your custom stubs directory
);
});
}
Step 4: Configuration
Create a configuration file:
Config: pkg_config.php
<?php
return [
'db_prefix' => env('PKG_DB_PREFIX', 'tenant'),
];
Now your migrations are fully runtime-configurable.
Usage
Default Laravel Behavior (Unchanged)
php artisan make:migration create_users_table --create=users
Generates:
Schema::create('users', ...)
Package-Prefixed Behavior
php artisan make:migration create_users_table --create=users --pkg
Generates:
Schema::create($this->table('users'), ...)
Why This Approach is Powerful
| Feature | Benefit |
|---|---|
| CLI Flag | Purely opt-in behavior. |
| Custom Stubs | Clean implementation without core hacks. |
| Runtime Config | Perfect for multi-tenant and reusable packages. |
| Future-Proof | Designed for Laravel 11/12. |
Important Warning ⚠️
Using config() inside migrations makes the output dependent on your runtime configuration. This is safe only if:
- The prefix is set once at installation.
- It never changes thereafter.
Otherwise, different environments may end up with mismatching schemas. This pattern is ideal for reusable packages, not for dynamic live production schemas.
Final Thoughts
By leveraging Laravel’s internal service container and customizing stubs, we’ve built a migration system that is both powerful and developer-friendly. This approach doesn't just solve the prefixing problem; it demonstrates a core philosophy of Laravel: the framework is built to be extended, not hacked.
You now have a solution that:
- Respects Framework Defaults: Keeps standard migrations untouched.
- Empowers Package Users: Provides a seamless, configuration-first installation.
- Scales with Your Project: Handles complex multi-tenant or SaaS architectures with ease.
Whether you're building the next great open-source package or a complex internal enterprise system, this pattern ensures your database layer remains flexible, maintainable, and—most importantly—future-proof. 💎
Just remember:
php artisan make:migration --pkg
It’s not just customization; it’s building the Laravel way.