Skip to content
LibxaFrame
Tutorials 12 August 2026 8 min read

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.

Keep reading