skip to content

For an in-house Laravel audit-trail package used by three apps, should its migrations be loaded with loadMigrationsFrom() or published, and why?

level: seniorimportance: should knowfreq 26%

answer

  1. load: run straight from vendor/
  2. publish: copied into database/migrations
  3. publishesMigrations renames the timestamps
  4. who owns later schema changes
  5. consistency versus local control

basics

~20 s

loadMigrationsFrom() runs the package's migrations straight from vendor/, so all three apps get new schema changes with each package update; publishing copies them into each app, which can then edit them but must re-publish to get later ones.

solid answer

~40 s

`loadMigrationsFrom(__DIR__.'/../database/migrations')` adds the package directory to the migrator's paths, so `php artisan migrate` runs the package's migrations alongside the app's without copying anything; a package update that adds a migration reaches every app on the next deploy. `publishesMigrations([...], 'audit-trail-migrations')` makes them publishable instead: `vendor:publish` copies them into `database/migrations` and rewrites their date prefix to the publish time, and from then on the app owns them. For a package whose schema must be identical across three apps, loading is usually the better default: one source of truth and no forgotten re-publish. Publishing fits when apps must change the schema (a different table name, extra indexes) or when each team wants every migration reviewed in its own repository. Whichever you choose, never change a migration after release; add a new one.

code

php · 24 lines
php
<?php

namespace Acme\AuditTrail;

use Illuminate\Support\ServiceProvider;

class AuditTrailServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/audit-trail.php', 'audit-trail');
    }

    public function boot(): void
    {
        $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'audit-trail');
        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');

        $this->publishes([
            __DIR__.'/../config/audit-trail.php' => config_path('audit-trail.php'),
        ], 'audit-trail-config');
    }
}

go deeper

for a junior

Know that a package's migrations either run directly from the package or are copied into database/migrations with vendor:publish.

for a middle

Explain loadMigrationsFrom adding a migrator path, and publishesMigrations copying files with new timestamps.

for a senior

Choose per package: loading for schema that must match across apps, publishing for app-owned schema, and guard against running both.

for a principal

Set the ownership model for shared schema across teams: who reviews changes, how upgrades are rolled out, and how divergence is prevented.

## The two mechanisms A Laravel package can ship database migrations in two ways, both configured in its service provider's `boot()` method. **Loading** registers a path with the migrator: ```php $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); ``` In source, the method waits until the `migrator` service is resolved and calls its `path()` method for each directory. `php artisan migrate` then looks in the app's `database/migrations` **and** the package directory, sorts all pending files by name and runs them. Nothing is copied. **Publishing** registers the files as copyable: ```php $this->publishesMigrations([ __DIR__.'/../database/migrations' => database_path('migrations'), ], 'audit-trail-migrations'); ``` `php artisan vendor:publish --tag=audit-trail-migrations` copies them into the app. When `database.migrations.update_date_on_publish` is enabled (it is in the skeleton's config), each copied file gets a new date prefix based on the publish time, so it sorts after the app's existing migrations. The package directory is **not** added to the migrator; only the copies run. ## Comparing them | Question | `loadMigrationsFrom()` | Publishing | |---|---|---| | Where the file lives | `vendor/acme/audit-trail/...` | the app's `database/migrations` | | New migration in a package update | runs on the next `migrate` | only after the app publishes again | | Can the app edit it | no (edits in `vendor/` are lost) | yes | | Reviewed in the app's repository | no | yes | | Schema identical across apps | by construction | only if every app publishes every release | | Rollback | runs from the package path | runs from the app's copy | ## Choosing for the audit-trail package Three internal apps share the package. The deciding questions: 1. **Must the schema be the same everywhere?** An audit log that the security team queries across all three apps benefits from one definition. Loading guarantees it. 2. **Will apps customise the tables?** If one app needs a different table name or extra columns, publishing gives it that freedom, but the package's code must then read the table name from config. 3. **Who reviews schema changes?** With loading, schema changes are reviewed in the package's repository and arrive through a version bump. With publishing, each app reviews its own copies. 4. **How disciplined are upgrades?** Publishing adds a manual step to every package release; a missed re-publish leaves one app's schema behind the code that expects it. A common compromise: **load by default**, and also register a publish tag for apps that need to customise. An app that publishes should then stop the package from also loading the same migrations, typically via a config flag the provider checks before calling `loadMigrationsFrom()`; otherwise the migrator would see two files that create the same table. ## Testing the choice Whichever way the package ships migrations, test it the way the apps will use it: 1. In the package's own test suite, boot a test application that registers the provider and run `migrate`, so a loaded migration that fails on the target database is caught before release. 2. In each consuming app, the normal test setup runs all migrations, including loaded package ones, so the app's tests exercise the package's tables automatically. 3. For published migrations, the app's tests run the copies; a package test that passes says nothing about an app whose copy is out of date. ## Rules that apply either way - **Never edit a released migration.** Apps that already ran it will not run it again; ship a new migration for every change. - **Name migrations uniquely.** The migrator tracks migrations by file name in the `migrations` table, so package migration names must not collide with app ones. - **Keep them runnable on every supported database** the apps use. - **Document the tag** in the package's README, even when loading is the default. ## What interviewers want to hear - The mechanical difference: a migrator path versus copied files with new timestamps. - The ownership consequence: package-owned schema versus app-owned schema. - A reasoned choice for the scenario, with the double-run pitfall of doing both.

  • What goes wrong if a package both loads its migrations and an app has published copies of them?
    The migrator sees two different files that create the same tables: the package original and the renamed copy. Whichever runs second fails because the table already exists. Packages that offer both usually let apps turn loading off with a config flag once they publish.
  • Why does publishesMigrations() rename the files it copies?
    Migrations run in file-name order, and a package's file dates are from when the package was written. Renaming to the publish time (when `update_date_on_publish` is enabled, as in the skeleton) makes the copies run after the app's existing migrations, which is usually what their foreign keys need.

saying these in an interview costs you the question

  • loadMigrationsFrom copies the migrations into database/migrations
  • Published migrations update themselves when the package is updated
  • An app can safely edit a loaded migration inside vendor/
  • Loading and publishing the same migrations together is harmless
  • Fix a released package migration by editing it in place