Working on field form tests, I just discovered and removed a hardcoded call to node_form_submit_build_node() in field_add_more_submit(), the submit handler used by the 'add more' button when JS is disabled.
That call, and the related code comment, were taken straight out of poll_more_choices_submit() :
// Set the form to rebuild and run submit handlers.
node_form_submit_build_node($form, $form_state);
At first sight, it seems that just replacing this call by $form_state['rebuild'] = TRUE;
does the trick. This makes me wonder, however, if this has us miss some special magic required for node forms (and if so, whether forms for other entities will possibly need their own specific magic too).
Advice from AHAH / FAPI gourous would probably be welcome here...
Comment | File | Size | Author |
---|---|---|---|
#69 | drupal.entity-form-367006-68.patch | 20.79 KB | yched |
#60 | drupal.entity-form.patch | 14.42 KB | fago |
#52 | drupal.entity-form.patch | 16.69 KB | fago |
#46 | drupal.entity-form.46.patch | 14.74 KB | sun |
#35 | drupal.entity-form.35.patch | 13.07 KB | sun |
Comments
Comment #1
yched CreditAttribution: yched commentedHm, turned out to be a tricky one.
We do need that call to node_form_submit_build_node(). Without it, all edits are lost and the form get reset to the current saved state of the node.
Now, of course we cannot hardcode anything specific to node in there.
This means that all fieldable entities (at least the ones that have forms) will need to:
- Tell us what's their function to build an object from submitted values.
I added one to field_test.module so that form tests can run. user.module currently doesn't have such a function, which means that the 'add more' button currently doesn't work without JS on user forms.
For now I let fieldable modules put that information in $form['#builder_function'] (we'd probably need a better name).
We could also chose to have this information in hook_fieldable_info(), or even settle on an implicit naming convention (like menu_get_object relies on a *_load() naming convention).
- Have their forms ready for multistep rebuilding, like node currently does (because it has previews) through node_form_submit_build_node() and $form_state['node'].
Bottomline is, if you are fieldable, then your forms are multistep forms. You'll want previews at some point anyway :-p.
That's a few more constraints for fieldable entities than what we had so far, but I'm afraid that's the price to pay for a generic 'add more' form element that needs to appear in various form workflows it doesn't control.
The overhead is not that terrible, as the fairly limited impact on field_test.module shows. It also encourages good practice : have a function build your $objects from form values. Except of course adapting this on an already established form workflow like the one in user.module might not be as simple...
Leaving open for comments and feedback, and for the remaining task of moving user.module to the right form workflow.
Comment #2
yched CreditAttribution: yched commentedAfter #367567: Use AJAX framework for "Add more" / load include file containing form / introduce $form_state['build_info'], field_add_more_submit() is now used for both JS and non-JS 'add more'.
Comment #3
sunHoly cow. Just tested by adding a multiple field to users, disabling JS, going to user/1/edit, clicking add more, nothing.
Comment #4
sunBetter title, tagging.
Comment #5
sunI'm preparing this over at #118345: Revamp hook_user_form/_register/_validate/_submit/_insert/_update.
Maybe I'm getting field_attach_submit() wrong. If I do, then please replace "field_attach_submit" with "#builder_function" in the following.
1) field_attach_form() should add $form['#submit'][] = 'field_attach_submit'. Killing manual invocations of that function.
2) field_attach_form() should add $form['#object_type']. Therefore, passing $object_type to field_attach_submit().
3) field_attach_submit() can then be abstracted:
Hah! I see now that field_attach_submit() is in that snippet I copied/merged from node_form_submit_build_node() and comment_form_submit_build_comment()... so I should have another look at the actual workflow.
Anyway, you get the point, hopefully ;)
Comment #6
sunok, what I don't get is: Why does http://api.drupal.org/api/function/node_form_submit/7, resp. http://api.drupal.org/api/function/node_form_submit_build_node/7, needs to invoke form submit handlers manually?!
Comment #7
yched CreditAttribution: yched commentedre #6: I wondered the same myself, but couldn't really figure it out :-(
re #5: Any idea to streamline this part for 'fieldable entities' modules is really welcome. Thanks for diving in !
- While I like the idea, isn't it a bit odd to have field module's submit handler take care of submitting and building the $object ?
-
$submit_callback = $object_type . '_submit';
: do you really think we should hardcode this, or is this just stub code ?- this still leaves fieldable entities with the job of merging in the incoming
$form_state[$object_type]
object at the beginning of their _form() function. The fact that there's such a thing as$form_state[$object_type]
to account for would now be 'hidden' inside a field API function. Not sure how / if this can be streamlined too.Comment #8
yched CreditAttribution: yched commentedre my own #7 : "While I like the idea, isn't it a bit odd to have field module's submit handler take care of submitting and building the $object ?"
Actually, I'd say the proper place to abstract this is at 'entity API' level, not 'field API' level...
Comment #9
fago#6: Read http://drupal.org/update/modules/5/6#nodeapi-presave. e.g. upload module uses that. Thus the submit button has its own submit handler, then invoking form level submit handlers. Probably it would be cleaner to don't use #submit, but make #builder_function a concept "#entity_builder" or so taking more handlers.
>Actually, I'd say the proper place to abstract this is at 'entity API' level, not 'field API' level...
Agreed!
@general workflow:
I don't see why we have to merge in values at all. I think we should load the entity initially and then pass it around modifying the same entity, till saving it at the end. Thus when there is an entity in form_state, use it instead of the fresh one.
While that sounds like it makes sense to put the entity $form_state['storage'], that would automatically turn on form rebuilds. Putting the entity in $form['#entity'] instead would be more convenient as it gets cached, but doesn't turn on rebuilding.
Update: We could use $form['#. $entity_type ] - as we already have that in many places. ($form['#node'], $form['#user'], ..). So we could use that entity, update it with the form values and save it once the form workflow is finished.
Comment #10
Frando CreditAttribution: Frando commentedVery true, and the attached patch does exactly that.
It abstracts form submitting into the EntityController classes and also adds a handy helper function to properly and easily enable multistep on entity forms. It sets rebuild to true and builds a proper entity out of the current form values which it then stores in $form_state['ENTITY_TYPE'] so that it can be used by the form callback when rebuilding. I implemented this for nodes, comments and taxonomy terms. Users should be easy to do. I also adopted node and comment preview to use it.
I also added extensive comments and a TODO about the two lines in which node.module calls the form submit handlers. Took me a while to figure it out, it is ugly and should be removed but not in this patch I think.
Also, submitFormValues now *always* returns an object for *all* entity types, so a bit less array vs object confusion. Inside the forms though the entities still differ, node keeps objects while comment converts to an array. This is also a simple followup issue then, because now $form_state['ENTITY_TYPE'] simply is always an object, as is the $node or $comment argument in the form callback. DX++.
I tested it extensively for node (previews and add more work fine with and without js), taxonomy term add more buttons work without JS but not with JS but that didn't work before either. Comment form incl. preview worked without fields, didn't test field add more there yet. So this is likely still CNW and I might not have much time to work on this. But I quite like my patch, and I really love how field API forces us to finally standardize some of our APIs..
Comment #11
Frando CreditAttribution: Frando commentedComment #13
sunsubmitFormValues() and submit() somehow look "replaced" here...
submitFormValues() is actually what submit() should be.
And submit() looks more like a convertFormValuesToObject() or whatever to me.
I may be wrong.
But let's make sure we keep "submit" as the "submit" we all know...
We can and should directly invoke the entity controller here.
Form submit handlers should be used for entity saving. If I read right in the past days, then I think I saw plenty of code in form.inc that assumes that a form is only ever really submitted when the primary submit button/handler of a form is triggered.
This review is powered by Dreditor.
Comment #14
fagoWrong variable name?
What about using $form['#'. $entity_type ] as described in #9?
The workflow would look like:
1. Initialize $form['#'. $entity_type ]
2. On submit pass on $form['#'. $entity_type ] to the next step in $form_state['#'. $entity_type ]
3. On rebuild, initialize $form['#'. $entity_type ] with $form_state['#'. $entity_type ]
4. On final submit, save $form['#'. $entity_type ].
-> I implemented that in the patch from http://drupal.org/node/301071#comment-2091868
Thus we
- never loose any changes even if we have multiple totally different steps
- don't need to load the entity again on rebuild
- save the "data merging" step
@$term = (object) $form_state['values'];
Do we still need that? The field API does already extract it's form values properly, thus we would just have to pull out any non-fields on our own. That way unwanted stuff like form_ids and so on doesn't end up in the entity...
@Controller:
Conceptually I wonder if this belongs into the controller or should sit directly in the entity's class? Thus we could start creating a DrupalEntity base class and derive DrupalNode, DrupalComment and so on from it. I think it would be much more intuitive to just write $node->submit($form, $form_state);
Comment #15
yched CreditAttribution: yched commentedThe directions taken in the recent followups look like a huge improvement over the current code. Also simplifies quite a bit the DX of writing a 'fieldable entity'.
I think fago is now on a vacation (and so am I), so unfortunately we can't count on him or me to move this forward. Frando, it would be so cool if you could drive this home :-/
Also, fago's point in #14 about $form['#'. $entity_type ]'s workflow seems to make sense, but I didn't have a close look at the code implications.
Comment #16
sunTagging absolutely critical clean-ups for D7. Do not touch this tag. Please either help with this or one of the other patches having this tag.
Comment #17
joachim CreditAttribution: joachim commentedI might be talking nonsense as I've not had breakfast and only skimread, but what about using the same pattern as system_settings($form) ?
Comment #18
sun0) It's very unlikely that we find a totally revamped solution within the next hours. So we have to think to the point here.
1) #594650: Provide central $form_state['values'] clearance will also help this issue, at least for button elements and internal form API values (which normally should be everything to cater for).
2) True is, we currently store entities in the top-level form element, as in $form['#node']. The question is, whether we need to support multiple entities in the same form. IIRC, the current code doesn't allow for more, so that's basically out for D7.
2a) This is the current pattern, and we also use it for other renderable arrays besides forms now. Elsewhere, it's $elements['#block'] (a block object), for example.
3) If we can presume that there is only one entity per form, then we can add another property that gives us a pointer about the current entity being processed. That would be $form['#entity'] = 'node';
4) Given #entity and #{$entity}, it looks to me as if we could do this in a semi-abstracted way, by just adding a #submit = 'field_attach_submit' at the very beginning of the form builder function (where ideally also #entity etc. are defined).
5) It looks like field_attach_submit() could just be a little bit smarter and accept both an object or array, and just convert that into an object, if it's an array.
6) I have little ideas about what to do about the $form_state thingy. To start with -- if that is a requirement from the beginning, then we should introduce a drupal_get_entity_form(), which is a clone of drupal_get_form(), but additionally massages ... ugh, no, it's drupal_build_form() that needs massaging. Point is: We pass an initial object, build a form out of that, submit that form, and if we get back to the form, we want to use the last state of the object. That's what $form_state is for, and with aforementioned points, field_attach_submit() was able to update.... oh, now I get it!
7) #571086: Allow to specify a 'wrapper callback' before executing a form builder function (don't get confused by this issue's status) already introduced CTools' concept of a "wrapper_callback", and that's why all form builder functions take $form as first argument now. The idea is to replace "drupal_get_form" in hook_menu with "field_get_form" (or "entity_get_form"). That callback does the same as drupal_get_form(), but additionally assigns $form_state['wrapper_callback']. This wrapper callback is invoked before the actual form builder is invoked and (with that patch being committed), it gets the same arguments as the actual form builder. Hence, we get the initial $node object and are able to put that into $form_state. We can also auto-add the #submit handler. If the form is rebuilt, then we come back again, identify that we already have a populated $form_state and use that instead of the passed in $node.
The more I think about it, this actually means that Field API has the same requirements as CTools/Panels/Views' object caching.
Funky stuff! :)
Comment #19
sunAnd, yeah, I also have a ready patch in #367567: Use AJAX framework for "Add more" / load include file containing form / introduce $form_state['build_info'] to introduce a $form_state['build_info'] that is cached separately from $form_state['storage'], which was one of the key problems mentioned earlier in this issue, and nicely goes along with the solution I outlined in 7).
Comment #20
plachsubscribe
Comment #21
sunSo the following two patches are what hold up a sane solution for this issue:
#367567: Use AJAX framework for "Add more" / load include file containing form / introduce $form_state['build_info']
#571086: Allow to specify a 'wrapper callback' before executing a form builder function
With those in place, we are able to properly leverage the 'wrapper_callback' and 'build_info' facilities, which allow us to prepare and store non-volatile entity + field information in $form_state and cache that info accordingly with the form.
The major problem of the current flow is that any custom #validate or #submit handler has absolutely no idea of whether to work on $form_state['node'], $form['#node'], or the submitted form values. It entirely depends on the function invocation order, and that is a horrible problem.
Two very closely connected issues need to be resolved here:
1) Kill #builder_function.
2) Make entity/field caching properly work without JS.
I'm not sold on the idea of extending the entity controller. This clearly boils down to having a properly workflow in Form API: We want reliable data in $form_state, nothing more and nothing less.
Comment #22
yched CreditAttribution: yched commented"extending the entity controller or not":
I think we need to figure how the newly committed patches let us reformulate the current 'add more' handling and workflow (that is not completely clear to me ;-)), for instance starting on node or comment, and then evaluate whether it is an 'acceptable level work or constraint' to require from every entity type ?
Comment #23
effulgentsia CreditAttribution: effulgentsia commentedSubscribe. I'm still trying to wrap my head around this issue and a related one: #622922: User input keeping during multistep forms. This is purely a naive gut feeling on my part, I'm by no means a FAPI guru, but it seems to me like we're fighting against an architectural limitation here. Seems like FAPI is designed with the idea that multi-step forms have form constructor functions that know it's a multi-step form and use $form_state['storage'] to build $form, and submit handlers that also know it's a multi-step form and set $form_state['storage'] as needed. But with AJAX enabled forms in general, which is what we're trying to figure out on that issue, and "add more" field items forms specifically, both js and non-js, we're trying to use drupal_rebuild_form(), and therefore all the assumptions that go with multi-step forms, in a context where neither the form constructor function nor the submit handlers know that it's a multi-step form.
I think it would be awesome if we could solve this at the FAPI level, by making a distinction between multi-step wizard-style forms (where the form constructor and submit handlers know that they are part of a multi-step form and use $form_state['storage']), and forms that have components that can be updated a few interim times before the "real" submit. And either come up with an alternate drupal_rebuild_form() function more appropriate to the latter case, or make the existing drupal_rebuild_form() function handle both types of multi-step forms.
Maybe I'm misreading the above discussion in this issue, but it seems like it's focusing on creating wrapper functions to get around what is undesirable in drupal_rebuild_form() rather than trying to fix or create an alternate drupal_rebuild_form(). Maybe there's a really good reason for that, but it makes it hard for me, and I imagine other non-FAPI-gurus, to make any sense of it.
Comment #24
sunIt is possible that #622922: User input keeping during multistep forms will help to fix the original issue with "Add more" not working without JS on the user account edit page.
Here, however, we are trying to fully understand the whole Form API and Field API integration as well as the data and storage flow that's involved, which is a big mess currently.
Since the blockers to streamline this workflow have landed in the meantime, I'll try to wrap up the streamlined solution I have in mind on this weekend.
Comment #25
fagoI just cleaned up the form workflow for the profile2 module. See http://github.com/fago/profile/commits/master.
It's now using the form storage:
* If the storage isn't set, populate it with the initial entity (loaded or created from scratch)
* Populate the form set the and initialize the form values with the entity from storage
* Once validated submit the values by updating the entity in the form storage.
* If the form is rebuilt, everything works fine as the udpated entity from the storage is used.
* On the final submit, save the updated entity.
That way we always have one single copy of the entity.
Compared to my proposed solution in #9 we even save copying it around between $form and $form_state, but we introduce the problem with form storage automatically turning on rebuilds. While it's usually handy, it doesn't make much sense that you cannot avoid that. Even redirects get ignored. I think we should address that and allow redirects even when the storage is set, then everything is fine.
Comment #26
fagoFor fixing redirects when using form storage see #634440: Remove auto-rebuilding magic for $form_state['storage'].
Comment #27
yched CreditAttribution: yched commented"but we introduce the problem with form storage automatically turning on rebuilds"
I think that's the reason $form_state['build_info'] was introduced ?
Comment #28
fago>I think that's the reason $form_state['build_info'] was introduced ?
Yes, but still this is "build_info". Thus using it for our data is imho 'misuse' - instead we should fix 'storage' so we can use it as it should be.
Comment #29
sunThere is no real guideline for 'build_info' and 'storage'. I mean, yes, it was designed to store internal form builder information, but at the end of the day, everyone is free to use either one, and I would say that all data that does not map 100% to the actual form implementation could and perhaps should be stored in 'build_info'.
For example, private data for multi-step form logic in the form implementation belongs into 'storage', because that's your private form storage bin. So if your multi-step form logic wants to store the amount of foobars for the next step, that goes there.
OTOH, all data that is required to build or rebuild a form belongs into 'build_info'. For example, the $node object with the form context or Field API's 'fields' reference for attached fields in the form.
Thus, we want to use 'build_info' for this challenge. I agree, however, that we also want to streamline 'storage' and form caching, but these are separate issues:
#583730: How many ways are there to cache a form?
#634440: Remove auto-rebuilding magic for $form_state['storage']
I will look into your implementation in profile.git ASAP.
Comment #30
fago>OTOH, all data that is required to build or rebuild a form belongs into 'build_info'. For example, the $node object with the form context or Field API's 'fields' reference for attached fields in the form.
I think you are mixing up "form construction" and "form building". I know the terminology was already unclear, but we need to distinct that. Let's use "form construction" when the users's functions constructs $form and "form building" when the form API is dealing with it (->drupal_build_form). I just read through some docs, they are now following this terminology too.
Thus 'build info' should be form API info regarding it's build process. -> The $node object doesn't belong there.
Also nobody says 'storage' is private, everyone is free to use. I'd even say it's the other way round, 'build info' belongs the form API, 'storage' is free to use by anybody (constructors or alter implementations).
So I think having storage carrying any user-data is a good distinction to build-info. Yes 'storage' needs some more fixes to be really useful - but if we stop using it we don't need to fix it any more ;)
Comment #31
fagorelated bug: #634984: Registration form breaks with add-more buttons
Comment #32
yched CreditAttribution: yched commentedCrosslinking to #629252-9: field_attach_form() should make available all field translations on submit.
This identifies a use case for having the *original* entity available at a standard place in $form_state.
Comment #33
sunI fear we're not getting anywhere. Let's do a general discussion in #635552: [meta issue] Major Form API/Field API problems and go back to individual issues afterwards. Thanks.
Comment #34
sun.
Comment #35
sunAnd here's the killing concept.
As explained in #18, including example conversion for node form.
A couple of tests will fail, because quite some code relies on $form['#node']. However, manually adding, editing, and previewing nodes works.
Comment #37
Frando CreditAttribution: Frando commentedI quite like #35. It definitely goes into the right direction. I would propose to move submission (i.e. node_submit) and the creation of empty stub entities into the entity controllers (this would be an API addition, but no change). Also, several node related modules currently form_alter the node form and add custom #submit callbacks on the form (e.g. menu.module). This is a problem because those submit callbacks do not run if a button with a button-level submit callback is clicked (e.g. add more). So these parts just have to move to the new hook_ENTITY_TYPE_submit(). This is much cleaner then.
Here's an initial review of the first part of the patch, ran out of time right now but will go on later hopefully.
I suggest $entity_type for $obj_type and $entity for $object. I think that's what was generally agreed upon with regard to the entity API.
Same here, this is confusing. Let's do $form_state['storage']['entity_type'].
No we cannot do this yet generally yet but we ought to. Proper way would be to add a createEmptyEntity() method to the entity controller.
Again, I'd replace 'object' with 'entity'
I'm on crack. Are you, too?
Comment #38
fagoThis seems wrong to me. Why do we have to recreate a fresh object? That way we quickly end up with a broken object, either some form values are set to #access FALSE so their values are missing, or someone hooks into the form and adds something which doesn't belong into the object. Why not just extract the things we really need and put it into the object?
A lot of bugs stem from the fact that the $node after submitting a form looks different - just have a look at the token code for dealing with terms if you need an example ;) So let's learn of that and make sure we always have a full and clean object that never looks different in any stage of the workflow.
For the profile2 module I just did:
The field API already extracts the values for us - so nothing more todo here. For the node form or others we just have to make sure any additional things in the form that are not fields are also extracted and are added to the object.
Also the above code assumes the entity is just a stdClass() object, which isn't true in general. E.g. entities of the entity CRUD API I implemented, aren't. See http://drupal.org/project/entity
Comment #39
sunSome good suggestions, fago, thanks!
However, we really want to use $form_state['storage']['object'] as stateful storage of the object that is being edited. And that's not limited to field_attach_submit(). As you can see in the patch, I changed node_form_submit() to also work from the latest $node object that is contained in the form storage (and save that).
The issue you're raising regarding #access and invisible properties/fields makes totally sense. So the only way to account for this would IMHO be to walk (perhaps even recurse) over the submitted form values and granularly update $form_state['storage']['object'] with it.
Would that make sense?
Comment #40
fagoYep, I was talking about updating the object like that.
The question is if we want to automatically pull everything of the form values into it or not. There might be data in it that doesn't belong in the object anyway, so perhaps it would be cleaner to just define an array of fields to copy over. Of course then modules would have to do that too in case they want to add something to the form that has to go into $node. But this would ensure that $node is always clean and doesn't look somehow different because of form values looking different than the structure loaded from db.
Comment #41
sunAs of now, this is what we currently do in such forms anyway. Form element value callbacks and element validation handlers ensure that submitted form values in $form_state['values'] are in the structure we do expect for the object. In addition to that, some elements may even use hard-coded #parents to ensure value proper placement on submission. Hence, plenty of ways to do this. Not sure, when exactly this started, but I already know this from Drupal 5.
I don't think we need another facility to do this. The current code already is already prepared for that.
One big question with walking/recursing over submitted form values is, however, the granularity in which we apply the values to the object. Or in other words: How do we know from which key we need to update $object in the following?
Comment #42
sunSorry, I forgot. The current code for Field API and other things does stuff like #629252: field_attach_form() should make available all field translations on submit, i.e. ALL values that should end up in the object that's derived from the form values are contained invisibly in the $form as #type => value.
So, erm... not sure whether we need this granularity at all. The current practice is to define such properties as #type => hidden/value in the form anyway, so the casted $object based on $form_state['values'] really contains everything it should.
Comment #43
fago@granularity:
Perhaps it would be best if we recursively merge the values - so we ensure nothing is getting lost?
@#type => value
That is something really unnecessary. The values are already in the object in form storage, so why pass them around another time?
Comment #44
yched CreditAttribution: yched commentedentity_create_stub_entity() ?
Mmh, although that function also clashes with fago's remark in #38: "Also the above code assumes the entity is just a stdClass() object, which isn't true in general. E.g. entities of the entity CRUD API I implemented, aren't. See http://drupal.org/project/entity".
The current awkward code around #builder_function had the (IMO) advantage of formalizing the notion of 'function that builds an object out of form values'. Each entity module is then free to do this more or less nicely, but at least at the entity level we don't assume a brute force cast of form values into an object.
[edit: ok, I looked closer and the code is indeed smarter that that. IMO it could be more clear if the actual $object was stored in
$form_state['storage']['object']
only at the end ?]Additionally, #629252: field_attach_form() should make available all field translations on submit uses
$form_state['storage']['object']
as "the original object, as currently saved in the db". It seems like we'll need both the source obj and the current state of the edited obj ?Do we really want the common submit to set it as TRUE by default, and leave it up to the 'Save' submit handler to set to FALSE ?
Or reversedly, require 'need rebuild' buttons ('add more', 'preview') to explicitly set it to TRUE ?
This review is powered by Dreditor.
Comment #45
sunfago and me discussed in IRC.
#access => FALSE
elements was a mistake and does not exist.#type => 'value|hidden'
to ensure that certain/internal object properties are contained in the form values and this is how it works for a long time already. It would definitely be worth to explore whether we can find a better process for not having all object properties invisible in a form, but that's definitely D8 material.What's important for now is that we can safely cast
$form_state['values']
into an $object, because that is exactly what the current code in HEAD does everywhere. So this is nothing new and not invented here.'page callback' => array('entity', 'form')
yet.Hence, to get this done, we will simply load entity.inc in full bootstrap.
Step 2 is to make the menu system support calling class methods for router item callbacks (separate issue).
Optional step 3 is to convert entity_get_form() into DrupalDefaultEntityController::form(), but that may as well be D8 material.
@yched:
$form_state['storage']['object']
always contains the latest state of the $object and is identical to $object, because $object is extracted by reference. The special form submit handler entity_form_submit() is invoked first, so any further submit handlers, including the main entity submit handler that will save the entity can safely base their work on it.$form_state['rebuild']
perhaps makes sense. I was equally unsure about it, which is why I didn't add an inline comment for that line. (should have added a @todo though ;)Comment #46
sunIncorporated some of the suggestions.
Comment #47
yched CreditAttribution: yched commentedfield_attach_*() vs entity_* namespace : see related thoughts in #636992-3: Entity loading needs protection from infinite recursion
Comment #48
yched CreditAttribution: yched commentedre sun #45:
a) yes, entity_create_stub_api() only can create an object with the properties it knows about from hook_entity_info(). Should be good enough for entity and field operations, though. The function will require some additional flexibility - and possibly a new entity hook, or entity_info property, to support Fago's "entities that are not StdClass", though.
b) yes, got the reference part. I tend to think that _submit() callback is actually misnamed. It's not "submit the $node" nor "react to the fact that $node is being submitted", it's "turn the form values being submitted into a real $node object". Before the callback is called, $object is not yet a real 'node'. The way we name our hooks and callbacks, this sounds more like a '_convert_form_values()' or (obviously) something better - not sure what, though...
c) Well, #629252: field_attach_form() should make available all field translations on submit does require the source object, and won't work with the 'current state of the edited $object' if I'm not mistaken. Bottomline is: with TF, no 'node' built solely from form submitted value will ever be able to match a $node loaded from the db, because of all the field values for other languages, that are missing in the form.
But more generally, yay for this Ctools concept making its way to core.
Comment #49
fago>Bottomline is: with TF, no 'node' built solely from form submitted value will ever be able to match a $node loaded from the db, because of all the field values for other languages, that are missing in the form.
Yep and as we have already the values in the storage object, why should we bother with putting them in the form values?
So as of know most is still in the form_values, but we have already the translations being not in there. I still think it's time to clean that up now and don't go with both ways being partly used.. The question is whether we should
a) automatically pull everything out of the form and update the object using a recursive function, or
b) change it only pull a list of fixed element names into the object and let modules extract their own data
b) changes probably to most in the current workflow, so it's no real option to go. Still a possible problem is when a module wants just create something related to the currently edited object, then still it's data would go into $object what isn't ideal. Ideally we would should only $form_state['values']['node'], but this change wouldn't be trivial.
* Probably we have to rename the existing $form_state['storage']['object'] to something like $form_state['storage']['source_object']
@object reference:
We are coding for PHP5 now, so we don't need to set the reference.
* field_attach_form_validate() reads the form values out of $form_state itself, so we can just pass the source object from storage to it.
* @stdClass: It would be easy to enhance entity_create_stub_api() to make use of an optional 'entity class' entity-info property. That's all we need to do to don't limit it to stdClass when we go with the updating values approach.
Comment #50
sunerrr... so the latest patch in #629252: field_attach_form() should make available all field translations on submit removes the code that puts the field values for inaccessible fields/translations and stuff into
#type => value
elements. Hence, it looks like we could still go with the proposed approach here, if we would not do that change.Comment #51
plachExactly. In the latest version we automatically pull out field values: we use the submitted field values if present, otherwise we use the original ones. This way the new
$object
is a merged version between a simple cast of$form_state['values']
and the original data structure. The advantage of this approach is we don't get unnecessary/unwanted form submitted values in$object
.I think both approaches could work in #629252: field_attach_form() should make available all field translations on submit. Probably the reason we went for the automatic handling is Field Form API already took this way. I think it would easy to come up with a new patch using
'value'
form elements instead of using$form_state['storage']['object']
.Comment #52
fagoSo what about this one:
* I took sun's patch and modified it to don't cast the form values to an object, but use the form values to update $object in a recursive way. So values not touched by the form will stay, eg. the other translations. Thus with that approach there should be no special fix necessary in #629252.
* I also fixed it be able to work with classes.
When updating the code I noted a problem. Field API extracts the values during build and as we have PHP5 the object in storage gets updated. Thus extracting the values again in 'submit' is a duplication. Furthermore I think the cache will be written although we have validation errors, thus the changes persist. Thus the object used in the form constructor is suddenly not properly validated any more!
Comment #53
fagoIs there a need to update the cache when validation fails? I tend to think we ran into another FAPI issue.
update: Well it might make sense to not cache storage when validation fails, but probably it's simpler staying without that magic.
Instead we could clone the object for validation and update the object in storage only in the submit handler.
Comment #55
sunPlease fix your IDE.
The problem with this approach is that I think we also need to account for removed values, especially when considering multiple value fields.
I'm on crack. Are you, too?
Comment #56
fago>Please fix your IDE.
:D The line needs to be fixed ;)
I'll turn it off.
>The problem with this approach is that I think we also need to account for removed values, especially when considering multiple value fields.
Indeed, so the recursive approach is a no-go. So let's do it like previously and update the whole value if it is in the form.
Comment #57
yched CreditAttribution: yched commented[edit: oops, crosspost with fago]
Yes, the code pointed by sun to extract values worries me. It writes in stone the current mess of '$form structure needs to map to $object structure' that is expressed by the horrible
$node = (object) $form_state['values']
that node_form_submit_build_node() currently does.At least in HEAD that poor logic is in a function whose official role is to make sense of form values (the #builder_functions). The functions themselves could do a better job than a brutal object cast, and the '#builder_function' property is poor 'entity abstraction of node.module patterns' that we did with the tools we had in the initial Field API patch, but the encapsulation is a good pattern IMO.
field_default_extract_form_values() follows the same logic (extract actual $obj->field_foo values out of form values), and the patch robs that function as well :-)
Additionally - we explicitly do not want field_attach_form_validate() to run on those 'values present in the source object but not in the form'. See comment #22 in #629252: field_attach_form() should make available all field translations on submit.
I also think we don't want them in field_attach_form_submit().
Comment #58
sunerrrr, again.
There is absolutely no way to get an entire revamp of form value => object => form value handling into D7.
I don't think there is a solid middle ground between total array-object casting and selective object property updating (requiring some yet unknown facility). I already tried to explain with trivial form value snippets above why I think it's not possible to do anything in between - because we simply miss the required information for entirely removed values and more complex form value data structures.
The selective object property updating seems out of the loop for me. Which means we need to go with the already existing array-object casting.
Hence. Yes. The approach taken to solve this issue could be seen as manifestation of the "$form structure needs to map to $object structure" design, having the slight difference that we always work from a stateful $object to base a $form representation on it. But that's what we have since D6, and I don't see how we could squeeze in a major API change that does not follow this paradigm into D7 right now.
In addition to that, I'm not even sure whether it's a wrong paradigm. Ideally, merlinofchaos should chime in here, but I fear he's far away from core HEAD currently. IIRC, he once mentioned the concept and idea of forms being just a representation of an object. Just like node_view() is the rendered view of an object, node_edit() should be the form representation of an object. This, at least, allows for some clever and bad-ass smart workflows when working with the form representation.
I fear we need to get our ideas, considerations, and goals in line before re-rolling the patch any further. But, please, let's do that quickly.
Comment #59
fagoOk, so let's resemble the previous way of
$node = $form_state['node'] + (array)$node;
by doing a simpleThat way this works for non-stdClass objects too. Re-rolled patch with that.
Better abstracting out the 'extract values' part sounds a good thing to do. It would make sense to first extract all values and then start with the validation, so each stage has all extracted values. However for that we'd need to refactor field API to have an extra 'extract values' attacher which is currently built into validate+submit. hook_entity_extract_values?
Also as field API already can pull the values intelligently out of the form it would be nice to have the field API doing it that way. We could copy the form-values into a temporary data array, let field API extract it's values + unset the extracted values. Afterwards the remaining values are set in the object like above.
@field_attach_form_validate
>Additionally - we explicitly do not want field_attach_form_validate() to run on those 'values present in the source object but not in the form'.
Agreed, the validation handler should of course get the updated object. But when only looking at field API the comment of the function states it pulls out the values of the form nevertheless, and it does. I think this is dangerous as it's not documented and could lead to not validated values going into the object, like it does with the current patch. Don't forget PHP5 passes objects always reference-like.
Comment #60
fagoand the patch..
Comment #61
fagoComment #63
yched CreditAttribution: yched commentedIf f_a_form_validate() and f_a_submit() calls are encapsulated within entity hardcoded form validation, I have no objection to move the 'extract form values' step they currently include into a separate func. f_a_extract_form_values() ?
The problem remains that f_a_form_validate() needs to run on an object that *only* includes the form values, *not* the 'updated full object'. We cannot block a user from submitting a form by raising validation errors on values he did not touch and cannot even fix because the value is not in the form to begin with.
If this leads to incorrect values being saved, then that's only because those incorrect values are already in the current db object and the definition of 'what's valid' changed meanwhile - see my example in #629252-22: field_attach_form() should make available all field translations on submit.
That would lead to: 'no one can edit any translation of a node, because there's always some invalid value in one of the other languages'.
Comment #64
yched CreditAttribution: yched commentedWhy does this need to accept a NULL $object ? (which then has to be initialized with entity_create_stub_entity() without any actual object id or bundle properties)
All calls in the patch always pass a $node.
I'm on crack. Are you, too?
Comment #65
sun@yched: That's because http://api.drupal.org/api/function/node_add/7 is a fairly special function. When I wrote that part, my presumption was that not every entity may be a nightmare like nodes. Of course, to simplify this patch and what we do for D7, we could simply require entity add forms to implement a custom menu page callback to prepare an empty object.
Comment #66
fagoWell, starting to use entity_create_stub_entity() probably makes sense and might help us saving the extra add forms. Anyway I didn't get the comment. We could do it by just alter the args in $form_state which is passed to the constructor by reference. But why do we have to update $args?
@#63:
Thanks, I understand the problem now. Is validation skipped when the values aren't there automatically? What if required values aren't edited currently? Having a half-baked object doesn't look that good to me. For proper entity_validation() we would still need a full object anyway, so one can validation dependent on another value.
So what about changing 'field_default_form_errors' to only mark errors represent in the current form? So we could go with a full object and the user has the possibility to fix it. I think that's the best option we have when have suddenly not more valid data in the db.
Comment #67
yched CreditAttribution: yched commented[crosspost with fago's #66]
re @fago #59:
Yup, something like that is probably needed, because the current
will crush $object->field_foo[$langcode] for any
$langcode != $langcode_of_the_form
.re @sun #65: Understood. FWIW, requiring entity add forms to implement a custom menu page callback to prepare an empty object sounds acceptable to me.
Comment #68
yched CreditAttribution: yched commentedre @fago #66:
- "Is validation skipped when the values aren't there automatically?"
Yes, since currently only values from the form are gathered in an $object that runs through hook_field_validate().
- "What if required values aren't edited currently? Having a half-baked object doesn't look that good to me."
'Required field' validation currently only goes through widgets and FAPI #required property. Ideally, this would be done at field level (#437604: Do not rely on FAPI for 'required' validation), but really not sure this will be tackled in D7...
As far as Field API is concerned, it's not exactly a half-baked object, it's an object with values for "some but possibly not all" fields - something that field API was built to support from the beginning (non accessible fields, partial form in a side block...)
- "For proper entity_validation() we would still need a full object anyway, so one can validation dependent on another value."
Use cases are unclear to me. That seems like custom code in hook_node_validate(). And if done on the entire $object it would carry the same danger of reporting errors the current user is not guilty of and can not fix.
Anyway that's not field level validation, which is 'field by field' by nature.
- "So what about changing 'field_default_form_errors' to only mark errors represent in the current form? So we could go with a full object and the user has the possibility to fix it. I think that's the best option we have when have suddenly not more valid data in the db."
Yeah, I mentioned that possibility in #629252: field_attach_form() should make available all field translations on submit. Dunno. Might be irrational but it kind of scares me a bit security wise, like 'I have an error reported but I choose to ditch it'.
Aside from that, would burn cycles to invoke hook_field_validate() on *all fields in all languages* and have it compute errors that we know we will only report a predictable subset and ditch the others ? Not that appealing...
Comment #69
yched CreditAttribution: yched commented- Rerolled (failed hunk in node.pages.inc)
- updated field_test.module's 'test_entity' for entity_get_form() and the new form workflow. Should make it possible to use 'Field form tests' ('add more' tests still fail, see below)
- adapted field_add_more_submit() to use $form_state['storage']['object'] instead of #builder_function.
'Add more' button still doesn't work, though. By the time we reach field_add_more_js() the $form does not contain the field-related elements. It looks as though entity_form_wrapper(), which takes care of calling f_a_form(), is not executed when the form is being rebuilt.
Side note:
Forces modules to define functions outside of their namespace if 'module name' != 'entity name', e.g field_test.module has test_entity_submit()
This review is powered by Dreditor.
Comment #71
sunRegarding the last follow-ups on partial form validation: Please take into account #370537: Allow suppress of form errors (hack to make "more" buttons work properly) in your considerations, which seems to be RTBC by now (and quite awesome on that note).
Comment #72
sunThis is an absolute show-blocker. Not sure how to resolve this other than requiring to pass the module name as first argument to entity_get_form().
Comment #73
fagoWhy not specify the module in hook_entity_info() or gather the information automatically? It's for sure useful in more cases to know which module defines the entity.
@validation: Ok, then let's pass the half-baked thing to the field API, however I think this should be documented in the validation related field-API functions. I fear modules passing around this half-baked object to API functions triggering obscure bugs. As field_attach_form_validate() reads out the form values itself we could even pass a new stub entity to it.
>>- "For proper entity_validation() we would still need a full object anyway, so one can validation dependent on another value."
>Use cases are unclear to me. That seems like custom code in hook_node_validate().
E.g. validate a field dependent on the node author.
>And if done on the entire $object it would carry the same danger of reporting errors the current user is not guilty of and can not fix.
We don't have a distinction of doing form validation and validating the object yet - except for fieldAPI. However hook_node_validate() is already tied to the form, which is good as having the form enables one to only validate what's in the actual form. But also passing $form_state as this patch does is important.
Comment #74
yched CreditAttribution: yched commented+1. That's how Field API does for field types, widget types, storage engines etc - gathered automatically in _field_info_collate_types(). entity_info didn't need 'module defining this entity type' so far, but if it does, let's be consistent.
Exactly - custom code in hook_node_validate().
True, and that's one point where we insisted on Field API being 'better than core': untying validation from form workflow and support validation on programmatic saves if the entity types want to - even though current core entities don't support it yet at their own level because of years of entangled legacy. Changing this in D7 is of course out of question, but it's good that Field API doesn't keep on with this tradition of tying validation to forms. Contrib entities can try this for their own {entity}_save() and invent the missing patterns for this (exceptions, etc...).
What I'm pointing is that the patch as it currently stands brings a major change in the validation workflow of core entities: entity validation (and related hook_{entity}_validate for 3rd party checks) runs on the fully stored object instead of only on actual form values. This brings a chance of blocking users on validation errors they cannot fix since they lie in the object as it currently exists. This is not only true for the specific case of field validation.
IMO all (form) validation should run on a stub object built only with the forms values. Or perhaps even just on raw form values without building a stub object. Entity validation is not untied from form submission, then lets draw the consequence.
Then field validation is a little specific because it happens that the _field_invoke() iterator mechanism and the structure of the various hook_field_[op]() require an $object (even an incomplete one) holding field values. But that's strictly the business of field validation. The $object can be built specifically for Field API validation purposes and ditched afterwards.
Comment #75
yched CreditAttribution: yched commentedfrom my post just above:
"IMO all (form) validation should run on a stub object built only with the forms values. Or perhaps even just on raw form values without building a stub object"
... and those validators can still access the source object from $form_state['storage']['object'] if they need to - like "form value 'field_user' should be different from node author". It just means that they should only flag errors caused by what's actually in the form.
Comment #76
fago@#75
Agreed, that makes sense. I'd prefer much the raw form values approach to make clear this is no entity-level validation, however no way to change this now for d7. But also we need clearly documented the this object is not fully baken. It might have not more than a single field in it!
Comment #77
yched CreditAttribution: yched commentedRegarding my remark in #69:
That's because the ajax workflow (ajax_form_callback() / ajax_get_form()) skips entity_get_form() entirely, and $form_state['wrapper_callback'] does not get set. Not sure how / if this can be fixed, but that looks like a blocker for the current approach.
Or is it what #641356: Cache more of $form_state when form caching is enabled is about ?
Comment #78
fagoupdate: Not directly, but as a side effect it should probably fix it for that case. Currently is probably only broken for ajax requests because the bypass entity_get_form, but once the wrapper_callback property is cached it should work in that case too.
Comment #79
yched CreditAttribution: yched commentedFWIW, I have very limited core dev time in december, + I will focus on #438570-61: Field API conversion leads to lots of expensive function calls in the next few days.
I'll review best I can, but it's going to be difficult for me to push the patch itself.
Comment #80
Crell CreditAttribution: Crell commentedSubscribing. I'm trying to develop an entity module right now and running head first into FAPI WTF land. There's stuff going on here that I can't even begin to comprehend, mostly the workflow of the $form['#builder_function'] stuff.
Comment #81
int CreditAttribution: int commentedComment #82
sunI'll try to have another stab at this, if no one beats me to it. Core maintainers know about this issue, but we have a hard deadline now - this time for real.
Comment #83
sunDiscussed this patch with frando in IRC last night... #370537: Allow suppress of form errors (hack to make "more" buttons work properly) basically made this entire approach impossible, because there can be unvalidated values in the submitted form values, which we must not merge into the entity. That's insane.
As far as I can see, the only way out would be to make field validation not based on the entire form values/object, but on a per-field level, so only validated form values are taken into account for field validation and submission.
Comment #84
fagoSo you think pulling the unvalidated values into the object, which is used to rebuild the form is dangerous? Well I think if that feature is used on the node form as it is now, it would already work that way (or just forget the changed values). But I can see that this might become dangerous as the form would be built with an object containing unvalidated values and all code using that object would have to respect that - but that becomes impossible once the object is passed to any API functions.
So it would be better when the object wouldn't change at all in the case of a button bypassing validation. We could just use the old object for form rebuilding again + the new form API feature of keeping the input values during a form rebuild. That should work, shouldn't it?
Also we may not forget what yched pointed out in #74:
Comment #85
mfer CreditAttribution: mfer commentedSubscribing because FAPI WTF is fun... sometimes :)
Comment #86
yched CreditAttribution: yched commented[Disclaimer: it's late and I'm badly sleep deprived so forgive me if I'm completely off.]
I still stand by the quote in #74, but I'm not sure that's really related to the issue raised by sun. entity_form_submit() in the latest patch would still put potentially invalid form values in the object.
fago #84: "So it would be better when the object wouldn't change at all in the case of a button bypassing validation. We could just use the old object for form rebuilding again + the new form API feature of keeping the input values during a form rebuild. That should work, shouldn't it?"
It seems that then
- with JS on, if you edit some values in a multiple field, then press it's 'add more' button, the AHAH form comes back with the edits wiped out
- with JS off, if you make any edit in any input, then press an 'add more' button, the whole form comes back with the edits wiped out
?
Comment #87
fagoWell currently the node form as well as the field API itself disables this feature from FAPI - have you enabled it for your tests?
For that we have to remove
from field_multiple_value_form(). Doing would also be the best way to fix #634984: Registration form breaks with add-more buttons - as it would fix that problem for any form. Also this feature is built to serve exactly that cases so I really think we should try to make use of it. However I fear the we might have troubles with sorting multiple values once activated.
Comment #88
sun.
Comment #89
sun.core CreditAttribution: sun.core commentedIt seems we have to live with "what no one understands" until D8.
Comment #90
joachim CreditAttribution: joachim commentedNo.
Bugs in D7 are not fixable until this is at the very least explained in documentation.
Eg #641314: Taxonomy term form being rebuilt even after final submit.
Comment #91
Frando CreditAttribution: Frando commentedThe problem is we cannot actually fix this without breaking major APIs. I thought about this for quite some while and have several early half-baked patches lying around here that tried to solve this. But the thing is the way the interaction between FAPI validation and submit handling on the one hand and entity submission and saving on the other is just not made for being generalized. It is of course possible and it will definitely have to be done for D8, hopefully quite at the beginning of the cycle together with some other needed FAPI/Field API/Entity API generalizations and cleanups.
For D7, I fear there is no actual clean approach, and certainly not without not breaking at least some APIs.
Anyway:
Here's a summary of the issue
(Note: I wanted to write this down for quite some while now, so it got rather detailed. This can likely serve as a starting point to document the #builder_function part of the field attach API, should it stay in D7).
Entity submission prior to Field API worked like this (based on node module):
node_validate()
, together with the $form array (for form error flagging).node_validate()
callshook_node_validate()
, again passing the form-values-as-stdClass and $form around.node_submit()
which, again, simply converts$form_state['values']
into an stdClass and then callsnode_save()
on it.This means that all validation handling happens purely in FAPI. On multistep forms, the form is rebuilt with the form-values-as-std-class in place of the original $node. On each multistep submit, the form values are again converted into a stdClass and completely replace the previous one. This is why it's e.g. so awefully difficult to create a node form as wizard module (because each time the form is submitted, the form values are validated as if they were a whole node. This means that on each submit, all values of the node have to be in $_POST or somehow inserted into the form values using the uglier parts of FAPI).
Then, Field API came. Field API has an advanced submission design, as it can valdiate on actual entities, and not on form values, and even has a seperate step to then flag form errors based on validation errors. This is how things are meant to be handled, and this is what we have to do for entities in D8.
Now, the basic problem is that, as explained above, there is no entity available for validation, just a form-values-as-stdclass with some entity-specific alterations, which is created by the entity's form validation handler and then again by the form's form level submit function.
So what Field API did is it made entity forms add a custom
#builder_function
property to their forms. In that callback, e.g.node_form_submit_build_node()
, the entity module is expected to construct an entity object out of the form values. Field API calls that callback in their multistep submit handler (the "add more" button's button-level submit handler) to receive an entity object to pass on to the field validation handlers. The #builder_function can be re-used by the entity's own submit or validation handler, however, this is not made very clear nor is it applied in any automatic way.The problem is that all this is not properly reconciled with the original entity forms.
The proper solution to all this will likely look like this:
entity_submit()
and their validation handler toentity_validate()
.#builder_function
or whatever. This callback is called *prior* to validation. For an entity form, this would be set toentity_build_submit()
. That function would invoke the entity'ssubmit()
handler, which will likely sit in the EntityController class, passing on the current form values and the previous state of the entity (stored in$form_state['entity'])
. The entity build submit handler can then look if it's interested in the form values and if so set them accordingly on the entity object. It also callshook_entity_submit()
.(Sidenote: I don't like the name "build submit" too much. I did not come up with a better one, though. Alternatives might be
build_from_form_values
,construct_from_form_values
,build_object
, dunno).entity_validate
will call the entity's validate handler (so we'll validate the entity, not the form values). If there are exceptions, theform_error
handler will be called (so we take Field APIs approach and use it for the whole entity).#limit_validation_errors
property for button level submit handlers. So this part will happen on FAPI level again, which is fine.entity_submit()
. It doesn't support #limit_validation_errors, so it means that validation must have passed completely. The entity constructed inentity_build_submit
prior to the last validation is still stored in$form_state
and can then be saved without further processing.So the main change will be that the entity object that's gonna be saved will be constructed before validation happens, so that validation can act upon the as-to-be-saved entity and not just on form values. This, however, is probably not doable in D7 because it involves several API breaks:
entity_*
functions.$form_state['entity']
.Also, all this is quite some work, because we will have to update node, user, comment and taxonomy modules to this scheme. Comment.module's form handling for example looks in parts quite ancient..
I thought about doing this partially and doing a part still in D7, but I never found a solution that would be notably cleaner than what we have now without getting back to doing it more or less as outlined above.
What we can still do in D7 is cleaning up the individual entity forms, generalizing on common principles.
Maybe we can also still do API change A) as outlined above. This would kill some ugliness in node module, where the #builder_function currently calls the node form's entity level submit handlers (to e.g. make menu.module work in multistep scenarios). However, it doesn't make a lot of sense without generalizing it to the other entities and basically introducing a hook_entity_validate() and hook_enitity_submit().
Easier ones to still tackle for D7: We can for example always store the entity object in $form_state['entity'] after it has been constructed by the #builder_function.
And we can make it obligatory for all button level submit handlers on an entity form to call the form's #builder_function callback. When rebuilding the form, we just check in the main form callback whether $form_state['entity'] is present and if so use that instead of the entity passed as an argument.
This is basically how node.module does it already. Doing that for all other core entities would definitely make things easier for contrib (e.g. to form_alter a core entity form into a multistep form).
Comment #92
fagothanks frando for the detailed write-up - makes totally sense.
However I don't think a hook_enitity_submit() is the way to go, as extracting form values into the entity depends on the form. There might multiple, different forms that edit the entity, thus a single hook isn't suitable. Instead it probably would make more sense for modules to register their callbacks for building the entity just like they are able to register validation and submit handlers.
As of now http://api.drupal.org/api/function/hook_node_validate/7 is imho kind of broken as the node object is not properly built by node_form_submit_build_node() yet. So I really think we should fix it - which means we have to do that API change nevertheless.
Comment #93
fagoOne thing not considered in #91 is the problem of marking validation failures that can't be fixed by the user, see #74 & #75. This is a problem with entity level validation and a possible way out would be to just not do it as suggested in #75. However that way it's hard for a module to do entity validation as it cannot know all entity forms beforehand, thus a hook is necessary.
So what about something like hook_node_validate() is now, but
* pass in the previous node object, which has already passed validation but doesn't contain the current changes
* pass $form and $form_state
-> That way modules doing validation have to make use of the form values for validation and so they can only do so if the current form really changed the value to be validated. Also that way we can avoid half-baken objects. It looks to me as even the field API doesn't build half-baken objects for validation any more.
When we do so also the necessity of a #builder_function is gone and we can stick with the usual #validation for validation and #submit for extracting values into the entity, thus we can call hook_entity_submit() just on #submit which is likely that what developers would expect to happen.
Comment #94
chx CreditAttribution: chx commentedI think the only way we can move forward, alas, is to tackle this in 8.x and write some docs for 7.x to make groking the state of things easier. Also writing multistep node forms is trivial in D7, if ppl think it's hard then that's a doc that needs to be written too.
Comment #95
fago>Also writing multistep node forms is trivial in D7
Well writing your *own* multistep form isn't hard, yep. But making the node_form() in multiple steps is as it requires you to have all form values there, else values not in there won't be in $node and are lost once the node submit handler calls node_save() on that.
Anyway that's not hard to fix: Just make $form_state['node'] a full node object that gets the form values changes merged in. So at least a little cleanup of the workflow makes still sense for sure.
Apart from that I'd still argue that node_validate() is kind of broken as it passes around a node object that's just an array to object conversion of the form values - thus we have a not fully built object being passed around which might lead weird bugs as we had it already in D5 & D6 - e.g. think of token trying to support multiple, different looking node objects which stem from different form-stages. That's just bad API design I'd prefer to be fixed for d7 instead of being documented ;)
@limit-validation-errors problem noted by sun in #83
-> It's in poll.module: #735800: node form triggers form level submit functions on button level submits, without validation.
Comment #96
chx CreditAttribution: chx commentedThe node form multistep tutorial is at http://drupal4hu.com/node/246 .
And while fixing that would be marvelous I do not believe such a miracle can happen in time given the amount of change necessary as described in #91
Comment #97
fagoYour code has the same problem as the poll module issue mentioned above: On previous it skips validation, but still updates $node with the form level submit handlers. But yes setting #access == FALSE on all other elements does it.
Comment #98
jhodgdonAfter talking briefly with chx in IRC, I filed this issue in the Documentation project issue queue about adding comment #91 as a page to the Handbook:
#738750: Addition to field API pages
I've tentatively assigned that issue to myself, but if someone else would like to take it on, that would be fine with me (since I really have only a small clue what's going on).
Presumably this is now a non-critical issue, if it's been booted to Drupal 8?
Comment #99
Dries CreditAttribution: Dries commentedI just committed #735808: fix multiple field value form to work with form API persistence, by the way.
Comment #100
sunfago's patch over in #735800: node form triggers form level submit functions on button level submits, without validation looks very promising, and might be one of the corner-stones to achieve at least some sanity for D7. That patch, and the possibility to entirely remove #builder_function in a follow-up issue, lets me hope again.
I'm slightly reclassifying this issue into a meta issue, since it served very well for various spin offs we already managed to tackle successfully. Thanks all for your work on those issues! Let's also keep this issue critical. We most probably won't put in dedicated API functions for entity forms, but at least the summary of this and all spinned off issues will have a major impact on the documentation that needs to be written.
Comment #101
catchNo patch here.
Comment #102
Damien Tournoud CreditAttribution: Damien Tournoud commented#91 is a great description, but I probably miss something big here.
I don't understand why we couldn't simply:
Comment #103
effulgentsia CreditAttribution: effulgentsia commentedOne problem with #builder_function is that it needs to be called from all button-level clicks, including ones with #limit_validation_errors. So it gets in the way of fixing #763376: Not validated form values appear in $form_state, and it means every seemingly unrelated button on an entity form (like the file field remove button: #736298: Refactor file.module to use proper button click detection, enabling FAPI to improve button click detection security) that triggers a rebuild must know to call #builder_function: will cause much grief to contrib module authors.
With any luck, we'll be able to get rid of #builder_function entirely. #735800: node form triggers form level submit functions on button level submits, without validation does it for nodes (see #38 and #39 of that issue for a summary), and if we can do it for nodes, surely we can do it for the other entities.
Other than that, I think #102 is basically right and what we're attempting in #735800: node form triggers form level submit functions on button level submits, without validation.
Comment #104
Damien Tournoud CreditAttribution: Damien Tournoud commentedSo if I understand all this correctly, it's our failure to move every element-level validation to #element_validate that causes all this mess in the first place.
All this because we have a dilema:
I see no way out except then assuming that, when we enter the validation, every element has been sufficiently validated and that we can safely build the entity object based on the form values. After that step is done, all subsequent validation should be performed on the built object.
Comment #105
fagoBut if validation fails, the updates would still be in the entity object - except we clone it for validation or prevent caching of the updated object. Also we may not do so in case #limit_validation_errors is turned on, as some values might not have passed validation even the form validates fine. Thus we may not introduce something like #builder_function that is automatically invoked - we really should scratch that. Instead we have that nice feature of form API value persistence when rebuilding a form, so we don't have to update $entity in order to keep the values during rebuild any more.
Also yched pointed out in #74 in regard to entity level validation on fully built objects:
To avoid that, we cannot untie entity level validation from the form workflow. Thus we could just bring entity validation back to the form level with a little helper hook like
- hook_entitytype_validate($unchanged_entity, $form, $form_state);
-> That way we can use hook_entitytype_submit($entity, $form, $form_state) to update the entity afterwards. Imho that's the way to go for D7, as we don't have the complicated "build entity before validation" step involved at all.
Whether we want to implement entity level validation or not, imo that's d8 stuff.
Comment #106
effulgentsia CreditAttribution: effulgentsia commentedFriends: I think the patch in #735800-65: node form triggers form level submit functions on button level submits, without validation gets us as far as we can get with this in D7. Please review it to see if you agree. A long summary is in the comment just below the one with the patch. The patch reflects #105 that validation pipeline changes need to wait until D8. @Damien: given your experience with contrib-defined entities, your review would be much appreciated.
Comment #107
fagoFinally #735800: node form triggers form level submit functions on button level submits, without validation landed! Now entity forms should be much easier :)
However for working out how they are properly extended I opened #830704: Entity forms cannot be properly extended.
Comment #108
effulgentsia CreditAttribution: effulgentsia commentedThanks for the follow-up issue, fago.
I'm not sure there's anything left here that's critical for D7. Please reset this issue's priority if I'm wrong. I'm tempted to change the version of this issue to "8.x-dev". Does this meta issue still have a purpose for D7?
Comment #109
effulgentsia CreditAttribution: effulgentsia commentedBumping to D8 and removing tags. I think this meta issue has run its course for D7, though many more improvements are needed in D8. If new individual issues are needed (whether for D7 or D8), please open them, and link to them from here. If someone wants to bump this back to D7 and assign tags you believe to still be useful, please do so.
Comment #110
xjmCouple of related, targeted issues:
#1261310: uid is not available in hook_field_attach_validate()
#1220212: Provide original entity in hook_field_attach_validate()
This is me miming a productive comment so sun doesn't scold me for subscribing. ;)
Comment #111
sunI think we've improved the entity/field API integration as much as possible for D7, and for D8, the move to classed entities as well as the upcoming #1499596: Introduce a basic entity form controller will improve it further, but from the ground up. So let's close this meta issue.
FWIW, the last major summary was in #91, and was mainly about dealing with unvalidated form input being used on entity properties. If any todo is left with regard to that, then we should open a separate, focused issue for that.
Re-adding tags for lookup/statistical purposes.