Writing a LibxaFrame package
What the framework actually looks for: extra.Libxa.providers in composer.json, commands auto-discovered from src/Console/Commands, and the ServiceProvider helpers that work. Written while building two.
What the framework actually looks for when you install a package, written while building two of them and finding out the hard way.
The manifest
Discovery reads extra.Libxa.providers from your composer.json:
{
"name": "you/your-package",
"require": {
"php": "^8.3",
"libxa/framework": "^0.10.2"
},
"autoload": {
"psr-4": { "You\\YourPackage\\": "src/" }
},
"extra": {
"Libxa": {
"providers": [
"You\\YourPackage\\YourServiceProvider"
]
}
}
}
Anything under vendor/libxa/ or vendor/libxaframe/ is scanned at boot.
Packages under other vendor names are read from the manifest that
php libxa package:discover writes.
The provider
use Libxa\Container\ServiceProvider;
final class YourServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(__DIR__ . '/../config/yours.php', 'yours');
$this->app->singleton('yours.thing', fn ($app) => new Thing(/* ... */));
}
public function boot(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
}
}
Bind lazily and construct nothing in register(). A package that reads
configuration or opens a connection while providers are still being registered
runs before the application is assembled, and the failure looks like a
framework bug rather than yours.
Available on the base class: loadRoutesFrom, loadViewsFrom,
loadMigrationsFrom, loadTranslationsFrom, mergeConfigFrom, publishes.
Commands are found by directory
Anything in src/Console/Commands is instantiated and registered
automatically. No list to maintain, and no registration call:
final class YourCommand extends Command
{
protected static $defaultName = 'yours:do-something';
public function __construct(protected Application $app)
{
parent::__construct();
}
}
The constructor signature matters: the discovery code calls
new $className($this->app), so a command taking anything else fails at
startup.
Migrations need 0.10.2
loadMigrationsFrom() did nothing at all before that version. It is guarded by
a check for a migrator binding that nothing ever created, so it silently
returned, always. If your package ships a migration, require ^0.10.2 or the
table is never created and nothing anywhere explains why.
That is its own story.
Work unconfigured
The best thing you can do for adoption is make composer require produce
something that already runs.
Libxa Secure needs encryption keys. Rather than refusing to boot without them,
it falls back to APP_KEY, which every application already has. Rotation is
then available when it is wanted rather than being a precondition for anything
at all.
A package that will not start until you have read its configuration reference is a package people uninstall before they have seen what it does.
Say what is quietly wrong
Some settings can be wrong without producing an error, and those are worth a command of their own.
Secure's threat counters need a cache shared between processes. Without one they count per process, so with four workers a threshold of ten is really forty. Nothing fails. The limit simply is not the limit.
php libxa secure:status
Every line that command prints exists because the setting it reports can be wrong invisibly. That is a good filter for what belongs in one.
Test against a real application
Unit tests will not catch a wrong composer.json, a provider that is never
registered, a command whose constructor does not match, or a migration path
nobody reads.
Install your package into a real generated project with a path repository:
composer config repositories.yours path ../your-package
composer require "you/your-package:@dev" -W
php libxa list
Everything interesting found while building these two was found this way, and none of it by a unit test.