My module URL field serializes an array of attributes in url_field_presave(). Things work great if an entity with an URL field that has default values is saved via the UI.
Relevant module code:
function url_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
foreach ($entities as $id => $entity) {
foreach ($items[$id] as $delta => &$item) {
if (empty($item['attributes'])) {
$item['attributes'] = array();
}
else {
$item['attributes'] = unserialize($item['attributes']);
}
}
}
}
function url_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
foreach ($items as $delta => &$item) {
...
// Serialize the attributes array.
$item['attributes'] = !empty($item['attributes']) ? serialize($item['attributes']) : NULL;
}
}
function url_field_widget_form($form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
...
$element['attributes'] = array(
'#type' => 'value',
'#value' => !empty($items[$delta]['attributes']) ? $items[$delta]['attributes'] : array(),
);
return $element;
}
The problem is that when I go to save my field's settings using the field UI, a default value is saved into $instance['default_value'] in the following format:
array (
0 =>
array (
'value' => 'http://example.com/',
'title' => '',
'attributes' =>
array (
),
),
)
url_field_presave() is not run on the default value $items before it is saved. Interestingly, the field settings does run hook_field_validate. The correct format of the default value should be the following:
array (
0 =>
array (
'value' => 'http://example.com/',
'title' => '',
'attributes' => NULL,
),
)
And so now whenever an entity is saved programmatically, the only field hooks that get invoked are the presave hooks via field_attach_presave (which at that point $items is still empty), and then in field_default_insert() (via field_attach_insert) the default value is added, as is, right before being saved by the field storage engine. Obviously, the SQL storage cannot save a PHP array into a text database field without it being serialized first, so we can a PDO exception:
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')' at line 1: INSERT INTO {field_data_field_google_plus_url} (entity_type, entity_id, revision_id, bundle, delta, language, field_google_plus_url_value, field_google_plus_url_title, field_google_plus_url_attributes) VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6, :db_insert_placeholder_7, ); Array ( [:db_insert_placeholder_0] => user [:db_insert_placeholder_1] => 137 [:db_insert_placeholder_2] => 137 [:db_insert_placeholder_3] => user [:db_insert_placeholder_4] => 0 [:db_insert_placeholder_5] => und [:db_insert_placeholder_6] => http://example.com/ [:db_insert_placeholder_7] => ) in field_sql_storage_field_storage_write() (line 448 of /home/dave/Dropbox/Projects/drupal7dev/modules/field/modules/field_sql_storage/field_sql_storage.module).
Steps to reproduce
- Install and enable the URL field module.
- Add an URL field to the user entity with a default value of http://example.com/.
- Run the following PHP code:
$new_user = array( 'name' => 'omg-another-field-bug', 'pass' => user_password(), 'mail' => 'omg-another-field-bug@example.com', 'init' => 'omg-another-field-bug@example.com', 'status' => 1, 'access' => REQUEST_TIME, 'roles' => array(), ); $account = user_save(NULL, $new_user);
Proposed resolution
Have field_ui_field_edit_form_submit() run the field-level hook_field_presave() on its version of $items before saving the default value in the instance settings.
Comments
Comment #1
Dave ReidLikely this needs to be confirmed in Drupal 8 as well. Patch against D7 attached. Should also need tests.
Comment #2
Dave ReidPatch without tests against D8.
Comment #4
Dave ReidInteresting, I'm wondering how the call to hook_field_validate() works in field_ui_field_edit_form_validate(). I'm guessing we don't have coverage for taxonomy_field_validate() being used in a default value?
Comment #6
Dave ReidRe-rolled for HEAD.
Comment #9
johnvMarked (older issue , but without patches) #1944678: On field settings form, hook_field_load() and hook_field_presave() are not called. as a duplicate of this one.
Encountered this problem when developing the office_hours module.
Comment #10
johnvIMO these issues are related, if not duplicates:
#1994594: hook_field_presave() not called reliably
#1899498: Field default values do not get hook_field_presave() run on them
Comment #11
johnv@Dave, the initial post contains hook_field_load(). It is not in one of the patches.
In the settings form, both hook_field_load() and hook_field_presave() are not called. IMO these are a couple.
Comment #12
torotil CreditAttribution: torotil commentedI'm attaching a patch that invokes hook_field_presave() as well as hook_field_load(). I'm also reassigning this to D7 as this issue is not applicable to D8 due to the field-API being rewritten in a OOP fashion.
Comment #14
torotil CreditAttribution: torotil commentedI simply forgot the arguments for entity_extract_ids(). Obviously it was late yesterday :)
Comment #16
torotil CreditAttribution: torotil commentedEDIT: never mind.
Comment #17
torotil CreditAttribution: torotil commentedAnother attempt. (Locally the previously failing tests pass now)
Comment #17.0
torotil CreditAttribution: torotil commentedAdding url_field_load() in relevant code.
Comment #18
Dave ReidComment #19
bineetchaubey CreditAttribution: bineetchaubey commentedi am always getting empty value of $items in hook_field_presave(). while i am using field api .