it-swarm.com.de

Generiertes Erkennen geänderter Felder in einem benutzerdefinierten Formular vor dem Speichern eines Knotens

Ich füge bestimmte Felder aus einem Inhaltstyp mit field_attach_form () in ein benutzerdefiniertes Formular ein. Wenn das Formular gesendet wird, verarbeite ich diese Felder, indem ich field_attach_form_validate () und field_attach_submit () aus #validate- und #submit-Rückrufen aufrufe.

An diesem Punkt möchte ich das vorbereitete, vorbereitete Knotenobjekt mit dem ursprünglichen Knoten vergleichen und mich nur dann mit node_save () befassen, wenn sich eines der Felder geändert hat. Daher lade ich zunächst den ursprünglichen Knoten mit entity_load_unchanged().

Leider stimmen die Feldarrays im ursprünglichen Knotenobjekt nicht mit den Feldarrays im vorbereiteten Knotenobjekt überein, die darauf warten, gespeichert zu werden, selbst wenn keine Änderungen an den Feldern vorgenommen wurden. Daher ein einfaches "$ old_field == $ new_field" "Vergleich ist unmöglich. Beispielsweise erscheint im Original ein einfaches Textfeld wie folgt:

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

Während es im vorbereiteten Knoten so aussieht.

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

Sie könnten denken, Sie würden nur den 'Wert'-Schlüssel vergleichen, aber dann stoßen Sie auf Felder, die aus anderen Elementen bestehen, die keine' Wert'-Schlüssel haben. Schauen wir uns zum Beispiel ein Adressfeld an, in dem es keinen 'Wert'-Schlüssel gibt nd Es gibt Schlüssel sowohl im alten als auch im vorbereiteten Knoten, die keine Gegenstücke haben.

Alter Knoten

$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,
);

Vorbereiteter Knoten

$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',
);

Bei leeren Feldern gibt es noch eine weitere Diskrepanz.

Alter Knoten

$old_node->field_text = array();

Vorbereiteter Knoten

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

Kann ich den alten und den neuen Wert eines Felds generisch vergleichen, um festzustellen, ob es sich geändert hat oder nicht?
Ist das nur eine Unmöglichkeit?

12
morbiD

Dies sollte endlich als generische Lösung funktionieren. Vielen Dank an Clive und morbiD für alle Beiträge.

Übergeben Sie beide Versionen des Knotens an die folgende Funktion. Es wird:

  1. Ziehen Sie alle bearbeitbaren Felder des erkannten Inhaltstyps und ihre bearbeitbaren Spalten (d. H. Elemente, die möglicherweise im benutzerdefinierten Formular angezeigt werden) in einer einzigen Abfrage aus der Datenbank.

  2. Ignorieren Sie Felder und Spalten, die in beiden Versionen vollständig leer sind.

  3. Behandeln Sie ein Feld mit einer unterschiedlichen Anzahl von Werten zwischen den beiden Versionen als Änderung.

  4. Durchlaufen Sie jedes Feld, jeden Wert und jede Spalte und vergleichen Sie die beiden Versionen.

  5. Vergleichen Sie Elemente nicht identisch (! =), Wenn sie numerisch sind, und identisch (! ==), wenn sie etwas anderes sind.

  6. Geben Sie bei der ersten erkannten Änderung sofort TRUE zurück (da eine Änderung ausreicht, um zu wissen, dass der Knoten erneut gespeichert werden muss).

  7. Geben Sie FALSE zurück, wenn nach dem Vergleich aller Werte keine Änderung festgestellt wird.

  8. Vergleichen Sie Feldsammlungen rekursiv, indem Sie sie und ihr Schema laden und die Ergebnisse an sich selbst übergeben. Dies sollte es sogar ermöglichen, verschachtelte Feldsammlungen zu vergleichen. Der Code sollte KEINE Abhängigkeit vom Feldsammlungsmodul haben.

Lassen Sie mich wissen, ob dieser Code weitere Fehler oder Tippfehler enthält.

/*
 * 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;
}

Manchmal möchten Sie wissen, welche Felder sich geändert haben. Um dies zu wissen, können Sie diese Version der Funktion verwenden:

/*
 * 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;
}

Manchmal möchten Sie es möglicherweise so gestalten, dass das Ändern bestimmter Felder eines Knotens nicht dazu führt, dass der "geänderte" Zeitstempel dieses Knotens aktualisiert wird. Dies könnte wie folgt implementiert werden:

/**
 * 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;
  }
}

EDIT (30.07.2013) Verstärkte Unterstützung für die Feldsammlung. Unterstützung für Felder mit mehreren Werten hinzugefügt.

EDIT (31.07.2015) Version der Funktion hinzugefügt, die zurückgibt, welche Felder geändert wurden, und Beispielanwendungsfall.

9
Eric N

Hier ist ein weiterer, einfacherer Ansatz, der die komplexen serverseitigen Wertevergleiche vermeidet und mit jeder Form funktioniert:

  1. Verwenden Sie jQuery, um festzustellen, ob sich die Formularwerte geändert haben
  2. Legen Sie einen Wert für ausgeblendete Elemente fest, um anzuzeigen, dass sich das Formular geändert hat.
  3. Überprüfen Sie die Server-Seite für den Wert versteckter Elemente und verarbeiten Sie sie nach Bedarf.

Sie können ein jQuery-Plugin für schmutzige Formulare wie https://github.com/codedance/jquery.AreYouSure verwenden

Obwohl andere, mit denen Sie den Status "Form geändert/verschmutzt" anhören können, ebenfalls funktionieren würden.

Fügen Sie einen Listener hinzu, um den Wert eines ausgeblendeten Formularelements festzulegen:

Setzen Sie das ausgeblendete Formularelement auf den Standardwert 'geändert', um es standardmäßig für Benutzer mit deaktiviertem Javascript zu speichern (~ 2%).

z.B:

// 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('');
      }
    }
 });

Sie können dann den Wert des ausgeblendeten Elements überprüfen

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

in Ihrem Formular Handler validieren/senden.

3
David Thomas

Ich bin mir nicht sicher, ob das perfekt ist, aber warum nicht umgekehrt, durch Vergleichen der Formulare anstelle der Knotenobjekte?

Ich bin mir nicht sicher, ob Sie sich ausschließlich in einem Knotenformular befinden, aber Sie können das Formular trotzdem mit Ihrem alten und Ihrem neuen Knoten rendern:

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);

Vergleichen Sie Ihre Formulare ...

Ich hoffe es ist eine gute Strecke ... lass es mich wissen.

0

Hier ist eine Methode, die hook_node_presave ($ node) verwendet. Es ist nur ein Modell, wenn Sie denken, dass es hilft, testen Sie es und verbessern Sie es an Ihre Bedürfnisse!

  /**
   * 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);
  }

Ich nehme an, dass für jeden Feldwert die Instanzen, die in $ node definiert sind, in $ node_before definiert und gleich sein müssen. Die Felder des Feldwerts, die sich in $ node_before und nicht in $ node befinden, sind mir egal. Ich nehme an, sie bleiben gleich.

0
dxvargas

Die Antwort ist großartig und hat mir geholfen, aber ich musste etwas korrigieren.

// 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);
}

In der foreach() - Schleife musste ich von $new_field Zu $old_field Wechseln. Ich weiß nicht, ob dies eine neue Version von Drupal oder nur mein Code (möglicherweise aufgrund eines anderen Codes an einer anderen Stelle) ist, aber ich habe keinen Zugriff auf $new_field['entity'].

0
Doud

Dies ist nur ein Code, den ich zusammengeschustert habe. Alle Gutschriften müssen an @eclecto gehen, um die gesamte Beinarbeit zu erledigen. Dies ist nur eine (ähnlich ungetestete) Variante, die die Knotenobjekte direkt aufnimmt, die DB-Treffer ein wenig reduziert und sich um die Sprachaushandlung kümmert.

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;
}
0
Clive

Danke für den Beitrag, hat mir wirklich viel Zeit gespart. Ich habe eine Reihe von Warnungen und Hinweisen behoben, die von der Funktion ausgegeben wurden:

/*
 * 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;
}
0
pwaterz