Mercurial > hg > rr-repo
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 } |