NaturalHistoryMuseum / scratchpads2

Scratchpads 2.0
http://scratchpads.org
GNU General Public License v2.0
199 stars 83 forks source link

Investigate and use entity metadata wrappers #3627

Closed informatics-dev closed 11 years ago

informatics-dev commented 11 years ago

Description:

Talking about this with Simon, we thought it would be good to start using this rather than accessing arrays:

http://drupal.org/node/1021556

informatics-dev commented 11 years ago

Comment by Alice Heaton

I've been looking through this. Here are my personal notes (mostly inspired from the page above, but with some gotchas and info):

h2. Create a wrapper:

$node = node_load(1);
$wrapper = entity_metadata_wrapper('node', $node);

h2. Accessing field values:

Simple chaining returns the metadata wrapper for each field, on which the 'value' method can be called:

$wrapper->title->value();

A value that contains several items (for instance a long_text with filtered text which has contains both a 'value' for the text and a 'format' for the text format) would require more chaining:

$wrapper->field_biology->value->value();
$wrapper->field_biology->format->value();

(confusingly this is the case for long text fields with filtered text, but not for long text fields that are plain text)

h2. Using multi-value fields:

The following returns an array of user objects:

$wrapper->field_publication_authors->value();

We can get the metadata wrapper for a given user object by using the [] operator :

$wrapper->field_publication_authors[0]->field_user_title->value();

We can also iterate over the metadata wrappers of each user object:

$wrapper->field_publication_authors[0]->field_user_title->value();
foreach ($wrapper->field_publication_authors->getIterator() as $user_wrapper) {
}

h2. Setting values:

This can be done by direct assignment, or by calling the 'set' method. For values that have multiple items, the assignment can be done at either level:

$wrapper->field_biology->value = “The text of the field”;

or

$wrapper->field_biology = array(
  'value' => “The text of the field”,
  'format' => “filtered_html”
);

h2. Deleting values:

$wrapper->field_biology->set();
$wrapper->field_publication_authors[0]->set();

h2. Metadata wrapper info:

$info = $wrapper->field_publication_abstract->info();

If $info['property info'] is defined, then calling 'value()' for that field will return another wrapper (the fields of which are the keys of 'property info') otherwise it will return a value (a string, an entity, etc.)

$info['type'] contains the type of the field. If this field is multi-valued, then this will be of the form “list” (including the “<>”). In that case array operators and getIterator() can be used on the field.

informatics-dev commented 11 years ago

Comment by Alice Heaton

Annoying things with entity metadata wrapper:

  1. The wrapper you get is different whether the field is multi-valued or not. So unlike the field arrays where you can always simply run a 'foreach' whether it's multi-valued or not, here you have to treat it differently ;

  2. The value you get is different whether the field has more than one property or not. (eg. for a text field without filtering, calling ->value() returns the actual text, while for a text field with filtering calling ->value() returns an array with the keys 'value' and 'format'). Again, working with field arrays you always get an array of values (apart from special fields such as title) whether there is only one property or not.

These two points mean that the metadata_wrapper is very useful when you know what fields you're working with, but less useful when you don't because you need to do more checking.

As an example I wrote this for my use which :

function entity_metadata_simple_values($field) {
  $fields = array();
  $values = array();
  $info = $field->info();
  // XXX need a better way to know it's multi-valued...
  if (strpos($info['type'], 'list<') === 0) {
    foreach ($field->getIterator() as $field_iteration) {
      $fields[] = $field_iteration;
    }
  } else {
    $fields[] = $field;
  }
  foreach ($fields as $final_field) {
    $ff_info = $final_field->info();
    if (isset($ff_info['property info'])) {
      $column = reset(array_keys($ff_info['property info']));
      $values[] = $final_field->{$column}->value();
    } else {
      $values[] = $final_field->value();
    }
  }

  return $values;
}

You'd use it like:

$wrapper = entity_metadata_wrapper(node_load(1));
$values = entity_metadata_simple_values($wrapper->field_biology);
foreach ($values as $val) {
...
}
informatics-dev commented 11 years ago

Comment by Alice Heaton

Here is another example. I wrote this to modify content of an already migrated site (in that case, updating the path to images on ICZN which couldn't be re-migrated as a lot of work had been done on it). The code parses all fields of a given content type, finds the field of appropriate type and replaces the content if necessary. The fact that field/property information is readily available for each field makes this simpler than the alternative, which would require fetching the field info separately.

entityCondition('entity_type', $entity_type)->execute();
  $entities = $result[$entity_type];
  if (!$quiet) echo "  - Found " . count($entities) . " for type\n";
  foreach ($entities as $entity_id) {
    if ($entity_type == 'user' && $entity_id->uid == 0) {
      continue;
    }
    $entity_id = (array)$entity_id;
    $entity_id = reset($entity_id);
    $entity = reset(entity_load($entity_type, array($entity_id)));
    $wrapper = entity_metadata_wrapper($entity_type, $entity);
    $fields = $wrapper->getPropertyInfo();
    $modified = FALSE;
    foreach ($fields as $field_name => $field_info) {
      if (empty($field_info['type'])) {
        continue;
      }
      $type = entity_property_extract_innermost_type($field_info['type']);
      if ($type != 'text_formatted') {
        continue;
      }
      $m1 = _scr_replace_entity_field($wrapper->{$field_name});
      $modified = $modified || $m1;
    }    
    if ($modified) {
      $modified_count[$entity_type]++;
      $wrapper->save();
    }
    // Output some info
    $entity_progress++;
    $entity_count++;
    if (!$quiet && $entity_progress >= count($entities)/10) {
      echo "  - Done " . intval(100*$entity_count/count($entities)) . "%\n";
      $entity_progress = 0;
    }
  }
  if (!$quiet) echo "\n";
}

echo "Number of modified entities:\n";
var_dump($modified_count);
echo "Be sure to clear the cache!\n";
return;

/**
 * Recurse through field/property definition to find
 * the property to modifiy. Return TRUE field was
 * modified, FALSE otherwise
 */
function _scr_replace_entity_field($w) {
  if (entity_property_list_extract_type($w->type())) {
    foreach ($w->getIterator() as $iteration) {
      $m1 = _scr_replace_entity_field($iteration);
      $modified = $modified || $m1;
    }
  } else {
    $info = $w->info();
    if (isset($info['property info'])) {
      $value = $w->value();
      foreach($info['property info'] as $property => $prop_info) {
        if (isset($prop_info['type']) && $prop_info['type'] == 'text') {
          $rep = _scr_fix_image_field($value[$property]);
          if ($rep) { 
            $value[$property] = $rep;
            $w->set($value);
            $modified = TRUE;
          }
        }
      }
    } else {
      $rep = _scr_fix_image_field($w->value());
      if ($rep) { 
        $w->set($rep);
        $modified = TRUE;
      }
    }
  }

  return $modified;
}

/**
 * Replace src="/sites/xxxx/files by src="/files in a string
 * Return the string if modified, FALSE if not.
 */
function _scr_fix_image_field($text) {
  $pat = '%src=(["|\'])/sites/[^/]+/files%';
  if (preg_match($pat, $text)) {
    return preg_replace($pat, 'src=$1/files', $text);
  } else {
    return FALSE;
  }
}
informatics-dev commented 11 years ago

Comment by Simon Rycroft

Khalid has discovered that calling ->set() without an argument causes a PHP NOTICE/WARNING. To unset a value, set() should be given the arguments "NULL".

h2. Deleting values:

$wrapper->field_biology->set(NULL);
$wrapper->field_publication_authors[0]->set(NULL);
informatics-dev commented 11 years ago

Comment by Simon Rycroft

Hopefully this issue has been read many times by our development team. Thanks Alice.