Version 6 pre-stable
This version of Silverstripe CMS has not yet been given a stable release. See the release roadmap for more information. Go to documentation for the most recent stable version.

How to encapsulate forms

Form definitions can often get long, complex and often end up cluttering up a Controller definition. We may also want to reuse the Form across multiple Controller classes rather than just one. A nice way to encapsulate the logic and code for a Form is to create it as a subclass to Form. Let's look at a example of a Form which is on our Controller but would be better written as a subclass.

// app/src/PageType/SearchPageController.php
namespace App\PageType;

use PageController;
use SilverStripe\Forms\CheckboxSetField;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\Validation\RequiredFieldsValidator;

class SearchPageController extends PageController
{
    // ...

    public function searchForm()
    {
        $fields = FieldList::create(
            HeaderField::create('Header', 'Step 1. Basics'),
            OptionsetField::create('Type', '', [
                'foo' => 'Search Foo',
                'bar' => 'Search Bar',
                'baz' => 'Search Baz',
            ]),
            CompositeField::create(
                HeaderField::create('Header2', 'Step 2. Advanced '),
                CheckboxSetField::create('Foo', 'Select Option', [
                    'qux' => 'Search Qux',
                ]),
                CheckboxSetField::create('Category', 'Category', [
                    'Foo' => 'Foo',
                    'Bar' => 'Bar',
                ]),
                NumericField::create('Minimum', 'Minimum'),
                NumericField::create('Maximum', 'Maximum')
            )
        );

        $actions = FieldList::create(
            FormAction::create('doSearchForm', 'Search')
        );

        $required = RequiredFieldsValidator::create([
            'Type',
        ]);

        $form = Form::create($this, __FUNCTION__, $fields, $actions, $required);
        $form->setFormMethod('GET');

        $form->addExtraClass('no-action-styles');
        $form->disableSecurityToken();

        return $form;
    }

    public function doSearchForm(array $data, Form $form)
    {
        // Do something with the data, return results, or redirect
    }
}

That's quite a lot of code to include on our controller and generally makes the file look much more complex than it should be. Good practice would be to move this to a Form subclass and create a new instance for your particular controller.

// app/src/Form/SearchForm.php
namespace App\Form;

use SilverStripe\Forms\CheckboxSetField;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\Validation\RequiredFieldsValidator;

class SearchForm extends Form
{
    /**
     * Our constructor only requires the controller and the name of the form
     * method. We'll create the fields and actions in here.
     */
    public function __construct($controller, $name)
    {
        $fields = FieldList::create(
            HeaderField::create('Header', 'Step 1. Basics'),
            OptionsetField::create('Type', '', [
                'foo' => 'Search Foo',
                'bar' => 'Search Bar',
                'baz' => 'Search Baz',
            ]),
            CompositeField::create(
                HeaderField::create('Header2', 'Step 2. Advanced '),
                CheckboxSetField::create('Foo', 'Select Option', [
                    'qux' => 'Search Qux',
                ]),
                CheckboxSetField::create('Category', 'Category', [
                    'Foo' => 'Foo',
                    'Bar' => 'Bar',
                ]),
                NumericField::create('Minimum', 'Minimum'),
                NumericField::create('Maximum', 'Maximum')
            )
        );

        $actions = FieldList::create(
            FormAction::create('doSearchForm', 'Search')
        );

        $required = RequiredFieldsValidator::create([
            'Type',
        ]);

        // now we create the actual form with our fields and actions defined
        // within this class
        parent::__construct($controller, $name, $fields, $actions, $required);

        // any modifications we need to make to the form.
        $this->setFormMethod('GET');

        $this->addExtraClass('no-action-styles');
        $this->disableSecurityToken();
    }

    /**
     * This method could be on the controller, but putting it here means we get the same
     * behaviour regardless of which controller uses this form.
     */
    public function doSearchForm(array $data, Form $form)
    {
        // Do something with the data, return results, or redirect
    }
}

Our controller will now just have to create a new instance of this form object. Keeping the file light and easy to read.

// app/src/PageType/SearchPageController.php
namespace App\PageType;

use App\Form\SearchForm;
use PageController;

class SearchPageController extends PageController
{
    private static $allowed_actions = [
        'searchForm',
    ];

    public function searchForm()
    {
        return SearchForm::create($this, __FUNCTION__);
    }
}

Related documentation

API documentation