A package could not ship a migration, and nothing said so
loadMigrationsFrom() was guarded by a check for a binding nothing ever created, so it silently did nothing. Every package following the documented API shipped a migration that could never run. Found by building one.
Libxa Secure ships a migration for its audit table. It never ran, and nothing anywhere said why.
That turned out to be a framework bug affecting every package author, and it had been there the whole time because nobody had written a package with a migration before.
The symptom
composer require libxa/secure
php libxa migrate
Running migrations...
Nothing to migrate.
The table did not exist. secure:status reported it as not migrated, which was
at least honest, but migrate had nothing to say at all: not an error, not a
warning, not a mention of the package.
Three things had to line up
One. The documented way for a package to register migrations is
loadMigrationsFrom() in its service provider, which the framework provides:
protected function loadMigrationsFrom(string $path): void
{
if ($this->app->has('migrator')) {
$this->app->make('migrator')->addPath($path);
}
}
Nothing ever bound migrator. The guard was always false, so the method
silently did nothing, always, for everyone. A package could call the documented
API correctly and get no behaviour and no complaint.
Guards like that are how a method becomes decorative. The condition is there to be defensive, the thing it defends against turns out to be permanent, and the whole body is dead code that looks alive.
Two. Even with the binding in place, the migrate command built its own:
$migrator = new Migrator();
$migrator->addPath($this->app->basePath('src/database/migrations'));
A fresh instance, so anything a service provider had registered during boot was discarded before the command ever looked at it. Two independent reasons for the same nothing.
Three. addPath() appended unconditionally. Once several places
contributed paths, which is the whole point of the fix, a repeated path meant
running every migration in it twice.
The fix
migrator is now a shared binding created by the database provider, with the
application's own path already added. The command resolves it instead of
constructing one. addPath() ignores a path it already holds.
Released as 0.10.2, and libxa/secure requires ^0.10.2 for exactly this
reason: on anything earlier its table is silently never created and the package
looks broken for a reason that is not in its repository.
Why it survived
Nothing in the framework's own test suite ships a migration from a package, because until now no package did. The application's migrations come from the default path, which always worked. Modules were scanned explicitly, which also worked. The one path nobody exercised was the one the documentation recommended.
That is the ordinary shape of this kind of bug: not complicated, not subtle, just never run. It took writing a real package to find, which is a decent argument for building on your own framework before telling other people to.