entity.controller.inc 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981
  1. <?php
  2. /**
  3. * @file
  4. * Provides a controller building upon the core controller but providing more
  5. * features like full CRUD functionality.
  6. */
  7. /**
  8. * Interface for EntityControllers compatible with the entity API.
  9. */
  10. interface EntityAPIControllerInterface extends DrupalEntityControllerInterface {
  11. /**
  12. * Delete permanently saved entities.
  13. *
  14. * In case of failures, an exception is thrown.
  15. *
  16. * @param $ids
  17. * An array of entity IDs.
  18. */
  19. public function delete($ids);
  20. /**
  21. * Invokes a hook on behalf of the entity. For hooks that have a respective
  22. * field API attacher like insert/update/.. the attacher is called too.
  23. */
  24. public function invoke($hook, $entity);
  25. /**
  26. * Permanently saves the given entity.
  27. *
  28. * In case of failures, an exception is thrown.
  29. *
  30. * @param $entity
  31. * The entity to save.
  32. *
  33. * @return
  34. * SAVED_NEW or SAVED_UPDATED is returned depending on the operation
  35. * performed.
  36. */
  37. public function save($entity);
  38. /**
  39. * Create a new entity.
  40. *
  41. * @param array $values
  42. * An array of values to set, keyed by property name.
  43. * @return
  44. * A new instance of the entity type.
  45. */
  46. public function create(array $values = array());
  47. /**
  48. * Exports an entity as serialized string.
  49. *
  50. * @param $entity
  51. * The entity to export.
  52. * @param $prefix
  53. * An optional prefix for each line.
  54. *
  55. * @return
  56. * The exported entity as serialized string. The format is determined by
  57. * the controller and has to be compatible with the format that is accepted
  58. * by the import() method.
  59. */
  60. public function export($entity, $prefix = '');
  61. /**
  62. * Imports an entity from a string.
  63. *
  64. * @param string $export
  65. * An exported entity as serialized string.
  66. *
  67. * @return
  68. * An entity object not yet saved.
  69. */
  70. public function import($export);
  71. /**
  72. * Builds a structured array representing the entity's content.
  73. *
  74. * The content built for the entity will vary depending on the $view_mode
  75. * parameter.
  76. *
  77. * @param $entity
  78. * An entity object.
  79. * @param $view_mode
  80. * View mode, e.g. 'full', 'teaser'...
  81. * @param $langcode
  82. * (optional) A language code to use for rendering. Defaults to the global
  83. * content language of the current request.
  84. * @return
  85. * The renderable array.
  86. */
  87. public function buildContent($entity, $view_mode = 'full', $langcode = NULL);
  88. /**
  89. * Generate an array for rendering the given entities.
  90. *
  91. * @param $entities
  92. * An array of entities to render.
  93. * @param $view_mode
  94. * View mode, e.g. 'full', 'teaser'...
  95. * @param $langcode
  96. * (optional) A language code to use for rendering. Defaults to the global
  97. * content language of the current request.
  98. * @param $page
  99. * (optional) If set will control if the entity is rendered: if TRUE
  100. * the entity will be rendered without its title, so that it can be embedded
  101. * in another context. If FALSE the entity will be displayed with its title
  102. * in a mode suitable for lists.
  103. * If unset, the page mode will be enabled if the current path is the URI
  104. * of the entity, as returned by entity_uri().
  105. * This parameter is only supported for entities which controller is a
  106. * EntityAPIControllerInterface.
  107. * @return
  108. * The renderable array, keyed by entity name or numeric id.
  109. */
  110. public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL);
  111. }
  112. /**
  113. * Interface for EntityControllers of entities that support revisions.
  114. */
  115. interface EntityAPIControllerRevisionableInterface extends EntityAPIControllerInterface {
  116. /**
  117. * Delete an entity revision.
  118. *
  119. * Note that the default revision of an entity cannot be deleted.
  120. *
  121. * @param $revision_id
  122. * The ID of the revision to delete.
  123. *
  124. * @return boolean
  125. * TRUE if the entity revision could be deleted, FALSE otherwise.
  126. */
  127. public function deleteRevision($revision_id);
  128. }
  129. /**
  130. * A controller implementing EntityAPIControllerInterface for the database.
  131. */
  132. class EntityAPIController extends DrupalDefaultEntityController implements EntityAPIControllerRevisionableInterface {
  133. protected $cacheComplete = FALSE;
  134. protected $bundleKey;
  135. protected $defaultRevisionKey;
  136. /**
  137. * Overridden.
  138. * @see DrupalDefaultEntityController#__construct()
  139. */
  140. public function __construct($entityType) {
  141. parent::__construct($entityType);
  142. // If this is the bundle of another entity, set the bundle key.
  143. if (isset($this->entityInfo['bundle of'])) {
  144. $info = entity_get_info($this->entityInfo['bundle of']);
  145. $this->bundleKey = $info['bundle keys']['bundle'];
  146. }
  147. $this->defaultRevisionKey = !empty($this->entityInfo['entity keys']['default revision']) ? $this->entityInfo['entity keys']['default revision'] : 'default_revision';
  148. }
  149. /**
  150. * Overrides DrupalDefaultEntityController::buildQuery().
  151. */
  152. protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
  153. $query = parent::buildQuery($ids, $conditions, $revision_id);
  154. if ($this->revisionKey) {
  155. // Compare revision id of the base and revision table, if equal then this
  156. // is the default revision.
  157. $query->addExpression('CASE WHEN base.' . $this->revisionKey . ' = revision.' . $this->revisionKey . ' THEN 1 ELSE 0 END', $this->defaultRevisionKey);
  158. }
  159. return $query;
  160. }
  161. /**
  162. * Builds and executes the query for loading.
  163. *
  164. * @return The results in a Traversable object.
  165. */
  166. public function query($ids, $conditions, $revision_id = FALSE) {
  167. // Build the query.
  168. $query = $this->buildQuery($ids, $conditions, $revision_id);
  169. $result = $query->execute();
  170. if (!empty($this->entityInfo['entity class'])) {
  171. $result->setFetchMode(PDO::FETCH_CLASS, $this->entityInfo['entity class'], array(array(), $this->entityType));
  172. }
  173. return $result;
  174. }
  175. /**
  176. * Overridden.
  177. * @see DrupalDefaultEntityController#load($ids, $conditions)
  178. *
  179. * In contrast to the parent implementation we factor out query execution, so
  180. * fetching can be further customized easily.
  181. */
  182. public function load($ids = array(), $conditions = array()) {
  183. $entities = array();
  184. // Revisions are not statically cached, and require a different query to
  185. // other conditions, so separate the revision id into its own variable.
  186. if ($this->revisionKey && isset($conditions[$this->revisionKey])) {
  187. $revision_id = $conditions[$this->revisionKey];
  188. unset($conditions[$this->revisionKey]);
  189. }
  190. else {
  191. $revision_id = FALSE;
  192. }
  193. // Create a new variable which is either a prepared version of the $ids
  194. // array for later comparison with the entity cache, or FALSE if no $ids
  195. // were passed. The $ids array is reduced as items are loaded from cache,
  196. // and we need to know if it's empty for this reason to avoid querying the
  197. // database when all requested entities are loaded from cache.
  198. $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
  199. // Try to load entities from the static cache.
  200. if ($this->cache && !$revision_id) {
  201. $entities = $this->cacheGet($ids, $conditions);
  202. // If any entities were loaded, remove them from the ids still to load.
  203. if ($passed_ids) {
  204. $ids = array_keys(array_diff_key($passed_ids, $entities));
  205. }
  206. }
  207. // Support the entitycache module if activated.
  208. if (!empty($this->entityInfo['entity cache']) && !$revision_id && $ids && !$conditions) {
  209. $cached_entities = EntityCacheControllerHelper::entityCacheGet($this, $ids, $conditions);
  210. // If any entities were loaded, remove them from the ids still to load.
  211. $ids = array_diff($ids, array_keys($cached_entities));
  212. $entities += $cached_entities;
  213. // Add loaded entities to the static cache if we are not loading a
  214. // revision.
  215. if ($this->cache && !empty($cached_entities) && !$revision_id) {
  216. $this->cacheSet($cached_entities);
  217. }
  218. }
  219. // Load any remaining entities from the database. This is the case if $ids
  220. // is set to FALSE (so we load all entities), if there are any ids left to
  221. // load or if loading a revision.
  222. if (!($this->cacheComplete && $ids === FALSE && !$conditions) && ($ids === FALSE || $ids || $revision_id)) {
  223. $queried_entities = array();
  224. foreach ($this->query($ids, $conditions, $revision_id) as $record) {
  225. // Skip entities already retrieved from cache.
  226. if (isset($entities[$record->{$this->idKey}])) {
  227. continue;
  228. }
  229. // For DB-based entities take care of serialized columns.
  230. if (!empty($this->entityInfo['base table'])) {
  231. $schema = drupal_get_schema($this->entityInfo['base table']);
  232. foreach ($schema['fields'] as $field => $info) {
  233. if (!empty($info['serialize']) && isset($record->$field)) {
  234. $record->$field = unserialize($record->$field);
  235. // Support automatic merging of 'data' fields into the entity.
  236. if (!empty($info['merge']) && is_array($record->$field)) {
  237. foreach ($record->$field as $key => $value) {
  238. $record->$key = $value;
  239. }
  240. unset($record->$field);
  241. }
  242. }
  243. }
  244. }
  245. $queried_entities[$record->{$this->idKey}] = $record;
  246. }
  247. }
  248. // Pass all entities loaded from the database through $this->attachLoad(),
  249. // which attaches fields (if supported by the entity type) and calls the
  250. // entity type specific load callback, for example hook_node_load().
  251. if (!empty($queried_entities)) {
  252. $this->attachLoad($queried_entities, $revision_id);
  253. $entities += $queried_entities;
  254. }
  255. // Entitycache module support: Add entities to the entity cache if we are
  256. // not loading a revision.
  257. if (!empty($this->entityInfo['entity cache']) && !empty($queried_entities) && !$revision_id) {
  258. EntityCacheControllerHelper::entityCacheSet($this, $queried_entities);
  259. }
  260. if ($this->cache) {
  261. // Add entities to the cache if we are not loading a revision.
  262. if (!empty($queried_entities) && !$revision_id) {
  263. $this->cacheSet($queried_entities);
  264. // Remember if we have cached all entities now.
  265. if (!$conditions && $ids === FALSE) {
  266. $this->cacheComplete = TRUE;
  267. }
  268. }
  269. }
  270. // Ensure that the returned array is ordered the same as the original
  271. // $ids array if this was passed in and remove any invalid ids.
  272. if ($passed_ids && $passed_ids = array_intersect_key($passed_ids, $entities)) {
  273. foreach ($passed_ids as $id => $value) {
  274. $passed_ids[$id] = $entities[$id];
  275. }
  276. $entities = $passed_ids;
  277. }
  278. return $entities;
  279. }
  280. /**
  281. * Overrides DrupalDefaultEntityController::resetCache().
  282. */
  283. public function resetCache(array $ids = NULL) {
  284. $this->cacheComplete = FALSE;
  285. parent::resetCache($ids);
  286. // Support the entitycache module.
  287. if (!empty($this->entityInfo['entity cache'])) {
  288. EntityCacheControllerHelper::resetEntityCache($this, $ids);
  289. }
  290. }
  291. /**
  292. * Implements EntityAPIControllerInterface.
  293. */
  294. public function invoke($hook, $entity) {
  295. // entity_revision_delete() invokes hook_entity_revision_delete() and
  296. // hook_field_attach_delete_revision() just as node module does. So we need
  297. // to adjust the name of our revision deletion field attach hook in order to
  298. // stick to this pattern.
  299. $field_attach_hook = ($hook == 'revision_delete' ? 'delete_revision' : $hook);
  300. if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $field_attach_hook)) {
  301. $function($this->entityType, $entity);
  302. }
  303. if (!empty($this->entityInfo['bundle of']) && entity_type_is_fieldable($this->entityInfo['bundle of'])) {
  304. $type = $this->entityInfo['bundle of'];
  305. // Call field API bundle attachers for the entity we are a bundle of.
  306. if ($hook == 'insert') {
  307. field_attach_create_bundle($type, $entity->{$this->bundleKey});
  308. }
  309. elseif ($hook == 'delete') {
  310. field_attach_delete_bundle($type, $entity->{$this->bundleKey});
  311. }
  312. elseif ($hook == 'update' && $entity->original->{$this->bundleKey} != $entity->{$this->bundleKey}) {
  313. field_attach_rename_bundle($type, $entity->original->{$this->bundleKey}, $entity->{$this->bundleKey});
  314. }
  315. }
  316. // Invoke the hook.
  317. module_invoke_all($this->entityType . '_' . $hook, $entity);
  318. // Invoke the respective entity level hook.
  319. if ($hook == 'presave' || $hook == 'insert' || $hook == 'update' || $hook == 'delete') {
  320. module_invoke_all('entity_' . $hook, $entity, $this->entityType);
  321. }
  322. // Invoke rules.
  323. if (module_exists('rules')) {
  324. rules_invoke_event($this->entityType . '_' . $hook, $entity);
  325. }
  326. }
  327. /**
  328. * Implements EntityAPIControllerInterface.
  329. *
  330. * @param $transaction
  331. * Optionally a DatabaseTransaction object to use. Allows overrides to pass
  332. * in their transaction object.
  333. */
  334. public function delete($ids, DatabaseTransaction $transaction = NULL) {
  335. $entities = $ids ? $this->load($ids) : FALSE;
  336. if (!$entities) {
  337. // Do nothing, in case invalid or no ids have been passed.
  338. return;
  339. }
  340. $transaction = isset($transaction) ? $transaction : db_transaction();
  341. try {
  342. $ids = array_keys($entities);
  343. db_delete($this->entityInfo['base table'])
  344. ->condition($this->idKey, $ids, 'IN')
  345. ->execute();
  346. if (isset($this->revisionTable)) {
  347. db_delete($this->revisionTable)
  348. ->condition($this->idKey, $ids, 'IN')
  349. ->execute();
  350. }
  351. // Reset the cache as soon as the changes have been applied.
  352. $this->resetCache($ids);
  353. foreach ($entities as $id => $entity) {
  354. $this->invoke('delete', $entity);
  355. }
  356. // Ignore slave server temporarily.
  357. db_ignore_slave();
  358. }
  359. catch (Exception $e) {
  360. $transaction->rollback();
  361. watchdog_exception($this->entityType, $e);
  362. throw $e;
  363. }
  364. }
  365. /**
  366. * Implements EntityAPIControllerRevisionableInterface::deleteRevision().
  367. */
  368. public function deleteRevision($revision_id) {
  369. if ($entity_revision = entity_revision_load($this->entityType, $revision_id)) {
  370. // Prevent deleting the default revision.
  371. if (entity_revision_is_default($this->entityType, $entity_revision)) {
  372. return FALSE;
  373. }
  374. db_delete($this->revisionTable)
  375. ->condition($this->revisionKey, $revision_id)
  376. ->execute();
  377. $this->invoke('revision_delete', $entity_revision);
  378. return TRUE;
  379. }
  380. return FALSE;
  381. }
  382. /**
  383. * Implements EntityAPIControllerInterface.
  384. *
  385. * @param $transaction
  386. * Optionally a DatabaseTransaction object to use. Allows overrides to pass
  387. * in their transaction object.
  388. */
  389. public function save($entity, DatabaseTransaction $transaction = NULL) {
  390. $transaction = isset($transaction) ? $transaction : db_transaction();
  391. try {
  392. // Load the stored entity, if any.
  393. if (!empty($entity->{$this->idKey}) && !isset($entity->original)) {
  394. // In order to properly work in case of name changes, load the original
  395. // entity using the id key if it is available.
  396. $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->idKey});
  397. }
  398. $entity->is_new = !empty($entity->is_new) || empty($entity->{$this->idKey});
  399. $this->invoke('presave', $entity);
  400. if ($entity->is_new) {
  401. $return = drupal_write_record($this->entityInfo['base table'], $entity);
  402. if ($this->revisionKey) {
  403. $this->saveRevision($entity);
  404. }
  405. $this->invoke('insert', $entity);
  406. }
  407. else {
  408. // Update the base table if the entity doesn't have revisions or
  409. // we are updating the default revision.
  410. if (!$this->revisionKey || !empty($entity->{$this->defaultRevisionKey})) {
  411. $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey);
  412. }
  413. if ($this->revisionKey) {
  414. $return = $this->saveRevision($entity);
  415. }
  416. $this->resetCache(array($entity->{$this->idKey}));
  417. $this->invoke('update', $entity);
  418. // Field API always saves as default revision, so if the revision saved
  419. // is not default we have to restore the field values of the default
  420. // revision now by invoking field_attach_update() once again.
  421. if ($this->revisionKey && !$entity->{$this->defaultRevisionKey} && !empty($this->entityInfo['fieldable'])) {
  422. field_attach_update($this->entityType, $entity->original);
  423. }
  424. }
  425. // Ignore slave server temporarily.
  426. db_ignore_slave();
  427. unset($entity->is_new);
  428. unset($entity->is_new_revision);
  429. unset($entity->original);
  430. return $return;
  431. }
  432. catch (Exception $e) {
  433. $transaction->rollback();
  434. watchdog_exception($this->entityType, $e);
  435. throw $e;
  436. }
  437. }
  438. /**
  439. * Saves an entity revision.
  440. *
  441. * @param Entity $entity
  442. * Entity revision to save.
  443. */
  444. protected function saveRevision($entity) {
  445. // Convert the entity into an array as it might not have the same properties
  446. // as the entity, it is just a raw structure.
  447. $record = (array) $entity;
  448. // File fields assumes we are using $entity->revision instead of
  449. // $entity->is_new_revision, so we also support it and make sure it's set to
  450. // the same value.
  451. $entity->is_new_revision = !empty($entity->is_new_revision) || !empty($entity->revision) || $entity->is_new;
  452. $entity->revision = &$entity->is_new_revision;
  453. $entity->{$this->defaultRevisionKey} = !empty($entity->{$this->defaultRevisionKey}) || $entity->is_new;
  454. // When saving a new revision, set any existing revision ID to NULL so as to
  455. // ensure that a new revision will actually be created.
  456. if ($entity->is_new_revision && isset($record[$this->revisionKey])) {
  457. $record[$this->revisionKey] = NULL;
  458. }
  459. if ($entity->is_new_revision) {
  460. drupal_write_record($this->revisionTable, $record);
  461. $update_default_revision = $entity->{$this->defaultRevisionKey};
  462. }
  463. else {
  464. drupal_write_record($this->revisionTable, $record, $this->revisionKey);
  465. // @todo: Fix original entity to be of the same revision and check whether
  466. // the default revision key has been set.
  467. $update_default_revision = $entity->{$this->defaultRevisionKey} && $entity->{$this->revisionKey} != $entity->original->{$this->revisionKey};
  468. }
  469. // Make sure to update the new revision key for the entity.
  470. $entity->{$this->revisionKey} = $record[$this->revisionKey];
  471. // Mark this revision as the default one.
  472. if ($update_default_revision) {
  473. db_update($this->entityInfo['base table'])
  474. ->fields(array($this->revisionKey => $record[$this->revisionKey]))
  475. ->condition($this->idKey, $entity->{$this->idKey})
  476. ->execute();
  477. }
  478. return $entity->is_new_revision ? SAVED_NEW : SAVED_UPDATED;
  479. }
  480. /**
  481. * Implements EntityAPIControllerInterface.
  482. */
  483. public function create(array $values = array()) {
  484. // Add is_new property if it is not set.
  485. $values += array('is_new' => TRUE);
  486. if (isset($this->entityInfo['entity class']) && $class = $this->entityInfo['entity class']) {
  487. return new $class($values, $this->entityType);
  488. }
  489. return (object) $values;
  490. }
  491. /**
  492. * Implements EntityAPIControllerInterface.
  493. *
  494. * @return
  495. * A serialized string in JSON format suitable for the import() method.
  496. */
  497. public function export($entity, $prefix = '') {
  498. $vars = get_object_vars($entity);
  499. unset($vars['is_new']);
  500. return entity_var_json_export($vars, $prefix);
  501. }
  502. /**
  503. * Implements EntityAPIControllerInterface.
  504. *
  505. * @param $export
  506. * A serialized string in JSON format as produced by the export() method.
  507. */
  508. public function import($export) {
  509. $vars = drupal_json_decode($export);
  510. if (is_array($vars)) {
  511. return $this->create($vars);
  512. }
  513. return FALSE;
  514. }
  515. /**
  516. * Implements EntityAPIControllerInterface.
  517. *
  518. * @param $content
  519. * Optionally. Allows pre-populating the built content to ease overridding
  520. * this method.
  521. */
  522. public function buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array()) {
  523. // Remove previously built content, if exists.
  524. $entity->content = $content;
  525. $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
  526. // Allow modules to change the view mode.
  527. $context = array(
  528. 'entity_type' => $this->entityType,
  529. 'entity' => $entity,
  530. 'langcode' => $langcode,
  531. );
  532. drupal_alter('entity_view_mode', $view_mode, $context);
  533. // Make sure the used view-mode gets stored.
  534. $entity->content += array('#view_mode' => $view_mode);
  535. // By default add in properties for all defined extra fields.
  536. if ($extra_field_controller = entity_get_extra_fields_controller($this->entityType)) {
  537. $wrapper = entity_metadata_wrapper($this->entityType, $entity);
  538. $extra = $extra_field_controller->fieldExtraFields();
  539. $type_extra = &$extra[$this->entityType][$this->entityType]['display'];
  540. $bundle_extra = &$extra[$this->entityType][$wrapper->getBundle()]['display'];
  541. foreach ($wrapper as $name => $property) {
  542. if (isset($type_extra[$name]) || isset($bundle_extra[$name])) {
  543. $this->renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, $entity->content);
  544. }
  545. }
  546. }
  547. // Add in fields.
  548. if (!empty($this->entityInfo['fieldable'])) {
  549. // Perform the preparation tasks if they have not been performed yet.
  550. // An internal flag prevents the operation from running twice.
  551. $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL;
  552. field_attach_prepare_view($this->entityType, array($key => $entity), $view_mode);
  553. $entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode);
  554. }
  555. // Invoke hook_ENTITY_view() to allow modules to add their additions.
  556. if (module_exists('rules')) {
  557. rules_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode);
  558. }
  559. else {
  560. module_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode);
  561. }
  562. module_invoke_all('entity_view', $entity, $this->entityType, $view_mode, $langcode);
  563. $build = $entity->content;
  564. unset($entity->content);
  565. return $build;
  566. }
  567. /**
  568. * Renders a single entity property.
  569. */
  570. protected function renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, &$content) {
  571. $info = $property->info();
  572. $content[$name] = array(
  573. '#label_hidden' => FALSE,
  574. '#label' => $info['label'],
  575. '#entity_wrapped' => $wrapper,
  576. '#theme' => 'entity_property',
  577. '#property_name' => $name,
  578. '#access' => $property->access('view'),
  579. '#entity_type' => $this->entityType,
  580. );
  581. $content['#attached']['css']['entity.theme'] = drupal_get_path('module', 'entity') . '/theme/entity.theme.css';
  582. }
  583. /**
  584. * Implements EntityAPIControllerInterface.
  585. */
  586. public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL) {
  587. // For Field API and entity_prepare_view, the entities have to be keyed by
  588. // (numeric) id.
  589. $entities = entity_key_array_by_property($entities, $this->idKey);
  590. if (!empty($this->entityInfo['fieldable'])) {
  591. field_attach_prepare_view($this->entityType, $entities, $view_mode);
  592. }
  593. entity_prepare_view($this->entityType, $entities);
  594. $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
  595. $view = array();
  596. foreach ($entities as $entity) {
  597. $build = entity_build_content($this->entityType, $entity, $view_mode, $langcode);
  598. $build += array(
  599. // If the entity type provides an implementation, use this instead the
  600. // generic one.
  601. // @see template_preprocess_entity()
  602. '#theme' => 'entity',
  603. '#entity_type' => $this->entityType,
  604. '#entity' => $entity,
  605. '#view_mode' => $view_mode,
  606. '#language' => $langcode,
  607. '#page' => $page,
  608. );
  609. // Allow modules to modify the structured entity.
  610. drupal_alter(array($this->entityType . '_view', 'entity_view'), $build, $this->entityType);
  611. $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL;
  612. $view[$this->entityType][$key] = $build;
  613. }
  614. return $view;
  615. }
  616. }
  617. /**
  618. * A controller implementing exportables stored in the database.
  619. */
  620. class EntityAPIControllerExportable extends EntityAPIController {
  621. protected $entityCacheByName = array();
  622. protected $nameKey, $statusKey, $moduleKey;
  623. /**
  624. * Overridden.
  625. *
  626. * Allows specifying a name key serving as uniform identifier for this entity
  627. * type while still internally we are using numeric identifieres.
  628. */
  629. public function __construct($entityType) {
  630. parent::__construct($entityType);
  631. // Use the name key as primary identifier.
  632. $this->nameKey = isset($this->entityInfo['entity keys']['name']) ? $this->entityInfo['entity keys']['name'] : $this->idKey;
  633. if (!empty($this->entityInfo['exportable'])) {
  634. $this->statusKey = isset($this->entityInfo['entity keys']['status']) ? $this->entityInfo['entity keys']['status'] : 'status';
  635. $this->moduleKey = isset($this->entityInfo['entity keys']['module']) ? $this->entityInfo['entity keys']['module'] : 'module';
  636. }
  637. }
  638. /**
  639. * Support loading by name key.
  640. */
  641. protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
  642. // Add the id condition ourself, as we might have a separate name key.
  643. $query = parent::buildQuery(array(), $conditions, $revision_id);
  644. if ($ids) {
  645. // Support loading by numeric ids as well as by machine names.
  646. $key = is_numeric(reset($ids)) ? $this->idKey : $this->nameKey;
  647. $query->condition("base.$key", $ids, 'IN');
  648. }
  649. return $query;
  650. }
  651. /**
  652. * Overridden to support passing numeric ids as well as names as $ids.
  653. */
  654. public function load($ids = array(), $conditions = array()) {
  655. $entities = array();
  656. // Only do something if loaded by names.
  657. if (!$ids || $this->nameKey == $this->idKey || is_numeric(reset($ids))) {
  658. return parent::load($ids, $conditions);
  659. }
  660. // Revisions are not statically cached, and require a different query to
  661. // other conditions, so separate the revision id into its own variable.
  662. if ($this->revisionKey && isset($conditions[$this->revisionKey])) {
  663. $revision_id = $conditions[$this->revisionKey];
  664. unset($conditions[$this->revisionKey]);
  665. }
  666. else {
  667. $revision_id = FALSE;
  668. }
  669. $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
  670. // Care about the static cache.
  671. if ($this->cache && !$revision_id) {
  672. $entities = $this->cacheGetByName($ids, $conditions);
  673. }
  674. // If any entities were loaded, remove them from the ids still to load.
  675. if ($entities) {
  676. $ids = array_keys(array_diff_key($passed_ids, $entities));
  677. }
  678. $entities_by_id = parent::load($ids, $conditions);
  679. $entities += entity_key_array_by_property($entities_by_id, $this->nameKey);
  680. // Ensure that the returned array is keyed by numeric id and ordered the
  681. // same as the original $ids array and remove any invalid ids.
  682. $return = array();
  683. foreach ($passed_ids as $name => $value) {
  684. if (isset($entities[$name])) {
  685. $return[$entities[$name]->{$this->idKey}] = $entities[$name];
  686. }
  687. }
  688. return $return;
  689. }
  690. /**
  691. * Overridden.
  692. * @see DrupalDefaultEntityController::cacheGet()
  693. */
  694. protected function cacheGet($ids, $conditions = array()) {
  695. if (!empty($this->entityCache) && $ids !== array()) {
  696. $entities = $ids ? array_intersect_key($this->entityCache, array_flip($ids)) : $this->entityCache;
  697. return $this->applyConditions($entities, $conditions);
  698. }
  699. return array();
  700. }
  701. /**
  702. * Like cacheGet() but keyed by name.
  703. */
  704. protected function cacheGetByName($names, $conditions = array()) {
  705. if (!empty($this->entityCacheByName) && $names !== array() && $names) {
  706. // First get the entities by ids, then apply the conditions.
  707. // Generally, we make use of $this->entityCache, but if we are loading by
  708. // name, we have to use $this->entityCacheByName.
  709. $entities = array_intersect_key($this->entityCacheByName, array_flip($names));
  710. return $this->applyConditions($entities, $conditions);
  711. }
  712. return array();
  713. }
  714. protected function applyConditions($entities, $conditions = array()) {
  715. if ($conditions) {
  716. foreach ($entities as $key => $entity) {
  717. $entity_values = (array) $entity;
  718. // We cannot use array_diff_assoc() here because condition values can
  719. // also be arrays, e.g. '$conditions = array('status' => array(1, 2))'
  720. foreach ($conditions as $condition_key => $condition_value) {
  721. if (is_array($condition_value)) {
  722. if (!isset($entity_values[$condition_key]) || !in_array($entity_values[$condition_key], $condition_value)) {
  723. unset($entities[$key]);
  724. }
  725. }
  726. elseif (!isset($entity_values[$condition_key]) || $entity_values[$condition_key] != $condition_value) {
  727. unset($entities[$key]);
  728. }
  729. }
  730. }
  731. }
  732. return $entities;
  733. }
  734. /**
  735. * Overridden.
  736. * @see DrupalDefaultEntityController::cacheSet()
  737. */
  738. protected function cacheSet($entities) {
  739. $this->entityCache += $entities;
  740. // If we have a name key, also support static caching when loading by name.
  741. if ($this->nameKey != $this->idKey) {
  742. $this->entityCacheByName += entity_key_array_by_property($entities, $this->nameKey);
  743. }
  744. }
  745. /**
  746. * Overridden.
  747. * @see DrupalDefaultEntityController::attachLoad()
  748. *
  749. * Changed to call type-specific hook with the entities keyed by name if they
  750. * have one.
  751. */
  752. protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
  753. // Attach fields.
  754. if ($this->entityInfo['fieldable']) {
  755. if ($revision_id) {
  756. field_attach_load_revision($this->entityType, $queried_entities);
  757. }
  758. else {
  759. field_attach_load($this->entityType, $queried_entities);
  760. }
  761. }
  762. // Call hook_entity_load().
  763. foreach (module_implements('entity_load') as $module) {
  764. $function = $module . '_entity_load';
  765. $function($queried_entities, $this->entityType);
  766. }
  767. // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
  768. // always the queried entities, followed by additional arguments set in
  769. // $this->hookLoadArguments.
  770. // For entities with a name key, pass the entities keyed by name to the
  771. // specific load hook.
  772. if ($this->nameKey != $this->idKey) {
  773. $entities_by_name = entity_key_array_by_property($queried_entities, $this->nameKey);
  774. }
  775. else {
  776. $entities_by_name = $queried_entities;
  777. }
  778. $args = array_merge(array($entities_by_name), $this->hookLoadArguments);
  779. foreach (module_implements($this->entityInfo['load hook']) as $module) {
  780. call_user_func_array($module . '_' . $this->entityInfo['load hook'], $args);
  781. }
  782. }
  783. public function resetCache(array $ids = NULL) {
  784. $this->cacheComplete = FALSE;
  785. if (isset($ids)) {
  786. foreach (array_intersect_key($this->entityCache, array_flip($ids)) as $id => $entity) {
  787. unset($this->entityCacheByName[$this->entityCache[$id]->{$this->nameKey}]);
  788. unset($this->entityCache[$id]);
  789. }
  790. }
  791. else {
  792. $this->entityCache = array();
  793. $this->entityCacheByName = array();
  794. }
  795. }
  796. /**
  797. * Overridden to care about reverted entities.
  798. */
  799. public function delete($ids, DatabaseTransaction $transaction = NULL) {
  800. $entities = $ids ? $this->load($ids) : FALSE;
  801. if ($entities) {
  802. parent::delete($ids, $transaction);
  803. foreach ($entities as $id => $entity) {
  804. if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE)) {
  805. entity_defaults_rebuild(array($this->entityType));
  806. break;
  807. }
  808. }
  809. }
  810. }
  811. /**
  812. * Overridden to care about reverted bundle entities and to skip Rules.
  813. */
  814. public function invoke($hook, $entity) {
  815. if ($hook == 'delete') {
  816. // To ease figuring out whether this is a revert, make sure that the
  817. // entity status is updated in case the providing module has been
  818. // disabled.
  819. if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE) && !module_exists($entity->{$this->moduleKey})) {
  820. $entity->{$this->statusKey} = ENTITY_CUSTOM;
  821. }
  822. $is_revert = entity_has_status($this->entityType, $entity, ENTITY_IN_CODE);
  823. }
  824. if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) {
  825. $function($this->entityType, $entity);
  826. }
  827. if (isset($this->entityInfo['bundle of']) && $type = $this->entityInfo['bundle of']) {
  828. // Call field API bundle attachers for the entity we are a bundle of.
  829. if ($hook == 'insert') {
  830. field_attach_create_bundle($type, $entity->{$this->bundleKey});
  831. }
  832. elseif ($hook == 'delete' && !$is_revert) {
  833. field_attach_delete_bundle($type, $entity->{$this->bundleKey});
  834. }
  835. elseif ($hook == 'update' && $id = $entity->{$this->nameKey}) {
  836. if ($entity->original->{$this->bundleKey} != $entity->{$this->bundleKey}) {
  837. field_attach_rename_bundle($type, $entity->original->{$this->bundleKey}, $entity->{$this->bundleKey});
  838. }
  839. }
  840. }
  841. // Invoke the hook.
  842. module_invoke_all($this->entityType . '_' . $hook, $entity);
  843. // Invoke the respective entity level hook.
  844. if ($hook == 'presave' || $hook == 'insert' || $hook == 'update' || $hook == 'delete') {
  845. module_invoke_all('entity_' . $hook, $entity, $this->entityType);
  846. }
  847. }
  848. /**
  849. * Overridden to care exportables that are overridden.
  850. */
  851. public function save($entity, DatabaseTransaction $transaction = NULL) {
  852. // Preload $entity->original by name key if necessary.
  853. if (!empty($entity->{$this->nameKey}) && empty($entity->{$this->idKey}) && !isset($entity->original)) {
  854. $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->nameKey});
  855. }
  856. // Update the status for entities getting overridden.
  857. if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE) && empty($entity->is_rebuild)) {
  858. $entity->{$this->statusKey} |= ENTITY_CUSTOM;
  859. }
  860. return parent::save($entity, $transaction);
  861. }
  862. /**
  863. * Overridden.
  864. */
  865. public function export($entity, $prefix = '') {
  866. $vars = get_object_vars($entity);
  867. unset($vars[$this->statusKey], $vars[$this->moduleKey], $vars['is_new']);
  868. if ($this->nameKey != $this->idKey) {
  869. unset($vars[$this->idKey]);
  870. }
  871. return entity_var_json_export($vars, $prefix);
  872. }
  873. /**
  874. * Implements EntityAPIControllerInterface.
  875. */
  876. public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL) {
  877. $view = parent::view($entities, $view_mode, $langcode, $page);
  878. if ($this->nameKey != $this->idKey) {
  879. // Re-key the view array to be keyed by name.
  880. $return = array();
  881. foreach ($view[$this->entityType] as $id => $content) {
  882. $key = isset($content['#entity']->{$this->nameKey}) ? $content['#entity']->{$this->nameKey} : NULL;
  883. $return[$this->entityType][$key] = $content;
  884. }
  885. $view = $return;
  886. }
  887. return $view;
  888. }
  889. }