Composer packages

Plugins vs providers

KindHooks into Pionia boot?Example
PluginNo — plain PHP libraryValidator, HTTP client, DTO mapper
ProviderYes — middleware, auth, routes, commandsacme/pionia-billing

Plugins are normal Composer packages. Require them and use them from services.

Providers extend Pionia\Base\Provider\Provider and register capabilities during application boot. See App providers.

Minimal plugin

composer.json in your package:

{
  "name": "acme/phone-normalizer",
  "require": { "php": ">=8.5" },
  "autoload": { "psr-4": { "Acme\\Phone\\": "src/" } }
}

src/Normalizer.php — no Pionia imports required:

namespace Acme\Phone;

final class Normalizer
{
    public static function e164(string $raw): string
    {
        return preg_replace('/\D/', '', $raw) ?? '';
    }
}

Use from a service:

use Acme\Phone\Normalizer;

protected function registerAction(\Pionia\Collections\Arrayable $data): \Pionia\Http\Response\ApiResponse
{
    $phone = Normalizer::e164((string) $data->get('phone'));

    return response(0, 'OK', ['phone' => $phone]);
}

Package with a provider

Structure:

acme-pionia-billing/
  composer.json
  src/
    BillingProvider.php
    BillingSwitch.php
    Middleware/BillingContextMiddleware.php
    Commands/SyncInvoicesCommand.php

BillingProvider.php

namespace Acme\Billing;

use Pionia\Base\Provider\Provider;
use Pionia\Http\Routing\PioniaRouter;
use Pionia\Middlewares\MiddlewareChain;

class BillingProvider extends Provider
{
    public function middlewares(MiddlewareChain $chain): MiddlewareChain
    {
        return $chain->add(Middleware\BillingContextMiddleware::class);
    }

    public function commands(): array
    {
        return ['billing:sync' => Commands\SyncInvoicesCommand::class];
    }

    public function routes(PioniaRouter $router): PioniaRouter
    {
        // Use a unique version slug — not "v2" unless you own that API surface
        return $router->switch(BillingSwitch::class, 'billing');
    }
}

CLI command in the package — extend Pionia\Console\Command:

namespace Acme\Billing\Commands;

use Pionia\Console\Command;

class SyncInvoicesCommand extends Command
{
    protected string $name = 'billing:sync';
    protected string $description = 'Pull invoices from the billing API';

    protected function handle(): int
    {
        $this->info('Syncing…');

        return Command::SUCCESS;
    }
}

Input helpers live under Pionia\Console\Input\ (InputArgument, InputOption) — same concepts as other PHP CLIs, but native to Pionia.

Consumer app wiring

After composer require acme/pionia-billing:

; environment/settings.ini
[app_providers]
billing=Acme\Billing\BillingProvider

Or in bootstrap/application.php:

$app = AppRealm::create(__DIR__);
$app->web()->addAppProvider(\Acme\Billing\BillingProvider::class);
return $app;

Clear cache when removing a provider:

php pionia cache:clear

Package development loop

  1. Create a local app with composer create-project pionia/pionia-app sandbox.
  2. Add a path repository to composer.json:
"repositories": [
  { "type": "path", "url": "../acme-pionia-billing", "options": { "symlink": true } }
],
"require": {
  "acme/pionia-billing": "@dev"
}
  1. Run composer update acme/pionia-billing and register the provider.
  2. Hit /api/billing/ (or your chosen version) and php pionia billing:sync.

Checklist before Packagist

  • Provider FQCN documented in README
  • Unique API version string in routes()
  • No hard dependency on the consumer app’s Application\ namespace
  • Optional RoadRunner / Redis features declared in suggest, not require
  • @moonlight-* tags on public actions if you ship HTTP API docs