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'