Problem/Motivation

EntityForm currently does not actually save the entity, but instead makes every single implementation do that on its own.

Proposed resolution

Because core entity forms provide a proper drupal_set_message() and set a form redirect saved code is actually minimal (as it's literally one line that is being replaced with the parent::save() call). However, this change allows to actually use EntityForm as an actual form for development purposes or for custom modules, where an up-to-the-standards UX (i.e. a drupal_set_message() and a form redirect) is not necessarily required.

Many core forms do not yet follow the pattern of having a base form class which are extended by dedicated add and edit form classes. Instead they use the return value of EntityInterface::save() (SAVED_NEW or SAVED_UPDATED) to execute different logic. As this is no longer possible with this change - instead of hacking around the issue - the forms are converted to the new pattern.

As part of that instead of copying outdated code snippets into the new classes, the code is modernized along the way. This includes the following changes:

Current version Replacement
t() $this->t()
l() $this->getLinkGenerator()->generateFromUrl()
node_type_get_label() $this->nodeTypeStorage->load()->label()

These changes are important as reproducing the outdated code as part of the newly added classes would conflict with efforts to obsolete and drop support for said code.

Because entity delete forms of course do not want to save an entity but want to delete it, EntityDeleteFormBase and ContentEntityDeleteFormBase are introduced to centralize the deletion in save(). The confusion caused by a method called save() performing a deletion, is owed to the structure of EntityForm. This could easily be fixed however either as part of this issue or as a separate issue. It is a pre-existing condition, however, even if it is exposed through this issue.

As currently no forms call parent::save() not much would actually break if we did not change the forms. We would only have to fix forms which currently do not overide save() as for them the behavior changes, but those are not many. So if this patch is deemed to large to review and commit in a timely fashion any number of form conversions can be pushed to a follow-up.

Remaining tasks

Write patch.

User interface changes

None.

API changes

EntityForm::save() now saves the entity.

Comments

tstoeckler’s picture

Issue summary: View changes
Status: Active » Needs review
StatusFileSize
new75.75 KB

Here we go. This updates all config entity forms up to C, all content entity forms up to C and all content entity confirm forms. That is roughly a third of all entity forms.

Status: Needs review » Needs work

The last submitted patch, 1: 2312133-entity-form-save-1.patch, failed testing.

tstoeckler’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new131.51 KB

Here we go. Re-rolled after #2225353: Convert $form_state to an object and provide methods like setError() and #2272481: Remove usages of watchdog() from forms, plugins and controllers.

This completes the content entity forms. I didn't touch TaxonomyForm, as it is extended by ForumForm and thus the usual add-edit pattern doesn't really work there. That is, it's totally doable, but it deserves it's own issue, as it is non-trivial and thus needs to discussed, etc. separately.

Let's see.

Status: Needs review » Needs work

The last submitted patch, 3: 2312133-3-entity-form-save.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
StatusFileSize
new11.94 KB
new41.77 KB
new9.71 KB
new124.74 KB
new643 bytes
new117.99 KB

Here we go. This completes the rest of the forms.

I also started fixing a couple things from inspecting the test output above.

Uploading the interdiffs from 1 to 3 as well as from 3 to 5 here as I had forgotte them above. There are multiple interdiffs because of merges in between.

Status: Needs review » Needs work

The last submitted patch, 5: 2312133-5-entity-form-save.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
StatusFileSize
new19.02 KB
new117.5 KB

Finally figured the problem out. I was passing $form_state by reference all over the place but EntityForm::save() does not. PhpStorm could really be more helpful in such cases.

Let's see what's left.

Status: Needs review » Needs work

The last submitted patch, 7: 2312133-7-entity-form-save.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
StatusFileSize
new35.5 KB
new255.13 KB

Here we go. This should be green, or at least almost green. (Yes, I ran all those locally, can I get a cookie... ;-))

I found a view conversions which are in fact not necessary for this issue, I will split those out into separate issues.

Also found a problem with ViewTestBase and XDebug cookie forwarding, will open a separate issue for that as well.
So check the "related issues" block, in case you're interested in that.

Edit: Darn, I forgot -C -M when generating the patch, that's why it's so huge. So don't bother reviewing it (or at least those parts) I will post a proper patch in case this is green.

Status: Needs review » Needs work

The last submitted patch, 9: 2312133-9-entity-form-save.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
StatusFileSize
new89.36 KB
new149.33 KB

Let's see. Crossing my fingers.

Status: Needs review » Needs work

The last submitted patch, 11: 2312133-11-entity-form-save.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
StatusFileSize
new3.82 KB
new151.34 KB

Meh, run in circles much, @tstoeckler?!

Status: Needs review » Needs work

The last submitted patch, 13: 2312133-13-entity-form-save.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
StatusFileSize
new1.35 KB
new151.38 KB

Ahh, so this conflicted with #2278567: Standardize node route names by relationship.
For anyone that still doesn't believe it: This is proof that converting stuff to new APIs while in the context of fixing other stuff (or moving around code, as it is here) is very important and in no way scope-creep.

tstoeckler’s picture

Status: Needs review » Needs work

Here's a self-review now that this passes. I will now also work on a "minimal" version of this patch, which excludes all changes that are not strictly necessary, now that the approach is proven to work for all forms, without much overhead (only 1 form in core needs to override save() to opt out of the saving).

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDeleteFormBase.php
    @@ -0,0 +1,24 @@
    +abstract class ContentEntityDeleteFormBase extends ContentEntityConfirmFormBase {
    ...
    +  public function save(array $form, FormStateInterface $form_state) {
    +    $this->entity->delete();
    
    +++ b/core/lib/Drupal/Core/Entity/EntityDeleteFormBase.php
    @@ -0,0 +1,26 @@
    +abstract class EntityDeleteFormBase extends EntityConfirmFormBase {
    ...
    +  public function save(array $form, FormStateInterface $form_state) {
    +    $this->entity->delete();
    

    Let's actually override actions() to provide a proper delete() submission handler.

  2. +++ b/core/modules/aggregator/src/Form/FeedDeleteForm.php
    @@ -40,8 +40,9 @@ public function getConfirmText() {
    +    ¶
    

    Boo!

  3. +++ b/core/modules/block_content/src/Form/BlockContentAddForm.php
    @@ -0,0 +1,44 @@
    +    $block_type = entity_load('block_content_type', $block->bundle());
    
    +++ b/core/modules/block_content/src/Form/BlockContentEditForm.php
    @@ -0,0 +1,35 @@
    +    $block_type = entity_load('block_content_type', $block->bundle());
    

    Boo!

  4. +++ b/core/modules/comment/src/Form/CommentAddForm.php
    @@ -0,0 +1,13 @@
    +<?php
    +/**
    
    +++ b/core/modules/comment/src/Form/CommentEditForm.php
    @@ -0,0 +1,13 @@
    +<?php
    +/**
    
    +++ b/core/modules/menu_link_content/src/Form/MenuLinkContentAddForm.php
    @@ -0,0 +1,13 @@
    +<?php
    +/**
    
    +++ b/core/modules/menu_link_content/src/Form/MenuLinkContentEditForm.php
    @@ -0,0 +1,13 @@
    +<?php
    +/**
    
    +++ b/core/modules/taxonomy/src/Form/TermAddForm.php
    @@ -0,0 +1,33 @@
    +<?php
    +/**
    
    +++ b/core/modules/taxonomy/src/Form/TermEditForm.php
    @@ -0,0 +1,33 @@
    +<?php
    +/**
    

    Missing empty line.

  5. +++ b/core/modules/shortcut/src/ShortcutSetForm.php
    @@ -64,10 +64,12 @@ public function validate(array $form, FormStateInterface $form_state) {
    +    parent::save($form, $form_state);
    +
         $entity = $this->entity;
         $is_new = !$entity->getOriginalId();
    -    $entity->save();
    

    Order seems important here. It needs to be verified that this still works.

  6. +++ b/core/modules/user/src/RegisterForm.php
    @@ -98,15 +98,13 @@ public function submit(array $form, FormStateInterface $form_state) {
    +    parent::save($form, $form_state);
    ...
         $account = $this->entity;
         $pass = $account->getPassword();
    ...
    -    $account->save();
    

    It needs to be verified that this still works due to the different order.

tstoeckler’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new25.25 KB
new152.03 KB

Here we go.

Next step: Provide a "minimal" patch.

Status: Needs review » Needs work

The last submitted patch, 17: 2312133-17-entity-form-save.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
StatusFileSize
new878 bytes
new152.02 KB

Here we go. ImageStyleDeleteForm::submit() was no longer being called after we renamed the submit handlers in EntityDeleteFormBase::actions().

slashrsm’s picture

Wow. Impressive patch. One nitpik and two questions.

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDeleteFormBase.php
    @@ -0,0 +1,33 @@
    + */
    
    +++ b/core/modules/aggregator/src/FeedForm.php
    @@ -64,7 +64,10 @@ public function validate(array $form, FormStateInterface $form_state) {
    +
    +    parent::save($form, $form_state);
    +
    +    // @ŧodo Split this into dedicated add and edit forms.
         if ($insert) {
    

    More a question... A lot of times I see this @todo-s in codebase and can't find follow-up issues. Do we have any policy about that? DO we open follow-ups when patch with @todo goes in or it is more relaxed kind of thing ("somebody should do it at some point in time" kind of thing)?

  2. +++ b/core/modules/aggregator/src/Form/FeedDeleteForm.php
    @@ -40,8 +40,9 @@ public function getConfirmText() {
    +  public function delete(array $form, FormStateInterface $form_state) {
    +    parent::delete($form, $form_state);
    +    ¶
         $this->logger('aggregator')->notice('Feed %feed deleted.', array('%feed' => $this->entity->label()));
    

    Whitespace.

  3. +++ b/core/modules/comment/src/Form/CommentAddForm.php
    @@ -0,0 +1,14 @@
    +/**
    + * Provides a form for adding comments.
    + */
    +class CommentAddForm extends CommentFormBase {
    +}
    diff --git a/core/modules/comment/src/Form/CommentEditForm.php b/core/modules/comment/src/Form/CommentEditForm.php
    

    What is the idea behind having empty classes?

tstoeckler’s picture

Status: Needs review » Needs work

Thanks for the review!

1. Yeah, I was afraid someone would ask for that. Will open some follow-ups :-)

2. Noooo!!!!!!!!!! Will fix

3. Yeah, that's kind of weird. CommentForm can't be really be split as is because it doesn't provide any specialized messages or so. So I think it makes more sense to just leave those out and have both the 'add' and the 'edit' operation point to the same class (CommentForm).

marcingy’s picture

+1 on this idea. I am wonder if stuff such as

- *       "add" = "Drupal\block_content\BlockContentTypeForm",
- *       "edit" = "Drupal\block_content\BlockContentTypeForm",
+ *       "add" = "Drupal\block_content\Form\BlockContentTypeAddForm",
+ *       "edit" = "Drupal\block_content\Form\BlockContentTypeEditForm"

ie moving files around should be done in a different patch to reduce the size of the patch some what.

berdir’s picture

+++ b/core/lib/Drupal/Core/Entity/EntityDeleteFormBase.php
@@ -0,0 +1,35 @@
+/**
+ * Provides a generic base class for an entity deletion form.
+ *
+ * @ingroup entity_api
+ */
+abstract class EntityDeleteFormBase extends EntityConfirmFormBase {

Is there any chance that we could make this an implementation that just works instead of an abstract class?

Not strictly related, agreed, but we're changing a lot already, and we have *many* of those delete forms that are all very similar, and mostly differ in slightly different messages and cancel/confirm redirect behavior and watchdog/dsm() messages.

Confirm messages already have a pattern for putting many things in different methods to make it easy to override them, we could extend that.

See also #2316171: [meta] Improve DX of entity defining (if you want a UI), which already links to this issue.

We could push this to a separate issue, but then it might be easier to extract all the delete related stuff from this issue and do that first, instead of changing all those classes twice?

tstoeckler’s picture

Status: Needs work » Needs review
StatusFileSize
new49.74 KB
new160.21 KB

Fixed #20. See the related issues for the follow-ups I opened.

Had a stab at #23, that was a really great suggestion! Let me know what you think.

Re #22: I still on plan on providing a "minimal" version of this patch, but the problem is that many forms currently check the return value of $this->entity->save() to decide whether it is adding or updating an entity. If we want to utilize parent::save() that does not work anymore. So instead of replacing one workaround with another workaround with another one I'm fixing things properly. In the "minimal" patch I will just leave as many save() functions as possible untouched.

Status: Needs review » Needs work

The last submitted patch, 24: 2312133-24-entity-form-save.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
StatusFileSize
new328.62 KB
berdir’s picture

Looks like the last patch is a diff against an older branch?

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php
    @@ -0,0 +1,73 @@
    +   */
    +  public function getCancelUrl() {
    +    return new Url($this->entity->urlInfo());
    +  }
    

    That looks wrong, urlInfo() already returns a Url object.

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php
    @@ -0,0 +1,73 @@
    +   */
    +  public function actions(array $form, FormStateInterface $form_state) {
    +    $actions = parent::actions($form, $form_state);
    +    $actions['submit']['#submit'] = array(array($this, 'delete'));
    +    return $actions;
    +  }
    

    Make sure you follow #2309323: Allow #submit and #validate to be specified as methods of the form object, that will allow us to simplify the #submit's. and #validate's. Will probably conflict quite a bit with this, sorry in advance :)

  3. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDeleteForm.php
    @@ -0,0 +1,73 @@
    +    $form_state->setRedirectUrl($this->getCancelUrl());
    

    This seems problematic, after deletion, it doesn't make sense to go back to the list, but we unfortunately currently don't have a link annotation for this :(

    Maybe we can use the new standardized routes for this, assuming that they actually updated that one too? Checking.. Nope, they did not, obviously that would have made too much sense ;)

  4. +++ b/core/lib/Drupal/Core/Entity/EntityForm.php
    @@ -281,7 +281,7 @@ public function submit(array $form, FormStateInterface $form_state) {
       public function save(array $form, FormStateInterface $form_state) {
    -    // @todo Perform common save operations.
    +    $this->entity->save();
       }
    

    Instead of needing different forms, can we save the return value somewhere, something like $this->saveStatus = $this->entity->save() or something?

Status: Needs review » Needs work

The last submitted patch, 27: 2312133-27-entity-form-save.patch, failed testing.

tstoeckler’s picture

So I recently saw #2022875: Resolve difference between submitForm(), submit(), and save() in EntityFormController and that it is making a lot of the same changes as here, which demotivated a bit to work on this issue any further... :-)

Let's see if that gets in anytime soon.

berdir’s picture

That got in :)

Re-roll is going to be painful I think...

Can give it a try, but as mentioned, looks like the last patch includes unrelated changes. Is that just a re-roll of the previous one?

berdir’s picture

Ok, worked on rerolling this, for save, there really isn't much left other than all the form subclasses, which I don't really like. But I do like the entity delete forms, so I extracted those and started with just those in #1728804: Introduce (Content)EntityDeleteForm and children to handle entity deletions.

I'd suggest to close this as a duplicate.

andypost’s picture

Maybe better to separate save() and delete() as form methods here?

berdir’s picture

Status: Needs work » Closed (duplicate)

@andypost: no idea what you meant.

Closing this as suggested, I think there is nothing left here not covered by those other issues.