Built-in Recipes
The PHPNomad CLI ships with 16 built-in recipes covering common scaffolding tasks. These range from single-file recipes like listeners and facades, to multi-file recipes like datastores, to composite recipes like database-datastore that stack multiple recipes together to scaffold entire features from one command.
Built-in recipes are referenced by name. When you pass --from=listener, the scaffolder looks for listener.json in its bundled Recipes/ directory. You do not need a file path for these.
The recipes are organized into three categories:
- Single-artifact recipes create one PHP file each: listener, event, command, controller, facade, task, task-handler, mutation, table, graphql-type, initializer, model, model-adapter.
- Multi-file recipes create several related files: datastore, database-handler.
- Composite recipes stack other recipes together: database-datastore.
listener
Creates an event listener class that implements CanHandle and registers it in an initializer's getListeners() method.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Listener class name (e.g. SendWelcomeEmail) |
event |
string | FQCN of the event class to listen to |
initializer |
string | FQCN of the initializer to register in |
Command
phpnomad make --from=listener '{"name":"SendWelcomeEmail","event":"App\\Events\\UserCreated","initializer":"App\\AppInit"}'
Generated file
Output path: lib/Listeners/SendWelcomeEmail.php
<?php
namespace App\Listeners;
use PHPNomad\Events\Interfaces\CanHandle;
use PHPNomad\Events\Interfaces\Event;
use App\Events\UserCreated;
class SendWelcomeEmail implements CanHandle
{
public function __construct(
// TODO: Add constructor dependencies
) {
}
public function handle(Event $event): void
{
// TODO: Implement listener logic
}
}
The {{namespace}} token is resolved automatically from the output path and your project's PSR-4 config. The {{event}} token produces the use statement for the event class, so the event's short name is available in the file.
Registration
The scaffolder opens the initializer class (e.g. App\AppInit) and adds a map entry to getListeners():
UserCreated::class => SendWelcomeEmail::class
The registration type is map, meaning the event class is the key and the listener class is the value. If the event key already exists with a single listener, the mutator wraps the existing value and the new one into an array, matching PHPNomad's convention for multiple listeners on the same event.
The registration targets the HasListeners interface (PHPNomad\Events\Interfaces\HasListeners). If the initializer does not yet implement HasListeners, the scaffolder adds it automatically.
Output
Recipe: listener
Creates an event listener and registers it in an initializer
Created: lib/Listeners/SendWelcomeEmail.php
Registered: getListeners() in App\AppInit
Done: 1 file(s) created, 1 registration(s) performed.
event
Creates an event class that implements Event with a static getId() method. Events do not require registration because they are referenced directly by listeners.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Event class name (e.g. UserCreated) |
eventId |
string | Unique event identifier (e.g. user.created) |
Command
phpnomad make --from=event '{"name":"UserCreated","eventId":"user.created"}'
Generated file
Output path: lib/Events/UserCreated.php
<?php
namespace App\Events;
use PHPNomad\Events\Interfaces\Event;
class UserCreated implements Event
{
public function __construct(
// TODO: Add event properties
) {
}
public static function getId(): string
{
return 'user.created';
}
}
The constructor is left as a TODO. Events in PHPNomad typically carry data as public readonly properties passed through the constructor, so you fill this in with the fields your listeners need.
Registration
None. Events are passive data carriers. They get referenced by class name in listener registrations and dispatched through the event system at runtime.
Output
Recipe: event
Creates an event class with a unique ID
Created: lib/Events/UserCreated.php
Done: 1 file(s) created, 0 registration(s) performed.
command
Creates a CLI command class that implements Command and registers it in an initializer's getCommands() method.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Command class name (e.g. DeployCommand) |
signature |
string | Command signature string (e.g. deploy {env:Target environment}) |
description |
string | Short description for help output |
initializer |
string | FQCN of the initializer to register in |
Command
phpnomad make --from=command '{"name":"DeployCommand","signature":"deploy {env:Target environment} {--force:Force deployment}","description":"Deploy to environment","initializer":"App\\AppInit"}'
Generated file
Output path: lib/Commands/DeployCommand.php
<?php
namespace App\Commands;
use PHPNomad\Console\Interfaces\Command;
use PHPNomad\Console\Interfaces\Input;
use PHPNomad\Console\Interfaces\OutputStrategy;
class DeployCommand implements Command
{
public function __construct(
protected OutputStrategy $output
) {
}
public function getSignature(): string
{
return 'deploy {env:Target environment} {--force:Force deployment}';
}
public function getDescription(): string
{
return 'Deploy to environment';
}
public function handle(Input $input): int
{
// TODO: Implement command logic
return 0;
}
}
The generated command class injects OutputStrategy through the constructor, giving you access to $this->output for writing to the console. The handle() method receives an Input instance you can use to read arguments and options parsed from the signature.
Registration
The scaffolder opens the initializer class and adds a list entry to getCommands():
DeployCommand::class
The registration type is list, meaning the command class is appended as a simple array item. The registration targets the HasCommands interface (PHPNomad\Console\Interfaces\HasCommands). If the initializer does not yet implement HasCommands, the scaffolder adds it automatically.
Output
Recipe: command
Creates a CLI command and registers it in an initializer
Created: lib/Commands/DeployCommand.php
Registered: getCommands() in App\AppInit
Done: 1 file(s) created, 1 registration(s) performed.
controller
Creates a REST controller class and registers it in an initializer's getControllers() method.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Controller class name (e.g. GetUsers) |
method |
string | HTTP method (GET, POST, PUT, DELETE, PATCH) |
endpoint |
string | Endpoint path (e.g. /users) |
initializer |
string | FQCN of the initializer to register in |
Command
phpnomad make --from=controller '{"name":"GetUsers","method":"GET","endpoint":"/users","initializer":"App\\AppInit"}'
Generated file
Output path: lib/Rest/GetUsers.php
<?php
namespace App\Rest;
class GetUsers
{
public function __construct(
// TODO: Add constructor dependencies
) {
}
public function getMethod(): string
{
return 'GET';
}
public function getEndpoint(): string
{
return '/users';
}
public function handle()
{
// TODO: Implement controller logic
}
}
The generated controller is a lightweight skeleton. Unlike the full Controller interface from phpnomad/rest (which uses Request/Response contracts and the Method enum), the scaffolded controller provides the basic structure with plain string returns for getMethod() and getEndpoint(). You can refine the class to implement the full Controller interface and inject Response as needed for your project.
Registration
The scaffolder opens the initializer class and adds a list entry to getControllers():
GetUsers::class
The registration type is list. The registration targets the HasControllers interface (PHPNomad\Rest\Interfaces\HasControllers). If the initializer does not yet implement HasControllers, the scaffolder adds it automatically.
Output
Recipe: controller
Creates a REST controller and registers it in an initializer
Created: lib/Rest/GetUsers.php
Registered: getControllers() in App\AppInit
Done: 1 file(s) created, 1 registration(s) performed.
facade
Creates a facade class that extends PHPNomad\Facade\Abstracts\Facade and registers it in an initializer's getFacades() method. Facades provide a static interface to services resolved from the container.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Facade class name (e.g. PaymentFacade) |
interface |
string | FQCN of the interface to proxy |
initializer |
string | FQCN of the initializer to register in |
Command
phpnomad make --from=facade '{"name":"PaymentFacade","interface":"App\\Services\\PaymentService","initializer":"App\\AppInit"}'
Generated file
Output path: lib/Facades/PaymentFacade.php
<?php
namespace App\Facades;
use PHPNomad\Facade\Abstracts\Facade;
use App\Services\PaymentService;
class PaymentFacade extends Facade
{
protected static function getAbstraction(): string
{
return PaymentService::class;
}
}
The {{interface}} variable produces a use statement and is referenced by its short name in the getAbstraction() method.
Registration
The scaffolder opens the initializer class and adds a list entry to getFacades():
PaymentFacade::class
The registration type is list. The registration targets the HasFacades interface. If the initializer does not yet implement HasFacades, the scaffolder adds it automatically.
task
Creates a task class implementing Task with getId(), toPayload(), and fromPayload() methods. Tasks are data objects dispatched through the task system. They do not require registration.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Task class name (e.g. SendEmailTask) |
taskId |
string | Unique task identifier (e.g. send.email) |
Command
phpnomad make --from=task '{"name":"SendEmailTask","taskId":"send.email"}'
Registration
None. Tasks are referenced by class name when dispatched and when registering their handlers.
task-handler
Creates a handler class implementing CanHandleTask and registers it in an initializer's getTaskHandlers() method.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Handler class name (e.g. HandleSendEmail) |
task |
string | FQCN of the task class to handle |
initializer |
string | FQCN of the initializer to register in |
Command
phpnomad make --from=task-handler '{"name":"HandleSendEmail","task":"App\\Tasks\\SendEmailTask","initializer":"App\\AppInit"}'
Registration
The scaffolder opens the initializer class and adds a map entry to getTaskHandlers():
SendEmailTask::class => HandleSendEmail::class
The registration type is map, with the task class as the key and the handler class as the value. The registration targets the HasTaskHandlers interface. If the initializer does not yet implement HasTaskHandlers, the scaffolder adds it automatically.
mutation
Creates a mutation handler class that uses the CanMutateFromAdapter trait and registers it in an initializer's getMutations() method. Mutations handle data transformations identified by a plain string action key rather than a class reference.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Mutation class name (e.g. CreateUserMutation) |
action |
string | Action string key (e.g. create_user) |
initializer |
string | FQCN of the initializer to register in |
Command
phpnomad make --from=mutation '{"name":"CreateUserMutation","action":"create_user","initializer":"App\\AppInit"}'
Registration
The scaffolder opens the initializer class and adds a map entry to getMutations():
'create_user' => CreateUserMutation::class
The registration type is map, but the key is a plain string (the action) rather than a ::class reference. The registration targets the HasMutations interface. If the initializer does not yet implement HasMutations, the scaffolder adds it automatically.
table
Creates a table class extending Table with getUnprefixedName() and getColumns() methods. Tables define the schema for database-backed datastores. They do not require registration.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Table class name (e.g. PayoutsTable) |
tableName |
string | Database table name (e.g. payouts) |
Command
phpnomad make --from=table '{"name":"PayoutsTable","tableName":"payouts"}'
Registration
None. Table classes are referenced directly by database handlers that use them.
graphql-type
Creates a GraphQL type definition class implementing TypeDefinition with getSdl() and getResolvers() methods, then registers it in an initializer's getTypeDefinitions() method.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Type definition class name (e.g. UserType) |
typeName |
string | GraphQL type name as it appears in the schema (e.g. User) |
initializer |
string | FQCN of the initializer to register in |
Command
phpnomad make --from=graphql-type '{"name":"UserType","typeName":"User","initializer":"App\\AppInit"}'
Registration
The scaffolder opens the initializer class and adds a list entry to getTypeDefinitions():
UserType::class
The registration type is list. The registration targets the HasTypeDefinitions interface. If the initializer does not yet implement HasTypeDefinitions, the scaffolder adds it automatically.
initializer
Creates an initializer class that uses HasClassDefinitions and CanSetContainer. Initializers are the central wiring point for PHPNomad applications. This recipe creates the class itself. It does not register anything, because initializers are typically referenced from the application bootstrap, not from other initializers.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Initializer class name (e.g. AppInit) |
Command
phpnomad make --from=initializer '{"name":"AppInit"}'
Registration
None. Initializers are loaded by the application bootstrap, not registered in other initializers.
model
Creates a data model class that uses the HasSingleIntIdentity trait. Models are plain data objects that represent a single record from a datastore. They do not require registration.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Model class name (e.g. Payout) |
Command
phpnomad make --from=model '{"name":"Payout"}'
Registration
None. Models are referenced by model adapters and datastores that work with them.
model-adapter
Creates a model adapter class with toModel() and toArray() methods for converting between raw data and model objects. Model adapters do not require registration.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Adapter class name (e.g. PayoutAdapter) |
model |
string | FQCN of the model class to adapt |
Command
phpnomad make --from=model-adapter '{"name":"PayoutAdapter","model":"App\\Models\\Payout"}'
Registration
None. Model adapters are referenced by database handlers and other components that need to convert data.
datastore
Creates three files: a datastore interface, a handler interface, and an implementation class with decorator traits. The implementation is registered in an initializer's getClassDefinitions() method, binding the datastore interface to its concrete implementation.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Datastore name in PascalCase (e.g. Payout) |
initializer |
string | FQCN of the initializer to register in |
Command
phpnomad make --from=datastore '{"name":"Payout","initializer":"App\\AppInit"}'
Generated files
The recipe creates three files:
| File | Description |
|---|---|
lib/Datastores/Payout/PayoutDatastore.php |
Datastore interface |
lib/Datastores/Payout/PayoutDatastoreHandler.php |
Handler interface |
lib/Datastores/Payout/PayoutDatastoreImpl.php |
Implementation with decorator traits |
<?php
namespace App\Datastores\Payout;
interface PayoutDatastore
{
// TODO: Define datastore methods
}
<?php
namespace App\Datastores\Payout;
interface PayoutDatastoreHandler
{
// TODO: Define handler methods
}
<?php
namespace App\Datastores\Payout;
use PHPNomad\Datastore\Traits\CanDecorate;
use PHPNomad\Datastore\Traits\CanFilter;
class PayoutDatastoreImpl implements PayoutDatastore
{
use CanDecorate;
use CanFilter;
public function __construct(
protected PayoutDatastoreHandler $handler
) {
}
// TODO: Implement datastore methods
}
Registration
The scaffolder opens the initializer class and adds a map entry to getClassDefinitions():
PayoutDatastore::class => PayoutDatastoreImpl::class
The registration type is map. The registration targets the HasClassDefinitions interface (PHPNomad\Di\Interfaces\HasClassDefinitions). If the initializer does not yet implement HasClassDefinitions, the scaffolder adds it automatically.
database-handler
Creates a database-specific handler class that extends IdentifiableDatabaseDatastoreHandler and registers it in an initializer's getClassDefinitions() method. This recipe is typically used alongside the datastore recipe to provide the database implementation for a datastore's handler interface.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Handler class name (e.g. PayoutDatabaseHandler) |
handlerInterface |
string | FQCN of the handler interface to implement |
model |
string | FQCN of the model class |
modelAdapter |
string | FQCN of the model adapter class |
table |
string | FQCN of the table class |
initializer |
string | FQCN of the initializer to register in |
Command
phpnomad make --from=database-handler '{"name":"PayoutDatabaseHandler","handlerInterface":"App\\Datastores\\Payout\\PayoutDatastoreHandler","model":"App\\Models\\Payout","modelAdapter":"App\\Adapters\\PayoutAdapter","table":"App\\Tables\\PayoutsTable","initializer":"App\\AppInit"}'
Registration
The scaffolder opens the initializer class and adds a map entry to getClassDefinitions():
PayoutDatastoreHandler::class => PayoutDatabaseHandler::class
The registration type is map. The registration targets the HasClassDefinitions interface. If the initializer does not yet implement HasClassDefinitions, the scaffolder adds it automatically.
database-datastore (composite)
This is a composite recipe that uses recipe stacking to scaffold an entire database-backed datastore from a single command. It stacks five recipes together: model, model-adapter, table, datastore, and database-handler. The result is seven files covering the full stack from model to database handler.
Required variables
| Variable | Type | Description |
|---|---|---|
name |
string | Feature name in PascalCase (e.g. Payout) |
tableName |
string | Database table name (e.g. payouts) |
initializer |
string | FQCN of the initializer to register in |
Command
phpnomad make --from=database-datastore '{"name":"Payout","tableName":"payouts","initializer":"App\\AppInit"}'
Generated files
The composite recipe creates seven files across its five child recipes:
| File | Source recipe |
|---|---|
lib/Models/Payout.php |
model |
lib/Adapters/PayoutAdapter.php |
model-adapter |
lib/Tables/PayoutsTable.php |
table |
lib/Datastores/Payout/PayoutDatastore.php |
datastore |
lib/Datastores/Payout/PayoutDatastoreHandler.php |
datastore |
lib/Datastores/Payout/PayoutDatastoreImpl.php |
datastore |
lib/Datastores/Payout/PayoutDatabaseHandler.php |
database-handler |
Registration
Two registrations are performed in the initializer's getClassDefinitions() method:
PayoutDatastore::class => PayoutDatastoreImpl::class,
PayoutDatastoreHandler::class => PayoutDatabaseHandler::class,
Both use the HasClassDefinitions interface.
How it works
The database-datastore recipe does not define its own files or registrations. Instead, it defines a recipes array that references the five child recipes and maps variables from the parent into each child. The child recipes execute sequentially, and each one creates its files and performs its registrations independently. See Recipe Stacking for how this mechanism works.
Recipe stacking
Recipe stacking lets a recipe reference other recipes instead of (or in addition to) defining its own files. A parent recipe declares a recipes array, and each entry names a child recipe and maps variables from the parent scope into the child's variable scope.
How it works
When the scaffolder encounters a recipes array in a recipe spec, it processes each child entry in order:
- The child recipe is loaded by name (the same lookup used for
--from). - The parent's variables are merged with any overrides specified in the child entry's
varsobject. - The
rootNamespacevariable is automatically computed from the project's PSR-4 config, giving child recipes the base namespace they need to construct FQCNs for cross-references. - The child recipe runs through the full pipeline: preflight validation, file generation, and registration.
- Execution continues with the next child.
This means composite recipes get all the same behavior as running each child recipe individually. Preflight validation, duplicate detection, and auto-registration all apply normally.
The rootNamespace variable
When recipes are stacked, child recipes often need to reference classes created by sibling recipes. For example, the database-handler recipe needs the FQCN of the model, model adapter, and table classes. The rootNamespace variable is auto-computed from the project's PSR-4 autoload configuration in composer.json and made available to all child recipes. A project with "App\\": "lib/" in its PSR-4 config would get rootNamespace set to App.
Example
The database-datastore composite recipe looks roughly like this:
{
"name": "database-datastore",
"description": "Creates a full database-backed datastore with model, adapter, table, and handlers",
"vars": {
"name": { "type": "string", "description": "Feature name in PascalCase" },
"tableName": { "type": "string", "description": "Database table name" },
"initializer": { "type": "string", "description": "FQCN of the initializer" }
},
"recipes": [
{ "recipe": "model", "vars": {} },
{ "recipe": "model-adapter", "vars": { "model": "{{rootNamespace}}\\Models\\{{name}}" } },
{ "recipe": "table", "vars": {} },
{ "recipe": "datastore", "vars": {} },
{
"recipe": "database-handler",
"vars": {
"handlerInterface": "{{rootNamespace}}\\Datastores\\{{name}}\\{{name}}DatastoreHandler",
"model": "{{rootNamespace}}\\Models\\{{name}}",
"modelAdapter": "{{rootNamespace}}\\Adapters\\{{name}}Adapter",
"table": "{{rootNamespace}}\\Tables\\{{name}}Table"
}
}
]
}
Running phpnomad make --from=database-datastore '{"name":"Payout","tableName":"payouts","initializer":"App\\AppInit"}' executes all five child recipes in sequence, producing seven files and two registrations from a single command.
Auto-registration behavior
All recipes that include a registrations section in their JSON spec trigger auto-registration in the target initializer file. The scaffolder uses nikic/php-parser to modify the AST, so the rest of your file (formatting, comments, whitespace) is preserved.
Three scenarios determine what happens when a registration is performed.
Method already exists
If the initializer already has the target method (e.g. getListeners()), the scaffolder finds the return statement, locates the array literal, and appends the new entry. For list type registrations, it appends a new ClassName::class item. For map type registrations, it appends a new KeyClass::class => ValueClass::class entry.
If the event key already exists in a map registration (e.g. you already have a listener for UserCreated), the mutator wraps both values into a nested array rather than creating a duplicate key.
Method does not exist
If the initializer does not have the target method at all, the scaffolder creates the entire method with the correct return array, adds the corresponding Has* interface (e.g. HasListeners, HasCommands, HasControllers) to the class's implements list, and inserts the use statement at the top of the file.
This means a freshly created initializer with no event handling can receive a listener registration, and the scaffolder will add everything it needs in one pass.
Duplicate detection
Before appending any entry, the scaffolder checks whether the exact same registration already exists in the return array. For list registrations, it compares ::class constant fetches by FQCN. For map registrations, it compares both the key and value FQCNs, including values nested inside arrays.
If a duplicate is found, the scaffolder reports "Already registered" and skips the modification. No file is written.
Complex return statements
If the return statement is not a simple array literal (for example, it uses array_merge() or a conditional), the scaffolder cannot safely modify it. In this case, it prints an error with a manual instruction showing exactly what entry to add by hand.
What's next
- Scaffolder Introduction covers the full architecture and engine components.
- Recipe Spec documents the JSON schema for writing your own custom recipes.
- Commands covers the
makecommand and the other CLI commands.