Projet

Général

Profil

Paste
Télécharger (34,8 ko) Statistiques
| Branche: | Révision:

root / htmltest / sites / all / modules / entity / includes / entity.controller.inc @ dd54aff9

1
<?php
2

    
3
/**
4
 * @file
5
 * Provides a controller building upon the core controller but providing more
6
 * features like full CRUD functionality.
7
 */
8

    
9
/**
10
 * Interface for EntityControllers compatible with the entity API.
11
 */
12
interface EntityAPIControllerInterface extends DrupalEntityControllerInterface {
13

    
14
  /**
15
   * Delete permanently saved entities.
16
   *
17
   * In case of failures, an exception is thrown.
18
   *
19
   * @param $ids
20
   *   An array of entity IDs.
21
   */
22
  public function delete($ids);
23

    
24
  /**
25
   * Invokes a hook on behalf of the entity. For hooks that have a respective
26
   * field API attacher like insert/update/.. the attacher is called too.
27
   */
28
  public function invoke($hook, $entity);
29

    
30
  /**
31
   * Permanently saves the given entity.
32
   *
33
   * In case of failures, an exception is thrown.
34
   *
35
   * @param $entity
36
   *   The entity to save.
37
   *
38
   * @return
39
   *   SAVED_NEW or SAVED_UPDATED is returned depending on the operation
40
   *   performed.
41
   */
42
  public function save($entity);
43

    
44
  /**
45
   * Create a new entity.
46
   *
47
   * @param array $values
48
   *   An array of values to set, keyed by property name.
49
   * @return
50
   *   A new instance of the entity type.
51
   */
52
  public function create(array $values = array());
53

    
54
  /**
55
   * Exports an entity as serialized string.
56
   *
57
   * @param $entity
58
   *   The entity to export.
59
   * @param $prefix
60
   *   An optional prefix for each line.
61
   *
62
   * @return
63
   *   The exported entity as serialized string. The format is determined by
64
   *   the controller and has to be compatible with the format that is accepted
65
   *   by the import() method.
66
   */
67
  public function export($entity, $prefix = '');
68

    
69
  /**
70
   * Imports an entity from a string.
71
   *
72
   * @param string $export
73
   *   An exported entity as serialized string.
74
   *
75
   * @return
76
   *   An entity object not yet saved.
77
   */
78
  public function import($export);
79

    
80
  /**
81
   * Builds a structured array representing the entity's content.
82
   *
83
   * The content built for the entity will vary depending on the $view_mode
84
   * parameter.
85
   *
86
   * @param $entity
87
   *   An entity object.
88
   * @param $view_mode
89
   *   View mode, e.g. 'full', 'teaser'...
90
   * @param $langcode
91
   *   (optional) A language code to use for rendering. Defaults to the global
92
   *   content language of the current request.
93
   * @return
94
   *   The renderable array.
95
   */
96
  public function buildContent($entity, $view_mode = 'full', $langcode = NULL);
97

    
98
  /**
99
   * Generate an array for rendering the given entities.
100
   *
101
   * @param $entities
102
   *   An array of entities to render.
103
   * @param $view_mode
104
   *   View mode, e.g. 'full', 'teaser'...
105
   * @param $langcode
106
   *   (optional) A language code to use for rendering. Defaults to the global
107
   *   content language of the current request.
108
   * @param $page
109
   *   (optional) If set will control if the entity is rendered: if TRUE
110
   *   the entity will be rendered without its title, so that it can be embeded
111
   *   in another context. If FALSE the entity will be displayed with its title
112
   *   in a mode suitable for lists.
113
   *   If unset, the page mode will be enabled if the current path is the URI
114
   *   of the entity, as returned by entity_uri().
115
   *   This parameter is only supported for entities which controller is a
116
   *   EntityAPIControllerInterface.
117
   * @return
118
   *   The renderable array, keyed by entity name or numeric id.
119
   */
120
  public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL);
121
}
122

    
123
/**
124
 * Interface for EntityControllers of entities that support revisions.
125
 */
126
interface EntityAPIControllerRevisionableInterface extends EntityAPIControllerInterface {
127

    
128
  /**
129
   * Delete an entity revision.
130
   *
131
   * Note that the default revision of an entity cannot be deleted.
132
   *
133
   * @param $revision_id
134
   *   The ID of the revision to delete.
135
   *
136
   * @return boolean
137
   *   TRUE if the entity revision could be deleted, FALSE otherwise.
138
   */
139
  public function deleteRevision($revision_id);
140

    
141
}
142

    
143
/**
144
 * A controller implementing EntityAPIControllerInterface for the database.
145
 */
146
class EntityAPIController extends DrupalDefaultEntityController implements EntityAPIControllerRevisionableInterface {
147

    
148
  protected $cacheComplete = FALSE;
149
  protected $bundleKey;
150
  protected $defaultRevisionKey;
151

    
152
  /**
153
   * Overridden.
154
   * @see DrupalDefaultEntityController#__construct()
155
   */
156
  public function __construct($entityType) {
157
    parent::__construct($entityType);
158
    // If this is the bundle of another entity, set the bundle key.
159
    if (isset($this->entityInfo['bundle of'])) {
160
      $info = entity_get_info($this->entityInfo['bundle of']);
161
      $this->bundleKey = $info['bundle keys']['bundle'];
162
    }
163
    $this->defaultRevisionKey = !empty($this->entityInfo['entity keys']['default revision']) ? $this->entityInfo['entity keys']['default revision'] : 'default_revision';
164
  }
165

    
166
  /**
167
   * Overrides DrupalDefaultEntityController::buildQuery().
168
   */
169
  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
170
    $query = parent::buildQuery($ids, $conditions, $revision_id);
171
    if ($this->revisionKey) {
172
      // Compare revision id of the base and revision table, if equal then this
173
      // is the default revision.
174
      $query->addExpression('base.' . $this->revisionKey . ' = revision.' . $this->revisionKey, $this->defaultRevisionKey);
175
    }
176
    return $query;
177
  }
178

    
179
  /**
180
   * Builds and executes the query for loading.
181
   *
182
   * @return The results in a Traversable object.
183
   */
184
  public function query($ids, $conditions, $revision_id = FALSE) {
185
    // Build the query.
186
    $query = $this->buildQuery($ids, $conditions, $revision_id);
187
    $result = $query->execute();
188
    if (!empty($this->entityInfo['entity class'])) {
189
      $result->setFetchMode(PDO::FETCH_CLASS, $this->entityInfo['entity class'], array(array(), $this->entityType));
190
    }
191
    return $result;
192
  }
193

    
194
  /**
195
   * Overridden.
196
   * @see DrupalDefaultEntityController#load($ids, $conditions)
197
   *
198
   * In contrast to the parent implementation we factor out query execution, so
199
   * fetching can be further customized easily.
200
   */
201
  public function load($ids = array(), $conditions = array()) {
202
    $entities = array();
203

    
204
    // Revisions are not statically cached, and require a different query to
205
    // other conditions, so separate the revision id into its own variable.
206
    if ($this->revisionKey && isset($conditions[$this->revisionKey])) {
207
      $revision_id = $conditions[$this->revisionKey];
208
      unset($conditions[$this->revisionKey]);
209
    }
210
    else {
211
      $revision_id = FALSE;
212
    }
213

    
214
    // Create a new variable which is either a prepared version of the $ids
215
    // array for later comparison with the entity cache, or FALSE if no $ids
216
    // were passed. The $ids array is reduced as items are loaded from cache,
217
    // and we need to know if it's empty for this reason to avoid querying the
218
    // database when all requested entities are loaded from cache.
219
    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
220

    
221
    // Try to load entities from the static cache.
222
    if ($this->cache && !$revision_id) {
223
      $entities = $this->cacheGet($ids, $conditions);
224
      // If any entities were loaded, remove them from the ids still to load.
225
      if ($passed_ids) {
226
        $ids = array_keys(array_diff_key($passed_ids, $entities));
227
      }
228
    }
229

    
230
    // Support the entitycache module if activated.
231
    if (!empty($this->entityInfo['entity cache']) && !$revision_id && $ids && !$conditions) {
232
      $cached_entities = EntityCacheControllerHelper::entityCacheGet($this, $ids, $conditions);
233
      // If any entities were loaded, remove them from the ids still to load.
234
      $ids = array_diff($ids, array_keys($cached_entities));
235
      $entities += $cached_entities;
236

    
237
      // Add loaded entities to the static cache if we are not loading a
238
      // revision.
239
      if ($this->cache && !empty($cached_entities) && !$revision_id) {
240
        $this->cacheSet($cached_entities);
241
      }
242
    }
243

    
244
    // Load any remaining entities from the database. This is the case if $ids
245
    // is set to FALSE (so we load all entities), if there are any ids left to
246
    // load or if loading a revision.
247
    if (!($this->cacheComplete && $ids === FALSE && !$conditions) && ($ids === FALSE || $ids || $revision_id)) {
248
      $queried_entities = array();
249
      foreach ($this->query($ids, $conditions, $revision_id) as $record) {
250
        // Skip entities already retrieved from cache.
251
        if (isset($entities[$record->{$this->idKey}])) {
252
          continue;
253
        }
254

    
255
        // For DB-based entities take care of serialized columns.
256
        if (!empty($this->entityInfo['base table'])) {
257
          $schema = drupal_get_schema($this->entityInfo['base table']);
258

    
259
          foreach ($schema['fields'] as $field => $info) {
260
            if (!empty($info['serialize']) && isset($record->$field)) {
261
              $record->$field = unserialize($record->$field);
262
              // Support automatic merging of 'data' fields into the entity.
263
              if (!empty($info['merge']) && is_array($record->$field)) {
264
                foreach ($record->$field as $key => $value) {
265
                  $record->$key = $value;
266
                }
267
                unset($record->$field);
268
              }
269
            }
270
          }
271
        }
272

    
273
        $queried_entities[$record->{$this->idKey}] = $record;
274
      }
275
    }
276

    
277
    // Pass all entities loaded from the database through $this->attachLoad(),
278
    // which attaches fields (if supported by the entity type) and calls the
279
    // entity type specific load callback, for example hook_node_load().
280
    if (!empty($queried_entities)) {
281
      $this->attachLoad($queried_entities, $revision_id);
282
      $entities += $queried_entities;
283
    }
284

    
285
    // Entitycache module support: Add entities to the entity cache if we are
286
    // not loading a revision.
287
    if (!empty($this->entityInfo['entity cache']) && !empty($queried_entities) && !$revision_id) {
288
      EntityCacheControllerHelper::entityCacheSet($this, $queried_entities);
289
    }
290

    
291
    if ($this->cache) {
292
      // Add entities to the cache if we are not loading a revision.
293
      if (!empty($queried_entities) && !$revision_id) {
294
        $this->cacheSet($queried_entities);
295

    
296
        // Remember if we have cached all entities now.
297
        if (!$conditions && $ids === FALSE) {
298
          $this->cacheComplete = TRUE;
299
        }
300
      }
301
    }
302
    // Ensure that the returned array is ordered the same as the original
303
    // $ids array if this was passed in and remove any invalid ids.
304
    if ($passed_ids && $passed_ids = array_intersect_key($passed_ids, $entities)) {
305
      foreach ($passed_ids as $id => $value) {
306
        $passed_ids[$id] = $entities[$id];
307
      }
308
      $entities = $passed_ids;
309
    }
310
    return $entities;
311
  }
312

    
313
  /**
314
   * Overrides DrupalDefaultEntityController::resetCache().
315
   */
316
  public function resetCache(array $ids = NULL) {
317
    $this->cacheComplete = FALSE;
318
    parent::resetCache($ids);
319
    // Support the entitycache module.
320
    if (!empty($this->entityInfo['entity cache'])) {
321
      EntityCacheControllerHelper::resetEntityCache($this, $ids);
322
    }
323
  }
324

    
325
  /**
326
   * Implements EntityAPIControllerInterface.
327
   */
328
  public function invoke($hook, $entity) {
329
    // entity_revision_delete() invokes hook_entity_revision_delete() and
330
    // hook_field_attach_delete_revision() just as node module does. So we need
331
    // to adjust the name of our revision deletion field attach hook in order to
332
    // stick to this pattern.
333
    $field_attach_hook = ($hook == 'revision_delete' ? 'delete_revision' : $hook);
334
    if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $field_attach_hook)) {
335
      $function($this->entityType, $entity);
336
    }
337

    
338
    if (!empty($this->entityInfo['bundle of']) && entity_type_is_fieldable($this->entityInfo['bundle of'])) {
339
      $type = $this->entityInfo['bundle of'];
340
      // Call field API bundle attachers for the entity we are a bundle of.
341
      if ($hook == 'insert') {
342
        field_attach_create_bundle($type, $entity->{$this->bundleKey});
343
      }
344
      elseif ($hook == 'delete') {
345
        field_attach_delete_bundle($type, $entity->{$this->bundleKey});
346
      }
347
      elseif ($hook == 'update' && $entity->original->{$this->bundleKey} != $entity->{$this->bundleKey}) {
348
        field_attach_rename_bundle($type, $entity->original->{$this->bundleKey}, $entity->{$this->bundleKey});
349
      }
350
    }
351
    // Invoke the hook.
352
    module_invoke_all($this->entityType . '_' . $hook, $entity);
353
    // Invoke the respective entity level hook.
354
    if ($hook == 'presave' || $hook == 'insert' || $hook == 'update' || $hook == 'delete') {
355
      module_invoke_all('entity_' . $hook, $entity, $this->entityType);
356
    }
357
    // Invoke rules.
358
    if (module_exists('rules')) {
359
      rules_invoke_event($this->entityType . '_' . $hook, $entity);
360
    }
361
  }
362

    
363
  /**
364
   * Implements EntityAPIControllerInterface.
365
   *
366
   * @param $transaction
367
   *   Optionally a DatabaseTransaction object to use. Allows overrides to pass
368
   *   in their transaction object.
369
   */
370
  public function delete($ids, DatabaseTransaction $transaction = NULL) {
371
    $entities = $ids ? $this->load($ids) : FALSE;
372
    if (!$entities) {
373
      // Do nothing, in case invalid or no ids have been passed.
374
      return;
375
    }
376
    // This transaction causes troubles on MySQL, see
377
    // http://drupal.org/node/1007830. So we deactivate this by default until
378
    // is shipped in a point release.
379
    // $transaction = isset($transaction) ? $transaction : db_transaction();
380

    
381
    try {
382
      $ids = array_keys($entities);
383

    
384
      db_delete($this->entityInfo['base table'])
385
        ->condition($this->idKey, $ids, 'IN')
386
        ->execute();
387

    
388
      if (isset($this->revisionTable)) {
389
        db_delete($this->revisionTable)
390
          ->condition($this->idKey, $ids, 'IN')
391
          ->execute();
392
      }
393
      // Reset the cache as soon as the changes have been applied.
394
      $this->resetCache($ids);
395

    
396
      foreach ($entities as $id => $entity) {
397
        $this->invoke('delete', $entity);
398
      }
399
      // Ignore slave server temporarily.
400
      db_ignore_slave();
401
    }
402
    catch (Exception $e) {
403
      if (isset($transaction)) {
404
        $transaction->rollback();
405
      }
406
      watchdog_exception($this->entityType, $e);
407
      throw $e;
408
    }
409
  }
410

    
411
  /**
412
   * Implements EntityAPIControllerRevisionableInterface::deleteRevision().
413
   */
414
  public function deleteRevision($revision_id) {
415
    if ($entity_revision = entity_revision_load($this->entityType, $revision_id)) {
416
      // Prevent deleting the default revision.
417
      if (entity_revision_is_default($this->entityType, $entity_revision)) {
418
        return FALSE;
419
      }
420

    
421
      db_delete($this->revisionTable)
422
        ->condition($this->revisionKey, $revision_id)
423
        ->execute();
424

    
425
      $this->invoke('revision_delete', $entity_revision);
426
      return TRUE;
427
    }
428
    return FALSE;
429
  }
430

    
431
  /**
432
   * Implements EntityAPIControllerInterface.
433
   *
434
   * @param $transaction
435
   *   Optionally a DatabaseTransaction object to use. Allows overrides to pass
436
   *   in their transaction object.
437
   */
438
  public function save($entity, DatabaseTransaction $transaction = NULL) {
439
    $transaction = isset($transaction) ? $transaction : db_transaction();
440
    try {
441
      // Load the stored entity, if any.
442
      if (!empty($entity->{$this->idKey}) && !isset($entity->original)) {
443
        // In order to properly work in case of name changes, load the original
444
        // entity using the id key if it is available.
445
        $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->idKey});
446
      }
447
      $entity->is_new = !empty($entity->is_new) || empty($entity->{$this->idKey});
448
      $this->invoke('presave', $entity);
449

    
450
      if ($entity->is_new) {
451
        $return = drupal_write_record($this->entityInfo['base table'], $entity);
452
        if ($this->revisionKey) {
453
          $this->saveRevision($entity);
454
        }
455
        $this->invoke('insert', $entity);
456
      }
457
      else {
458
        // Update the base table if the entity doesn't have revisions or
459
        // we are updating the default revision.
460
        if (!$this->revisionKey || !empty($entity->{$this->defaultRevisionKey})) {
461
          $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey);
462
        }
463
        if ($this->revisionKey) {
464
          $return = $this->saveRevision($entity);
465
        }
466
        $this->resetCache(array($entity->{$this->idKey}));
467
        $this->invoke('update', $entity);
468

    
469
        // Field API always saves as default revision, so if the revision saved
470
        // is not default we have to restore the field values of the default
471
        // revision now by invoking field_attach_update() once again.
472
        if ($this->revisionKey && !$entity->{$this->defaultRevisionKey} && !empty($this->entityInfo['fieldable'])) {
473
          field_attach_update($this->entityType, $entity->original);
474
        }
475
      }
476

    
477
      // Ignore slave server temporarily.
478
      db_ignore_slave();
479
      unset($entity->is_new);
480
      unset($entity->is_new_revision);
481
      unset($entity->original);
482

    
483
      return $return;
484
    }
485
    catch (Exception $e) {
486
      $transaction->rollback();
487
      watchdog_exception($this->entityType, $e);
488
      throw $e;
489
    }
490
  }
491

    
492
  /**
493
   * Saves an entity revision.
494
   *
495
   * @param Entity $entity
496
   *   Entity revision to save.
497
   */
498
  protected function saveRevision($entity) {
499
    // Convert the entity into an array as it might not have the same properties
500
    // as the entity, it is just a raw structure.
501
    $record = (array) $entity;
502
    // File fields assumes we are using $entity->revision instead of
503
    // $entity->is_new_revision, so we also support it and make sure it's set to
504
    // the same value.
505
    $entity->is_new_revision = !empty($entity->is_new_revision) || !empty($entity->revision) || $entity->is_new;
506
    $entity->revision = &$entity->is_new_revision;
507
    $entity->{$this->defaultRevisionKey} = !empty($entity->{$this->defaultRevisionKey}) || $entity->is_new;
508

    
509

    
510

    
511
    // When saving a new revision, set any existing revision ID to NULL so as to
512
    // ensure that a new revision will actually be created.
513
    if ($entity->is_new_revision && isset($record[$this->revisionKey])) {
514
      $record[$this->revisionKey] = NULL;
515
    }
516

    
517
    if ($entity->is_new_revision) {
518
      drupal_write_record($this->revisionTable, $record);
519
      $update_default_revision = $entity->{$this->defaultRevisionKey};
520
    }
521
    else {
522
      drupal_write_record($this->revisionTable, $record, $this->revisionKey);
523
      // @todo: Fix original entity to be of the same revision and check whether
524
      // the default revision key has been set.
525
      $update_default_revision = $entity->{$this->defaultRevisionKey} && $entity->{$this->revisionKey} != $entity->original->{$this->revisionKey};
526
    }
527
    // Make sure to update the new revision key for the entity.
528
    $entity->{$this->revisionKey} = $record[$this->revisionKey];
529

    
530
    // Mark this revision as the default one.
531
    if ($update_default_revision) {
532
      db_update($this->entityInfo['base table'])
533
        ->fields(array($this->revisionKey => $record[$this->revisionKey]))
534
        ->condition($this->idKey, $entity->{$this->idKey})
535
        ->execute();
536
    }
537
    return $entity->is_new_revision ? SAVED_NEW : SAVED_UPDATED;
538
  }
539

    
540
  /**
541
   * Implements EntityAPIControllerInterface.
542
   */
543
  public function create(array $values = array()) {
544
    // Add is_new property if it is not set.
545
    $values += array('is_new' => TRUE);
546
    if (isset($this->entityInfo['entity class']) && $class = $this->entityInfo['entity class']) {
547
      return new $class($values, $this->entityType);
548
    }
549
    return (object) $values;
550
  }
551

    
552
  /**
553
   * Implements EntityAPIControllerInterface.
554
   *
555
   * @return
556
   *   A serialized string in JSON format suitable for the import() method.
557
   */
558
  public function export($entity, $prefix = '') {
559
    $vars = get_object_vars($entity);
560
    unset($vars['is_new']);
561
    return entity_var_json_export($vars, $prefix);
562
  }
563

    
564
  /**
565
   * Implements EntityAPIControllerInterface.
566
   *
567
   * @param $export
568
   *   A serialized string in JSON format as produced by the export() method.
569
   */
570
  public function import($export) {
571
    $vars = drupal_json_decode($export);
572
    if (is_array($vars)) {
573
      return $this->create($vars);
574
    }
575
    return FALSE;
576
  }
577

    
578
  /**
579
   * Implements EntityAPIControllerInterface.
580
   *
581
   * @param $content
582
   *   Optionally. Allows pre-populating the built content to ease overridding
583
   *   this method.
584
   */
585
  public function buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array()) {
586
    // Remove previously built content, if exists.
587
    $entity->content = $content;
588
    $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
589

    
590
    // By default add in properties for all defined extra fields.
591
    if ($extra_field_controller = entity_get_extra_fields_controller($this->entityType)) {
592
      $wrapper = entity_metadata_wrapper($this->entityType, $entity);
593
      $extra = $extra_field_controller->fieldExtraFields();
594
      $type_extra = &$extra[$this->entityType][$this->entityType]['display'];
595
      $bundle_extra = &$extra[$this->entityType][$wrapper->getBundle()]['display'];
596

    
597
      foreach ($wrapper as $name => $property) {
598
        if (isset($type_extra[$name]) || isset($bundle_extra[$name])) {
599
          $this->renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, $entity->content);
600
        }
601
      }
602
    }
603

    
604
    // Add in fields.
605
    if (!empty($this->entityInfo['fieldable'])) {
606
      // Perform the preparation tasks if they have not been performed yet.
607
      // An internal flag prevents the operation from running twice.
608
      $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL;
609
      field_attach_prepare_view($this->entityType, array($key => $entity), $view_mode);
610
      $entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode);
611
    }
612
    // Invoke hook_ENTITY_view() to allow modules to add their additions.
613
    if (module_exists('rules')) {
614
      rules_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode);
615
    }
616
    else {
617
      module_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode);
618
    }
619
    module_invoke_all('entity_view', $entity, $this->entityType, $view_mode, $langcode);
620
    $build = $entity->content;
621
    unset($entity->content);
622
    return $build;
623
  }
624

    
625
  /**
626
   * Renders a single entity property.
627
   */
628
  protected function renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, &$content) {
629
    $info = $property->info();
630

    
631
    $content[$name] = array(
632
      '#label_hidden' => FALSE,
633
      '#label' => $info['label'],
634
      '#entity_wrapped' => $wrapper,
635
      '#theme' => 'entity_property',
636
      '#property_name' => $name,
637
      '#access' => $property->access('view'),
638
      '#entity_type' => $this->entityType,
639
    );
640
    $content['#attached']['css']['entity.theme'] = drupal_get_path('module', 'entity') . '/theme/entity.theme.css';
641
  }
642

    
643
  /**
644
   * Implements EntityAPIControllerInterface.
645
   */
646
  public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL) {
647
    // For Field API and entity_prepare_view, the entities have to be keyed by
648
    // (numeric) id.
649
    $entities = entity_key_array_by_property($entities, $this->idKey);
650
    if (!empty($this->entityInfo['fieldable'])) {
651
      field_attach_prepare_view($this->entityType, $entities, $view_mode);
652
    }
653
    entity_prepare_view($this->entityType, $entities);
654
    $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
655

    
656
    $view = array();
657
    foreach ($entities as $entity) {
658
      $build = entity_build_content($this->entityType, $entity, $view_mode, $langcode);
659
      $build += array(
660
        // If the entity type provides an implementation, use this instead the
661
        // generic one.
662
        // @see template_preprocess_entity()
663
        '#theme' => 'entity',
664
        '#entity_type' => $this->entityType,
665
        '#entity' => $entity,
666
        '#view_mode' => $view_mode,
667
        '#language' => $langcode,
668
        '#page' => $page,
669
      );
670
      // Allow modules to modify the structured entity.
671
      drupal_alter(array($this->entityType . '_view', 'entity_view'), $build, $this->entityType);
672
      $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL;
673
      $view[$this->entityType][$key] = $build;
674
    }
675
    return $view;
676
  }
677
}
678

    
679
/**
680
 * A controller implementing exportables stored in the database.
681
 */
682
class EntityAPIControllerExportable extends EntityAPIController {
683

    
684
  protected $entityCacheByName = array();
685
  protected $nameKey, $statusKey, $moduleKey;
686

    
687
  /**
688
   * Overridden.
689
   *
690
   * Allows specifying a name key serving as uniform identifier for this entity
691
   * type while still internally we are using numeric identifieres.
692
   */
693
  public function __construct($entityType) {
694
    parent::__construct($entityType);
695
    // Use the name key as primary identifier.
696
    $this->nameKey = isset($this->entityInfo['entity keys']['name']) ? $this->entityInfo['entity keys']['name'] : $this->idKey;
697
    if (!empty($this->entityInfo['exportable'])) {
698
      $this->statusKey = isset($this->entityInfo['entity keys']['status']) ? $this->entityInfo['entity keys']['status'] : 'status';
699
      $this->moduleKey = isset($this->entityInfo['entity keys']['module']) ? $this->entityInfo['entity keys']['module'] : 'module';
700
    }
701
  }
702

    
703
  /**
704
   * Support loading by name key.
705
   */
706
  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
707
    // Add the id condition ourself, as we might have a separate name key.
708
    $query = parent::buildQuery(array(), $conditions, $revision_id);
709
    if ($ids) {
710
      // Support loading by numeric ids as well as by machine names.
711
      $key = is_numeric(reset($ids)) ? $this->idKey : $this->nameKey;
712
      $query->condition("base.$key", $ids, 'IN');
713
    }
714
    return $query;
715
  }
716

    
717
  /**
718
   * Overridden to support passing numeric ids as well as names as $ids.
719
   */
720
  public function load($ids = array(), $conditions = array()) {
721
    $entities = array();
722

    
723
    // Only do something if loaded by names.
724
    if (!$ids || $this->nameKey == $this->idKey || is_numeric(reset($ids))) {
725
      return parent::load($ids, $conditions);
726
    }
727

    
728
    // Revisions are not statically cached, and require a different query to
729
    // other conditions, so separate the revision id into its own variable.
730
    if ($this->revisionKey && isset($conditions[$this->revisionKey])) {
731
      $revision_id = $conditions[$this->revisionKey];
732
      unset($conditions[$this->revisionKey]);
733
    }
734
    else {
735
      $revision_id = FALSE;
736
    }
737
    $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
738

    
739
    // Care about the static cache.
740
    if ($this->cache && !$revision_id) {
741
      $entities = $this->cacheGetByName($ids, $conditions);
742
    }
743
    // If any entities were loaded, remove them from the ids still to load.
744
    if ($entities) {
745
      $ids = array_keys(array_diff_key($passed_ids, $entities));
746
    }
747

    
748
    $entities_by_id = parent::load($ids, $conditions);
749
    $entities += entity_key_array_by_property($entities_by_id, $this->nameKey);
750

    
751
    // Ensure that the returned array is keyed by numeric id and ordered the
752
    // same as the original $ids array and remove any invalid ids.
753
    $return = array();
754
    foreach ($passed_ids as $name => $value) {
755
      if (isset($entities[$name])) {
756
        $return[$entities[$name]->{$this->idKey}] = $entities[$name];
757
      }
758
    }
759
    return $return;
760
  }
761

    
762
  /**
763
   * Overridden.
764
   * @see DrupalDefaultEntityController::cacheGet()
765
   */
766
  protected function cacheGet($ids, $conditions = array()) {
767
    if (!empty($this->entityCache) && $ids !== array()) {
768
      $entities = $ids ? array_intersect_key($this->entityCache, array_flip($ids)) : $this->entityCache;
769
      return $this->applyConditions($entities, $conditions);
770
    }
771
    return array();
772
  }
773

    
774
  /**
775
   * Like cacheGet() but keyed by name.
776
   */
777
  protected function cacheGetByName($names, $conditions = array()) {
778
    if (!empty($this->entityCacheByName) && $names !== array() && $names) {
779
      // First get the entities by ids, then apply the conditions.
780
      // Generally, we make use of $this->entityCache, but if we are loading by
781
      // name, we have to use $this->entityCacheByName.
782
      $entities = array_intersect_key($this->entityCacheByName, array_flip($names));
783
      return $this->applyConditions($entities, $conditions);
784
    }
785
    return array();
786
  }
787

    
788
  protected function applyConditions($entities, $conditions = array()) {
789
    if ($conditions) {
790
      foreach ($entities as $key => $entity) {
791
        $entity_values = (array) $entity;
792
        // We cannot use array_diff_assoc() here because condition values can
793
        // also be arrays, e.g. '$conditions = array('status' => array(1, 2))'
794
        foreach ($conditions as $condition_key => $condition_value) {
795
          if (is_array($condition_value)) {
796
            if (!isset($entity_values[$condition_key]) || !in_array($entity_values[$condition_key], $condition_value)) {
797
              unset($entities[$key]);
798
            }
799
          }
800
          elseif (!isset($entity_values[$condition_key]) || $entity_values[$condition_key] != $condition_value) {
801
            unset($entities[$key]);
802
          }
803
        }
804
      }
805
    }
806
    return $entities;
807
  }
808

    
809
  /**
810
   * Overridden.
811
   * @see DrupalDefaultEntityController::cacheSet()
812
   */
813
  protected function cacheSet($entities) {
814
    $this->entityCache += $entities;
815
    // If we have a name key, also support static caching when loading by name.
816
    if ($this->nameKey != $this->idKey) {
817
      $this->entityCacheByName += entity_key_array_by_property($entities, $this->nameKey);
818
    }
819
  }
820

    
821
  /**
822
   * Overridden.
823
   * @see DrupalDefaultEntityController::attachLoad()
824
   *
825
   * Changed to call type-specific hook with the entities keyed by name if they
826
   * have one.
827
   */
828
  protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
829
    // Attach fields.
830
    if ($this->entityInfo['fieldable']) {
831
      if ($revision_id) {
832
        field_attach_load_revision($this->entityType, $queried_entities);
833
      }
834
      else {
835
        field_attach_load($this->entityType, $queried_entities);
836
      }
837
    }
838

    
839
    // Call hook_entity_load().
840
    foreach (module_implements('entity_load') as $module) {
841
      $function = $module . '_entity_load';
842
      $function($queried_entities, $this->entityType);
843
    }
844
    // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
845
    // always the queried entities, followed by additional arguments set in
846
    // $this->hookLoadArguments.
847
    // For entities with a name key, pass the entities keyed by name to the
848
    // specific load hook.
849
    if ($this->nameKey != $this->idKey) {
850
      $entities_by_name = entity_key_array_by_property($queried_entities, $this->nameKey);
851
    }
852
    else {
853
      $entities_by_name = $queried_entities;
854
    }
855
    $args = array_merge(array($entities_by_name), $this->hookLoadArguments);
856
    foreach (module_implements($this->entityInfo['load hook']) as $module) {
857
      call_user_func_array($module . '_' . $this->entityInfo['load hook'], $args);
858
    }
859
  }
860

    
861
  public function resetCache(array $ids = NULL) {
862
    $this->cacheComplete = FALSE;
863
    if (isset($ids)) {
864
      foreach (array_intersect_key($this->entityCache, array_flip($ids)) as $id => $entity) {
865
        unset($this->entityCacheByName[$this->entityCache[$id]->{$this->nameKey}]);
866
        unset($this->entityCache[$id]);
867
      }
868
    }
869
    else {
870
      $this->entityCache = array();
871
      $this->entityCacheByName = array();
872
    }
873
  }
874

    
875
  /**
876
   * Overridden to care about reverted entities.
877
   */
878
  public function delete($ids, DatabaseTransaction $transaction = NULL) {
879
    $entities = $ids ? $this->load($ids) : FALSE;
880
    if ($entities) {
881
      parent::delete($ids, $transaction);
882

    
883
      foreach ($entities as $id => $entity) {
884
        if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE)) {
885
          entity_defaults_rebuild(array($this->entityType));
886
          break;
887
        }
888
      }
889
    }
890
  }
891

    
892
  /**
893
   * Overridden to care about reverted bundle entities and to skip Rules.
894
   */
895
  public function invoke($hook, $entity) {
896
    if ($hook == 'delete') {
897
      // To ease figuring out whether this is a revert, make sure that the
898
      // entity status is updated in case the providing module has been
899
      // disabled.
900
      if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE) && !module_exists($entity->{$this->moduleKey})) {
901
        $entity->{$this->statusKey} = ENTITY_CUSTOM;
902
      }
903
      $is_revert = entity_has_status($this->entityType, $entity, ENTITY_IN_CODE);
904
    }
905

    
906
    if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) {
907
      $function($this->entityType, $entity);
908
    }
909

    
910
    if (isset($this->entityInfo['bundle of']) && $type = $this->entityInfo['bundle of']) {
911
      // Call field API bundle attachers for the entity we are a bundle of.
912
      if ($hook == 'insert') {
913
        field_attach_create_bundle($type, $entity->{$this->bundleKey});
914
      }
915
      elseif ($hook == 'delete' && !$is_revert) {
916
        field_attach_delete_bundle($type, $entity->{$this->bundleKey});
917
      }
918
      elseif ($hook == 'update' && $id = $entity->{$this->nameKey}) {
919
        if ($entity->original->{$this->bundleKey} != $entity->{$this->bundleKey}) {
920
          field_attach_rename_bundle($type, $entity->original->{$this->bundleKey}, $entity->{$this->bundleKey});
921
        }
922
      }
923
    }
924
    // Invoke the hook.
925
    module_invoke_all($this->entityType . '_' . $hook, $entity);
926
    // Invoke the respective entity level hook.
927
    if ($hook == 'presave' || $hook == 'insert' || $hook == 'update' || $hook == 'delete') {
928
      module_invoke_all('entity_' . $hook, $entity, $this->entityType);
929
    }
930
  }
931

    
932
  /**
933
   * Overridden to care exportables that are overridden.
934
   */
935
  public function save($entity, DatabaseTransaction $transaction = NULL) {
936
    // Preload $entity->original by name key if necessary.
937
    if (!empty($entity->{$this->nameKey}) && empty($entity->{$this->idKey}) && !isset($entity->original)) {
938
      $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->nameKey});
939
    }
940
    // Update the status for entities getting overridden.
941
    if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE) && empty($entity->is_rebuild)) {
942
      $entity->{$this->statusKey} |= ENTITY_CUSTOM;
943
    }
944
    return parent::save($entity, $transaction);
945
  }
946

    
947
  /**
948
   * Overridden.
949
   */
950
  public function export($entity, $prefix = '') {
951
    $vars = get_object_vars($entity);
952
    unset($vars[$this->statusKey], $vars[$this->moduleKey], $vars['is_new']);
953
    if ($this->nameKey != $this->idKey) {
954
      unset($vars[$this->idKey]);
955
    }
956
    return entity_var_json_export($vars, $prefix);
957
  }
958

    
959
  /**
960
   * Implements EntityAPIControllerInterface.
961
   */
962
  public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL) {
963
    $view = parent::view($entities, $view_mode, $langcode, $page);
964

    
965
    if ($this->nameKey != $this->idKey) {
966
      // Re-key the view array to be keyed by name.
967
      $return = array();
968
      foreach ($view[$this->entityType] as $id => $content) {
969
        $key = isset($content['#entity']->{$this->nameKey}) ? $content['#entity']->{$this->nameKey} : NULL;
970
        $return[$this->entityType][$key] = $content;
971
      }
972
      $view = $return;
973
    }
974
    return $view;
975
  }
976
}