Problem/Motivation
If you add a list field (any data type), set it to multiple and provide a default value, it is impossible to deselect all options for the field. The field system sees that that no values are supplied and supplies the default value on save. The default value populates the form for the field, so if the user explicitly unchecks them, the field system should not inject it's own defaults instead.
Proposed resolution
Indeed, I can reproduce with radios, checkboxes, but actually also for any field type and widget (e.g simple textfield widget for text or number fields).
1. Set a default value on a field
2. Go to the creation form for an entity using that field
3. Remove the pre-filled value (FAPI #default) and leave the field empty, save
4. The entity has the default value for the field.
This happens in field_default_insert(). Since #392696: Save default values on insert, default values get added on "entity insert" when the incoming $entity has no $entity->field_name property. That was to save default values on programmatic entity saves.
Later on, the 'Translatable fields' patch changed the condition, and default values get added also when
isset($entity->field_name[$langcode]) && count($entity->field_name[$langcode]) == 0
Yet $entity->field_name = array($langcode => array()) is exactly what comes out of any field that was left empty.
I cannot say I specifically remember the reasoning behind the code added by the TF patch, but it looks like we should rather check for !isset($entity->field_name[$langcode]).
We'll also need tests for this :-/.
Attachment Size Status Test result Operations
field_default_value-1253820-1.patchREVIEWSIMPLYTEST.ME 751 bytes Idle FAILED: [[SimpleTest]]: [MySQL] 33,661 pass(es), 1 fail(s), and 1 exception(es). View details | Re-test
Remaining tasks
(reviews needed, tests to be written or run, documentation to be written, etc.)
User interface changes
(New or changed features/functionality in the user interface, modules added or removed, changes to URL paths, changes to user interface text.)
API changes
(API changes/additions that would affect module, install profile, and theme developers, including examples of before/after code if appropriate.)
Related Issues
hook_entity_create() is invoked after creating a new entity object
Comments
Comment #1
yched CreditAttribution: yched commentedIndeed, I can reproduce with radios, checkboxes, but actually also for any field type and widget (e.g simple textfield widget for text or number fields).
1. Set a default value on a field
2. Go to the creation form for an entity using that field
3. Remove the pre-filled value (FAPI #default) and leave the field empty, save
4. The entity has the default value for the field.
This happens in field_default_insert(). Since #392696: Save default values on insert, default values get added on "entity insert" when the incoming $entity has no $entity->field_name property. That was to save default values on programmatic entity saves.
Later on, the 'Translatable fields' patch changed the condition, and default values get added also when
isset($entity->field_name[$langcode]) && count($entity->field_name[$langcode]) == 0
Yet
$entity->field_name = array($langcode => array())
is exactly what comes out of any field that was left empty.I cannot say I specifically remember the reasoning behind the code added by the TF patch, but it looks like we should rather check for
!isset($entity->field_name[$langcode])
.We'll also need tests for this :-/.
Comment #2
sunComment #4
yched CreditAttribution: yched commentedWait, doesn't work. The code comment specifically says "We also check that the current field translation is actually defined before assigning it a default value. This way we ensure that only the intended languages get a default value. Otherwise we could have default values for not yet open languages."
Then I think I'm a bit lost :
- if $entity->field_name[$some_lang] === array() (explicit empty field), we *don't* want a default value (currently broken in D8/D7)
- if $entity->field_name[$some_lang] === NULL, I'd say we *don't* want a default value either - an explicit NULL is understood as 'empty' in other parts of the code.
- if $entity->field_name[$some_lang] is not present, according to the code comment, we wouldn't want a default value, but then again that's precisely the case we wanted to address in #392696: Save default values on insert :
The code specifies nothing for field_foo, which happens to be configured with a default value, which should then be inserted...
Comment #5
plachAw, this is pretty tricky: while introducing the new language key level the rule of thumb was "if in the old code
!isset($entity->field_foo)
is valid in the new code, then!isset($entity->field_foo[$langcode])
should be equally valid". Clearly this does not work here.This cannot work unless we are able to define a default value for the field language too:
I don't think this needs to be a configurable value like the actual value, but it cannot be the default site language. It should be the entity language but we have no such concept in core atm. Anyway I'm going to post a clean up task issue, in order to respond to the many requests for a reliable
field_get_items()
/field_set_items()
pair, that should introduce it. I'll crosspost it here as soon as I write it.The alternative in the case of a non existing field property might be creating a default value for every available language, but I ain't sure this is a desirable default behavior.
Comment #6
yched CreditAttribution: yched commentedMaybe : create a default value for each language that is explicitly present in any of the other fields in the incoming entity ?
Comment #7
plachI am afraid this cannot always work: suppose only untranslatable fields were provided, we would have no clue about which language pick up. IMO the test should be changed in the following way:
Comment #8
plach#1260640: Improve field language API DX
Comment #9
yched CreditAttribution: yched commentedI might be missing something, but I'm not sure that's an issue - we're only dealing with programmatic creation of new entities here. The incoming $entity definition, containing the explicit data provided by the caller, is all we need to care about.
Proposal (assuming we're dealing with 'field_a', configured with a default value) :
- If field_a is not translatable, make sure it gets a value for 'und' language (filling in the default value if not)
- If field_a is translatable, make sure it has a value (filling in the default value if not) in every language present in any of the explicit field values present in the $entity (that is, every language explicitly 'opened' by the incoming $entity definition) - including 'und'.
If no language open (or if $entity has no explicit field value whatsoever) , make sure at least 'und' gets a value.
Comment #10
plachI ain't sure this would fly: if a field is translatable it's not supposed to hold LANGUAGE_NONE values, unless the entity's language is LANGUAGE_NONE (which atm happens only for nodes). Creating an 'und' value for a translatable field belonging to a node having language 'en' would cause it to disappear from the node form.
IMO introducing an
entity_language()
function working exactly asentity_label()
should give us the default we need and should not be absolutely hard or tricky to implement.Comment #11
plachComment #12
webchickFixing tag.
Comment #13
yched CreditAttribution: yched commentedMarked #1052070: If a field has a default value, it cannot be empty on insert. as duplicate.
Comment #14
plach@yched: are you ok with #10?
Comment #15
yched CreditAttribution: yched commentedSorry for the delay, I had a busy fortnight.
#10 will make it so that on a programmatic creation of an $entity spanning several languages, only the main language will receive default values. That sounds a bit unintuitive / inconsistent. Then again, I'm a bit baffled by the topic, so if that's the best we can do...
What about the following (modified from my #9 to address your #10) :
Assuming we're dealing with 'field_a', configured with a default value,
- If field_a is not translatable, make sure it gets a value for 'und' language (filling in the default value if not)
- If field_a is translatable, make sure it has a value (filling in the default value if not) in every language present in any of the explicit field values present in the $entity (that is, every language explicitly 'opened' by the incoming $entity definition) - not including 'und', but at least including the entity_language().
Comment #16
plachI think this can work, any idea which should be the right place to make it happen?
Comment #17
yched CreditAttribution: yched commentedfield_default_insert() is the place. I guess it could use a static var to extract the languages present in the incoming $entity on the 1st call (we can separate different entities because the new $entity being saved gets a nid before field_default_insert() gets called)
I'm leaving tomorrow for 3 weeks away from my coding env, so I probably won't be able to patch this myself, though :-/.
Comment #18
sunBetter title. Also, we should really start with tests here.
Comment #19
xjmI'll look into a test.
Comment #20
plachHere is the entity language issue this one depends on for a proper solution: #1495648: Introduce entity language support.
Comment #22
xjmAttached exposes the bug.
Comment #24
zserno CreditAttribution: zserno commentedRe-rolled @xjm's test from #22 against HEAD.
Comment #25
zserno CreditAttribution: zserno commentedLet's ask the bot. It should fail once.
Comment #27
tim.plunkettRerolled, going to take a look at a fix if I can.
Comment #29
tim.plunkettYeah, I have no idea how to fix this. I think this one is on yched or fago.
Comment #30
plachWorking on this.
Comment #31
plach@yched:
I think we have a problem with the proposed solution: it does not consider that empty submitted values are actually filtered out in
field_attach_submit()
. This makes impossible to skip default values in this case.The attached patch + tests should fix this bug when doing programmatic insertions, I have no clue how to address this for form-driven insertions (AAMOF the related tests are failing). Any suggestion?
Comment #33
plachComment #34
yched CreditAttribution: yched commentedJust thinking out loud, but:
We now have entity_create(), that's used on both programmatic entity creation, and as a preliminary step when showing an "entity add" form.
I'm wondering if this would be the correct place to initialize default values.
Then both flows (programmatic creation & form creation) then become basically the same:
"here's a fresh entity with defaults prepopulated, you can now edit it".
This seems much more sound that what we currently do (populate defaults as default widget values in creation forms + force add them at save time, which clashes).
This being said, I haven't really looked at what the implementation might look like...
Comment #35
plach#34 makes sense, although it's
hardlynot backportable. Here is a patch implementing it. There are some failing tests and API docs are missing, but this needs an architectural review before fixing those issues.Comment #37
plachComment #38
fagoI think so as well. For the Entity Field API we have #1777956: Provide a way to define default values for entity fields - let's sure to get it right there. Then for d8 we can inherit the solution by basing field API on the Entity Field API.
Comment #39
plachWhat do you think about #35? Could we move the approach there in the other issue and expand it to the Entity Field API?
Comment #40
fagoI agree that this hook is a good idea - I already thought about doing that also. Imho it should be hook_entity_create() and follow the entity+$entity_type hook pattern. "Act on entity creation." ?
There is a typo in "intially".
Still, I think it would be preferable to have default values applied via Entity Field API and keep the hook for any custom alterations of them.
Still, we could apply all default values there meanwhile?
Generally, I think the patch is a good improvement so I guess it's a good idea to do this now, while improving things with Entity Field API in a second step. Thus, setting to needs work for the hook name and docs.
Comment #41
yched CreditAttribution: yched commentedThe logic around default values in WidgetBase::form() will need to be removed too.
Comment #42
plachAlso, per #31, I think we cannot fix this in D7 unless we do very radical changes to the current behavior, which I guess we don't want to do at this point of D7 lifecycle.
Comment #43
plachThis should address @fago's review.
Comment #44
plachUps, I missed #41. Here's a new one and an interdiff against #35.
Comment #46
plachThis should fix the latest failures hopefully.
Comment #47
plachI thought we were close to RTBC with this: @fago or @yched can you confirm? This will help with the thresholds.
Comment #48
xjmThese need parameter typehints (no changes yet).
Attached is #46 with a test-only patch including my original test and other test hunks.
Comment #49
yched CreditAttribution: yched commentedMany thanks @plach !
phpdoc for field_populate_default_values():
This is directly copied from field_default_insert() but doesn't really reflect the new code flow now - this happens on entity_create(), so before and independently of both form submits and programmatic saves, and before an $entity->$field_name can even "be provided" ?
Also, do we really need those checks in the function now ? :
Probably related: I'm not sure about field_populate_default_values() as a standalone function. We only intend to do this on hook_entity_create(), not provide this as a general "you might want to do that too" API function, right ? (at least we currently don't, so I'm not sure we need to bother with this additional "api feature").
So maybe just inline the code in field_entity_create(), or at least use an underscored helper func ? (personal pref for the former I guess)
Comment #51
plach#48: field-default_value-1253820-46.patch queued for re-testing.
Comment #52
plachRight! Removed all that obsolete stuff and simplified the PHP doc as now there is not much to say about it. Fixed also missing type hints.
Yes. When calling
entity_create()
you can pass an array of initial values, without those checks the default values here might override those. There is a new test covering this.The main use case I have in mind is supporting the ability of populating defaults when adding a translation: in that case you already have an existing entity object, but you need to fill-in the defaults for the new language.
Comment #53
yched CreditAttribution: yched commented@plach #52 :
OK, makes sense. Where would that happen though ? In core ? In custom code ?
Comment #54
plachI'm guessing in the Entity API, we will need to either introduce entity translation CRUD hooks or make entity translations entities, see #1810370: Entity Translation API improvements.
Comment #55
yched CreditAttribution: yched commentedOK, thanks for the explanation.
The changes here are fine by me, I'll let @fago vouch for the entity API part and set to RTBC.
Comment #56
plach@fago already gave his ok in #40, except for two minor things fixed immediately afterwards :)Sorry, he didn't review the last entity API changes.
Comment #57
fagoI'm unsure here, as this would only cover defaults set by field module. Once we allow setting them via the hook you won't cover others. While this would be solved by having default-values as part of EntityNG/TypedData though, I think we should really start flushing out entity translation-crud API design now (or leave it).
Summarized, I'd see the in-line variant as the right solution, but we can still inline it once we've solved the translation use-case otherwise.
Sounds good. Maybe, should we mention in an extra line that this may be used to set custom default values?
This, adds hook_node_create() and so on also - thus we should create respective API docs for all entity types also.
Minor, but let's do an example that already uses Entity-NG here? -> Less code to convert.
Seems to be an unrelated change? Howsoever, I'm not sure the new code is better than the old one. Maybe just convert it to multiple lines using if()/else()?
This clashes with the comment-conversion patch, which contains a proper fix for it (loading the comment). But, if this one goes in first it should be fine as interim improvement.
Setting to needs-work for the per entity-type API docs.
Comment #58
plachHere is a patch adding the missing PHP docs.
It's needed to make a test pass: previously if you created an entity using a non exisiting language code the language() method returned false, which made lots of things go wrong.
Comment #59
plachComment not wrapping correctly.
Comment #60
fagoI see. In entityNG we have this workaround for the same problem:
Can we unify the work-a-rounds? LANGUAGE_NOT_SPECIFIED sounds reasonable.
Also could we just use if/else instead of ?: - imho having multiple lines of ?: statements is too much.
Is there a difference between initial values and default values?? If not, let's just use "This can be used to set custom default values for entity fields."
Comment #61
plachThis should unify
Entity::language()
andEntityNG::language()
as much as possible. We will be able to rely onhook_entity_create()
in the Language module to provide an initial value of thelangcode
key based on the content language setting, thus effectively removing the possibilty of not having a valid langcode in most situations.Well, actually "initial" sounds slightly more generic to me: the new hook could be used to provide a value just in a particular circumstance. In this case I wouldn't call it a default value.
Comment #62
fagoThanks, improvements look good!
I see, ok.
Sry for not coming up with this previously, but I just realized that the hook description might be confused by devs with hook_comment_insert(). Let's clarify this better. Also, what we are acting on is the entity (object) so what about that:
?
Then, another thing we missed previously: field_entity_create() should check whether the entity is fieldable.
Comment #63
plachAdded the fieldable check and improved PHP docs. I slightly adjusted your suggestion, because I think that the "instantiation" term should give a developer a clear idea of which phase we are talking about. I also streamlined the following sentence a bit to avoid repetitions.
Comment #64
fagoWorks for me - I'd just remove the "just" as we do not use it other hook descriptions either.
Comment #65
plachHere it is :)
Comment #66
fagoThanks - looks good.
Comment #67
tim.plunkettI think these should be generically in entity.api.php as hook_ENTITY_TYPE_create()
You could still avoid the extra $values, just have
on multiple lines
Comment #68
fagoWhile this is a good suggestion, it's not what we do currently. We should follow the current way of doing things and address this question in a general issue for all entity_* hooks, i.e. #1824184: Remove and replace entity type specific API hook documentation with generic entity API hook documentation.
Comment #69
plachI agree with @fago: let's stick with the current way of doing things for now. If/when the documentation policy changes all the docs will be updated together.
The attached patch inlines the
$values
array, although I don't see it exactly as a readability improvement.Comment #70
fagook, back to RTBC then.
Comment #71
tim.plunkettAh, sorry! I often forget if policy changes like that have been adopted already, I was indeed thinking of that exact issue. Carry on. :)
Comment #72
effulgentsia CreditAttribution: effulgentsia commentedWhat's not stated in the docs or clear from the name 'foo' is that this is only needed for entity fields/properties that aren't managed by field.module.
Also this example and the ones for the specific entity type hooks (hook_node_create(), etc.) assume EntityNG, but that's not stated anywhere, so if someone copies this documentation into a module they're working on now, it won't work. Perhaps a @todo would help clarify.
Unless someone beats me to it, I'll give the docs here a crack in a couple hours to keep this moving.
Comment #73
plachActually this would be false :) As I was saying in #61, the new hook could be used to provide a value just in a particular circumstance. In this case I wouldn't call it a default value and it wouldn't necessarily handled by the Field API.
Agreed with the @todo.
Comment #74
plachAdded @todo.
Comment #75
effulgentsia CreditAttribution: effulgentsia commentedHow about this? Similar spirit on the hook_entity_create() example, but for the specific entity types, matches what's already in HEAD for the _load() hook. Both will need to be converted as part of each entity type's conversion to EntityNG, but it shouldn't be harder to convert both together than just the _load() one alone, and in the meantime, we keep them consistent with each other.
Comment #76
plachWorks for me.
Comment #77
fagoYep, indeed the docu should match the current state of the API. +1
Comment #78
Dries CreditAttribution: Dries commented#75: field-default_value-1253820-75.patch queued for re-testing.
Comment #79
Dries CreditAttribution: Dries commentedI reviewed the patch and it looks good at first sight. Could have done a more in-depth review though. Stopped reviewing this patch because I don't think it currently applies. Asked for a re-test.
Comment #81
plachRerolled
Comment #82
plachhm
Comment #83
fagoReroll looks good. Back to RTBC then.
Comment #84
webchickAlex and I actually spent a bit of time walking through this the other day, and I think it's good to go.
Committed and pushed to 8.x. Thanks!
We should add a change notice for this.
Comment #85
plachHere is the change notice:
http://drupal.org/node/1891514
Comment #86
tim.plunkettAny reason that invokeHook() wasn't added to ConfigStorageController as well?
Comment #87
plachI think it's just an oversight :)
Small follow-up?
Comment #88
plachHere is a fix for #86. Btw, I think we should provide an abstract common ancestry to config and content entities when working on storage-agnostic entities and the Storage API.
Edit: I meant config and content entity storage controllers, obviously :)
Comment #89
chx CreditAttribution: chx commentedThe change notice is confusing:
Does that mean that after installing the entity providing module the first ever entity_create('foo') fires this hook or that on every request the first call to entity_create('foo') fires the hook? If the latter...
Comment #90
plachNope, I just meant that it's fired on every
entity_create()
but not when instantiating the object on entity load. Tried to make the first paragraph more clear.Comment #91
tim.plunkettThanks for #88
Comment #92
webchickHm. Seems like it should be possible to write a test for #86/#88, no?.
Comment #93
chx CreditAttribution: chx commentedHm, I do not see a new revision on the change notice.
Comment #94
plach@chx:
In #90 I was referring to this:
http://drupal.org/node/1891514/revisions/view/2523892/2523960
Comment #95
chx CreditAttribution: chx commentedThat's not helping at all. You need to clarify whether "first time" means install time (more or less) or per request first time.
Comment #96
plachOk, I removed the "first time" terminology since it is misleading. Hope now it looks better.
The attached patch provides test coverage for #88. There's also a test only version to prove it.
Comment #97
fagohm, I think the change notice was unnecessarily complex so I revised it. I also removed the field API example and replaced it by a simpler one. So what about that? http://drupal.org/node/1891514
Comment #98
fagoTrue - that said we already have an EntityCRUDTestCase which tests for hook invocations. We should test this hook there also.
Comment #99
plachI think the new version lost lots of useful information about the entity-type specific version of the hook and the main rationale behind it. Anyway, it seems I couldn't get anyone to like it so let's call that part closed.
Comment #100
plachLet's see what the bot thinks about this.
Comment #101
plachSorry, this is the correct interdiff.
Comment #102
plach#100 addresses @webchick's and @fago's concerns so back to RTBC.
Comment #103
webchickAwesome! Glad to see that the tests helped flesh out a few more places we needed this. Sorry for the hassle.
Committed and pushed to 8.x. Thanks!
Comment #104
plachAwesome, thanks!
Comment #105
panche CreditAttribution: panche commentedIf a poor soul reaches this thread and really needs a solution for D7, here's what I did:
Implement a hook_form_alter() like this:
And set a flag in the node object to be used later if the value in the $form_state is an empty string.
The submit callback looks like this:
And finally to check if the node object has the flag with hook_node_insert(), and empty the field value:
Comment #107
ParisLiakos CreditAttribution: ParisLiakos commentedi just stepped on this:/
@panche i couldnt make it work with hook_node_insert..even if fields are actually saved after the hook, it wouldnt work, so i used hook_node_presave.
also no need for the form alter, hook_node_submit also works..here is how i did it:
Comment #108
YesCT CreditAttribution: YesCT commentedrelated? #1500214: Optional radio button value saves as default value, if N/A selected
Comment #109
David_Rothstein CreditAttribution: David_Rothstein commentedThis is still an active bug for Drupal 7, though sounds like a different approach than the Drupal 8 patch will be needed?
Comment #110
plachI thought that per #31 we agreed that we cannot fix this in D7 without making very radical changes.
Comment #111
dcam CreditAttribution: dcam commentedhttp://drupal.org/node/1427826 contains instructions for updating the issue summary with the summary template.
The summary may need to be updated with information from comments.
Comment #112
Pancho@plach:
Well, #34 ff. aren't backportable, but #31 should be, though it's currently incomplete. Let's see how far we can get on this road, so:
field-default_value-1253820-31.patch queued for re-testing.
Comment #112.0
PanchoUpdated issue summary.
Comment #117
brockfanning CreditAttribution: brockfanning commentedThis is still an issue in D7, not surprisingly. #107 works for me as a workaround, though in my case the field in question is an entityreference field, so the 'value' needed to be 'target_id'.