#1913084: Introduce a Form interface for building, validating, and submitting forms adds a standard form interface.
We already have EntityFormControllerInterface for entity forms, but it comes strangely coupled to entity_get_form(), entity_form_state_defaults(), and entity_form_id().

Once that issue is in, we should convert the existing forms to comply with FormInterface.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tim.plunkett’s picture

tim.plunkett’s picture

Status: Postponed » Needs review
FileSize
82.83 KB

That's all in now.

Status: Needs review » Needs work

The last submitted patch, form-1913618-2.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
82.19 KB

Whoops. Made unneeded changes to EntityFormController::validate(), it's now untouched.

plach’s picture

Issue tags: +Entity forms
+++ b/core/includes/entity.inc
@@ -451,9 +427,10 @@ function entity_form_id(EntityInterface $entity, $operation = 'default') {
-  $form_state['build_info']['args'] = array($entity);
+  $controller->setEntity($entity);

It seems that most of the changes in this patch are originated by this hunk, which is not strictly required to implement FormInterface. Although it'll improve DX I'm afraid it has also the potential for introducing misalignments between the form controller and the form state entity references. This seems a bit out of scope here. We decided not to wrap the form state into the controller for similar reasons, see #1499596-96: Introduce a basic entity form controller.

Do you think this change is necessary here?

Btw, this is the issue the linked post is referring to: #1728812: Prevent entity form controllers from being cached twice (in $form and $form_state).

tim.plunkett’s picture

Yes, that will be required. This patch was just the set-up for a conversion, I didn't actually switch anything.
Ideally #1913856: Convert Views UI forms to use FormInterface will go in soon, and I can convert one of those.

The eventual goal is a method on the form controller itself that can be addressed by a route. I need to talk to fubhy about his idea to put controllers in the DIC for that.

tim.plunkett’s picture

FileSize
12.22 KB
90.5 KB

So this hardcodes a form controller to a route, which is not what we want, but is all I can do right now. Just a proof of concept.

Also, entity.inc contains entity_form_submit(), which mimics drupal_form_submit(). That seems like a huge hack, and to prove that point, it's only used by openid module to spoof a user registration.

I've also completely removed $form_state['entity'], so that there is only one place storing it. Either use $this->entity within the controller, or $form_state['controller']->getEntity() outside it (which should have been done anyway, since currently the location its stored in the form_state was variable!).

plach’s picture

So this hardcodes a form controller to a route, which is not what we want, but is all I can do right now. Just a proof of concept.

Isn't this out of scope wrt the straight conversion?

I've also completely removed $form_state['entity'], so that there is only one place storing it. Either use $this->entity within the controller, or $form_state['controller']->getEntity() outside it (which should have been done anyway, since currently the location its stored in the form_state was variable!).

Removing $form_state['entity'] and unifying the way we retrieve an entity in the form workflow was one of my original goals when working on the initial EFC patch, so I'm largely +1 on this. Moreover, in light of #5 this looks like the only sensible thing to do, although it makes the form controller a stateful object. I'd like to see what @fago thinks of this, since it was one of the points we discussed more.

+++ b/core/modules/views/views_ui/lib/Drupal/views_ui/ViewCloneFormController.php
@@ -12,6 +12,14 @@
+    return entity_build_form($this);

I guess we should use an injected entity manger to retrieve the needed controller in these cases. Similar issue: #1919322: entity_load_unchanged() should be part of the storage controller.

tim.plunkett’s picture

Sure, we can punt on actually using this correctly from routes, and continue to call entity_get_form(). It needs to happen, if you want a follow-up that's fine.

I'm not sure what you mean by injecting the manager, the referenced issue mentions it but doesn't explain it and the patch doesn't seem to do it.

plach’s picture

Sure, we can punt on actually using this correctly from routes, and continue to call entity_get_form(). It needs to happen, if you want a follow-up that's fine.

No strong preference on this, it's just that I can do a quite solid review of the form controller related code, but I have nfi of what the Views stuff is doing :)

I'm not sure what you mean by injecting the manager, the referenced issue mentions it but doesn't explain it and the patch doesn't seem to do it.

I mean that by calling entity_*_controller() we are relying on drupal_container(), instead of having a properly injected entity manager. This makes unit testing stuff harder/dirtier.

tim.plunkett’s picture

FileSize
20.58 KB
94.57 KB

Okay, I took a new approach.

I added EntityManager::getForm(), which is essentially OO-ified entity_get_form().

The best example of this is in the the user module (UserRouteController is now completely removed):

diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index ad70759..024b385 100644
--- a/core/modules/user/user.routing.yml
+++ b/core/modules/user/user.routing.yml
@@ -1,7 +1,9 @@
 user_register:
   pattern: '/user/register'
   defaults:
-    _content: '\Drupal\user\UserRouteController::register'
+    _controller: 'plugin.manager.entity:getForm'
+    entity: 'user'
+    operation: 'register'
   requirements:
     _access_user_register: 'TRUE'

This works for Views' use case, so I'm reasonably happy with this.

Status: Needs review » Needs work

The last submitted patch, entity-1913618-11.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
1.41 KB
94.6 KB

Fix two views bugs.

Status: Needs review » Needs work

The last submitted patch, form-1913618-13.patch, failed testing.

tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
Status: Needs work » Needs review
FileSize
2.49 KB
94.61 KB

Whoops, I forgot to commit my fix to that logic before uploading. This should pass, so unassigning. Reviews please!

tim.plunkett’s picture

FileSize
758 bytes

Wrong interdiff, right patch.

tim.plunkett’s picture

Issue tags: +FormInterface

Tagging.

tim.plunkett’s picture

#15: vdc-1913618-14.patch queued for re-testing.

andypost’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Entity/EntityFormController.phpundefined
@@ -23,30 +23,70 @@ class EntityFormController implements EntityFormControllerInterface {
+    $form_id = $entity_type;
+    if ($bundle != $entity_type) {
+      $form_id = $bundle . '_' . $form_id;
+    }
+    if ($this->operation != 'default') {
+      $form_id = $form_id . '_' . $this->operation;
+    }
+    return $form_id . '_form';

Suppose better to have entity type the leading name - {entity}_form__{bundle} and bundle (sub type) as context like hook_forms() usage

+++ b/core/lib/Drupal/Core/Entity/EntityManager.phpundefined
@@ -330,4 +330,29 @@ public function getAccessController($entity_type) {
+  public function getForm($entity, $operation = 'default') {
+    if (is_string($entity)) {
+      $entity = $this->getStorageController($entity)->create(array());

Suppose D8MI would like to pass lang_code here.
So third argument could be used for default values and having $entity as mixed parameter looks bad

+++ /dev/nullundefined
@@ -1,28 +0,0 @@
-class UserRouteController extends ContainerAware {

great!

+++ b/core/modules/views/views_ui/lib/Drupal/views_ui/Routing/ViewsUIController.phpundefined
@@ -76,19 +76,6 @@ public function listing() {
-  public function add() {
-    drupal_set_title(t('Add new view'));

@@ -200,20 +187,6 @@ public function ajaxOperation(ViewStorageInterface $view, $op, Request $request)
-   */
-  public function cloneForm(ViewStorageInterface $view) {
-    drupal_set_title(t('Clone of @human_name', array('@human_name' => $view->getHumanName())));

@@ -266,22 +239,6 @@ public function edit(ViewUI $view, $display_id = NULL) {
-  public function preview(ViewUI $view, $display_id = NULL) {
-    return entity_get_form($view, 'preview', array('display_id' => $display_id));

+++ b/core/modules/views/views_ui/views_ui.routing.ymlundefined
@@ -91,13 +97,16 @@ views_ui.edit.display:
 views_ui.preview:
-  pattern: '/admin/structure/views/view/{view}/preview/{display_id}'
+  pattern: '/admin/structure/views/view/{entity}/preview/{display_id}'
   options:
+    converters:
+      entity: 'view'
     tempstore:
-      view: 'views'
+      entity: 'views'
   defaults:
-    _controller: 'views_ui.controller:preview'
+    _controller: 'plugin.manager.entity:getForm'
     display_id: NULL
+    operation: 'preview'

Nice clean-up but settings title from form controller is bad

+++ b/core/modules/views/views_ui/lib/Drupal/views_ui/ViewAddFormController.phpundefined
@@ -17,16 +16,25 @@
+  public function init(array &$form_state) {
+    parent::init($form_state);
+
+    drupal_set_title(t('Add new view'));

This makes embedding of the form much harder. Not sure it's nice idea to affect page level elements from controllers!

+++ b/core/modules/views/views_ui/lib/Drupal/views_ui/ViewFormControllerBase.phpundefined
@@ -25,25 +24,26 @@
-  public function build(array $form, array &$form_state, EntityInterface $entity) {
-    if (isset($form_state['display_id'])) {
-      $this->displayID = $form_state['display_id'];
+  public function init(array &$form_state) {
+    parent::init($form_state);
+
+    if ($display_id = drupal_container()->get('request')->attributes->get('display_id')) {

Are you sure to move this to request level? needs code comment and mention in change notice

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett
Status: Needs work » Needs review
FileSize
13.88 KB
111.68 KB

Suppose better to have entity type the leading name - {entity}_form__{bundle} and bundle (sub type) as context like hook_forms() usage

Out of scope, see #1919930: Bundle entity form IDs violate module namespaces (both on server-side + front-end CSS)

Suppose D8MI would like to pass lang_code here.

That's fine if they so desire, but there is no need for it right now. It can be added.

Yes, putting drupal_set_title() in there is not optimal. Either #1830588: [META] remove drupal_set_title() and drupal_get_title() or #1889790: [Meta] Allow modules to register links for menus, breadcrumbs and tabs (if not with hook_menu) will have to handle that.

I'm rerolling this to take advantage of the recent routing/form patches.
This now depends on #1938980: Fix Ajax system; the last remnants of the old API must be swept away, and this is just the first pass.
I need to add docs, and figure out how to inject dependencies into a route enhancer.

Status: Needs review » Needs work

The last submitted patch, entity-1913618-20.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
6.3 KB
110.56 KB

Okay, we should be good to go again.
Leaving assigned so that it gets rerolled once the enhancer issue goes in, but otherwise I think this is fine.

Status: Needs review » Needs work

The last submitted patch, entity-form-1913618-22.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
654 bytes
110.56 KB

I renamed getFormArg to getFormObject in one class but not the other.

Status: Needs review » Needs work
Issue tags: -Entity forms, -FormInterface

The last submitted patch, entity-form-interface-1913618-24.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
Issue tags: +Entity forms, +FormInterface
tim.plunkett’s picture

Status: Needs review » Postponed
tim.plunkett’s picture

Status: Postponed » Needs review
FileSize
97.39 KB

That went in.

tim.plunkett’s picture

Issue tags: +Stalking Crell

Tagging for a review of the WSCCI bits here.

twistor’s picture

Issue tags: -Stalking Crell
+++ b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.phpundefined
@@ -0,0 +1,64 @@
+   * This would mean the edit form controller for the node entity should be use.

*used

+++ b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.phpundefined
@@ -0,0 +1,64 @@
+    // If no operation is provided, use 'default'.
+    $arg .= '.default';
+    list ($entity, $operation) = explode('.', $arg);

This is weird. Clever, but weird.

+++ b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.phpundefined
@@ -0,0 +1,64 @@
+    elseif (is_string($entity)) {
+      $entity = $manager->getStorageController($entity)->create(array());

Doesn't $entity have to be a string here?

+++ b/core/lib/Drupal/Core/Form/BaseFormInterface.phpundefined
@@ -0,0 +1,25 @@
+  /**
+   * Returns a string identifying the base form.
+   *
+   * @return string|false
+   *   The string identifying the base form or FALSE if this is not a base form.
+   *
+   *   The unique string identifying the form.
+   */

So forms that aren't base forms can implement the BaseFormInterface?

Also, "The unique string identifying the form." seems redundant.

tim.plunkett’s picture

Issue tags: +Stalking Crell

I'm glad you think its clever. Not sure if its too weird :)

I suppose that could be just else, no elseif.

That redundancy is a reroll slip-up, I'll fix that and the typo later.

Also, YAY, that passed!

EclipseGc’s picture

+++ b/core/lib/Drupal/Core/Form/BaseFormInterface.phpundefined
@@ -0,0 +1,25 @@
+interface BaseFormInterface extends FormInterface {

This just seems odd to me. I mean I get what you're doing here, but the name convention makes me think that FormInterface should extend BaseFormInterface. Maybe this could be BaseFormIdInterface? Does this need to be disconnected for the EntityFormControllerInterface?

$arg .= '.default';

Also not really ok with that line. It sounds like you aren't either so I'm just ++ing changing that line.

In general the updates to the implementations all look very sane. The actual changes to the EntityFormController itself give me pause since the updating process of the entity on the actual class is a sticky thing. It LOOKS right to me, but I don't feel comfortable being the person to rtbc this for that specific reason alone. The rest of it looks pretty good to me and like a good improvement. My one other question is just that it appears we now have a buildForm(), build() and form() methods and that gives me pause as well. Can you explain that a bit?

Eclipse

tim.plunkett’s picture

This replaces EntityFormControllerInterface::build() with FormInterface::buildForm().

EntityFormController::buildForm() calls:
EntityFormControllerInterface::init() to set up the form state and the entity
EntityFormControllerInterface::form() to build just the input elements
EntityFormControllerInterface::actionsElement() to allow easier adding of actions (submit/validate)

I've been very very tempted to move actionsElement() up to FormInterface, but as a protected helper method, it would need to be on a base form, and most of the awesomeness of FormInterface is that its an interface, and you can continue extending whatever base class you need. So I think we just need improved docs on EntityFormControllerInterface::form(), and leave the flow as is.

BaseFormInterface is poorly named, I'll revisit that.

I'm leaving the '.default' thing, according to the current structure of form controllers, its still necessary and that's a nice way to handle it.
This is the equivalent of entity_get_form($node), instead of entity_get_form($node, 'default').

I'll have a new patch up this evening, still leaving at needs review for more feedback.

EclipseGc’s picture

ok, but entity_get_form() does pass 'default' as the second parameter already in a default. Is this strictly necessary? If so I'll happily drop the topic. It's a pretty minor point and what you're doing is definitely not prone to error, so I don't have complaints other than it's sort of weird looking.

Ok, so build() is gone, buildForm() is in its place, and the call to form() is permanent? or a temporary shim you want to revisit. Just trying to be clear here, I'm cool either way.

Definitely let's not put the actions thing on FormInterface, I think it's working perfectly without that right now. :-)

Eclipse

tim.plunkett’s picture

Yes, entity_get_form() passes 'default', but that's only when the procedural function is called.
When addressed from a route, its cannot be added in as a default, and these 2 lines handle all of the different cases.

Yes, form() is not temporary, it will stay.

Crell’s picture

Just a quick skim through...

+++ b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.php
@@ -0,0 +1,64 @@
+  /**
+   * {@inheritdoc}
+   *
+   * Due to reflection, the argument must be named $_entity_form.
+   */
+  public function content(Request $request, $_entity_form) {
+    return parent::content($request, $_entity_form);
+  }

This seems redundant...

+++ b/core/lib/Drupal/Core/Form/BaseFormInterface.php
@@ -0,0 +1,25 @@
+/**
+ * Provides an interface for a Form that has a base form ID.
+ */
+interface BaseFormInterface extends FormInterface {
+
+  /**
+   * Returns a string identifying the base form.
+   *
+   * @return string|false
+   *   The string identifying the base form or FALSE if this is not a base form.
+   *
+   *   The unique string identifying the form.
+   */
+  public function getBaseFormID();
+

I don't even understand the purpose of this... That may help in coming up with a better name.

+++ b/core/lib/Drupal/Core/HtmlFormController.php
@@ -73,9 +61,34 @@ public function content(Request $request, $_form) {
+  protected function getFormObject(Request $request, $arg) {

It took me 3 reads to figure out that $arg was actually the name of a form. Can we call it something more descriptive, like, $form? $form_def? Something with "form" in it?

tim.plunkett’s picture

This addresses #30, #32, and #36, thanks!

ParisLiakos’s picture

This looks awesome! i think it is very RTBCable.
Just two reallyy minor points:

+++ b/core/lib/Drupal/Core/Entity/EntityFormController.phpundefined
@@ -23,30 +23,70 @@ class EntityFormController implements EntityFormControllerInterface {
+  public function setOperation($operation) {
+    if ($operation) {
+      $this->operation = $operation;
+    }

why is the conditional needed?

+++ b/core/lib/Drupal/Core/Entity/HtmlEntityFormController.phpundefined
@@ -0,0 +1,65 @@
+  /**
+   * {@inheritdoc}
+   *
+   * Due to reflection, the argument must be named $_entity_form. The parent
+   * method has $request and $_form, but the parameter must match the route.
+   */

This inheritdoc wont be parsed, cause it contains additional description..can we have it old style like below?

tim.plunkett’s picture

setOperation may be called later with NULL (happens in the Views UI) and we don't overwrite the old value. Added a line to that effect.

Good point about {@inheritdoc}, fixed that, some wrapping, and some ambiguous variable names.

ParisLiakos’s picture

Edit, Nvm

ParisLiakos’s picture

Status: Needs review » Reviewed & tested by the community

a lot better now:)

tim.plunkett’s picture

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Needs a reroll

error: patch failed: core/modules/shortcut/lib/Drupal/shortcut/ShortcutFormController.php:53
error: core/modules/shortcut/lib/Drupal/shortcut/ShortcutFormController.php: patch does not apply
amateescu’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
97.69 KB

Rerolled.

Status: Reviewed & tested by the community » Needs work
Issue tags: -Entity forms, -Stalking Crell, -FormInterface

The last submitted patch, entity-form-interface-1913618-44.patch, failed testing.

amateescu’s picture

Status: Needs work » Needs review
Issue tags: +Entity forms, +Stalking Crell, +FormInterface
ParisLiakos’s picture

Status: Needs review » Reviewed & tested by the community

back to rtbc

tim.plunkett’s picture

Reroll for patch context conflicts, no actual changes.

tim.plunkett’s picture

Git is smart.
Rerolled after #1820414: CHANGE NOTICE: Move views_ui.module directly into /core/modules/, it figured it all out for me.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, entity-form-controller-1913618-49.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
606 bytes
97.62 KB

LIES.

tim.plunkett’s picture

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Couple of minor nits otherwise looks great...

+++ b/core/includes/entity.incundefined
@@ -468,9 +444,10 @@ function entity_form_id(EntityInterface $entity, $operation = 'default') {
   $controller = drupal_container()->get('plugin.manager.entity')->getFormController($entity->entityType(), $operation);
-  $form_state['build_info']['callback'] = array($controller, 'build');
-  $form_state['build_info']['base_form_id'] = $entity->entityType() . '_form';
-  $form_state['build_info']['args'] = array($entity);
+  $controller->setEntity($entity);

Lets use Drupal::service() here

+++ b/core/lib/Drupal/Core/Entity/EntityFormController.phpundefined
@@ -23,30 +23,71 @@ class EntityFormController implements EntityFormControllerInterface {
     // During the initial form build, add the entity to the form state for use
     // during form building and processing. During a rebuild, use what is in the
     // form state.
-    if (!$this->getEntity($form_state)) {
-      $this->init($form_state, $entity);
+    if (!isset($form_state['controller'])) {
+      $this->init($form_state);
     }

The comment no longer seems to be true...

tim.plunkett’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
1.57 KB
97.89 KB

Roger that.

alexpott’s picture

Title: Convert EntityFormControllerInterface to extend FormInterface » Change notice: Convert EntityFormControllerInterface to extend FormInterface
Priority: Normal » Critical
Status: Reviewed & tested by the community » Active
Issue tags: +Needs change record

Committed 620d497 and pushed to 8.x. Thanks!

dawehner’s picture

Crell’s picture

Issue tags: -Stalking Crell
tim.plunkett’s picture

Title: Change notice: Convert EntityFormControllerInterface to extend FormInterface » Convert EntityFormControllerInterface to extend FormInterface
Priority: Critical » Normal
Status: Active » Fixed
Issue tags: -Needs change record
plach’s picture

I updated the change notice to fix ::getEntity() calls in a couple of examples.

andypost’s picture

Status: Fixed » Needs review

So what is better approach now:
1) $form_state['controller']->getEntity()?
2) $this->entity?

tim.plunkett’s picture

1) is for hook_form_alter()
2) is for your class.

andypost’s picture

tim.plunkett’s picture

Status: Needs review » Fixed
plach’s picture

Looks good to me as well, thanks, just replaced FormInterface with EntityFormControllerInterface :)

Edit: Oh, well, I added another example ;)

tim.plunkett’s picture

Automatically closed -- issue fixed for 2 weeks with no activity.