code16/sharp
Code-driven CMS framework for Laravel (PHP 8.3+/Laravel 11+). Build admin/CMS sections with a clean UI and strong DX: CRUD with validation, search/sort/filter, bulk or custom commands, and authorization—no front-end code required, data-agnostic.
A Wizard is a multistep Command. A common example would be a first step with a resource selection, and a second step with a message box, pre-filled with the previous selection.
In Sharp, Wizard are similar to Commands in many ways: they can be scoped to an instance or to an entity, and can be attached to an Entity List, a Show Page or a Dashboard.
A Wizard Command can not be configured as bulk (meaning: with instance selection).

php artisan sharp:make:entity-command <class_name> --wizard [--model=<model_name>]
php artisan sharp:make:instance-command <class_name> --wizard [--model=<model_name>]
The class must extend either:
Code16\Sharp\EntityList\Commands\Wizards\EntityWizardCommand: for an Entity command, on an Entity ListCode16\Sharp\EntityList\Commands\Wizards\InstanceWizardCommand: for an Instance command, on an Entity List or a Show PageCode16\Sharp\Dashboard\Commands\DashboardWizardCommand: for a Dashboard CommandLike any Command, you must extend label(): string function, and can extend buildCommandConfig(): void (see Commands documentation).
Instead of execute(), you must implement executeFirstStep(array $data): array, or executeFirstStep(mixed $instanceId, array $data): array in an instance case. This method, as expected, must contain the execution code of your first step:
class SendEmailWithPostsWizardCommand extends EntityWizardCommand
{
// [...]
public function executeFirstStep(array $data): array
{
// Do something
}
}
You must also implement protected function buildFormFieldsForFirstStep(FieldsContainer $formFields): void, to build the first step's form:
Wizard Commands needs forms, one for each step. To build the forms, we use the same API as usual (see Commands documentation), the only difference is where to put the code. For the first step, you already have the answer, it's in buildFormFieldsForFirstStep:
class SendEmailWithPostsWizardCommand extends EntityWizardCommand
{
// [...]
public function buildFormFieldsForFirstStep(FieldsContainer $formFields): void
{
$formFields->addField(
SharpFormSelectField::make('posts', Post::pluck('name', 'id')->toArray())
->setMultiple()
->setLabel('Posts to add to the message')
);
}
protected function buildFormLayoutForFirstStep(FormLayoutColumn &$column): void
{
$column->withField('posts');
}
}
::: tip The layout is optional: if you don't define one, fields will appear in the order of declaration. In the above case, the method can be entirely removed without any impact. :::
And finally, if you need to set initial data for the form, you should implement:
protected function initialDataForFirstStep(): array
{
return ['name' => 'Bob'];
}
To tell Sharp to go to the next step, Wizard commands expose a new toStep(string $step) action, which expects a string key representing you step:
public function executeFirstStep(array $data): array
{
// Do something
return $this->toStep('compose-message');
}
This string key must be "sluggable": only chars, carets and underscores.
There are two options:
If your Wizard is small, this could be the right way to proceed. Simply extend the buildFormFieldsForStep(string $step, FieldsContainer $formFields): void method, with a test on $step:
class SendEmailWithPostsWizardCommand extends EntityWizardCommand
{
// [...]
protected function buildFormFieldsForStep(string $step, FieldsContainer $formFields): void
{
if ($step === 'compose-message') {
$formFields->addField(
SharpFormTextareaField::make('message')
->setLabel('Message text')
->setRowCount(8),
);
} elseif ($step === 'my-other-step') {
// ...
}
}
}
This should be a better option in many cases, to clarify things in the Wizard class; you can define a buildFormFieldsForStepXXX(FieldsContainer $formFields): void, where XXX is the camel cased name of you step. So in our example:
class SendEmailWithPostsWizardCommand extends EntityWizardCommand
{
// [...]
public function buildFormFieldsForStepComposeMessage(FieldsContainer $formFields): void
{
$formFields
->addField(
SharpFormTextareaField::make('message')
->setLabel('Message text')
->setRowCount(8),
);
}
}
By default, fields will appear in the order of declaration, like for a regular Command. In case you need more control, you might want to define a layout; once again, you can use one global method:
protected function buildFormLayoutForStep(string $step, FormLayoutColumn &$column): void
{
// ...
}
... or define one per step:
protected function buildFormLayoutForStepComposeMessage(FormLayoutColumn &$column): void
{
// ...
}
You will start to notice a pattern; one method for all:
protected function initialDataForStep(string $step): array
{
// ...
}
or one method per step:
protected function initialDataForStepComposeMessage(): array
{
// ...
}
In the Instance case, methods have an $instanceId param: initialDataForFirstStep(mixed $instanceId): array and initialDataForStep(string $step, mixed $instanceId): array.
Very much like first step, you must define the execution code of each step. And like form declaration, this could be done either in one method, or in one by step:
Entity and Dashboard case:
class SendEmailWithPostsWizardCommand extends EntityWizardCommand
{
// [...]
public function executeStep(string $step, array $data = []): array
{
if ($step === 'compose-message') {
return $this->toStep('checkout');
} else {
// ...
}
}
}
Instance case:
public function executeStep(string $step, mixed $instanceId, array $data = []): array
{
// ...
}
Similarly to forms and layouts; for Entity and Dashboard cases:
public function executeStepComposeMessage(array $data = []): array
{
// ...
}
Instance case:
public function executeStepComposeMessage(mixed $instanceId, array $data = []): array
{
// ...
}
Validation works the same as for regular Commands, with $this->validate():
public function executeStepComposeMessage(array $data = []): array
{
$this->validate($data, ['message' => 'required']);
// ...
}
As seen before, Wizard commands provide a new toStep(string $step) action that can be returned in execution methods.
An any point, if a step returns another action (view, download, info...), this will lead to terminate the Wizard. This means that steps are dynamically linked: you can finish after the first step if some data was entered, or link to another one in other cases.
If an exception is thrown (an in particular, a SharpApplicativeException), the Wizard is also stopped.
This is a key part of Wizard commands: each step may need data from the previous one. To achieve this, you may use you regular storage (database) to store some state, but often it's better not to persist anything before the end of the Wizard.
For this purpose, you have access to a shared context, maintained between each step, via $this->getWizardContext(). You can:
$this->getWizardContext()->put('name', 'value') (typically, in the execute() method)$this->getWizardContext()->get('name') (in the initialData() method)$this->getWizardContext()->validate('name', $rules) (in the initialData() method)Consider the following example; first we build and execute the first step; in the process, we save the select post ids in the context:
class SendEmailWithPostsWizardCommand extends EntityWizardCommand
{
// [...]
public function buildFormFieldsForFirstStep(FieldsContainer $formFields): void
{
$formFields->addField(
SharpFormSelectField::make('posts', Post::pluck('name', 'id')->toArray())
->setMultiple()
->setLabel('Posts to add to the message')
);
}
public function executeFirstStep(array $data = []): array
{
$this->validate($data, ['posts' => 'required']);
$this->getWizardContext()->put('posts', $data['posts']);
return $this->toStep('compose_message');
}
}
For the compose_message step, we initialize data based on what is in the context, after validating that te context has post ids (to ensure we are coming from step 1):
class SendEmailWithPostsWizardCommand extends EntityWizardCommand
{
// [...]
protected function initialDataForStepComposeMessage(): array
{
$this->getWizardContext()->validate(['posts' => 'required']);
return [
'message' => collect(
['Here’s a list of posts I think you may like:'])
->merge(
Post::whereIn('id', $this->getWizardContext()->get('posts'))
->get()
->pluck('title')
)
->implode("\n"),
];
}
}
We build the form, and store a result useful for the next step in the context (and so on, until the end):
class SendEmailWithPostsWizardCommand extends EntityWizardCommand
{
// [...]
public function buildFormFieldsForStepComposeMessage(FieldsContainer $formFields): void
{
$formFields->addField(
SharpFormTextareaField::make('message')->setLabel('Message text')
);
}
public function executeStepComposeMessage(array $data = []): array
{
$this->validate($data, ['message' => 'required']);
$this->getWizardContext()->put('message', $data['message']);
return $this->toStep('choose_recipients');
}
}
Sharp forces you to define the whole Wizard, with all its steps, in a single Command class. This is wanted, since the Command still has only one label, config, authorization... and is seen as a single process, as it should be. But it can lead to a big file with many rows; for this, there is a simple solution provided by PHP: using traits, one per step.
Maybe something like this, where each trait contains step related methods (initialData, buildFormField, buildFormLayout, execute):
class SendEmailWithPostsWizardCommand extends EntityWizardCommand
{
use SendEmailStepChoosePosts,
SendEmailStepComposeMessage,
SendEmailStepSelectRecipients;
public function label(): ?string
{
return 'Compose an email with chosen posts...';
}
public function authorize(): bool
{
return auth()->user()->hasRole('admin');
}
}
How can I help you explore Laravel packages today?