NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Package Toolkit for Laravel

Publish-only migrations

A package built on the toolkit never auto-loads its migrations. The host publishes them and runs the migrator:

php artisan vendor:publish --tag=comments-migrations
php artisan migrate

Declaring migrations

hasMigrations() publishes every database/migrations/*.php file. hasMigration() adds a single database/migrations/<name>.php.stub source — use it only for .php.stub files, because .php files are already picked up by hasMigrations(). Both publish under <name>-migrations:

$package
    ->name('comments')
    ->hasMigrations()                                  // every database/migrations/*.php
    ->hasMigration('create_comment_reactions_table');  // database/migrations/create_comment_reactions_table.php.stub

Timestamps and order

Each file is published to database/migrations/<Y_m_d_His>_<name>.php, so it orders against the host’s own migrations. When a package ships several migrations, their timestamps step forward one second per file in the package directory’s sort order — migrations that depend on each other’s tables still run in the right sequence. .php.stub migrations declared with hasMigration() follow the .php files, in declaration order. Any timestamp the package prefixed its source file with is replaced by the publish timestamp:

package: database/migrations/
    2020_01_01_000000_create_comments_table.php
    create_comment_votes_table.php
    create_comment_reactions_table.php.stub

host, after vendor:publish --tag=comments-migrations: database/migrations/
    2026_07_15_101500_create_comments_table.php
    2026_07_15_101501_create_comment_votes_table.php
    2026_07_15_101502_create_comment_reactions_table.php

Republishing is safe

The destination resolver reuses the file a migration was already published to, whatever timestamp it carries. A forced republish therefore overwrites in place instead of dropping a second, differently timestamped copy of the same Schema::create():

php artisan vendor:publish --tag=comments-migrations --force

Your own migrations are never overwritten

A file only counts as the earlier copy when it has the same name and the package source’s contents — whitespace differences such as line endings are ignored. A same-named migration with different contents — your app’s own create_comments_table, another package’s, or a copy you edited after publishing — is never overwritten, not even with --force. The package’s migration is published beside it under a fresh timestamp, and you decide which one to keep:

host, before: database/migrations/
    2025_03_02_090000_create_comments_table.php   your app's own — different contents

host, after vendor:publish --tag=comments-migrations --force:
    2025_03_02_090000_create_comments_table.php   untouched
    2026_07_15_101500_create_comments_table.php   the package's, published beside it

The MigrationPublisher helper

The naming and destination logic is also available on its own, as RoundlyConsulting\PackageToolkit\Support\MigrationPublisher. destination() takes the package source file, so it can compare contents before reusing a host file:

use Illuminate\Support\Carbon;
use RoundlyConsulting\PackageToolkit\Support\MigrationPublisher;

MigrationPublisher::nameFor('2020_01_01_000000_create_comments_table.php');
// 'create_comments_table' — extension and any timestamp prefix stripped

MigrationPublisher::nameFor('create_comment_reactions_table.php.stub');
// 'create_comment_reactions_table'

MigrationPublisher::destination(
    __DIR__.'/../database/migrations/create_comments_table.php',   // the package source file
    database_path('migrations'),
    Carbon::now(),
);
// the file it was already published to (same name, same contents),
// or …/database/migrations/<Y_m_d_His>_create_comments_table.php

Because nothing is auto-loaded, a package’s own test suite must run its migrations explicitly — see Testing.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.