comparison sites/all/modules/entity/includes/entity.controller.inc @ 4:ce11bbd8f642

added modules
author danieleb <danielebarchiesi@me.com>
date Thu, 19 Sep 2013 10:38:44 +0100
parents
children
comparison
equal deleted inserted replaced
3:b28be78d8160 4:ce11bbd8f642
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 }