Skip to content

Create a custom generator

Create a generator when your team repeatedly writes the same class shape. This example adds make:report to vendor/bin/foundation using a stub, a command class, and the project’s tooling provider.

Install the CLI using the Foundation CLI installation guide. This example assumes the project’s Composer autoload.psr-4 mapping is Plugin\\src/. If you are new to providers, see Register Service Providers.

Create foundation/stubs/report/report.stub:

<?php declare(strict_types=1);

namespace {{ namespace }};

/**
 * Describe this report's purpose.
 */
final class {{ class }} {

	// Add the report's application dependencies and behavior here.
}

Create src/Tooling/Commands/Report_Command.php:

<?php declare(strict_types=1);

namespace Plugin\Tooling\Commands;

use StellarWP\Foundation\Cli\GeneratorCommand;

/**
 * Generate report classes from the project's report stub.
 */
final class Report_Command extends GeneratorCommand {

	public const string CONFIG_KEY = 'report';
	public const string NAME = 'make:' . self::CONFIG_KEY;
	public const string DEFAULT_NAMESPACE = 'Reports';

	/**
	 * Select the project's report template.
	 */
	protected function stub(): string {
		return 'foundation/stubs/report/report.stub';
	}
}

NAME identifies the console command. CONFIG_KEY selects its project settings, and DEFAULT_NAMESPACE is a suffix relative to the application’s Composer namespace: Reports produces Plugin\\Reports. The stub path may be project-relative or absolute.

The base supplies the name argument, --namespace, --path, PHP validation, and file creation. Keep its inherited constructor; define the constants and stub() in your generator.

Create src/Tooling/Tooling_Provider.php:

<?php declare(strict_types=1);

namespace Plugin\Tooling;

use Plugin\Tooling\Commands\Report_Command;
use StellarWP\Foundation\Cli\CliProvider;
use StellarWP\Foundation\Container\Contracts\Provider;
use StellarWP\Foundation\Container\Contracts\Resolver as C;

/**
 * Register the project's developer commands.
 */
final class Tooling_Provider extends Provider {

	private bool $registered = false;

	/**
	 * Wire the project's commands before the console application is resolved.
	 */
	public function register(): void {
		if ( $this->registered ) {
			return;
		}

		$this->container->mergeArrayVar( CliProvider::COMMANDS, static fn ( C $c ): array => [
			$c->get( Report_Command::class ),
		] );

		$this->registered = true;
	}
}

Foundation automatically loads this provider when its executable runs. Add future commands to the same collection and keep their dependency bindings in this provider. Command names and aliases must be unique; startup reports conflicting commands rather than replacing one.

Run from the project root:

vendor/bin/foundation make:report Sales_Report

This creates src/Reports/Sales_Report.php in Plugin\\Reports. List commands or inspect the generator’s options with:

vendor/bin/foundation list
vendor/bin/foundation help make:report

Generation refuses to overwrite existing files. Edit the existing class or choose another name. If a class name, namespace, or stub is invalid, correct the reported input and retry.

Add the following setting to the project’s optional root config.php, alongside existing application settings:

<?php declare(strict_types=1);

return [
	'generators' => [
		'report' => [
			'namespace' => 'Plugin\\Exports',
		],
	],
];

The same command now creates classes in src/Exports/. Foundation loads this shared configuration once at startup. See Configure the Container for environment values and configuration shared with the application.

vendor/bin/foundation make:report Sales_Report --namespace='Plugin\Reports\Sales' --path=src/Reports/Sales

--namespace takes precedence over project configuration. Without --path, the directory follows the most specific runtime Composer PSR-4 mapping. An explicit path selects the output directory for that invocation; keep Composer’s mapping consistent with the generated namespace.

See Project tooling to select a custom provider file, keep tooling under development autoloading, or load prerequisite package providers.

Commands that perform other tasks can extend Symfony’s Command and contribute to the same CliProvider::COMMANDS collection. Supply their constructor dependencies using the normal provider binding APIs. Use an ordinary Symfony command for specialized generation workflows that need additional arguments, coordinated files, or source editing.

Install the CLI with --dev. Generated application classes belong in normal Composer autoload mappings, and any packages they use belong in require. Exclude local tooling and stubs from production archives through the project’s .gitattributes when appropriate.

Run the generator in a disposable project using its normal Composer mapping. Check the default output location, a configured namespace, and an explicit override. Run the same command twice and confirm the second attempt fails while preserving the first file.

For automated command tests, use Symfony’s CommandTester with the command resolved through a tooling container. Test vendor/bin/foundation in a subprocess to verify provider lookup, configuration loading, and command registration together.