Skip to content

Installer Extension Contracts

Use installer-facing contracts when a package needs setup choices, install-time parameters, package selection, settings migrations, or cleanup behavior during a Capell install. Keep feature behavior in the package; the installer should orchestrate package-owned Actions rather than duplicate them.

Installer extension contract flow

Declare installer-visible metadata in capell.json:

  • providers.install for providers that must load before the package is enabled.
  • dependencies.requires for packages that must be installed first.
  • dependencies.supports for support packages the installer may add when applicable.
  • visibility: support for support packages that should not appear as standalone catalogue choices.
  • product.group and product.tier so installer grouping matches Marketplace and docs.

There is no install root key. manifest-version: 3 validates against a strict allow-list, and an unrecognised root field throws InvalidManifestException — “Capell manifest has invalid field install: is not part of manifest v3” — so the whole package fails to load.

Install-time values are declared alongside the legacy command that consumes them, as commands.installParams, commands.setupParams, commands.afterInstallParams, or commands.demoParams — each a string list.

Lifecycle Actions receive their values differently: the runner passes an array<string, mixed> $arguments into handle(), supplied by whatever triggers the lifecycle rather than declared in the manifest. An Action-only package should read what it needs from $arguments, fall back to its own config defaults, and never require a manifest-declared param list.

Settings migrations belong in database/settings/ and are published by Capell, not by your lifecycle Action: declare "database": { "settings": true } and InstallPackageAction publishes the directory with edit protection and then runs the settings migrator. Guard each migration with an existence check so reruns are safe. See Settings migration.

Installer work should be package-owned:

  • Use providers.install for install-only service providers and commands.
  • Put install, setup, and after-install writes in Actions.
  • Accept forwarded values through structured Data objects where the setup has more than a couple of scalar parameters.
  • Report progress through the install reporter passed to the package Action or command.
  • Clear package discovery with php artisan capell:package-cache:clear after manifest changes.

Do not publish host resources or schemas to customize installer behavior. Use the manifest, package lifecycle Actions, and package-owned commands.

A package’s install, setup, uninstall, and after-install work implements one interface:

namespace Capell\Core\Contracts;
interface PackageLifecycleAction
{
/**
* @param array<string, mixed> $arguments
*/
public function handle(
PackageData $package,
array $arguments = [],
?ProgressReporter $reporter = null,
): void;
}

Declare the class-string in capell.json under actions.install, actions.setup, actions.uninstall, or actions.afterInstall. PackageLifecycleRunner prefers an Action over a legacy commands entry, and a web-triggered install refuses a legacy command outright — see Lifecycle: actions and commands.

PackageLifecycleRunner throws when the declared class-string does not exist (“Install lifecycle action X for Y does not exist.”) or exists but does not implement PackageLifecycleAction. Both fail the install rather than being skipped, and capell:extension-audit does not check either — cover the wiring with a test.

The third argument is the reporter. It is the full interface — there is nothing else on it:

namespace Capell\Core\Contracts;
interface ProgressReporter
{
public function step(string $label): void;
public function report(string $line): void;
public function error(string $line): void;
}
  • step() names the unit of work now starting. The browser installer renders these as progress items.
  • report() records an informational line.
  • error() records a failure line. It does not throw or abort — throw from the Action when the install must stop.

The reporter is nullable because a lifecycle Action can run in contexts with nowhere to report. Always call it null-safely ($reporter?->step(...)) rather than requiring an instance.

Capell selects the implementation for the context: ConsoleProgressReporter for CLI installs, CacheProgressReporter and FileLogProgressReporter for web installs (these add markRunning(), markComplete(), and markFailed(), and the file logger exposes logPath()), and NullProgressReporter where output is discarded. Write against the interface only; do not type-hint a concrete reporter.

Anything passed to the reporter can surface in install output and support bundles. Never report credentials, tokens, or customer data.

Test the package in three layers:

  • Manifest/boot tests prove capell.json is valid and install providers load.
  • Action tests prove setup writes the expected state and handles missing optional inputs.
  • Command or installer-flow tests prove forwarded params reach the package Action without re-testing all Action internals.

Run the smallest package check first, then a host integration check when touching installer contracts:

Terminal window
vendor/bin/pest packages/<package>/tests --configuration=phpunit.xml
vendor/bin/pest packages/installer/tests --configuration=phpunit.xml