Skip to content

[RFC] [Form] Add Partial Bind Support #5576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from

Conversation

ericclemmons
Copy link
Contributor

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes
Fixes the following tickets: #1341
Todo: [list of todos pending]
License of the code: MIT
Documentation PR: Not yet

After much discussion in #1341, I've added the ability for FormType to support binding partial data via ignore_missing when client data is not set:

  1. When false, the current behavior of binding null onto the form takes place
  2. When true, the original model data (which is transformed into view data) is bound to the form (as if the user chose the existing value)
  3. When null (or not explicitly set), the parent form's ignore_missing value is used, and then follows option 1 or 2.

I'm still considering a way to turn on ignore_missing when calling $form->bind($request) (and the request is PATCH), but this can be done when creating the form:

$builder->create('form', null, array('ignore_missing' => 'PATCH' === $request->getMethod());

if (!$child->isValid()) {
if (!$this->config->getPartial() && !$child->isValid()) {
return false;
} elseif ($this->config->getPartial() && $child->isBound() && !$child->isValid()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use if, not elseif as the previous if returns

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, I don't think this makes much sense: the validator would still validate the object graph so it could add errors on these children. So it should be taken into account for isValid too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know, I wasn't sure how to deal with that. Personally, I kind of want the child to still be validated even if the parent skips binding. It's not the child's fault data hasn't been bound. This way, $child->isValid() would throw You cannot call isValid() on a form that is not bound..

So should I make it so the graph walker skips this child? This wouldn't make the child valid, but at least it wouldn't have any errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, we should not make the GraphWalker skip this child. It does not make any sense to forbid validating some values because the form has not changed them (what if you have a constraint validating that a property must be bigger than another one and only one of them is bound ?)

IMO, a better solution would be to bind all child forms, but using the existing data instead of null when the field is not submitted an partial is allowed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear, you're saying to use the existing model's data when there is missing submitted data?

Let's say the existing model has an Author. The form will have something like <option name="form[author]" value="1" selected />. If we did a partial bind and omitted &author=1, we would have to do the equivalent of:

$author = $this->config->getData()->getAuthor();
$authorId = doSomeTransformOnTheEntity($author);

$child->bind($authorId);

In short, reproduce the setData part as if the user actually provided the transformed, submitted data?

@housebolt
Copy link

+1 for solution 1, I used this same concept in my own Form extension.

I think the semantic 'partial' might be a bit confusing for people first coming to the Form component. Something like 'allow_partial_update', or 'allow_partial_bind' might be better IMO.

@ericclemmons
Copy link
Contributor Author

You're right. partial still sounds like something templating-related to me :)

@bschussek Recommended ALLOW_MISSING or IGNORE_MISSING, which @merk seconded.

@housebolt Because Stof suggested that I still bind with existing model data, IGNORE_MISSING works well with that. If we're all in agreement, I'll go ahead & find/replace this sucker right away!

@housebolt
Copy link

I do have one use case where I have a field that is an embedded form that needs full binding (if it exists in the submitted data array) while the rest of the form fields can be partially bound. Is there a way that ALLOW_MISSING or IGNORE_MISSING can handle this case? Or is there a better way to work around that on my end?

@ericclemmons
Copy link
Contributor Author

@housebolt Can you describe this a bit more for me? I'd like to build a test around the scenario, but I don't fully comprehend it. Perhaps you can give an example of how the form is setup (via the types) and what submitted data you expect & how it behaves. Thanks!

@housebolt
Copy link

I think I figured out a better way to handle it on the controller or handler side, if you do something like this:

$form; // FormType
$model; // Model

if ($this->get('request')->request->has('child')) {
    $model->setChild(new Child());
}

$form->setData($model);
$form->bind($this->request);

Then the form's view data won't contain the child data and when it's merged it won't validate if it's a partial bind. After some thought on the subject, my particular use case is pretty fringe. I tried coming up with a generic use case that could generally apply but couldn't.

@stof
Copy link
Member

stof commented Sep 25, 2012

@ericclemmons the partial option should be inherited from the parent form by default IMO, so that you can apply it on the whole form (and you can force some children to always bind fully by passing the option explicitly to false).
Your current implementation forces to define it to true explicitly for each child when you want the whole form to bind partially.
I think binding the whole form partially is more common than binding only the root partially and then forcing each child to be fully bound.

@PsySecCorp
Copy link

Looking at this code, this is exactly what I wanted to do, but I wanted to bind it to each field. Binding it to the form type does make sense if you are assuming that an form submission matches the entity directly.

However, for additional flexibility, one could allow a 'incumbent' value that gets used if the entity property does not match a form submission. For example:

<?php

class CampaignJobType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', 'text', array(
                'incumbent' => $options['data']->getName()
            ))
        ;

    }

This allows a user to choose if they want to use a value from an old entity (Found in $options['data']) or another value if they choose when the form fails to submit an entity property.

This allows for more flexibility, especially with REST APIs. Furthermore, it will make you compliant with RFC 5789 (http://tools.ietf.org/html/rfc5789) which, in its current state, the Forms are not.

@stof
Copy link
Member

stof commented Oct 13, 2012

@ericclemmons can you update your work to inherit the partial flag from the parent form by default ?
And then, a PR to the doc will be needed.

@ericclemmons
Copy link
Contributor Author

@stof Sure, I almost had that working, until I spoke with @bschussek about difficulties I was having. So, I moved some of the logic into Form.php (similar to isDisabled) and still have some work to do.

@@ -448,6 +453,14 @@ public function getCompound()
/**
* {@inheritdoc}
*/
public function getIgnoreMissing()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getter needs to be added in the interface too

@stof
Copy link
Member

stof commented Oct 14, 2012

@ericclemmons please squash your commits to remove the reverted commits from the history.

@ericclemmons
Copy link
Contributor Author

Sonofa. Rebased too much :)

@ericclemmons
Copy link
Contributor Author

All cleaned up now. RFC...

@celmaun
Copy link

celmaun commented Nov 9, 2012

@fabpot Use case: PATCH requests to a RESTful API


$this->assertEquals('app', $form->getData());
$this->assertEquals('norm', $form->getNormData());
$this->assertEquals('client', $form->getClientData());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getClientData is deprecated in favor of getViewData

@webmozart
Copy link
Contributor

@ericclemmons Thank you for the tremendous work you are doing on this so far. I like the direction that this is heading.

Some remarks:

  1. I would prefer the naming "allow_missing" over "ignore_missing" since this is consistent with the corresponding option in the Collection constraint.
  2. You currently do the check on a field by field basis. In other words, when a field is bound, it checks itself whether it can be missing. I think this should rather be handled in the parent form, that is, if a form has the "allow_missing" setting, any fields that are not included in the request will not be bound. This way you can effectively check with $form->get('someField')->isBound() whether the field has been submitted or not. Also, not binding a field improves overall performance.
  3. For 2. to work, isValid() has to be adapted, because it accesses isValid() on a form's children, which throws an exception if a field is not bound. This can be solved simply by checking isBound() before checking isValid().

At last, we had the two alternatives of specifying "allow_missing" as form option vs. as parameter to the bind() method. I like the way it is implemented now, but I am not sure whether we exclude use cases where a form is built and only at a later point in the code it is decided that the form should only be bound partially. Can you think of such use cases?

I would appreciate a listing of pros and cons of specifying the setting as an option vs. as parameter to bind(), so that we can take an informed decision and know about the drawbacks that we introduce.

Otherwise, great progress 👍

@webmozart
Copy link
Contributor

that is, if a form has the "allow_missing" setting, any fields that are not included in the request will not be bound.

Consequently we probably also need to adapt PropertyPathMapper to exclude unbound fields from the reverse mapping.

@Tobion
Copy link
Contributor

Tobion commented Nov 9, 2012

The name allow_missing is not good because currently a form also "allows" missing values and simply sets them to null. So allow doesn't express anything new. The name should express that missing values are not bound (ignored).

@ericclemmons
Copy link
Contributor Author

I'm very sorry for the delay on this. After over a month of considering this, we're aware of the following :

  • Documentation of the new option & change of behavior is required.
  • Cornerstone use-case is HTTP PATCH requests.
  • Naming the option something related to the bind process will introduce BC issues & behavior confusion, since bind may be changed to submit + process in the future.

With these in mind, I would like to revert f14d753 (where I named the option ignore_missing and then we discussed allow_missing) and use either:

  • partial (the original & my personal preference)
  • patch (after the HTTP method)

The option should not describe how it works (which allow_missing, ignore_missing, bind_missing, etc. did), but should describe what it does, which is supporting PATCH requests by binding partial data.

We use virtual, which admittedly doesn't describe what the heck it does, but you make the connection after reading the docs. This would be no different. No matter how the form internals change, partial or patch will have the same expected behavior.

I think it's time we nail down a decision & move on, especially since I have extra time over the Christmas break :)

@celmaun
Copy link

celmaun commented Dec 12, 2012

+1 for partial since it's the most self-describing IMO. There are use-cases for this other than PATCH requests so I don't think patch will be a good choice.

Edit: Or how about partial_bind? No confusion with partial templates ;) Sorry - overlooked 3rd bullet point in Eric's comment above (late night).

@alex88
Copy link
Contributor

alex88 commented Dec 12, 2012

The option as passed as the third argument to the form builder right? So we can set it per form and not only globally?

@stof
Copy link
Member

stof commented Dec 12, 2012

@alex88 it can be set for each form (not only the root of the form tree) and it is inherited from the parent by default

@ericclemmons
Copy link
Contributor Author

@salmanpk I was curious about the template confusion as well, but Symfony doesn't really have a partial template convention or renderPartial Twig method to be confused with. (When I used ZF, partials actually got their own folder, though Symfony leaves partial template organization up to the user).

I do recommend (per my 3rd bullet up above) that we not use _bind, _submit or anything indicative of the form API, as there are pending BC discussions about that.

I'll give it another day for comments to roll in, then continue with this feature & other updates...

@webmozart
Copy link
Contributor

@ericclemmons I just became aware of a major problem with the implementation as option. We currently use the sugar

$form->bind($request);

to hide away the extraction of request data from a form. What happens if the logic behind this sugar discovers that the request is a PATCH request? It can't change the partial option anymore.

So the only real solution seems to be your original proposal with an optional argument to bind() (I don't like an extra method in the interface):

public function bind($submittedData, $partial = false);

@ericclemmons
Copy link
Contributor Author

I was just thinking that we'd not lock that option and allow it to be modified by the listener. Otherwise, we would have to modify the method signature and I agree, it's not pretty =/

@webmozart
Copy link
Contributor

Was this a convoluted way of saying "I agree"? :) I'm sorry, I don't understand what you just said..

@ericclemmons
Copy link
Contributor Author

It was my convoluted way of saying "what about allowing modifications to the FormConfigBuilder?"

ericclemmons@f14d753#L1R806

Is that wrong?

@webmozart
Copy link
Contributor

Yes, this is wrong. Changing the FormConfig (note that form returns a FormConfigInterface instance, not a FormConfigBuilderInterface instance) has the same effect as changing an option later on. FormConfig is basically just a storage for the options.

@ericclemmons
Copy link
Contributor Author

In that case, it looks like we have an architectural limitation with changing a form option after a form has been built: bind($request) will be too late to change 'partial' => true.

Maybe we have two options:

  1. RequestExtension extends AbstractTypeExtension for FormType that, when using PATCH, will use $resolver->overload('partial', ...) and default the value to true rather than false (unless there's a user-defined value).
  2. Allow a request option on FormType that, when provided, would behave the same was as (1), but automatically bind($request) the form after creation. (There would need to be a FormEvents::CREATE for that behavior to exist).

1 fits in better with the existing architecture, while 2 would allow for all-in-one form creation + binding...

$form = $factory->create('user', $user, array('request' => $request));

if ($form->isValid()) {
    ...
}

@mvrhov
Copy link

mvrhov commented Dec 17, 2012

Option 2 won't work for those of us who use DIC to create the forms...

@ericclemmons
Copy link
Contributor Author

Yikes, you're right. Looks like there's a lot of bending over backwards to make something like BindRequestListener work with a configured option.

@webmozart
Copy link
Contributor

Why can't we add an argument to bind() as I proposed?

@ericclemmons
Copy link
Contributor Author

So with the 2nd ambiguous argument on bind(), is there a need for a partial option anymore?

@webmozart
Copy link
Contributor

No

@ericclemmons
Copy link
Contributor Author

Ok, so the ability to force complete binding would be removed per @stof's suggestion.

@alex88
Copy link
Contributor

alex88 commented Dec 17, 2012

Why bindPartially() cannot be used?

@ericclemmons
Copy link
Contributor Author

@alex88 "I don't like an extra method in the interface" - @bschussek

Even with the extra interface, internally bind would still be called to handle the majority of the logic. I believe this is what the suggestion would look like:

$form = $factory->create('user', $user);
...
$form->bind($request, $request->getMethod() === 'PATCH'));

@alex88
Copy link
Contributor

alex88 commented Dec 17, 2012

@ericclemmons oh sorry missed that

@webmozart
Copy link
Contributor

@ericclemmons 👍

@mvrhov
Copy link

mvrhov commented Dec 17, 2012

I'd also vote for additional parameter to bind

@ericclemmons
Copy link
Contributor Author

I'll update this branch and remove all the sexiness from it then. Kind of a shame, since this is what I figured we would do back in #1341 days...

@ruimarinho
Copy link

For those that are looking forward for this PR for a seamless Backbone.js integration, I thought I'd chime in to let you know that it is now officially supported as of v0.9.9 (released a few days ago) by passing model.save(attrs, {patch: true}).

@ericclemmons
Copy link
Contributor Author

@bschussek Man, I was thinking about this more and I just don't feel right about it as a parameter for bind(). BindRequestListener still wouldn't be able to force partial binding with PATCH, would it?

Just for comparison...

1 - As an Option (PR):

$form = $factory->create('user', $user, array('partial' => $request->getMethod() === 'PATCH'));
$form->bind($request);

2 - As a bind() parameter (suggested):

$form = $factory->create('user', $user);
$form->bind($request, $request->getMethod() === 'PATCH');

They look pretty similar, no? The difference I envisioned was in the ability for other types & extensions to explicitly set/override partial during the buildForm process. As a parameter for bind(), these extensions wouldn't be able to specify that behavior at all.

If a parameter on bind() is really the architecture you want, I'll go ahead & close this PR and we can have a new PR for that way.

@webmozart
Copy link
Contributor

@ericclemmons First of all, I'm sorry for the lost work. It just was not clear from the beginning that an option would not be feasible.

Second, don't worry about BindRequestListener because it will soon be replaced by RequestProcessor that will call bind() directly instead of modifying event data (see #5493).

@webmozart
Copy link
Contributor

The problem with your comparison of points 1. and 2. is that in both cases you have to check for the PATCH method yourself. My goal is a usage like this (again, see #5493):

$form = $this->createForm('my_type');

if ($form->process($request)->isValid()) {
    // save
}

This is not possible with "bind_partial" as option.

@ericclemmons
Copy link
Contributor Author

Well, crap. I was making this work for the current version of the form component, not with #5493 in mind, since that drastically changes how this sort of functionality would work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.