From 840f275235fff97132a3b541e83bcffe848ed407 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Thu, 20 Oct 2016 12:42:24 +1300 Subject: [PATCH] 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. --- Forms/DefaultFormFactory.php | 108 +++++++++++++++++ Forms/FormFactory.php | 35 ++++++ docs/en/04_Changelogs/4.0.0.md | 4 + tests/forms/FormFactoryTest.php | 209 ++++++++++++++++++++++++++++++++ tests/forms/FormFactoryTest.yml | 3 + 5 files changed, 359 insertions(+) create mode 100644 Forms/DefaultFormFactory.php create mode 100644 Forms/FormFactory.php create mode 100644 tests/forms/FormFactoryTest.php create mode 100644 tests/forms/FormFactoryTest.yml diff --git a/Forms/DefaultFormFactory.php b/Forms/DefaultFormFactory.php new file mode 100644 index 000000000..c7b6e0889 --- /dev/null +++ b/Forms/DefaultFormFactory.php @@ -0,0 +1,108 @@ +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']; + } +} diff --git a/Forms/FormFactory.php b/Forms/FormFactory.php new file mode 100644 index 000000000..b056cd36e --- /dev/null +++ b/Forms/FormFactory.php @@ -0,0 +1,35 @@ +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( + 'Preview', + $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('Preview', 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; + } +} diff --git a/tests/forms/FormFactoryTest.yml b/tests/forms/FormFactoryTest.yml new file mode 100644 index 000000000..a3808f4a0 --- /dev/null +++ b/tests/forms/FormFactoryTest.yml @@ -0,0 +1,3 @@ +FormFactoryTest_TestObject: + object: + Title: 'Test Object'