API Created a generic FormFactory interface (#6178)

Created a generic DataObject FormFactory interface that can be substituted in place of getCMSFields. Different FormFactories can depend on different kinds of context, such as
'Record' or 'Controller' - it's the responsibility of the code calling the factory to interpret and
supply this context.

The expected use-case is that rather than overriding getCMSFields(), developers can
change CMS UIs by manipulating the FormFactory associated with the given DataObject.

This is an experimental UI and may change before 4.0 stable is released.
This commit is contained in:
Damian Mooyman 2016-10-20 12:42:24 +13:00 committed by Sam Minnée
parent 3f773c826a
commit 840f275235
5 changed files with 359 additions and 0 deletions

View File

@ -0,0 +1,108 @@
<?php
namespace SilverStripe\Forms;
use InvalidArgumentException;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Extensible;
/**
* Default form builder class.
*
* @internal WARNING: Experimental and volatile API.
*
* Allows extension by either controller or object via the following methods:
* - updateFormActions
* - updateFormValidator
* - updateFormFields
* - updateForm
*/
class DefaultFormFactory implements FormFactory {
use Extensible;
public function __construct() {
$this->constructExtensions();
}
public function getForm(Controller $controller, $name = FormFactory::DEFAULT_NAME, $context = [])
{
// Validate context
foreach($this->getRequiredContext() as $required) {
if (!isset($context[$required])) {
throw new InvalidArgumentException("Missing required context $required");
}
}
$fields = $this->getFormFields($controller, $name, $context);
$actions = $this->getFormActions($controller, $name, $context);
$validator = $this->getFormValidator($controller, $name, $context);
$form = Form::create($controller, $name, $fields, $actions, $validator);
// Extend form
$this->invokeWithExtensions('updateForm', $form, $controller, $name, $context);
// Populate form from record
$form->loadDataFrom($context['Record']);
return $form;
}
/**
* Build field list for this form
*
* @param Controller $controller
* @param string $name
* @param array $context
* @return FieldList
*/
protected function getFormFields(Controller $controller, $name, $context = []) {
// Fall back to standard "getCMSFields" which itself uses the FormScaffolder as a fallback
// @todo Deprecate or formalise support for getCMSFields()
$fields = $context['Record']->getCMSFields();
$this->invokeWithExtensions('updateFormFields', $fields, $controller, $name, $context);
return $fields;
}
/**
* Build list of actions for this form
*
* @param Controller $controller
* @param string $name
* @param array $context
* @return FieldList
*/
protected function getFormActions(Controller $controller, $name, $context = []) {
// @todo Deprecate or formalise support for getCMSActions()
$actions = $context['Record']->getCMSActions();
$this->invokeWithExtensions('updateFormActions', $actions, $controller, $name, $context);
return $actions;
}
/**
* @param Controller $controller
* @param string $name
* @param array $context
* @return null|Validator
*/
protected function getFormValidator(Controller $controller, $name, $context = []) {
$validator = null;
if ($context['Record']->hasMethod('getCMSValidator')) {
// @todo Deprecate or formalise support for getCMSValidator()
$validator = $context['Record']->getCMSValidator();
}
// Extend validator
$this->invokeWithExtensions('updateFormValidator', $validator, $controller, $name, $context);
return $validator;
}
/**
* Return list of mandatory context keys
*
* @return mixed
*/
public function getRequiredContext()
{
return ['Record'];
}
}

35
Forms/FormFactory.php Normal file
View File

@ -0,0 +1,35 @@
<?php
namespace SilverStripe\Forms;
use SilverStripe\Control\Controller;
/**
* A service which can generate a form
*/
interface FormFactory {
/**
* Default form name.
*/
const DEFAULT_NAME = 'Form';
/**
* Generates the form
*
* @param Controller $controller Parent controller
* @param string $name
* @param array $context List of properties which may influence form scaffolding.
* E.g. 'Record' if building a form for a record.
* Custom factories may support more advanced parameters.
* @return Form
*/
public function getForm(Controller $controller, $name = self::DEFAULT_NAME, $context = []);
/**
* Return list of mandatory context keys
*
* @return mixed
*/
public function getRequiredContext();
}

View File

@ -1044,6 +1044,10 @@ The following filesystem synchronisation methods and tasks are also removed
actions available when editing records.
* `PopoverField` added to provide popup-menu behaviour in react forms (currently not available for
non-react forms).
* Introduction of experimental `FormFactory` API as a substitute for DataObject classes being responsible
for building their own form fields. This builds a form based on a given controller and model,
and can be customised on a case by case basis. This has been introduced initially for the asset-admin
module.
The following methods and properties on `Requirements_Backend` have been renamed:

View File

@ -0,0 +1,209 @@
<?php
use SilverStripe\Control\Controller;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Extension;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\DefaultFormFactory;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
class FormFactoryTest extends SapphireTest
{
protected $extraDataObjects = [
FormFactoryTest_TestObject::class,
];
protected static $fixture_file = 'FormFactoryTest.yml';
/**
* Test versioned form
*/
public function testVersionedForm() {
$controller = new FormFactoryTest_TestController();
$form = $controller->Form();
// Check formfields
$this->assertInstanceOf(TextField::class, $form->Fields()->fieldByName('Title'));
$this->assertInstanceOf(HiddenField::class, $form->Fields()->fieldByName('ID'));
$this->assertInstanceOf(HiddenField::class, $form->Fields()->fieldByName('SecurityID'));
// Check preview link
/** @var LiteralField $previewLink */
$previewLink = $form->Fields()->fieldByName('PreviewLink');
$this->assertInstanceOf(LiteralField::class, $previewLink);
$this->assertEquals(
'<a href="FormFactoryTest_TestController/preview/" rel="external" target="_blank">Preview</a>',
$previewLink->getContent()
);
// Check actions
$this->assertInstanceOf(FormAction::class, $form->Actions()->fieldByName('action_save'));
$this->assertInstanceOf(FormAction::class, $form->Actions()->fieldByName('action_publish'));
$this->assertTrue($controller->hasAction('publish'));
}
/**
* Removing versioning from an object should result in a simpler form
*/
public function testBasicForm() {
FormFactoryTest_TestObject::remove_extension(Versioned::class);
$controller = new FormFactoryTest_TestController();
$form = $controller->Form();
// Check formfields
$this->assertInstanceOf(TextField::class, $form->Fields()->fieldByName('Title'));
$this->assertNull($form->Fields()->fieldByName('PreviewLink'));
// Check actions
$this->assertInstanceOf(FormAction::class, $form->Actions()->fieldByName('action_save'));
$this->assertNull($form->Actions()->fieldByName('action_publish'));
}
}
/**
* @mixin Versioned
*/
class FormFactoryTest_TestObject extends DataObject {
private static $db = [
'Title' => 'Varchar',
];
private static $extensions = [
Versioned::class,
];
}
/**
* Edit controller for this form
*/
class FormFactoryTest_TestController extends Controller {
private static $extensions = [
FormFactoryTest_ControllerExtension::class,
];
public function Link($action = null) {
return Controller::join_links('FormFactoryTest_TestController', $action, '/');
}
public function Form() {
// Simple example; Just get the first draft record
$record = $this->getRecord();
$factory = new FormFactoryTest_EditFactory();
return $factory->getForm($this, 'Form', ['Record' => $record]);
}
public function save($data, Form $form) {
// Noop
}
/**
* @return DataObject
*/
protected function getRecord()
{
return Versioned::get_by_stage(FormFactoryTest_TestObject::class, Versioned::DRAFT)->first();
}
}
/**
* Provides versionable extensions to a controller / scaffolder
*/
class FormFactoryTest_ControllerExtension extends Extension {
/**
* Handlers for extra actions added by this extension
*
* @var array
*/
private static $allowed_actions = [
'publish',
'preview',
];
/**
* Adds additional form actions
*
* @param FieldList $actions
* @param Controller $controller
* @param string $name
* @param array $context
*/
public function updateFormActions(FieldList &$actions, Controller $controller, $name, $context = []) {
// Add publish button if record is versioned
if (empty($context['Record'])) {
return;
}
$record = $context['Record'];
if ($record->hasExtension(Versioned::class)) {
$actions->push(new FormAction('publish', 'Publish'));
}
}
/**
* Adds extra fields to this form
*
* @param FieldList $fields
* @param Controller $controller
* @param string $name
* @param array $context
*/
public function updateFormFields(FieldList &$fields, Controller $controller, $name, $context = []) {
// Add preview link
if (empty($context['Record'])) {
return;
}
$record = $context['Record'];
if ($record->hasExtension(Versioned::class)) {
$link = $controller->Link('preview');
$fields->unshift(new LiteralField(
"PreviewLink",
sprintf('<a href="%s" rel="external" target="_blank">Preview</a>', Convert::raw2att($link))
));
}
}
public function publish($data, $form) {
// noop
}
public function preview() {
// noop
}
}
/**
* Test factory
*/
class FormFactoryTest_EditFactory extends DefaultFormFactory {
private static $extensions = [
FormFactoryTest_ControllerExtension::class
];
protected function getFormFields(Controller $controller, $name, $context = [])
{
$fields = new FieldList(
new HiddenField('ID'),
new TextField('Title')
);
$this->invokeWithExtensions('updateFormFields', $fields, $controller, $name, $context);
return $fields;
}
protected function getFormActions(Controller $controller, $name, $context = [])
{
$actions = new FieldList(
new FormAction('save', 'Save')
);
$this->invokeWithExtensions('updateFormActions', $actions, $controller, $name, $context);
return $actions;
}
}

View File

@ -0,0 +1,3 @@
FormFactoryTest_TestObject:
object:
Title: 'Test Object'