在保存节点之前,通常以自定义形式检测更改的字段


12

我正在使用field_attach_form()将内容类型中的某些字段添加到自定义表单中。提交表单后,我正在通过#validate和#submit回调调用field_attach_form_validate()和field_attach_submit()处理这些字段。

在这一点上,我想将提交后的准备好的节点对象与原始节点进行比较,并且如果任何字段已更改,则仅对node_save()进行比较。因此,我首先使用加载原始节点entity_load_unchanged()

不幸的是,即使未对字段进行任何更改,原始节点对象中的字段数组也不与准备保存的节点对象中的字段数组匹配,因此,简单的“ $ old_field == $ new_field比较是不可能的。例如,原始文本中会出现一个简单的文本字段:

$old_node->field_text['und'][0] = array(
  'value' => 'Test',
  'format' => NULL,
  'safe_value' => 'Test',
);

而在准备好的节点中,它看起来像这样。

$node->field_text['und'][0] = array(
  'value' => 'Test',
);

您可能会想只比较“值”键,但随后会遇到由没有“值”键的其他元素组成的字段。例如,让我们看一个地址字段,其中没有“值”键在旧节点和准备好的节点中都没有对应的键。

旧节点

$old_node->field_address['und'][0] = array(
  'country' => 'GB',
  'administrative_area' => 'Test',
  'sub_administrative_area' => NULL,
  'locality' => 'Test',
  'dependent_locality' => NULL,
  'postal_code' => 'Test',
  'thoroughfare' => 'Test',
  'premise' => 'Test',
  'sub_premise' => NULL,
  'organisation_name' => 'Test',
  'name_line' => 'Test',
  'first_name' => NULL,
  'last_name' => NULL,
  'data' => NULL,
);

准备好的节点

$node->field_address['und'][0] = array(
  'element_key' => 'node|page|field_address|und|0',
  'thoroughfare' => 'Test',
  'premise' => 'Test',
  'locality' => 'Test',
  'administrative_area' => 'Test',
  'postal_code' => 'Test',
  'country' => 'GB',
  'organisation_name' => 'Test',
  'name_line' => 'Test',
);

对于空白字段,还有另一个差异。

旧节点

$old_node->field_text = array();

准备好的节点

$node->field_text = array(
  'und' => array(),
);

我是否可以一般地比较任何字段的旧值和新值以检测其是否已更改?
这只是不可能吗?


我认为您可以使用_field_invoke()“准备好”的节点准备完整的字段结构,或者渲染两个字段,然后简单地比较这些HTML字符串,或者进行一些相关的工作。只是一个主意。
kalabro

@kalabro是的,这绝对是正确的方法,尽管如此,我还是忍不住觉得这会对性能造成很大的影响-要使其通用,则需要使用表单提交来分别加载每一位字段信息。或者我猜您可以编写一个汇总查询来获取数据,但是重要的钩子可能不会触发。从概念上讲,这似乎是可能的,但我认为实现会非常复杂
Clive

@kalabro我不太了解这个想法。您可以张贴一些伪代码来演示如何准备字段结构,然后按照您的描述进行渲染吗?
2013年

Answers:


9

最后,这应该作为通用解决方案。感谢Clive和morbiD的所有投入。

将节点的两个版本都传递给以下函数。它会:

  1. 在单个查询中,从数据库中提取所有检测到的内容类型的可编辑字段及其可编辑列(即可能出现在自定义窗体上的项目)。

  2. 忽略两个版本中完全为空的字段和列。

  3. 将两个版本之间具有不同数量值的字段视为更改。

  4. 遍历每个字段,值和列,然后比较两个版本。

  5. 如果它们是数字,则比较不同的项目(!=),如果是其他项目,则进行相同的(!==)比较。

  6. 在它检测到的第一个更改上立即返回TRUE(因为一个更改足以知道我们需要重新保存该节点)。

  7. 比较所有值后,如果未检测到更改,则返回FALSE。

  8. 通过加载字段及其架构并将结果传递给自己来递归比较字段集合。这甚至应该允许它比较嵌套的字段集合。该代码不应与“字段收集”模块有任何依赖性。

让我知道此代码中是否还有其他错误或错别字。

/*
 * Pass both versions of the node to this function. Returns TRUE if it detects any changes and FALSE if not.
 * Pass field collections as an array keyed by field collection ID.
 *
 * @param object $old_entity
 *   The original (stored in the database) node object.
 *   This function may also pass itself a FieldCollectionItemEntity object to compare field collections.
 * @param object $new_entity
 *   The prepared node object for comparison.
 *   This function may also pass itself a FieldCollectionItemEntity object to compare field collections.
 */
function _fields_changed($old_entity, $new_entity) {
  // Check for node or field collection.
  $entity_is_field_collection = (get_class($old_entity) == 'FieldCollectionItemEntity');

  $bundle = ($entity_is_field_collection ? $old_entity->field_name : $old_entity->type);

  // Sanity check. Exit and throw an error if the content types don't match.
  if($bundle !== ($entity_is_field_collection ? $new_entity->field_name : $new_entity->type)) {
    drupal_set_message('Content type mismatch. Unable to save changes.', 'error');
    return FALSE;
  }

  // Get field info.
  $field_read_params = array(
    'entity_type' => ($entity_is_field_collection ? 'field_collection_item' : 'node'),
    'bundle' => $bundle
  );
  $fields_info = field_read_fields($field_read_params);

  foreach($fields_info as $field_name => $field_info) {
    $old_field = $old_entity->$field_name;
    $new_field = $new_entity->$field_name;

    // Check the number of values for each field, or if they are populated at all.
    $old_field_count = (isset($old_field[LANGUAGE_NONE]) ? count($old_field[LANGUAGE_NONE]) : 0);
    $new_field_count = (isset($new_field[LANGUAGE_NONE]) ? count($new_field[LANGUAGE_NONE]) : 0);

    if ($old_field_count != $new_field_count) {
      // The two versions have a different number of values. Something has changed.
      return TRUE;
    } elseif ($old_field_count > 0 && $new_field_count > 0) {
      // Both versions have an equal number of values. Time to compare.

      // See if this field is a field collection.
      if ($field_info['type'] == 'field_collection') {

        foreach ($new_field[LANGUAGE_NONE] as $delta => $values) {
          $old_field_collection = entity_load_unchanged('field_collection_item', $values['entity']->item_id);
          $new_field_collection = $values['entity'];

          if (_fields_changed($old_field_collection, $new_field_collection)) {
            return TRUE;
          }
        }
        unset($delta, $values);

      } else {
        foreach($old_field[LANGUAGE_NONE] as $delta => $value) {
          foreach($field_info['columns'] as $field_column_name => $field_column_info) {
            $old_value = $old_field[LANGUAGE_NONE][$delta][$field_column_name];
            $new_value = $new_field[LANGUAGE_NONE][$delta][$field_column_name];
            $field_column_type = $field_column_info['type'];

            // As with the overall field, exit if one version has a value and the other doesn't.
            if (isset($old_value) != isset($new_value)) {
              return TRUE;
            } elseif (isset($old_value) && isset($new_value)) {
              // The column stores numeric data so compare values non-identically.
              if (in_array($field_column_type, array('int', 'float', 'numeric'))) {
                if ($new_value != $old_value) {
                  return TRUE;
                }
              }
              // The column stores non-numeric data so compare values identically,
              elseif ($new_value !== $old_value) {
                return TRUE;
              }
            } else {
              // Included for clarity. Both values are empty so there was obviously no change.
            }
          } 
          unset($field_column_name, $field_column_info);
        }
        unset($delta, $value);
      }
    } else {
      // Included for clarity. Both values are empty so there was obviously no change.
    }
  }
  unset($field_name, $field_info);
  // End of field comparison loop.

  // We didn't find any changes. Don't resave the node.
  return FALSE;
}

有时您想知道哪些字段已更改。为了知道这一点,您可以使用以下版本的函数:

/*
 * Pass both versions of the node to this function. Returns an array of
 * fields that were changed or an empty array if none were changed.
 * Pass field collections as an array keyed by field collection ID.
 *
 * @param object $old_entity
 *   The original (stored in the database) node object.
 *   This function may also pass itself a FieldCollectionItemEntity object to compare field collections.
 * @param object $new_entity
 *   The prepared node object for comparison.
 *   This function may also pass itself a FieldCollectionItemEntity object to compare field collections.
 */
function _fields_changed($old_entity, $new_entity) {
  // Check for node or field collection.
  $entity_is_field_collection = (get_class($old_entity) == 'FieldCollectionItemEntity');

  $bundle = ($entity_is_field_collection ? $old_entity->field_name : $old_entity->type);

  // Sanity check. Exit and throw an error if the content types don't match.
  if ($bundle !== ($entity_is_field_collection ? $new_entity->field_name : $new_entity->type)) {
    drupal_set_message('Content type mismatch. Unable to save changes.', 'error');
    return FALSE;
  }

  // Get field info.
  $field_read_params = array(
    'entity_type' => ($entity_is_field_collection ? 'field_collection_item' : 'node'),
    'bundle' => $bundle
  );
  $fields_info = field_read_fields($field_read_params);

  $fields_changed = array();

  foreach ($fields_info as $field_name => $field_info) {
    $old_field = $old_entity->$field_name;
    $new_field = $new_entity->$field_name;

    // Check the number of values for each field, or if they are populated at all.
    $old_field_count = (isset($old_field[LANGUAGE_NONE]) ? count($old_field[LANGUAGE_NONE]) : 0);
    $new_field_count = (isset($new_field[LANGUAGE_NONE]) ? count($new_field[LANGUAGE_NONE]) : 0);

    if ($old_field_count != $new_field_count) {
      // The two versions have a different number of values. Something has changed.
      $fields_changed[] = $field_name;
    }
    elseif ($old_field_count > 0 && $new_field_count > 0) {
      // Both versions have an equal number of values. Time to compare.

      // See if this field is a field collection.
      if ($field_info['type'] == 'field_collection') {

        foreach ($new_field[LANGUAGE_NONE] as $delta => $values) {
          $old_field_collection = entity_load_unchanged('field_collection_item', $values['entity']->item_id);
          $new_field_collection = $values['entity'];

          $fields_changed = array_merge($fields_changed, _fields_changed($old_field_collection, $new_field_collection));
        }
        unset($delta, $values);

      }
      else {
        foreach ($old_field[LANGUAGE_NONE] as $delta => $value) {
          foreach ($field_info['columns'] as $field_column_name => $field_column_info) {
            $old_value = $old_field[LANGUAGE_NONE][$delta][$field_column_name];
            $new_value = $new_field[LANGUAGE_NONE][$delta][$field_column_name];
            $field_column_type = $field_column_info['type'];

            // As with the overall field, exit if one version has a value and the other doesn't.
            if (isset($old_value) != isset($new_value)) {
              $fields_changed[] = $old_field;
            }
            elseif (isset($old_value) && isset($new_value)) {
              // The column stores numeric data so compare values non-identically.
              if (in_array($field_column_type, array(
                'int',
                'float',
                'numeric'
              ))) {
                if ($new_value != $old_value) {
                  $fields_changed[] = $field_name;
                }
              }
              // The column stores non-numeric data so compare values identically,
              elseif ($new_value !== $old_value) {
                $fields_changed[] = $field_name;
              }
            }
            else {
              // Included for clarity. Both values are empty so there was obviously no change.
            }
          }
          unset($field_column_name, $field_column_info);
        }
        unset($delta, $value);
      }
    }
    else {
      // Included for clarity. Both values are empty so there was obviously no change.
    }
  }
  unset($field_name, $field_info);
  // End of field comparison loop.

  return $fields_changed;
}

有时您可能想要这样做,以便更改节点的某些字段不会导致该节点的“更改”时间戳被更新。可以这样实现:

/**
 * Implements hook_node_presave().
 */
function mymodule_node_presave($node) {
  $fields_changed = _fields_changed($node->original, $node);
  $no_update_timestamp_fields = array('field_subject', 'field_keywords');
  if (!empty($fields_changed) &&
    empty(array_diff($fields_changed, $no_update_timestamp_fields))) {
    // Don't change the $node->changed timestamp if one of the fields has
    // been changed that should not affect the timestamp.
    $node->changed = $node->original->changed;
  }
}

编辑(7/30/2013)加强了字段收集支持。添加了对具有多个值的字段的支持。

编辑(7/31/2015)添加了版本的函数,函数返回更改了哪些字段以及示例用例。


太好了,我觉得这应该在某种api模块中供开发人员使用。
耶尔(Jelle)

3

这是另一种更简单的方法,它避免了复杂的服务器端值比较,并且可以以任何形式使用:

  1. 使用jQuery检测表单值是否已更改
  2. 设置一个隐藏元素值以指示表单已更改。
  3. 检查隐藏元素值服务器端并根据需要进行处理。

您可以使用jQuery脏表单插件,例如https://github.com/codedance/jquery.AreYouSure

尽管其他可以让您听取表单更改/脏状态的消息也可以使用。

添加一个侦听器以设置隐藏表单元素的值:

将隐藏的表单元素设置为默认值“更改”,以默认情况下为那些禁用了javascript的用户(〜2%)保存。

例如:

// Clear initial state for js-enabled user
$('input#hidden-indicator').val('')
// Add changed listener
$('#my-form').areYouSure({
    change: function() {
      // Set hidden element value
      if ($(this).hasClass('dirty')) {
        $('input#hidden-indicator').val('changed');
      } else {
        $('input#hidden-indicator').val('');
      }
    }
 });

然后,您可以检查隐藏元素的值

if ($form_state['values']['hidden_indicator'] == 'changed') { /* node_save($node) */ }

在您的表单中验证/提交处理程序。


2
不错的解决方案,尽管显然有些用户没有js。另外,请查看drupal核心的misc / form.js文件中的Drupal.behaviors.formUpdated。还要注意的另一件事是,通过某些所见即所得编辑器及其drupal模块的工作方式,检测更改后的值并不总是那么应该直接。
rooby

是的,为隐藏的元素设置默认值'changed'将默认为那些没有启用js的少数用户保存-很小的百分比。有趣Drupal.behaviors.formUpdatedval()是,尽管看起来好像它会在没有实际更改值的情况下触发(例如,包括click事件),但可能与之相关,而专用插件则更擅长检测实际更改的表单值。
David Thomas

0

我不确定这是否完美,但是为什么不通过比较表单而不是节点对象来反过来呢?

我不确定您是否严格遵循节点表单,但是无论如何,您都可以使用旧节点和新节点来呈现表单:

module_load_include('inc', 'node', 'node.pages');
node_object_prepare($new_node);
$new_form = drupal_get_form($new_node->node_type . '_node_form', $new_node);
node_object_prepare($old_node);
$old_form = drupal_get_form($old_node->node_type . '_node_form', $old_node);

比较您的表格...

我希望这是一条好路...让我知道。


我已经研究过drupal_get_form(),但是我没有意识到您可以将$ node作为第二个参数传递给它。但是,不幸的是,我刚刚在上面测试了您的示例代码,尽管返回的数组结构相同,但值却不同。看一下我正在测试的地址字段的递归array_diff_assoc():i.imgur.com/LUDPu1R.jpg
morbiD 2013年

我看到了array_diff_assoc,但是您有时间给两个drupal_get_form的dpm吗?可能有办法解决。
格雷戈里·卡普斯汀

0

这是使用hook_node_presave($ node)的方法。它只是一个样机,如果您认为有帮助,请对其进行测试并根据需要进行改进!

  /**
   * Implements hook_node_presave().
   *
   * Look for changes in node fields, before they are saved
   */
  function mymodule_node_presave($node) {

    $changes = array();

    $node_before = node_load($node->nid);

    $fields = field_info_instances('node', $node->type);
    foreach (array_keys($fields) as $field_name) {

      $val_before = field_get_items('node', $node_before, $field_name);
      $val = field_get_items('node', $node, $field_name);

      if ($val_before != $val) {

        //if new field values has more instances then old one, it has changed
        if (count($val) != count($val_before)) {
          $changes[] = $field_name;
        } else {
          //cycle throught 1 or multiple field value instances
          foreach ($val as $k_i => $val_i) {
            if (is_array($val_i)) {
              foreach ($val_i as $k => $v) {
                if (isset($val_before[$k_i][$k]) && $val_before[$k_i][$k] != $val[$k_i][$k]) {
                  $changes[] = $field_name;
                }
              }
            }
          }
        }
      }
    }
    dpm($changes);
  }

我想对于每个字段值,必须在$ node中定义的实例必须在$ node_before中定义且相等。我不在乎$ node_before中和不在$ node中的字段值字段,我想它们保持不变。


也许我丢失了一些东西,但是hook_node_presave()并不暗示已调用node_save()吗?如果未更改任何字段,我们将尝试避免调用node_save()。
morbiD 2013年

确实,此钩子在node_save()内部被称为。但是您仍然可以通过在mymodule_node_presave()中调用drupal_goto()来取消保存。
dxvargas 2013年

2
@hiphip确实不是一个好主意,如果在中间进行重定向,则将节点保存在不一致的状态中
Clive

0

这只是我拼凑的一些代码。所有功劳必须归功于@eclecto。这只是一个(类似未经测试的)变体,它直接获取节点对象,减少了数据库命中率,并负责语言协商。

function _node_fields_have_changed($old_node, $new_node) {
  // @TODO Sanity checks (e.g. node types match).

  // Get the fields attached to the node type.
  $params = array('entity_type' => 'node', 'bundle' => $old_node->type);
  foreach (field_read_fields($params) as $field) {
    // Get the field data for both nodes.
    $old_field_data = field_get_items('node', $old_node, $field['field_name']);
    $new_field_data = field_get_items('node', $new_node, $field['field_name']);

    // If the field existed on the old node, but not the new, it's changed.
    if ($old_field_data && !$new_field_data) {
      return TRUE;
    }
    // Ditto but in reverse.
    elseif ($new_field_data && !$old_field_data) {
      return TRUE;
    }

    foreach ($field['columns'] as $column_name => $column) {
      // If there's data in both columns we need an equality check.
      if (isset($old_field_data[$column_name]) && isset($new_field_data[$column_name])) {
        // Equality checking based on column type.
        if (in_array($column['type'], array('int', 'float', 'numeric')) && $old_field_data[$column_name] != $new_field_data[$column_name]) {
          return TRUE;
        }
        elseif ($old_field_data[$column_name] !== $new_field_data[$column_name]) {
          return TRUE;
        }
      }
      // Otherwise, if there's data for one column but not the other,
      // something changed.
      elseif (isset($old_field_data[$column_name]) || isset($new_field_data[$column_name])) {
        return TRUE;
      }
    } 
  }

  return FALSE;
}

1
您让我与新版本的思路一致。我什至包括了节点类型的完整性检查。
Eric N

0

提供的答案很好,对我有帮助,但是我必须纠正一些问题。

// See if this field is a field collection.
if ($field_info['type'] == 'field_collection') {
  foreach ($old_field[LANGUAGE_NONE] as $delta => $values) {
    $old_field_collection = entity_load_unchanged('field_collection_item', $values['entity']->item_id);
    $new_field_collection = $values['entity'];

    $fields_changed = array_merge($fields_changed, erplain_api_fields_changed($old_field_collection, $new_field_collection));
  }
  unset($delta, $values);
}

foreach()循环中,我不得不从更改$new_field$old_field。我不知道这是Drupal的新版本还是仅是我的代码(可能归因于其他地方的其他代码),但我无权访问$new_field['entity']


我刚刚在新的Drupal 7.41安装上测试了_fields_changed()函数,并用field_collection保存一个节点,这给了我$ old_field和$ new_field。在我看来,您可能以错误的方式调用了带有$ old_entity和$ new_entity参数的_fields_changed()(或者您不小心在代码中的某个地方交换了变量名)。
morbiD 2015年

0

感谢您的帖子,确实为我节省了很多时间。我修复了一堆警告,并注意到该函数正在输出:

/*
 * Pass both versions of the node to this function. Returns an array of
 * fields that were changed or an empty array if none were changed.
 * Pass field collections as an array keyed by field collection ID.
 *
 * @param object $old_entity
 *   The original (stored in the database) node object.
 *   This function may also pass itself a FieldCollectionItemEntity object to compare field collections.
 * @param object $new_entity
 *   The prepared node object for comparison.
 *   This function may also pass itself a FieldCollectionItemEntity object to compare field collections.
 */
function _fields_changed($old_entity, $new_entity) {
  $fields_changed = array();

  // Check for node or field collection.
  if (is_object($old_entity)) {
    $entity_is_field_collection = (get_class($old_entity) == 'FieldCollectionItemEntity');
    $bundle = !empty($entity_is_field_collection) ? $old_entity->field_name : $old_entity->type;
  }

  // Sanity check. Exit and throw an error if the content types don't match.
  if (is_object($new_entity)) {
    if ($bundle !== (!empty($entity_is_field_collection) ? $new_entity->field_name : $new_entity->type)) {
      drupal_set_message('Content type mismatch. Unable to save changes.', 'error');
      return FALSE;
    }
  }

  // Get field info.
  $field_read_params = array(
    'entity_type' => !empty($entity_is_field_collection) ? 'field_collection_item' : 'node',
  );

  if (!empty($bundle)) {
    $field_read_params['bundle'] = $bundle;
  }

  $fields_info = field_read_fields($field_read_params);

  foreach ($fields_info as $field_name => $field_info) {
    $old_field = isset($old_entity->$field_name) ? $old_entity->$field_name : NULL;
    $new_field = isset($new_entity->$field_name) ? $new_entity->$field_name : NULL;

    // Check the number of values for each field, or if they are populated at all.
    $old_field_count = (isset($old_field[LANGUAGE_NONE]) ? count($old_field[LANGUAGE_NONE]) : 0);
    $new_field_count = (isset($new_field[LANGUAGE_NONE]) ? count($new_field[LANGUAGE_NONE]) : 0);

    if ($old_field_count != $new_field_count) {
      // The two versions have a different number of values. Something has changed.
      $fields_changed[] = $field_name;
    }
    elseif ($old_field_count > 0 && $new_field_count > 0) {
      // Both versions have an equal number of values. Time to compare.

      // See if this field is a field collection.
      if ($field_info['type'] == 'field_collection') {

        foreach ($new_field[LANGUAGE_NONE] as $delta => $values) {
          $old_field_collection = NULL;
          if (!empty($values['entity']->item_id)) {
            $old_field_collection = entity_load_unchanged('field_collection_item', $values['entity']->item_id);
          }

          $new_field_collection = NULL;
          if (isset($values['entity'])) {
            $new_field_collection = $values['entity'];
          }

          $fields_changed = array_merge($fields_changed, _fields_changed($old_field_collection, $new_field_collection));
        }
        unset($delta, $values);

      }
      else {
        foreach ($old_field[LANGUAGE_NONE] as $delta => $value) {
          foreach ($field_info['columns'] as $field_column_name => $field_column_info) {
            $old_value = isset($old_field[LANGUAGE_NONE][$delta][$field_column_name]) ? $old_field[LANGUAGE_NONE][$delta][$field_column_name] : NULL;
            $new_value = isset($new_field[LANGUAGE_NONE][$delta][$field_column_name]) ? $new_field[LANGUAGE_NONE][$delta][$field_column_name] : NULL;
            $field_column_type = $field_column_info['type'];

            // As with the overall field, exit if one version has a value and the other doesn't.
            if (isset($old_value) != isset($new_value)) {
              $fields_changed[] = $old_field;
            }
            elseif (isset($old_value) && isset($new_value)) {
              // The column stores numeric data so compare values non-identically.
              if (in_array($field_column_type, array(
                'int',
                'float',
                'numeric'
              ))) {
                if ($new_value != $old_value) {
                  $fields_changed[] = $field_name;
                }
              }
              // The column stores non-numeric data so compare values identically,
              elseif ($new_value !== $old_value) {
                $fields_changed[] = $field_name;
              }
            }
            else {
              // Included for clarity. Both values are empty so there was obviously no change.
            }
          }
          unset($field_column_name, $field_column_info);
        }
        unset($delta, $value);
      }
    }
    else {
      // Included for clarity. Both values are empty so there was obviously no change.
    }
  }
  unset($field_name, $field_info);
  // End of field comparison loop.

  return $fields_changed;
}

请说明该代码如何回答原始问题(仅发布一些代码不符合此处的规则)。
Pierre.Vriens '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.