🪄 Work in progress

This documentation is still a work in progress, so please don’t mind the mess. We also want to assure you that any corpses you see are used for completely legal and sanctioned necromantic purposes. Absolutely no funny business is taking place here.

Wizards

Fear is what makes us brave. Real courage is when you overcome your fear.
— Getafix, Asterix and the Vikings

The Wizard is the top-level component of your form. It takes care of navigating between steps, handling form submissions and providing shared data that should be available to all steps.

Creating new wizards

All wizards extend from Arcanist\AbstractWizard. This base class provides almost all the functionality you need out of the box.

RegistrationWizard.php
<?php

namespace App\Wizards\Registration;

use Arcanist\AbstractWizard;

class RegistrationWizard extends AbstractWizard
{
}

Generating Wizards

You can use the php artisan make:wizard command to have Arcanist conjure generate a new wizard skeleton for you. Check the section on commands for more information.

Registering wizards

Before our wizard can do anything, it needs to be registered so Arcanist knows about it. We can do so by adding it to the wizards array in our arcanist.php config file.

config/arcanist.php
return [

    // ...

    'wizards' => [
        App\Wizards\Registration\RegistrationWizard::class,
    ],

    // ...
];

If we check our application routes now using php artisan route:list, we would indeed see a bunch of new routes that were registered. Here are the routes you should see:

console
+--------------------------------------+--------------------------+
| URI                                  | Name                     |
+--------------------------------------+--------------------------+
| wizard/new-wizard                    | wizard.new-wizard.create |
| wizard/new-wizard                    | wizard.new-wizard.store  |
| wizard/new-wizard/{wizardId}         | wizard.new-wizard.delete |
| wizard/new-wizard/{wizardId}/{slug?} | wizard.new-wizard.show   |
| wizard/new-wizard/{wizardId}/{slug}  | wizard.new-wizard.update |
+--------------------------------------+--------------------------+

Since we haven’t really configured our wizard yet, Arcanist falls back to the defaults defined in the AbstractWizard base class. This why currently all routes are called new-wizard.

Filling in the details

Our wizard is still pretty bare bones, so let's start filling in some of the details.

Naming wizards

Every good wizard needs a name. To give your wizard a name, you can overwrite the static $title property that comes with the base class.

RegistrationWizard.php
<?php

namespace App\Wizards\Registration;

use Arcanist\AbstractWizard;

class RegistrationWizard extends AbstractWizard
{
    public static string $title = 'Join the fun';
}

If you need more control, for example because you want to localize your wizard’s title, you can override the title() method instead.

RegistrationWizard.php
<?php

namespace App\Wizards\Registration;

use Arcanist\AbstractWizard;

class RegistrationWizard extends AbstractWizard
{
    protected function title(): string
    {
        // Localize the wizard by using Laravel's
        // translation helpers.
        return __('Join the fun');
    }
}

Note

Arcanist doesn’t actually display the title anywhere. Instead, it will be passed to each view as part of the wizard key. Check the section on view data for more information about this.

Slugs

Now that our wizard is no longer nameless, let's configure the slug next. The slug is used to generate both the names as well as the actual URLs for all routes of this wizard.

When we looked at the routes that were registered above, we saw that they all contained new-wizard. This is because that’s the default slug provided by the AbstractWizard base class.

To make your wizard cool and unique, you can (and should) overwrite the static $slug property.

RegistrationWizard.php
<?php

namespace App\Wizards\Registration;

use Arcanist\AbstractWizard;

class RegistrationWizard extends AbstractWizard
{
    public static string $title = 'Join the fun';

    public static string $slug = 'register';
}

If we look at our application routes again, we can see that they now sound much cooler:

console
+------------------------------------+------------------------+
| URI                                | Name                   |
+------------------------------------+------------------------+
| wizard/register                    | wizard.register.create |
| wizard/register                    | wizard.register.store  |
| wizard/register/{wizardId}         | wizard.register.delete |
| wizard/register/{wizardId}/{slug?} | wizard.register.show   |
| wizard/register/{wizardId}/{slug}  | wizard.register.update |
+------------------------------------+------------------------+

Ah, much better.

Configuring the route prefix

By default, Arcanist uses the wizard prefix for all routes. This can be configured via the route_prefix option in the arcanist.php config file. Check out the configuration page for reference.

Adding steps

Our wizard now has a name and slick URLs, but it’s not actually doing anything yet. To change that, we specify the list of steps that make up this wizard in the steps property.

RegistrationWizard.php
<?php

namespace App\Wizards\Registration;

use Arcanist\AbstractWizard;
use App\Wizards\Registration\SelectPlanStep;
use App\Wizards\Registration\UploadUserAvatarStep;
use App\Wizards\Registration\EmailAndPasswordStep;

class RegistrationWizard extends AbstractWizard
{
    public static string $title = 'Join the fun';

    public static string $slug = 'register';

    protected array $steps = [
        EmailAndPasswordStep::class,
        SelectPlanStep::class,
        UploadUserAvatarStep::class,
    ];
}

For more information on how exactly steps work, check the next section of the docs.

Note that the order of the steps is important. The way we’ve set up our wizard now, the user needs to:

  1. Provide their email and password
  2. Select a subscription plan
  3. Upload a user avatar

Optional steps

Does that mean users are forced to upload an avatar? Maybe! It depends on the configuration of the UploadUserAvatarStep step.

After a step was submitted, Arcanist will automatically redirect to the next step in the wizard. If the submitted step happens to be the last one in the wizard, Arcanist will then call the wizard’s configured action.

“What action?” you might say. I’m glad you asked.

Configuring the action

True to their literary counterparts, wizards in Arcanist don’t actually do anything themselves. They instead delegate the actual work to a separate Action class once the last step of the wizard was submitted. Which class gets called is configured via the onCompleteAction property of the wizard.

RegistrationWizard.php
<?php

namespace App\Wizards\Registration;

use Arcanist\AbstractWizard;
use App\Wizards\Registration\SelectPlanStep;
use App\Wizards\Registration\CreateUserAction;
use App\Wizards\Registration\UploadUserAvatarStep;
use App\Wizards\Registration\EmailAndPasswordStep;

class RegistrationWizard extends AbstractWizard
{
    public static string $title = 'Join the fun';

    public static string $slug = 'register';

    protected array $steps = [
        EmailAndPasswordStep::class,
        SelectPlanStep::class,
        UploadUserAvatarStep::class,
    ];

    protected string $onCompleteAction = CreateUserAction::class;
}

More on actions

Check out the page on actions for more information on how actions work.

Passing data to actions

By default, the action gets passed an array containing all data that has been collected by the wizard’s steps. This data can be accessed using the $this->data(?string $key, mixed $default = null) method of the wizard.

Sometimes you might want to transform the data before it gets passed to the action, however. You might want to transform the data array into a data transfer object (DTO), for example. You can do this by overwriting the transformWizardData() method.

<?php

namespace App\Wizards\Registration;

use Arcanist\AbstractWizard;
use App\Wizards\Registration\SelectPlanStep;
use App\Wizards\Registration\CreateUserAction;
use App\Wizards\Registration\UploadUserAvatarStep;
use App\Wizards\Registration\DTO\RegistrationData;
use App\Wizards\Registration\EmailAndPasswordStep;

class RegistrationWizard extends AbstractWizard
{
    public static string $title = 'Join the fun';

    public static string $slug = 'register';

    protected array $steps = [
        EmailAndPasswordStep::class,
        SelectPlanStep::class,
        UploadUserAvatarStep::class,
    ];

    protected string $onCompleteAction = CreateUserAction::class;

    protected function transformWizardData(): mixed
    {
        $subscription = Subscription::find(
            $this->data('subscription')
        );

        return new RegistrationData([
            'email' => $this->data('email'),
            'password' => $this->data('password'),
            'subscription' => $subscription,
            'avatarPath' => $this->data('avatarPath'),
        ]);
    }
}
<?php

namespace App\Wizards\Registration\DTO;

final class RegistrationData
{
    public function __construct(
        public string $email,
        public string $password,
        public Subscription $subscription,
        public string $avatarPath
    ) {
    }
}

Redirecting after completing the wizard

After the action was run successfully, Arcanist will redirect the user to a different page. The target of the redirect can be configured in multiple ways, both globally, as well as on a per-wizard basis.

Default redirect target

If no explicit redirect target is defined in the wizard, Arcanist will fall back to using the route defined in the redirect_url key in the arcanist.php config file.

Package configuration

Check out the configuration section of the documentation for a complete list of all available configuration options.

Wizard-specific redirects

You probably want to redirect the user to a specific page based on the wizard most of the time. You can configure this in one of the following ways.

For simple cases, you can implement the redirectTo method of the wizard and return the intended target URL.

RegistrationWizard.php
<?php

namespace App\Wizards\Registration;

use Arcanist\AbstractWizard;
use App\Wizards\Registration\SelectPlanStep;
use App\Wizards\Registration\CreateUserAction;
use App\Wizards\Registration\UploadUserAvatarStep;
use App\Wizards\Registration\DTO\RegistrationData;
use App\Wizards\Registration\EmailAndPasswordStep;

class RegistrationWizard extends AbstractWizard
{
    public static string $title = 'Join the fun';

    public static string $slug = 'register';

    protected array $steps = [
        EmailAndPasswordStep::class,
        SelectPlanStep::class,
        UploadUserAvatarStep::class,
    ];

    protected string $onCompleteAction = CreateUserAction::class;

    protected function transformWizardData(): mixed { /* ... */ }

    protected function redirectTo(): string
    {
        return route('pages.dashboard');
    }
}

If you need full control over the redirect response, you can use the onAfterComplete method instead. This method is passed an ActionResult instance—which contains the result of calling the wizard’s action—and needs to return a RedirectResponse. This allows you to redirect to the detail page of a model that was created inside the action, for example.

<?php

namespace App\Wizards\Registration;

use Arcanist\AbstractWizard;
use Arcanist\Action\ActionResult;
use Illuminate\Http\RedirectResponse;
use App\Wizards\Registration\SelectPlanStep;
use App\Wizards\Registration\CreateUserAction;
use App\Wizards\Registration\UploadUserAvatarStep;
use App\Wizards\Registration\DTO\RegistrationData;
use App\Wizards\Registration\EmailAndPasswordStep;

class RegistrationWizard extends AbstractWizard
{
    public static string $title = 'Join the fun';

    public static string $slug = 'register';

    protected array $steps = [
        EmailAndPasswordStep::class,
        SelectPlanStep::class,
        UploadUserAvatarStep::class,
    ];

    protected string $onCompleteAction = CreateUserAction::class;

    protected function transformWizardData(): mixed { /* ... */ }

    protected function onAfterComplete(ActionResult $result): RedirectResponse
    {
        return redirect()->route(
            'users.profile',
            $result->get('user')
        );
    }
}
<?php

namespace App\Wizards\Registration;

use Arcanist\Action\WizardAction;
use Arcanist\Action\ActionResult;
use App\Wizards\Registration\DTO\RegistrationData;

class CreateUserAction extends WizardAction
{
    /**
     * @param RegistrationData $data
     */
    public function execute($data): ActionResult
    {
        $user = /* ... */;

        // Lengthy process to create user, upload their avatar
        // and subscribe them to their chosen plan.

        return $this->success([
            'user' => $user
        ]);
    }
}

Configuring middleware

If you want to apply a specific set of middleware to a particular wizard, you can implement the static middleware method in your wizard class.

RegistrationWizard.php
<?php

namespace App\Wizards\Registration;

use Arcanist\AbstractWizard;
use App\Wizards\Registration\SelectPlanStep;
use App\Wizards\Registration\CreateUserAction;
use App\Wizards\Registration\UploadUserAvatarStep;
use App\Wizards\Registration\EmailAndPasswordStep;

class RegistrationWizard extends AbstractWizard
{
    public static string $title = 'Join the fun';

    public static string $slug = 'register';

    protected array $steps = [
        EmailAndPasswordStep::class,
        SelectPlanStep::class,
        UploadUserAvatarStep::class,
    ];

    protected string $onCompleteAction = CreateUserAction::class;

    public static function middleware(): array
    {
        return ['guest'];
    }
}

This middleware gets merged with the global wizard middleware defined in the configuration.