Projet

Général

Profil

Paste
Télécharger (35 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / entity / includes / entity.controller.inc @ 503b3f7b

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('CASE WHEN base.' . $this->revisionKey . ' = revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', $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
    $transaction = isset($transaction) ? $transaction : db_transaction();
377

    
378
    try {
379
      $ids = array_keys($entities);
380

    
381
      db_delete($this->entityInfo['base table'])
382
        ->condition($this->idKey, $ids, 'IN')
383
        ->execute();
384

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

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

    
406
  /**
407
   * Implements EntityAPIControllerRevisionableInterface::deleteRevision().
408
   */
409
  public function deleteRevision($revision_id) {
410
    if ($entity_revision = entity_revision_load($this->entityType, $revision_id)) {
411
      // Prevent deleting the default revision.
412
      if (entity_revision_is_default($this->entityType, $entity_revision)) {
413
        return FALSE;
414
      }
415

    
416
      db_delete($this->revisionTable)
417
        ->condition($this->revisionKey, $revision_id)
418
        ->execute();
419

    
420
      $this->invoke('revision_delete', $entity_revision);
421
      return TRUE;
422
    }
423
    return FALSE;
424
  }
425

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

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

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

    
472
      // Ignore slave server temporarily.
473
      db_ignore_slave();
474
      unset($entity->is_new);
475
      unset($entity->is_new_revision);
476
      unset($entity->original);
477

    
478
      return $return;
479
    }
480
    catch (Exception $e) {
481
      $transaction->rollback();
482
      watchdog_exception($this->entityType, $e);
483
      throw $e;
484
    }
485
  }
486

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

    
504

    
505

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

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

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

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

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

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

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

    
585
    // Allow modules to change the view mode.
586
    $context = array(
587
      'entity_type' => $this->entityType,
588
      'entity' => $entity,
589
      'langcode' => $langcode,
590
    );
591
    drupal_alter('entity_view_mode', $view_mode, $context);
592
    // Make sure the used view-mode gets stored.
593
    $entity->content += array('#view_mode' => $view_mode);
594

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

    
602
      foreach ($wrapper as $name => $property) {
603
        if (isset($type_extra[$name]) || isset($bundle_extra[$name])) {
604
          $this->renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, $entity->content);
605
        }
606
      }
607
    }
608

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

    
630
  /**
631
   * Renders a single entity property.
632
   */
633
  protected function renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, &$content) {
634
    $info = $property->info();
635

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

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

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

    
684
/**
685
 * A controller implementing exportables stored in the database.
686
 */
687
class EntityAPIControllerExportable extends EntityAPIController {
688

    
689
  protected $entityCacheByName = array();
690
  protected $nameKey, $statusKey, $moduleKey;
691

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

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

    
722
  /**
723
   * Overridden to support passing numeric ids as well as names as $ids.
724
   */
725
  public function load($ids = array(), $conditions = array()) {
726
    $entities = array();
727

    
728
    // Only do something if loaded by names.
729
    if (!$ids || $this->nameKey == $this->idKey || is_numeric(reset($ids))) {
730
      return parent::load($ids, $conditions);
731
    }
732

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

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

    
753
    $entities_by_id = parent::load($ids, $conditions);
754
    $entities += entity_key_array_by_property($entities_by_id, $this->nameKey);
755

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

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

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

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

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

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

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

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

    
880
  /**
881
   * Overridden to care about reverted entities.
882
   */
883
  public function delete($ids, DatabaseTransaction $transaction = NULL) {
884
    $entities = $ids ? $this->load($ids) : FALSE;
885
    if ($entities) {
886
      parent::delete($ids, $transaction);
887

    
888
      foreach ($entities as $id => $entity) {
889
        if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE)) {
890
          entity_defaults_rebuild(array($this->entityType));
891
          break;
892
        }
893
      }
894
    }
895
  }
896

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

    
911
    if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) {
912
      $function($this->entityType, $entity);
913
    }
914

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

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

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

    
964
  /**
965
   * Implements EntityAPIControllerInterface.
966
   */
967
  public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL) {
968
    $view = parent::view($entities, $view_mode, $langcode, $page);
969

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