Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Make Migration Pivot Laravel Package

josezenem/laravel-make-migration-pivot

A Laravel package that adds an Artisan command for generating pivot table migrations between two models, simplifying many-to-many setup with correctly named foreign keys and timestamps while reducing boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package as a dev dependency (it only adds an Artisan command, so nothing needs to ship to production):

composer require --dev josezenem/laravel-make-migration-pivot

The package auto-registers its service provider. Confirm the command is available and check its exact signature before scripting it:

php artisan list | grep -i pivot

The command mirrors the old Laravel 4 generators convention: pass two model class names and it writes a new closure-style migration using foreignIdFor() and a composite primary key. For example, to wire up a many-to-many between User and Role:

php artisan make:migration:pivot User Role

This produces something like database/migrations/xxxx_xx_xx_xxxxxx_create_role_user_table.php:

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('role_user', function (Blueprint $table) {
            $table->foreignIdFor(Role::class)->constrained()->cascadeOnDelete();
            $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete();
            $table->primary(['role_id', 'user_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('role_user');
    }
};

First use case: you're starting a feature with a many-to-many relationship and don't want to hand-type the pivot schema, guess the conventional table name, or remember the composite-key incantation. The command gives you a correct starting point in one line, then you edit the file as needed.

Look first at:

  1. The generated migration — verify column order, FK ordering, and table name before running migrate.
  2. The package README for the exact command name and any options (--table, --no-constraints, etc., if supported).
  3. database/migrations — check the timestamp is after the two referenced tables' migrations.

Implementation Patterns

Feature-driven pivot creation. The most common daily use is generating a pivot alongside a new feature branch. Create the two parent migrations first, then:

php artisan make:migration:pivot Post Tag
php artisan migrate

Because both parent tables already exist, the constrained() foreign keys will resolve cleanly.

Extending the generated stub by hand. The generated file is a normal migration — treat it as a template. The command gives you the correct convention; you own the rest:

Schema::create('post_tag', function (Blueprint $table) {
    $table->foreignIdFor(Post::class)->constrained()->cascadeOnDelete();
    $table->foreignIdFor(Tag::class)->constrained()->cascadeOnDelete();

    $table->unsignedInteger('order')->default(0);
    $table->timestamps();                    // add if using withTimestamps()

    $table->primary(['post_id', 'tag_id']);
    $table->index(['tag_id', 'post_id']);     // reverse lookup index
});

Then mirror it on the relationship:

public function tags(): BelongsToMany
{
    return $this->belongsToMany(Tag::class)
        ->withPivot('order')
        ->withTimestamps()
        ->orderByPivot('order');
}

Namespaced models. If models live in subnamespaces (App\Models\Billing\Invoice), pass the full class path — the command should resolve the class to derive the table name and foreignIdFor() target. If it doesn't, fall back to generating the file and editing foreignIdFor() calls manually.

Overriding the pivot table name. Laravel's convention is the singular model names in alphabetical order joined by _ (e.g. role_user, post_tag). If you need a non-conventional name (legacy DB, cross-schema), generate with the command and change the string passed to Schema::create(), then pass the same name as the second argument to belongsToMany() on both models.

Seeding and factories. Because the pivot has a composite key, you can't use a Pivot model with a single id in factory chains. Seed via the relationship instead:

User::factory()
    ->hasAttached(Role::factory()->count(2), ['assigned_at' => now()])
    ->create();

Rollback safety in CI. Keep down() as generated (dropIfExists) so repeated migrate:fresh/migrate:rollback in CI stays idempotent. Add the pivot migrations to the same deploy step as their parents — never split them across releases.

Gotchas and Tips

Class resolution requires autoloadable models. The command reads the class to infer table names and FK types. If you pass a class that isn't autoloaded (typo, wrong namespace, model not yet created), you'll get an error or a mis-named table. Create the models first, then generate the pivot.

Migration ordering matters. constrained() runs at migration time, not generation time. If you generate the pivot before the parent migrations, the file gets a later timestamp — usually fine — but if you shuffle files or rename them, FKs can fail with "table does not exist." Keep the pivot migration timestamp after both parents.

Alphabetical table naming is baked in. For User + Role you get role_user, not user_role. If you call belongsToMany() without a table name, Laravel expects the same alphabetical order — so the generated name matches conventions. Only override when you have a reason, and override on both sides of the relationship.

Custom key types break foreignIdFor(). If either model uses UUIDs or ULIDs (HasUuids / HasUlids), foreignIdFor() will emit a bigint — wrong. After generating, replace the column:

$table->foreignUuid('user_id')->constrained()->cascadeOnDelete();
$table->foreignUlid('post_id')->constrained()->cascadeOnDelete();

Composite primary key vs. Eloquent. The generated $table->primary([...]) is correct SQL, but Eloquent models don't natively support composite keys. Don't try to make a Pivot model with two $primaryKeys — use belongsToMany() with withPivot() and insert via the relationship, or add a surrogate id() if you truly need a first-class model.

Deleting a parent won't cascade automatically on MySQL unless you explicitly chain ->cascadeOnDelete() (the stub uses it, but double-check after editing). Conversely, don't add cascade if you want to preserve the pivot for auditing — switch to ->restrictOnDelete().

Timestamps require opting in twice. $table->timestamps() in the migration isn't enough — you also need ->withTimestamps() on the relationship, or Laravel will insert null and DB defaults will silently take over.

Timestamps in the filename can collide. Generating two pivots in the same second can produce identical migration names. If you're scripting pivot creation (e.g., in a project scaffold), add a short sleep or check database/migrations before generating.

The command is a scaffolding aid, not a runtime dependency. You can safely composer remove --dev it after generating if it causes any friction in CI, since the generated migrations are standalone. Keep it in require-dev for ongoing feature work.

Extension point — publish and edit the stub (if the package supports vendor:publish for stubs). If your team has conventions (tenant IDs, soft-deletes on pivots, audit columns), publish the stub once and every generated pivot inherits them. Run php artisan vendor:publish --provider="Josezenem\MakeMigrationPivot\..." and check the README for the exact tag.

Debugging failed migrations: run php artisan migrate:status, then php artisan migrate --pretend on the specific pivot to inspect the emitted SQL, and check SHOW CREATE TABLE <pivot> after running to confirm the composite primary key and FK actions landed as intended.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core