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 migrateDeclaring 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.stubTimestamps 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.phpRepublishing 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 --forceYour 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 itThe 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.phpBecause 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 cryptoBy 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.