Problem/Motivation
hook_field_attach_validate() allows modules to add their own validation for field values. However, the hook is not provided with the original values for the field, which means that in order to validate changes to values, one has to load the entity in the validation hook, something like the following:
function mymodule_field_attach_validate($entity_type, $entity, &$errors) {
// Check for a pre-existing entity (i.e., the entity is being updated).
list($entity_id, , $bundle) = entity_extract_ids($entity_type, $entity);
if ($entity_id) {
// This is gross.
$orig_entity = entity_load($entity_type, array($entity_id));
$orig_entity = $orig_entity[$entity_id];
}
else {
$orig_entity = FALSE;
}
// Now do stuff.
}
Furthermore, even the above workaround will not work for user profile forms, because the $entity
does not even include the uid
in that case.
Proposed resolution
Pass the original values to hook_field_attach_validate()
in $entity->original
.
The orignal values are not directly available in field_attach_validate(), but they were presumably available to that function's caller. The only caller in core is field_attach_form_validate(), which has both new and original values in the form data.
Remaining tasks
Review proposed solution and patch.
Related issues:
- #1261310: uid is not available in hook_field_attach_validate()
- #367006: [meta] Field attach API integration for entity forms is ungrokable
User interface changes
None.
API changes
None. (The actual entity is not being altered; only the "pseudo entity" used during validation.)
Comment | File | Size | Author |
---|---|---|---|
#58 | form_orig_entity-1220212-58-tests+fix.patch | 7.72 KB | adharris |
#53 | form_orig_entity-1220212-53-tests.patch | 4.12 KB | adharris |
#53 | form_orig_entity-1220212-53-tests+fix.patch | 7.72 KB | adharris |
#53 | form_orig_entity-1220212-53-interdiff.txt | 2.47 KB | adharris |
#49 | form_orig_entity-1220212-49-tests.patch | 4.16 KB | adharris |
Comments
Comment #1
marcingy CreditAttribution: marcingy commentedYou could even append the old entity onto $entity in a similar way to node_save -
Using this approach would remove the need for an api change.
Comment #2
xjmEdit: I think I misunderstood what you were saying. You are suggesting that the entity be in
$entity->original
before it gets passed on tofield_attach_validate()
?Comment #3
marcingy CreditAttribution: marcingy commentedYes given that is the pattern already used in core.
Comment #4
xjmJust noticed that when editing user profiles for existing users, the uid isn't even in the entity during field validation. So the example in the original post won't even work in all cases, because the entity object is not necessarily complete.
Comment #4.0
xjmPurely cosmetic markup change.
Comment #5
xjmComment #6
xjmTagging.
Comment #6.0
xjmAdded note that it doesn't even work for users; updated with new recommended solution.
Comment #7
xjmI finally got around to opening #1261310: uid is not available in hook_field_attach_validate() for #4.
Comment #7.0
xjmAdded clarification as to why this is not a true entity property addition.
Comment #8
xjmBy the way, I think that
$form_state['build_info']['args'][0];
is not a good solution--or at least needs a better comment--but unsure yet what a better fix might be.Comment #9
BTMash CreditAttribution: BTMash commentedMuch like for #1170198: The build variable name for the entity should be the same regardless of type of entity., having a variable with the original entity would be an easier way to go than what looks a bit like magic. What if the various entities on their form() functions added in a
form_state['previous_entity'] = $form_state[$entity_type] = $entity
. Some logic of$form_state[$entity_type] = $entity
seems to already be in place so adding this seems pretty trivial. I'm marking as 'needs work' just to get more ideas about.Comment #10
BTMash CreditAttribution: BTMash commentedTo get conversation going further, here is an initial patch. Hopefully, it helps in getting things going in the right direction. One caveat that I see is that any non-core modules that implement forms for their entities will also need this piece in there. So we'll need to figure out a way to document this for other developers as well.
Comment #11
sunif at all, then it would have to be in $form_state, not in the $form structure
Comment #12
BTMash CreditAttribution: BTMash commentedLet's give this another try :) This now has the change to common.inc to be able to utilize the previous values of entity.
Comment #13
xjmI think this is a pattern that could hold us over until we (hopefully) have some standardized entity form handling from some of the core initiatives. It's also something we could build on to fix #1261310: uid is not available in hook_field_attach_validate() (by merging in the default values from the entity, thereby providing the uid in the field attach hooks). Certainly it's a lot better than using some bizarre, obscure thing from the build args. :) Interested to see what sun thinks.
Comment #13.0
xjmUpdated issue summary.
Comment #14
sunIn _save() operations we already have a notion of "original entities", and those are consistently stored/provided in a ->original property.
For D8, 'entity_original' is probably OK, as I think our goal should be to standardize entity form processing to consistently work on 'entity' instead of the '[entitytypename]'.
For D7, however, we probably want to stay consistent with current $form_state keys; i.e., 'comment_original', etc.
27 days to next Drupal core point release.
Comment #15
xjmMaybe this plus something like in #1170198: The build variable name for the entity should be the same regardless of type of entity.? Or is that too much redundancy?
Comment #16
xjmHere's a D8 version that just renames the original entity to
$pseudo_entity->original
per #14.Comment #17
xjmCouple of potential followup issues:
$form['#user']
instead of$form_state['user']
; perhaps we should add$form_state['user']
and the same note about deprecation as in user_profile_form. Could be a good novice issue. Edit: #1267978: Clean up use of $form['#user'].$form_state['entity']
everywhere in D8; not sure if that would be worthwhile or not as a first step toward generic entity form building. Ideally I think that some generic base class would provide a $entity->form() or suchlike.Comment #18
xjmD7 variant that uses the pattern
$form_state['entitytype_original']
as per #14.Edit: just realized we need to check whether this thing is empty before setting it or it will cause notices for contrib/custom entities.
Comment #19
xjmChecking that the original entity is there before setting it.
Comment #20
xjmD8 version that also checks if the original entity is non-empty.
Comment #21
BTMash CreditAttribution: BTMash commentedFor term (in patch #19), I believe the entity_type being passed should be 'taxonomy_term' so you have
$form_state['taxonomy_term_original'] = $term;
based on what I see in http://api.drupal.org/api/drupal/modules--taxonomy--taxonomy.admin.inc/f...? Other than that, the backport looks great. The D8 patch also looks sound.Comment #22
xjmHmm, this would put us in the same situation as in #1170198: The build variable name for the entity should be the same regardless of type of entity., if the entity type is
taxonomy_term
and the form component is$form_state['term']
. So we either need that extra, redundant key as in #1170198: The build variable name for the entity should be the same regardless of type of entity. that indicates what the name of the entity form component is, or to just go with the genericentity_original
in D7 as well.Comment #23
pav CreditAttribution: pav commentedJust tried applying the patch (#19). Returns
I've renamed the patch, but otherwise it is unaltered. I'm pretty new at this. Have I got something wrong?
Comment #24
xjm@pav: The messages you got indicate the patch needs to be rerolled. I'll do so. And then do so again after #22336: Move all core Drupal files under a /core folder to improve usability and upgrades. :)
Comment #25
xjmFailed hunk was due to entity code being moved into its own module. Patch below fixes this.
Comment #26
pav CreditAttribution: pav commentedHi, xjm. Thanks for the reply and the trouble, but #19 was a D7 variant rather than a D8 one.
PS. I don't know if this is of any use to you, but I came to try to install this patch, because another patch for the D7 version of og_create_perms.module requires it. http://drupal.org/node/1220212#comment-5181306
Comment #27
xjmHmm, I was able to apply #19 to the 7.x branch with just offset. In any case, here's a reroll. Note that it is against the current 7.x branch, which means it's for the 7.x-dev version of Drupal.
Is there an issue for the module you mention that references this one?
Comment #28
BrightBoldPatch from #27 applied cleanly for me on D7.9.
I am also chasing down the set of errors reported in the OG Create Permissions issue mentioned in #26, #1321028: EntityMalformedException: Missing bundle property on entity of type node. in entity_extract_ids(). In that issue, mrfelton offers a patch that relies on the patch in this issue. (And the reason I care about that issue is that I'm hoping it's at the root of #1275152: EntityMalformedException error on Content Translation Upgrade with Drupal 7.8. Aah, the fun of cascading errors!)
Comment #29
pav CreditAttribution: pav commentedxjm, same issue. I'm sure this is connected to how I was applying the patch - from inside the includes directory, using
patch commons inc form_orig_entity_12220212_25-d7.patch
I've tried doing it from drupal's home directory with
patch -p0 < form_orig_entity_12220212_25-d7.patch
but with even poorer results. Could you advise on the correct way to apply the patch. Thanks,
Pav
Comment #30
xjmThe patch was created with git. Place the file in the root of your Drupal installation and use the
-p1
option:patch -p1 < form_orig_entity_12220212_25-d7.patch
See also: http://drupal.org/patch
Also, if you are testing the patch on D7, please make note of the questions in #21 and #22.
Comment #31
xjmRerolled for
core/
directory.Comment #32
pav CreditAttribution: pav commentedthanks, xjm. The D7 patch applied smoothly and, so far, working like a dream. :-)
Comment #33
pav CreditAttribution: pav commentedYour patch, for me, fixes the problem with og_create_perms, but I've just spotted another situation which throws up a similar set of error messages, which also look like an og/entities conflict. Thought this might be of some use to you... After trying to set a group context filter for a panel layout, on page load I get
Comment #34
xjmRegarding #33, you'll probably want to file that in the queue for that OG module. :)
Comment #35
pav CreditAttribution: pav commentedCool. Issue in #33 filed at http://drupal.org/node/1328264.
Comment #36
othermachines CreditAttribution: othermachines commentedPatch from #27 got rid of the following issue for me in 7.8:http://drupal.org/node/1324748
Darn. Scratch that.
Comment #37
xjmRelated issue in TAC:
#1362210: Notice: Undefined property: stdClass::$name in taxonomy_access_field_attach_validate() of taxonomy_access.module (line 299)
Comment #38
BTMash CreditAttribution: BTMash commented#31: d8_form_orig_entity_12220212-POST-APOCALYPSE.patch queued for re-testing.
Comment #40
xjmNeeds another reroll, I guess. Tagging novice for that task.
Comment #41
oriol_e9gReroll
Comment #42
xjmThanks @oriol_e9g!
Still needs an automated test that takes advantage of this property for various entity types.
Comment #43
adharris CreditAttribution: adharris commentedI'll work on getting some tests for this.
Comment #44
adharris CreditAttribution: adharris commentedThese should work, but I was a bit unsure the best place to put the tests for this, so if anyone has feedback on a better place to put them let me know and I'll adjust.
Comment #45
BrightBoldWould someone be willing to reroll for D7? Thanks!
Comment #46
xjm@BrightBold: The issue is against 8.x, but it is tagged "needs backport to D7." This means that, if the patch is accepted, a D7 patch will be created and committed once the D8 change is finalized.
Comment #47
xjmThanks @adharris! The test coverage there looks correct and thorough. I reviewed and have one remark about the placement of the tests, plus a few minor points for cleanup.
Hmm, I'm not sure it's wise to add this to an existing class. I think we should either create our own subclass of
FieldAttachTestCase
, or place the tests incomment.test
,taxonomy.test
, etc. (The latter is probably more correct, since we are relying on specific implementations provided by these entity modules rather than the base entity class... but of course it's less practical. And also I could be totally wrong, so maybe let's go with the simpler option for now.)drupal_set_message() and hook_field_attach_validate() should have parens in the comments here to clearly indicate that they are function names.
Should begin with "Tests" (rather than "Test") and end in a period. Reference: http://drupal.org/node/1354#functions
missing space after
foreach
here. (It's a language construct rather than a function, so it follows the standard for control structures.)The assertion message here should be what is displayed when the assertion is true... so, in this case, it's actually that the original entity is present. (It's when the assertion fails that the original entity is missing.)
As above, in these messages (and the assertions that test for them), there should be parens on hook_field_attach_validate() to indicate that it's a function.
Also, we should probably use
format_string()
to place the$entity_type
in the message (both here and in the assertions that test for these messages).Comment #48
adharris CreditAttribution: adharris commented@xjm Thanks for the feedback, I'll make the above changes.
In response to your first point: it seems to me that if the individual tests are moved to their respective components, there would have to be a test class in each of
comment
,taxonomy
, etc that loadsfield
andfield_test
. For some reason that seems just as odd as loading those modules infield.test
. I'll move the test case into it's own class and see how that looks.Comment #49
adharris CreditAttribution: adharris commentedThis moves the tests to their own class, but leaves them in
field.test
. Also fixes the code style issues from #47.Comment #50
adharris CreditAttribution: adharris commentedAnd I forgot to queue for testing.
Comment #51
xjmThanks @adharris. The latest patch looks good to me except for a few minor style issues (and one question):
We need to add a docblock for the new class, with a summary something like: "Tests field attach hooks for multiple core entity types."
Still missing a period here.
I was about to say the message texts here should also use
format_string()
, but, looking at this now, I realize we can probably just leave off the message parameter entirely here. The default message will be informative enough and save us this redundancy. :) (See the assertTextHelper() docs for the message it will display.) What do you think?Looks like this message is missing the placeholder. (Oops!)
Leaving at NR for others to weigh in on the question of test placement. (I also probably can't RTBC this since it started out as my patch.)
Comment #52
xjmActually I realize now my 4th point interferes with the test coverage, so marking NW for that.
Comment #53
adharris CreditAttribution: adharris commentedI swear I check for the code styles before I do these. Don't know how i miss so many things like that :).
Comment #54
xjmAlright, everything in #53 looks correct to me now, and the test-only patch shows the correct assertion failures.
Comment #55
agentrickardThere's a typo in the test function field_test_field_attach_validate().
Comment #56
xjm@agentrickard also said in IRC that the patch looks good to him other than that, so let's have someone clean up that minor issue and I think this is RTBC. Thanks!
Comment #57
agentrickardYes. I had to run. But it is RTBC except for typos.
Comment #58
adharris CreditAttribution: adharris commentedfixed the typo
Comment #59
agentrickardComment #60
fagoThis won't be the original entity. If the form is reloaded multiple times it's the entity before the last reload.
entity_load_unchanged() should be used for retrieving the original entity, however it should be done only once. Thus $entity->original has the be already populated before doing the entity-save to avoid loading the original twice.
We could also save the original entity during the *whole* form workflow (as this patch tried too). Still, then the original entity should be pre-populated before saving so the same original is available during presave/insert/udpate hooks.
Comment #61
HongPong CreditAttribution: HongPong commentedThis may be a tangent but if you have malformed files entities - see #1446440: Unable to view me media management pages (EntityMalformedException: Missing bundle property...) for my one-off SQL fix for column type in the files_managed table.
Comment #61.0
HongPong CreditAttribution: HongPong commentedAdding related meta issue to summary.
Comment #74
smustgrave CreditAttribution: smustgrave at Mobomo commentedIn https://www.drupal.org/node/1882428 looks like the function in question has changed.
So wonder if still a valid task?