Hi -

I'm porting a module to D7/Fields and having a bit of trouble with the hook_field_validate().

I would expect that I could change $items and have that change the actual value, but I can't. Even though I change $items (and the values look good) it's still the same old $items when we get to hook_field_format_views().

The use case: People enter one of several formats for the ASIN. They get converted in hook_field_validate(), which then updates the $items array (wishfully).

The code is below. Am I doing something crazy? It seems like most form functions allow updating.

function asin_field_validate($entity_type, $entity, $field, $instance, $langcode, &$items, &$errors) {
  foreach ($items as $delta => &$item) {
    $asin = $item['asin'];
    if (!empty($asin)) { // Only continue if the ASIN is populated.
      $results = _asin_load_items($asin);
      if (!empty($results)) {
        $item['asin'] = key($results);
      }
      else {
        $errors[$field['field_name']][$langcode][$delta][] = array(
          'error' => t('No Amazon product with the ASIN "%id" could be located.', array('%id' => $item['asin'])),
        );
      }
    }
  }
}

Comments

yched’s picture

field_attach_form_validate() and field_attach_submit() both start with fresh $items extracted from $form_state['values'], so the $items you change in validate don't live up to submit time.

For that matter, one level up, node_form_validate() itself builds a "$node", runs node_validate() and field_attach_form_validate() on it, then ditches it as well. The $items you'd alter in hook_field_validate() end up in a $node that is thrown away in node module.

Alterations need to happen in hook_field_presave().

More globally, I might get it wrong, but your code seems to assume an incoming $item['asin'] in a different format that the one that gets stored in the end (since the code needs to do some 'translation' with $item['asin'] = key($results)).
If that's the case, then the check and the 'translation' should happen in a FAPI #validate_callback set by the widget. It's the given widget type that allows a given user-friendly input format and has to handle it. A different (imaginary) widget might have different needs and input format, hook_field_validate() cannot handle all cases for widgets it doesn't know about.
hook_field_validate() comes after FAPI validation, and should only validate data in the 'canonical' format that gets stored and loaded.
You should be able provide f_a_validate() with a freshly loaded node :

$node = node_load($nid);
$node->field_foo = some modification;
field_attach_validate($node);

But, again, I might get your code wrong, and it's possibly correct as is.

+ 'error' => t('No Amazon ...') should be 'message' => t('No Amazon ...'), and 'error' should be an error code (an arbitrary machine-name string that makes sense for the error - 'amazon_invalid_product' or something)

rfay’s picture

Thanks so much for the help! That's top quality. I'll give it a shot today.

rfay’s picture

Status: Active » Fixed

Thanks very much for your help.

I did a #element_validate on the FAPI textfield. It seemed a bit awkward, but does in fact seem to work correctly. Any other ways I could have done this with FAPI? (Thanks for the suggestions above on how to do it with hook_field_presave()).


/**
 * Widget validation function.
 * 
 * Checks to see if we can look up an ASIN, URL, or ISBN-13 using the
 * provided text. If we can, it's OK and we turn it into an ASIN.
 * Otherwise, flag as error.
 */
function asin_field_widget_element_validate($element, &$form_state) {
  $field_name = $element['#field_name'];
  $langcode = $form_state['langcode'];

  foreach ($form_state['values'][$field_name][$langcode] as $delta => &$item) {
    $asin = $item['asin'];
    if (!empty($asin) && is_numeric($delta)) {
      $results = _asin_load_items($asin);
      if (empty($results)) {
        form_set_error("{$field_name}][{$langcode}][{$delta}][asin]", t('No Amazon product with the ASIN "%id" could be located.', array('%id' => $asin)));
      }
      else {
        $item['asin'] = (string)key($results);
      }
    }
  }
}
yched’s picture

Nope, that's exactly the idea.

Only thing is to use form_error($element) instead of form_set_error("hard_to_find_element_name")
and form_set_value($element) instead of directly writing to $form_state['values'].
See how number_field_widget_validate() does (I should probably have pointed you to that function earlier :-/).

Also note that number_field_widget_validate() (widget-level validation as a FAPI callback) only checks that the user input is well formed.
number.module's hook_field_validate() (field-level validatation) does the generic, widget-agnostic checks (min and max value...).
Once you get that, you grok the (admittedly tricky) separation between widget validation and field validation ;-).

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.