Problem/Motivation

We want to support service callbacks as form callbacks such as submit and validate, so that they can use DI and live in services.

Steps to reproduce

N/A

Proposed resolution

Use the callable_resolver if it is available to resolve the callback.

Remaining tasks

N/A

User interface changes

N/A

Introduced terminology

N/A

API changes

Form api callbacks can now use PHP callables that the CallableResolver can resolve.

Data model changes

N/A

Release notes snippet

TBD

Issue fork drupal-3536726

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

berdir created an issue. See original summary.

berdir’s picture

Status: Active » Needs review

Created a quick proof of concept.

All form callbacks go through FormState::getCallback(), which is nice in that it gives us a single place to adjust, but FormState isn't something we can inject, so we have to use Drupal::service() which I absolutely expect will break some unit tests.

Might want to deprecate this and add something else.

nicxvan’s picture

Yep a lot of container not initialized yet failures.

kim.pepper made their first commit to this issue’s fork.

berdir’s picture

Status: Needs review » Needs work
kim.pepper’s picture

Added #3537261: Move file form hooks to FileFormHooks class for the hooks cleanup mentioned above.

kim.pepper’s picture

Status: Needs work » Needs review

Fixed the failing test by handling the exception. Not sure if this is the right place to handle it.

kim.pepper’s picture

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

kim.pepper’s picture

Status: Needs work » Needs review

Rebased on 11.x

nicxvan’s picture

Issue summary: View changes

This needs a fleshed out issue summary and CR. I'll work on the CR.

I tested this in a few other places and it works so I think the code is good to go.

Edit: CR needs work and some more examples I think.

nicxvan’s picture

Status: Needs review » Reviewed & tested by the community

Spent some time thinking about it and pulled an example from this MR and clarified a bit.

It's possible that it needs some additional examples, but I'm not sure that will make it clearer.

I think this would be good to get in sooner so we can show examples.

With OOP hooks I'm seeing more and more people including myself inadvertently add [$this, 'callback']

berdir’s picture

I'm still not entirely sure in a good state and the current approach is cheating a bit, because FormState is a value object that we can't inject into. So we get DI and access to services by accessing the global container. See merge request discussion for the alternative, which is injecting CallableResolver into the 6 different services that call prepareCallback() and call it separately everywhere.

nicxvan’s picture

Status: Reviewed & tested by the community » Needs work

kim.pepper’s picture

I spent a couple of hours looking at how we could use CallableResolver and avoid calling it in FormState.

I deprecated \Drupal\Core\Form\FormStateInterface::prepareCallback() and looked at where it was being called from.

Obvously a lot of test fails 😬 but could be a good first step.

I think a lot of the errors are because we no longer have the context of the FormState instance.

kim.pepper’s picture

Status: Needs work » Needs review

OK. I think this is ready for reviews again.

I went down the path of trying to add CallableResolver to entity form constructor and every sub-class constructor. I think that would be very disruptive. All the unit tests would need to be updated etc. I ended up with checking if the container was available.

Not ideal that Entity form calls a service as it should be a value object like other forms.

kim.pepper changed the visibility of the branch 3536726-use-callableresolver-for to hidden.

kim.pepper’s picture

Squashed commits and rebased on 11.x

nicxvan’s picture

Status: Needs review » Reviewed & tested by the community

I think this is ready now, the feed back looks like it's all been addressed.

Took another pass through and didn't see anything else that stood out.

joachim’s picture

> Using $this as a pointer for form callback [$this, 'someMethod'] is an antipattern and should be avoided.

Instead of just saying it's bad, we just not allow it and throw an exception for it?

What's the reason it's bad though?

berdir’s picture

I don't see how we could disallow it, it's a valid callable reference, we have no way to decide which objects are allowed and which aren't in callbacks.

it's bad because if you do that inside a hook class for example, you serialize that object including its dependencies (hook services are regular services and do not use DependencySerializationTrait), that might include non-serializable objects such as the database connection if you inject that. Even when it works, it creates duplicate objects on re-initializing and uses extra time for serialization and unserialization and storage.

joachim’s picture

We could just disallow an object in that notation that isn't a Form, but I see plugins do it:

      $form['expose_button']['button'] = [
        '#limit_validation_errors' => [],
        '#type' => 'submit',
        '#value' => $this->t('Expose sort'),
        '#submit' => [[$this, 'displayExposedForm']],
      ];
nicxvan’s picture

We missed the 11.3.0 window so I updated the deprecation messages to 11.4.0

nicxvan’s picture

Rebased and bumped back down to an 11.3 deprecation since @longwave indicated we might be able to get this in in the next 12 hours.

Keeping an eye on tests.

longwave’s picture

Version: 11.x-dev » 11.3.x-dev
Status: Reviewed & tested by the community » Fixed

Yeah I think this is a nice feature to have for 11.3.x.

Committed and pushed d3a9ff853ca to 11.x and 1f5b3ee85cc to 11.3.x. Thanks!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

  • longwave committed 1f5b3ee8 on 11.3.x
    task: #3536726 Use CallableResolver for form callbacks
    
    By: berdir
    By:...

  • longwave committed d3a9ff85 on 11.x
    task: #3536726 Use CallableResolver for form callbacks
    
    By: berdir
    By:...

kim.pepper’s picture

Woo! 🎉

Updated the CR to add more detailed explanation.

joachim’s picture

   * Checks if the service container is available and uses the callable
   * resolver service to get the callable from the definition. Otherwise, it
   * returns the definition as is.

The CR should say that there's this special case too.

kim.pepper’s picture

Updated CR

Status: Fixed » Closed (fixed)

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

smustgrave’s picture

Fyi I opened #3574721: FormBuilder service order causes failure, when the deprecation for this is removed then a non required service is followed by a required

adam-vessey’s picture

This causes some confusing errors where forms might've been referencing non-public methods for a callback. With a protected/private callable now being passed outside and tested with an `is_callable()` by the `CallableResolver`:

- the `is_callable()` test fails: https://git.drupalcode.org/project/drupal/-/blob/11.3.x/core/lib/Drupal/... , as you are outside of a scope where the method is visible.
- building the exception message fails due to trying to implode the form instance and name of the method: https://git.drupalcode.org/project/drupal/-/blob/11.3.x/core/lib/Drupal/...

Uncaught PHP Exception Error: "Object of class {class name} could not be converted to string" at /var/www/html/web/core/lib/Drupal/Core/Utility/CallableResolver.php line 69

Might it make sense to short-circuit earlier, with an `is_callable()` check from inside the class, before calling out for external resolvers, in https://git.drupalcode.org/project/drupal/-/blob/11.3.x/core/lib/Drupal/... ?

berdir’s picture

I'm not sure I understand. the methods were always invoked externally, so is just that the error being displayed is now bad/confusing or did it break something that used to work?

Can you create a follow-up with example code that triggers this? This is committed and released and will not be reopened.

adam-vessey’s picture

@berdir

Yeah, it's breaking something that used to work.

> the methods were always invoked externally

Not quite true; though, might just be in `EntityForm::buildEntity()` that it is possible, and I just happened to run into it. Per the diff https://git.drupalcode.org/project/drupal/-/commit/1f5b3ee85cc272e9f2115... , the `call_user_func_array([...])` still has access to private/protected methods of the containing class (so, `EntityForm` or any descendants). The move to call with `$callable([...])` should still work, but because it now first tries to pass the callable out to the `CallableResolver` which then doesn't have permission to call the callable, we get that error (from #38) bubbling out of the `CallableResolver` when trying to throw the exception about an array not being a valid callable.

As for code reproducing, anything extending `EntityForm`, with a callback in `#entity_builders` that points at a protected/private method.

berdir’s picture

Ah I see, for entity builders, that's true. That is a regression.

Again, to improve something, a new issue will need to be created. We could maybe add some checks on something being an object and not going through the callable resolver. then at all, assuming we already have a valid callable. Or just call is_callable() on it inside EntityForm.

adam-vessey’s picture

Yeah, just an early-returning is_callable() seems most simple. Threw together https://www.drupal.org/project/drupal/issues/3578661 with an accompanying MR making it so.

Thanks!