Package Development

Introduction

Packages are the primary way of adding functionality to PHP-Framework. Some packages are stand-alone and work with any PHP framework via your composer.json file, while others are specifically intended for use with PHP-Framework.

Note: You must always use global helper functions (e.g., app(), config(), response()) or autowiring when building packages.

Packages should include copyable asset directories for consumers to easily publish manually:

my-org/my-package/
├── src/
│   ├── MyPackageProvider.php
│   └── Services/
├── config/
│   └── my-package.php
├── resources/
│   ├── views/
│   │   ├── invoice.template.php
│   │   └── mail/
│   │       └── welcome.template.php
│   └── lang/
│       ├── en/
│       │   └── messages.php
│       └── es/
│           └── messages.php
├── composer.json
└── README.md

Service Providers

Service providers are the connection point between your package and PHP-Framework. A service provider is responsible for binding things into the framework’s service container (which directly manages its own explicit $availableBindings map) and informing PHP-Framework where to load package resources.

A service provider extends the MacropaySolutions\Kernel\Support\ServiceProvider class and contains two methods: register and boot.

What Works vs. What Does NOT Work

Because the framework is heavily optimized for zero-overhead boot times, dynamic asset registration methods have been removed.

What Works (Do This): Only use the register() method with container bindings:

public function register(): void
{
    $this->app->singleton(InvoiceService::class, function () {
        return new InvoiceService(config('my-package'));
    });
}

What Does NOT Work (MISSING Methods):

  • loadViewsFrom() — Use manual file copying instead.
  • loadTranslationsFrom() — Use manual file copying instead.
  • loadJsonTranslationsFrom() — Use manual file copying instead.
  • loadMigrationsFrom() — Use manual file copying instead.
  • publishes() — Use manual file copying instead.

Registering Your Package Provider

For consumers to use your package, they must explicitly register your service provider in their application. This is done in bootstrap/app.php:

$app->register(\Vendor\Package\PackageServiceProvider::class);

This ensures full control over package initialization and avoids automatic discovery that could introduce performance overhead or unwanted side effects.

If your service provider’s boot method is empty, it can be registered as a deferred provider analog to \MacropaySolutions\Kernel\Mail\MailServiceProvider. See \App\Application::registerMailBindings. Alternatively, the bindings can be manually registered into \App\Application::registerExplicitBindingsMap.

Autowiring Discovery

While service providers must be manually registered, PHP-Framework does support autodiscovery for autowiring configuration. To avoid runtime reflection and improve performance, packages can automatically append their classes to the autowiring:cache command by declaring them in their composer.json file:

{
    "extra": {
        "php-framework": {
            "autowiring": [
                {
                    "path": "src/ExampleFolder",
                    "methods": []
                },
                {
                    "path": "\\Vendor\\ExampleClass",
                    "methods": []
                }
            ]
        }
    }
}

When users deploy their application and run php run autowiring:cache, the framework will read these paths and cache the reflection data for all specified constructors and methods. See app.autowirings config for more details.

Resources

Configuration

Package configuration files should be stored in a config directory within your package. To make these available to consumers, they must call $app->configure('package_config') in bootstrap/app.php:

if (!$app->configurationIsCached()) {
    $app->configure('app');
    $app->configure('crufd_wizard');
    $app->configure('package_config');
}

The consumer can then manually copy the package’s configuration file to their application’s config directory.

Warning: Configuration files must not contain closures, as they will break the config:cache command. All configuration values must be static arrays, strings, integers, or other non-callable values.

Routes

If your package contains routes, you may load them using the loadRoutesFrom method. This method will automatically determine if the application’s routes are cached and will not load your routes file if the routes have already been cached:

public function boot(): void
{
    $this->loadRoutesFrom(__DIR__ . '/../routes/api.php');
}

Strict Architectural Constraint: Route Closures are completely forbidden by the internal engine backed by fast-route and will throw a RuntimeException. All routes defined in your package MUST point to a Controller class method. Using an absolute namespace is recommended to prevent namespace grouping collisions:

// routes/api.php
$router->post('/package/action', '\Vendor\Package\Http\Controllers\PackageController@action');

Migrations

Because loadMigrationsFrom() does NOT exist in ServiceProvider, package migrations must reside in the application’s database/migrations directory.

Add the migration folder mapping to your package’s composer.json:

"extra": {
    "publish-assets": {
        "your-path/migrations": "database/migrations"
    }
}

When consumers install or update your package, Composer automatically places the migrations into their database/migrations directory without overwriting existing files.

Manual Installation Alternative

If consumers do not use the asset publisher plugin, instruct them in your README.md to copy the migration files manually:

cp -r vendor/my-vendor/my-package/your-path/migrations/* database/migrations/

Views & Language Files

Because loadViewsFrom(), loadTranslationsFrom(), and double-colon namespace syntax (namespace::view) do NOT exist in PHP-Framework, view templates and language files must reside in the application space.

Instruct consumers to publish these via publish-assets:

"extra": {
    "publish-assets": {
        "resources/views": "resources/views/vendor/my-package",
        "resources/lang": "resources/lang"
    }
}

View files in the application space are then rendered using standard dot-notation:

return view('vendor.my-package.invoice', $data);

Commands

To register your package’s console commands, you may use the commands method in your service provider’s boot method when the application is running in console mode:

use Vendor\Package\Console\Commands\InstallCommand;
use Vendor\Package\Console\Commands\NetworkCommand;

public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->commands([
            InstallCommand::class,
            NetworkCommand::class,
        ]);
    }
}

NOTE: Please register these commands in your composer.json for the autowiring cache to optimize their execution.

Manual Asset Installation

Because PHP-Framework is tailored for maximum API performance, if your package requires configuration files, public assets, database migrations, or HTML view templates (such as pagination or email layouts) to be moved into the consuming application, you must document the manual copy commands directly in your README.md so consumers can add them to their installation steps or CI/CD build scripts.

Automated Asset Installation (Composer Alternative)

While documenting manual copy commands works, the recommended approach is to utilize the framework’s built-in Composer asset publisher plugin. The macropay-solutions/php-kernel-dev dependency ships with a Composer plugin that completely bypasses the HTTP boot sequence while automating asset deployment.

In your package’s composer.json, instruct the plugin which files to publish by defining the publish-assets configuration within the extra block:

{
    "extra": {
        "publish-assets": {
            "config/my-package.php": "config/my-package.php",
            "database/migrations": "database/migrations",
            "resources/views": "resources/views/vendor/my-package",
            "resources/lang": "resources/lang",
            "public": "public/vendor/my-package"
        }
    }
}

The keys are the original package local path and the values are the destination path.

NOTE When consumers install or update your package, Composer will automatically synchronize these files into their application space without overwriting any existing files. If you need them republished, simply delete the local copies and run composer reinstall <vendor>/<package>. Consumers will still need to manually register the service provider.


This site uses Just the Docs, a documentation theme for Jekyll.