entity.module 17 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
<?php

/**
 * @file
 * Entity API for handling entities like nodes or users.
 */

/**
 * Implements hook_help().
 */
function entity_help($path, $arg) {
  switch ($path) {
    case 'admin/help#entity':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Entity module provides an API for managing entities like nodes and users, i.e. an API for loading and identifying entities. For more information, see the online handbook entry for <a href="!url">Entity module</a>', array('!url' => 'http://drupal.org/handbook/modules/entity')) . '</p>';
      return $output;
  }
}

/**
 * Implements hook_modules_preenable().
 */
function entity_modules_preenable() {
  entity_info_cache_clear();
}

/**
 * Implements hook_modules_disabled().
 */
function entity_modules_disabled() {
  entity_info_cache_clear();
}

/**
 * Gets the entity info array of an entity type.
 *
 * @param $entity_type
 *   The entity type, e.g. node, for which the info shall be returned, or NULL
 *   to return an array with info about all types.
41 42 43
 *
 * @see hook_entity_info()
 * @see hook_entity_info_alter()
44 45
 */
function entity_get_info($entity_type = NULL) {
46
  global $language_interface;
47 48 49 50 51 52 53 54 55 56

  // Use the advanced drupal_static() pattern, since this is called very often.
  static $drupal_static_fast;
  if (!isset($drupal_static_fast)) {
    $drupal_static_fast['entity_info'] = &drupal_static(__FUNCTION__);
  }
  $entity_info = &$drupal_static_fast['entity_info'];

  // hook_entity_info() includes translated strings, so each language is cached
  // separately.
57
  $langcode = $language_interface->langcode;
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124

  if (empty($entity_info)) {
    if ($cache = cache()->get("entity_info:$langcode")) {
      $entity_info = $cache->data;
    }
    else {
      $entity_info = module_invoke_all('entity_info');
      // Merge in default values.
      foreach ($entity_info as $name => $data) {
        $entity_info[$name] += array(
          'fieldable' => FALSE,
          'controller class' => 'DrupalDefaultEntityController',
          'static cache' => TRUE,
          'field cache' => TRUE,
          'load hook' => $name . '_load',
          'bundles' => array(),
          'view modes' => array(),
          'entity keys' => array(),
          'translation' => array(),
        );
        $entity_info[$name]['entity keys'] += array(
          'revision' => '',
          'bundle' => '',
        );
        foreach ($entity_info[$name]['view modes'] as $view_mode => $view_mode_info) {
          $entity_info[$name]['view modes'][$view_mode] += array(
            'custom settings' => FALSE,
          );
        }
        // If no bundle key is provided, assume a single bundle, named after
        // the entity type.
        if (empty($entity_info[$name]['entity keys']['bundle']) && empty($entity_info[$name]['bundles'])) {
          $entity_info[$name]['bundles'] = array($name => array('label' => $entity_info[$name]['label']));
        }
        // Prepare entity schema fields SQL info for
        // DrupalEntityControllerInterface::buildQuery().
        if (isset($entity_info[$name]['base table'])) {
          $entity_info[$name]['schema_fields_sql']['base table'] = drupal_schema_fields_sql($entity_info[$name]['base table']);
          if (isset($entity_info[$name]['revision table'])) {
            $entity_info[$name]['schema_fields_sql']['revision table'] = drupal_schema_fields_sql($entity_info[$name]['revision table']);
          }
        }
      }
      // Let other modules alter the entity info.
      drupal_alter('entity_info', $entity_info);
      cache()->set("entity_info:$langcode", $entity_info);
    }
  }

  if (empty($entity_type)) {
    return $entity_info;
  }
  elseif (isset($entity_info[$entity_type])) {
    return $entity_info[$entity_type];
  }
}

/**
 * Resets the cached information about entity types.
 */
function entity_info_cache_clear() {
  drupal_static_reset('entity_get_info');
  // Clear all languages.
  cache()->deletePrefix('entity_info:');
}

/**
125
 * Extracts ID, revision ID, and bundle name from an entity.
126 127 128 129 130
 *
 * @param $entity_type
 *   The entity type; e.g. 'node' or 'user'.
 * @param $entity
 *   The entity from which to extract values.
131
 *
132 133 134
 * @return
 *   A numerically indexed array (not a hash table) containing these
 *   elements:
135 136 137
 *   - 0: Primary ID of the entity.
 *   - 1: Revision ID of the entity, or NULL if $entity_type is not versioned.
 *   - 2: Bundle name of the entity.
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
 */
function entity_extract_ids($entity_type, $entity) {
  $info = entity_get_info($entity_type);

  // Objects being created might not have id/vid yet.
  $id = isset($entity->{$info['entity keys']['id']}) ? $entity->{$info['entity keys']['id']} : NULL;
  $vid = ($info['entity keys']['revision'] && isset($entity->{$info['entity keys']['revision']})) ? $entity->{$info['entity keys']['revision']} : NULL;

  if (!empty($info['entity keys']['bundle'])) {
    // Explicitly fail for malformed entities missing the bundle property.
    if (!isset($entity->{$info['entity keys']['bundle']}) || $entity->{$info['entity keys']['bundle']} === '') {
      throw new EntityMalformedException(t('Missing bundle property on entity of type @entity_type.', array('@entity_type' => $entity_type)));
    }
    $bundle = $entity->{$info['entity keys']['bundle']};
  }
  else {
    // The entity type provides no bundle key: assume a single bundle, named
    // after the entity type.
    $bundle = $entity_type;
  }

  return array($id, $vid, $bundle);
}

/**
163
 * Assembles an object structure with initial IDs.
164 165 166 167 168 169 170 171
 *
 * This function can be seen as reciprocal to entity_extract_ids().
 *
 * @param $entity_type
 *   The entity type; e.g. 'node' or 'user'.
 * @param $ids
 *   A numerically indexed array, as returned by entity_extract_ids(),
 *   containing these elements:
172 173 174
 *   - 0: Primary ID of the entity.
 *   - 1: Revision ID of the entity, or NULL if $entity_type is not versioned.
 *   - 2: Bundle name of the entity, or NULL if $entity_type has no bundles.
175 176
 *
 * @return
177
 *   An entity object, initialized with the IDs provided.
178 179
 */
function entity_create_stub_entity($entity_type, $ids) {
180
  $values = array();
181
  $info = entity_get_info($entity_type);
182
  $values[$info['entity keys']['id']] = $ids[0];
183
  if (!empty($info['entity keys']['revision']) && isset($ids[1])) {
184
    $values[$info['entity keys']['revision']] = $ids[1];
185 186
  }
  if (!empty($info['entity keys']['bundle']) && isset($ids[2])) {
187
    $values[$info['entity keys']['bundle']] = $ids[2];
188
  }
189 190
  // @todo Once all entities are converted, just rely on entity_create().
  return isset($info['entity class']) ? entity_create($entity_type, $values) : (object) $values;
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
}

/**
 * Loads entities from the database.
 *
 * This function should be used whenever you need to load more than one entity
 * from the database. The entities are loaded into memory and will not require
 * database access if loaded again during the same page request.
 *
 * The actual loading is done through a class that has to implement the
 * DrupalEntityControllerInterface interface. By default,
 * DrupalDefaultEntityController is used. Entity types can specify that a
 * different class should be used by setting the 'controller class' key in
 * hook_entity_info(). These classes can either implement the
 * DrupalEntityControllerInterface interface, or, most commonly, extend the
 * DrupalDefaultEntityController class. See node_entity_info() and the
 * NodeController in node.module as an example.
 *
 * @param $entity_type
 *   The entity type to load, e.g. node or user.
 * @param $ids
 *   An array of entity IDs, or FALSE to load all entities.
 * @param $conditions
 *   (deprecated) An associative array of conditions on the base table, where
 *   the keys are the database fields and the values are the values those
 *   fields must have. Instead, it is preferable to use EntityFieldQuery to
 *   retrieve a list of entity IDs loadable by this function.
 * @param $reset
 *   Whether to reset the internal cache for the requested entity type.
 *
 * @return
 *   An array of entity objects indexed by their ids.
 *
 * @todo Remove $conditions in Drupal 8.
225 226 227 228 229
 *
 * @see hook_entity_info()
 * @see DrupalEntityControllerInterface
 * @see DrupalDefaultEntityController
 * @see EntityFieldQuery
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
 */
function entity_load($entity_type, $ids = FALSE, $conditions = array(), $reset = FALSE) {
  if ($reset) {
    entity_get_controller($entity_type)->resetCache();
  }
  return entity_get_controller($entity_type)->load($ids, $conditions);
}

/**
 * Loads the unchanged, i.e. not modified, entity from the database.
 *
 * Unlike entity_load() this function ensures the entity is directly loaded from
 * the database, thus bypassing any static cache. In particular, this function
 * is useful to determine changes by comparing the entity being saved to the
 * stored entity.
 *
 * @param $entity_type
 *   The entity type to load, e.g. node or user.
 * @param $id
249
 *   The ID of the entity to load.
250 251 252 253 254 255 256 257 258 259
 *
 * @return
 *   The unchanged entity, or FALSE if the entity cannot be loaded.
 */
function entity_load_unchanged($entity_type, $id) {
  entity_get_controller($entity_type)->resetCache(array($id));
  $result = entity_get_controller($entity_type)->load(array($id));
  return reset($result);
}

260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
/**
 * Deletes multiple entities permanently.
 *
 * @param $entity_type
 *   The type of the entity.
 * @param $ids
 *   An array of entity IDs of the entities to delete.
 */
function entity_delete_multiple($entity_type, $ids) {
  entity_get_controller($entity_type)->delete($ids);
}

/**
 * Constructs a new entity object, without saving it to the database.
 *
 * @param $entity_type
 *   The type of the entity.
 * @param $values
 *   An array of values to set, keyed by property name. If the entity type has
 *   bundles the bundle key has to be specified.
 *
 * @return EntityInterface
 *   A new entity object.
 */
function entity_create($entity_type, array $values) {
  $info = entity_get_info($entity_type) + array('entity class' => 'Entity');
  $class = $info['entity class'];
  return new $class($values, $entity_type);
}

290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
/**
 * Gets the entity controller class for an entity type.
 */
function entity_get_controller($entity_type) {
  $controllers = &drupal_static(__FUNCTION__, array());
  if (!isset($controllers[$entity_type])) {
    $type_info = entity_get_info($entity_type);
    $class = $type_info['controller class'];
    $controllers[$entity_type] = new $class($entity_type);
  }
  return $controllers[$entity_type];
}

/**
 * Invokes hook_entity_prepare_view().
 *
 * If adding a new entity similar to nodes, comments or users, you should
 * invoke this function during the ENTITY_build_content() or
 * ENTITY_view_multiple() phases of rendering to allow other modules to alter
 * the objects during this phase. This is needed for situations where
 * information needs to be loaded outside of ENTITY_load() - particularly
 * when loading entities into one another - i.e. a user object into a node, due
 * to the potential for unwanted side-effects such as caching and infinite
 * recursion. By convention, entity_prepare_view() is called after
 * field_attach_prepare_view() to allow entity level hooks to act on content
 * loaded by field API.
 *
 * @param $entity_type
 *   The type of entity, i.e. 'node', 'user'.
 * @param $entities
 *   The entity objects which are being prepared for view, keyed by object ID.
321 322
 *
 * @see hook_entity_prepare_view()
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
 */
function entity_prepare_view($entity_type, $entities) {
  // To ensure hooks are only run once per entity, check for an
  // entity_view_prepared flag and only process items without it.
  // @todo: resolve this more generally for both entity and field level hooks.
  $prepare = array();
  foreach ($entities as $id => $entity) {
    if (empty($entity->entity_view_prepared)) {
      // Add this entity to the items to be prepared.
      $prepare[$id] = $entity;

      // Mark this item as prepared.
      $entity->entity_view_prepared = TRUE;
    }
  }

  if (!empty($prepare)) {
    module_invoke_all('entity_prepare_view', $prepare, $entity_type);
  }
}

/**
 * Returns the uri elements of an entity.
 *
 * @param $entity_type
 *   The entity type; e.g. 'node' or 'user'.
 * @param $entity
 *   The entity for which to generate a path.
 *
 * @return
 *   An array containing the 'path' and 'options' keys used to build the uri of
 *   the entity, and matching the signature of url(). NULL if the entity has no
 *   uri of its own.
356 357 358
 *
 * @todo
 *   Remove once all entity types are implementing the EntityInterface.
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
 */
function entity_uri($entity_type, $entity) {
  $info = entity_get_info($entity_type);
  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);

  // A bundle-specific callback takes precedence over the generic one for the
  // entity type.
  if (isset($info['bundles'][$bundle]['uri callback'])) {
    $uri_callback = $info['bundles'][$bundle]['uri callback'];
  }
  elseif (isset($info['uri callback'])) {
    $uri_callback = $info['uri callback'];
  }
  else {
    return NULL;
  }

  // Invoke the callback to get the URI. If there is no callback, return NULL.
377
  if (isset($uri_callback)) {
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
    $uri = $uri_callback($entity);
    // Pass the entity data to url() so that alter functions do not need to
    // lookup this entity again.
    $uri['options']['entity_type'] = $entity_type;
    $uri['options']['entity'] = $entity;
    return $uri;
  }
}

/**
 * Returns the label of an entity.
 *
 * See the 'label callback' component of the hook_entity_info() return value
 * for more information.
 *
 * @param $entity_type
 *   The entity type; e.g., 'node' or 'user'.
 * @param $entity
 *   The entity for which to generate the label.
 *
 * @return
 *   The entity label, or FALSE if not found.
400 401 402
 *
 * @todo
 *   Remove once all entity types are implementing the EntityInterface.
403 404 405 406
 */
function entity_label($entity_type, $entity) {
  $label = FALSE;
  $info = entity_get_info($entity_type);
407
  if (isset($info['label callback'])) {
408 409 410 411 412 413 414 415 416 417
    $label = $info['label callback']($entity_type, $entity);
  }
  elseif (!empty($info['entity keys']['label']) && isset($entity->{$info['entity keys']['label']})) {
    $label = $entity->{$info['entity keys']['label']};
  }

  return $label;
}

/**
418
 * Attaches field API validation to entity forms.
419 420 421 422 423 424 425 426 427 428 429 430
 */
function entity_form_field_validate($entity_type, $form, &$form_state) {
  // All field attach API functions act on an entity object, but during form
  // validation, we don't have one. $form_state contains the entity as it was
  // prior to processing the current form submission, and we must not update it
  // until we have fully validated the submitted input. Therefore, for
  // validation, act on a pseudo entity created out of the form values.
  $pseudo_entity = (object) $form_state['values'];
  field_attach_form_validate($entity_type, $pseudo_entity, $form, $form_state);
}

/**
431
 * Copies submitted values to entity properties for simple entity forms.
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476
 *
 * During the submission handling of an entity form's "Save", "Preview", and
 * possibly other buttons, the form state's entity needs to be updated with the
 * submitted form values. Each entity form implements its own builder function
 * for doing this, appropriate for the particular entity and form, whereas
 * modules may specify additional builder functions in $form['#entity_builders']
 * for copying the form values of added form elements to entity properties.
 * Many of the main entity builder functions can call this helper function to
 * re-use its logic of copying $form_state['values'][PROPERTY] values to
 * $entity->PROPERTY for all entries in $form_state['values'] that are not field
 * data, and calling field_attach_submit() to copy field data. Apart from that
 * this helper invokes any additional builder functions that have been specified
 * in $form['#entity_builders'].
 *
 * For some entity forms (e.g., forms with complex non-field data and forms that
 * simultaneously edit multiple entities), this behavior may be inappropriate,
 * so the builder function for such forms needs to implement the required
 * functionality instead of calling this function.
 */
function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_state) {
  $info = entity_get_info($entity_type);
  list(, , $bundle) = entity_extract_ids($entity_type, $entity);

  // Copy top-level form values that are not for fields to entity properties,
  // without changing existing entity properties that are not being edited by
  // this form. Copying field values must be done using field_attach_submit().
  $values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $bundle)) : $form_state['values'];
  foreach ($values_excluding_fields as $key => $value) {
    $entity->$key = $value;
  }

  // Invoke all specified builders for copying form values to entity properties.
  if (isset($form['#entity_builders'])) {
    foreach ($form['#entity_builders'] as $function) {
      $function($entity_type, $entity, $form, $form_state);
    }
  }

  // Copy field values to the entity.
  if ($info['fieldable']) {
    field_attach_submit($entity_type, $entity, $form, $form_state);
  }
}

/**
477
 * Defines an exception thrown when a malformed entity is passed.
478 479
 */
class EntityMalformedException extends Exception { }