mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
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:
parent
3f773c826a
commit
840f275235
108
Forms/DefaultFormFactory.php
Normal file
108
Forms/DefaultFormFactory.php
Normal 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
35
Forms/FormFactory.php
Normal 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();
|
||||||
|
}
|
@ -1044,6 +1044,10 @@ The following filesystem synchronisation methods and tasks are also removed
|
|||||||
actions available when editing records.
|
actions available when editing records.
|
||||||
* `PopoverField` added to provide popup-menu behaviour in react forms (currently not available for
|
* `PopoverField` added to provide popup-menu behaviour in react forms (currently not available for
|
||||||
non-react forms).
|
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:
|
The following methods and properties on `Requirements_Backend` have been renamed:
|
||||||
|
|
||||||
|
209
tests/forms/FormFactoryTest.php
Normal file
209
tests/forms/FormFactoryTest.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
3
tests/forms/FormFactoryTest.yml
Normal file
3
tests/forms/FormFactoryTest.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
FormFactoryTest_TestObject:
|
||||||
|
object:
|
||||||
|
Title: 'Test Object'
|
Loading…
Reference in New Issue
Block a user