entity.controller.inc 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976
  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 embeded
  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('base.' . $this->revisionKey . ' = revision.' . $this->revisionKey, $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. // This transaction causes troubles on MySQL, see
  341. // http://drupal.org/node/1007830. So we deactivate this by default until
  342. // is shipped in a point release.
  343. // $transaction = isset($transaction) ? $transaction : db_transaction();
  344. try {
  345. $ids = array_keys($entities);
  346. db_delete($this->entityInfo['base table'])
  347. ->condition($this->idKey, $ids, 'IN')
  348. ->execute();
  349. if (isset($this->revisionTable)) {
  350. db_delete($this->revisionTable)
  351. ->condition($this->idKey, $ids, 'IN')
  352. ->execute();
  353. }
  354. // Reset the cache as soon as the changes have been applied.
  355. $this->resetCache($ids);
  356. foreach ($entities as $id => $entity) {
  357. $this->invoke('delete', $entity);
  358. }
  359. // Ignore slave server temporarily.
  360. db_ignore_slave();
  361. }
  362. catch (Exception $e) {
  363. if (isset($transaction)) {
  364. $transaction->rollback();
  365. }
  366. watchdog_exception($this->entityType, $e);
  367. throw $e;
  368. }
  369. }
  370. /**
  371. * Implements EntityAPIControllerRevisionableInterface::deleteRevision().
  372. */
  373. public function deleteRevision($revision_id) {
  374. if ($entity_revision = entity_revision_load($this->entityType, $revision_id)) {
  375. // Prevent deleting the default revision.
  376. if (entity_revision_is_default($this->entityType, $entity_revision)) {
  377. return FALSE;
  378. }
  379. db_delete($this->revisionTable)
  380. ->condition($this->revisionKey, $revision_id)
  381. ->execute();
  382. $this->invoke('revision_delete', $entity_revision);
  383. return TRUE;
  384. }
  385. return FALSE;
  386. }
  387. /**
  388. * Implements EntityAPIControllerInterface.
  389. *
  390. * @param $transaction
  391. * Optionally a DatabaseTransaction object to use. Allows overrides to pass
  392. * in their transaction object.
  393. */
  394. public function save($entity, DatabaseTransaction $transaction = NULL) {
  395. $transaction = isset($transaction) ? $transaction : db_transaction();
  396. try {
  397. // Load the stored entity, if any.
  398. if (!empty($entity->{$this->idKey}) && !isset($entity->original)) {
  399. // In order to properly work in case of name changes, load the original
  400. // entity using the id key if it is available.
  401. $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->idKey});
  402. }
  403. $entity->is_new = !empty($entity->is_new) || empty($entity->{$this->idKey});
  404. $this->invoke('presave', $entity);
  405. if ($entity->is_new) {
  406. $return = drupal_write_record($this->entityInfo['base table'], $entity);
  407. if ($this->revisionKey) {
  408. $this->saveRevision($entity);
  409. }
  410. $this->invoke('insert', $entity);
  411. }
  412. else {
  413. // Update the base table if the entity doesn't have revisions or
  414. // we are updating the default revision.
  415. if (!$this->revisionKey || !empty($entity->{$this->defaultRevisionKey})) {
  416. $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey);
  417. }
  418. if ($this->revisionKey) {
  419. $return = $this->saveRevision($entity);
  420. }
  421. $this->resetCache(array($entity->{$this->idKey}));
  422. $this->invoke('update', $entity);
  423. // Field API always saves as default revision, so if the revision saved
  424. // is not default we have to restore the field values of the default
  425. // revision now by invoking field_attach_update() once again.
  426. if ($this->revisionKey && !$entity->{$this->defaultRevisionKey} && !empty($this->entityInfo['fieldable'])) {
  427. field_attach_update($this->entityType, $entity->original);
  428. }
  429. }
  430. // Ignore slave server temporarily.
  431. db_ignore_slave();
  432. unset($entity->is_new);
  433. unset($entity->is_new_revision);
  434. unset($entity->original);
  435. return $return;
  436. }
  437. catch (Exception $e) {
  438. $transaction->rollback();
  439. watchdog_exception($this->entityType, $e);
  440. throw $e;
  441. }
  442. }
  443. /**
  444. * Saves an entity revision.
  445. *
  446. * @param Entity $entity
  447. * Entity revision to save.
  448. */
  449. protected function saveRevision($entity) {
  450. // Convert the entity into an array as it might not have the same properties
  451. // as the entity, it is just a raw structure.
  452. $record = (array) $entity;
  453. // File fields assumes we are using $entity->revision instead of
  454. // $entity->is_new_revision, so we also support it and make sure it's set to
  455. // the same value.
  456. $entity->is_new_revision = !empty($entity->is_new_revision) || !empty($entity->revision) || $entity->is_new;
  457. $entity->revision = &$entity->is_new_revision;
  458. $entity->{$this->defaultRevisionKey} = !empty($entity->{$this->defaultRevisionKey}) || $entity->is_new;
  459. // When saving a new revision, set any existing revision ID to NULL so as to
  460. // ensure that a new revision will actually be created.
  461. if ($entity->is_new_revision && isset($record[$this->revisionKey])) {
  462. $record[$this->revisionKey] = NULL;
  463. }
  464. if ($entity->is_new_revision) {
  465. drupal_write_record($this->revisionTable, $record);
  466. $update_default_revision = $entity->{$this->defaultRevisionKey};
  467. }
  468. else {
  469. drupal_write_record($this->revisionTable, $record, $this->revisionKey);
  470. // @todo: Fix original entity to be of the same revision and check whether
  471. // the default revision key has been set.
  472. $update_default_revision = $entity->{$this->defaultRevisionKey} && $entity->{$this->revisionKey} != $entity->original->{$this->revisionKey};
  473. }
  474. // Make sure to update the new revision key for the entity.
  475. $entity->{$this->revisionKey} = $record[$this->revisionKey];
  476. // Mark this revision as the default one.
  477. if ($update_default_revision) {
  478. db_update($this->entityInfo['base table'])
  479. ->fields(array($this->revisionKey => $record[$this->revisionKey]))
  480. ->condition($this->idKey, $entity->{$this->idKey})
  481. ->execute();
  482. }
  483. return $entity->is_new_revision ? SAVED_NEW : SAVED_UPDATED;
  484. }
  485. /**
  486. * Implements EntityAPIControllerInterface.
  487. */
  488. public function create(array $values = array()) {
  489. // Add is_new property if it is not set.
  490. $values += array('is_new' => TRUE);
  491. if (isset($this->entityInfo['entity class']) && $class = $this->entityInfo['entity class']) {
  492. return new $class($values, $this->entityType);
  493. }
  494. return (object) $values;
  495. }
  496. /**
  497. * Implements EntityAPIControllerInterface.
  498. *
  499. * @return
  500. * A serialized string in JSON format suitable for the import() method.
  501. */
  502. public function export($entity, $prefix = '') {
  503. $vars = get_object_vars($entity);
  504. unset($vars['is_new']);
  505. return entity_var_json_export($vars, $prefix);
  506. }
  507. /**
  508. * Implements EntityAPIControllerInterface.
  509. *
  510. * @param $export
  511. * A serialized string in JSON format as produced by the export() method.
  512. */
  513. public function import($export) {
  514. $vars = drupal_json_decode($export);
  515. if (is_array($vars)) {
  516. return $this->create($vars);
  517. }
  518. return FALSE;
  519. }
  520. /**
  521. * Implements EntityAPIControllerInterface.
  522. *
  523. * @param $content
  524. * Optionally. Allows pre-populating the built content to ease overridding
  525. * this method.
  526. */
  527. public function buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array()) {
  528. // Remove previously built content, if exists.
  529. $entity->content = $content;
  530. $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
  531. // By default add in properties for all defined extra fields.
  532. if ($extra_field_controller = entity_get_extra_fields_controller($this->entityType)) {
  533. $wrapper = entity_metadata_wrapper($this->entityType, $entity);
  534. $extra = $extra_field_controller->fieldExtraFields();
  535. $type_extra = &$extra[$this->entityType][$this->entityType]['display'];
  536. $bundle_extra = &$extra[$this->entityType][$wrapper->getBundle()]['display'];
  537. foreach ($wrapper as $name => $property) {
  538. if (isset($type_extra[$name]) || isset($bundle_extra[$name])) {
  539. $this->renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, $entity->content);
  540. }
  541. }
  542. }
  543. // Add in fields.
  544. if (!empty($this->entityInfo['fieldable'])) {
  545. // Perform the preparation tasks if they have not been performed yet.
  546. // An internal flag prevents the operation from running twice.
  547. $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL;
  548. field_attach_prepare_view($this->entityType, array($key => $entity), $view_mode);
  549. $entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode);
  550. }
  551. // Invoke hook_ENTITY_view() to allow modules to add their additions.
  552. if (module_exists('rules')) {
  553. rules_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode);
  554. }
  555. else {
  556. module_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode);
  557. }
  558. module_invoke_all('entity_view', $entity, $this->entityType, $view_mode, $langcode);
  559. $build = $entity->content;
  560. unset($entity->content);
  561. return $build;
  562. }
  563. /**
  564. * Renders a single entity property.
  565. */
  566. protected function renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, &$content) {
  567. $info = $property->info();
  568. $content[$name] = array(
  569. '#label_hidden' => FALSE,
  570. '#label' => $info['label'],
  571. '#entity_wrapped' => $wrapper,
  572. '#theme' => 'entity_property',
  573. '#property_name' => $name,
  574. '#access' => $property->access('view'),
  575. '#entity_type' => $this->entityType,
  576. );
  577. $content['#attached']['css']['entity.theme'] = drupal_get_path('module', 'entity') . '/theme/entity.theme.css';
  578. }
  579. /**
  580. * Implements EntityAPIControllerInterface.
  581. */
  582. public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL) {
  583. // For Field API and entity_prepare_view, the entities have to be keyed by
  584. // (numeric) id.
  585. $entities = entity_key_array_by_property($entities, $this->idKey);
  586. if (!empty($this->entityInfo['fieldable'])) {
  587. field_attach_prepare_view($this->entityType, $entities, $view_mode);
  588. }
  589. entity_prepare_view($this->entityType, $entities);
  590. $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
  591. $view = array();
  592. foreach ($entities as $entity) {
  593. $build = entity_build_content($this->entityType, $entity, $view_mode, $langcode);
  594. $build += array(
  595. // If the entity type provides an implementation, use this instead the
  596. // generic one.
  597. // @see template_preprocess_entity()
  598. '#theme' => 'entity',
  599. '#entity_type' => $this->entityType,
  600. '#entity' => $entity,
  601. '#view_mode' => $view_mode,
  602. '#language' => $langcode,
  603. '#page' => $page,
  604. );
  605. // Allow modules to modify the structured entity.
  606. drupal_alter(array($this->entityType . '_view', 'entity_view'), $build, $this->entityType);
  607. $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL;
  608. $view[$this->entityType][$key] = $build;
  609. }
  610. return $view;
  611. }
  612. }
  613. /**
  614. * A controller implementing exportables stored in the database.
  615. */
  616. class EntityAPIControllerExportable extends EntityAPIController {
  617. protected $entityCacheByName = array();
  618. protected $nameKey, $statusKey, $moduleKey;
  619. /**
  620. * Overridden.
  621. *
  622. * Allows specifying a name key serving as uniform identifier for this entity
  623. * type while still internally we are using numeric identifieres.
  624. */
  625. public function __construct($entityType) {
  626. parent::__construct($entityType);
  627. // Use the name key as primary identifier.
  628. $this->nameKey = isset($this->entityInfo['entity keys']['name']) ? $this->entityInfo['entity keys']['name'] : $this->idKey;
  629. if (!empty($this->entityInfo['exportable'])) {
  630. $this->statusKey = isset($this->entityInfo['entity keys']['status']) ? $this->entityInfo['entity keys']['status'] : 'status';
  631. $this->moduleKey = isset($this->entityInfo['entity keys']['module']) ? $this->entityInfo['entity keys']['module'] : 'module';
  632. }
  633. }
  634. /**
  635. * Support loading by name key.
  636. */
  637. protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
  638. // Add the id condition ourself, as we might have a separate name key.
  639. $query = parent::buildQuery(array(), $conditions, $revision_id);
  640. if ($ids) {
  641. // Support loading by numeric ids as well as by machine names.
  642. $key = is_numeric(reset($ids)) ? $this->idKey : $this->nameKey;
  643. $query->condition("base.$key", $ids, 'IN');
  644. }
  645. return $query;
  646. }
  647. /**
  648. * Overridden to support passing numeric ids as well as names as $ids.
  649. */
  650. public function load($ids = array(), $conditions = array()) {
  651. $entities = array();
  652. // Only do something if loaded by names.
  653. if (!$ids || $this->nameKey == $this->idKey || is_numeric(reset($ids))) {
  654. return parent::load($ids, $conditions);
  655. }
  656. // Revisions are not statically cached, and require a different query to
  657. // other conditions, so separate the revision id into its own variable.
  658. if ($this->revisionKey && isset($conditions[$this->revisionKey])) {
  659. $revision_id = $conditions[$this->revisionKey];
  660. unset($conditions[$this->revisionKey]);
  661. }
  662. else {
  663. $revision_id = FALSE;
  664. }
  665. $passed_ids = !empty($ids) ? array_flip($ids) : FALSE;
  666. // Care about the static cache.
  667. if ($this->cache && !$revision_id) {
  668. $entities = $this->cacheGetByName($ids, $conditions);
  669. }
  670. // If any entities were loaded, remove them from the ids still to load.
  671. if ($entities) {
  672. $ids = array_keys(array_diff_key($passed_ids, $entities));
  673. }
  674. $entities_by_id = parent::load($ids, $conditions);
  675. $entities += entity_key_array_by_property($entities_by_id, $this->nameKey);
  676. // Ensure that the returned array is keyed by numeric id and ordered the
  677. // same as the original $ids array and remove any invalid ids.
  678. $return = array();
  679. foreach ($passed_ids as $name => $value) {
  680. if (isset($entities[$name])) {
  681. $return[$entities[$name]->{$this->idKey}] = $entities[$name];
  682. }
  683. }
  684. return $return;
  685. }
  686. /**
  687. * Overridden.
  688. * @see DrupalDefaultEntityController::cacheGet()
  689. */
  690. protected function cacheGet($ids, $conditions = array()) {
  691. if (!empty($this->entityCache) && $ids !== array()) {
  692. $entities = $ids ? array_intersect_key($this->entityCache, array_flip($ids)) : $this->entityCache;
  693. return $this->applyConditions($entities, $conditions);
  694. }
  695. return array();
  696. }
  697. /**
  698. * Like cacheGet() but keyed by name.
  699. */
  700. protected function cacheGetByName($names, $conditions = array()) {
  701. if (!empty($this->entityCacheByName) && $names !== array() && $names) {
  702. // First get the entities by ids, then apply the conditions.
  703. // Generally, we make use of $this->entityCache, but if we are loading by
  704. // name, we have to use $this->entityCacheByName.
  705. $entities = array_intersect_key($this->entityCacheByName, array_flip($names));
  706. return $this->applyConditions($entities, $conditions);
  707. }
  708. return array();
  709. }
  710. protected function applyConditions($entities, $conditions = array()) {
  711. if ($conditions) {
  712. foreach ($entities as $key => $entity) {
  713. $entity_values = (array) $entity;
  714. // We cannot use array_diff_assoc() here because condition values can
  715. // also be arrays, e.g. '$conditions = array('status' => array(1, 2))'
  716. foreach ($conditions as $condition_key => $condition_value) {
  717. if (is_array($condition_value)) {
  718. if (!isset($entity_values[$condition_key]) || !in_array($entity_values[$condition_key], $condition_value)) {
  719. unset($entities[$key]);
  720. }
  721. }
  722. elseif (!isset($entity_values[$condition_key]) || $entity_values[$condition_key] != $condition_value) {
  723. unset($entities[$key]);
  724. }
  725. }
  726. }
  727. }
  728. return $entities;
  729. }
  730. /**
  731. * Overridden.
  732. * @see DrupalDefaultEntityController::cacheSet()
  733. */
  734. protected function cacheSet($entities) {
  735. $this->entityCache += $entities;
  736. // If we have a name key, also support static caching when loading by name.
  737. if ($this->nameKey != $this->idKey) {
  738. $this->entityCacheByName += entity_key_array_by_property($entities, $this->nameKey);
  739. }
  740. }
  741. /**
  742. * Overridden.
  743. * @see DrupalDefaultEntityController::attachLoad()
  744. *
  745. * Changed to call type-specific hook with the entities keyed by name if they
  746. * have one.
  747. */
  748. protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
  749. // Attach fields.
  750. if ($this->entityInfo['fieldable']) {
  751. if ($revision_id) {
  752. field_attach_load_revision($this->entityType, $queried_entities);
  753. }
  754. else {
  755. field_attach_load($this->entityType, $queried_entities);
  756. }
  757. }
  758. // Call hook_entity_load().
  759. foreach (module_implements('entity_load') as $module) {
  760. $function = $module . '_entity_load';
  761. $function($queried_entities, $this->entityType);
  762. }
  763. // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
  764. // always the queried entities, followed by additional arguments set in
  765. // $this->hookLoadArguments.
  766. // For entities with a name key, pass the entities keyed by name to the
  767. // specific load hook.
  768. if ($this->nameKey != $this->idKey) {
  769. $entities_by_name = entity_key_array_by_property($queried_entities, $this->nameKey);
  770. }
  771. else {
  772. $entities_by_name = $queried_entities;
  773. }
  774. $args = array_merge(array($entities_by_name), $this->hookLoadArguments);
  775. foreach (module_implements($this->entityInfo['load hook']) as $module) {
  776. call_user_func_array($module . '_' . $this->entityInfo['load hook'], $args);
  777. }
  778. }
  779. public function resetCache(array $ids = NULL) {
  780. $this->cacheComplete = FALSE;
  781. if (isset($ids)) {
  782. foreach (array_intersect_key($this->entityCache, array_flip($ids)) as $id => $entity) {
  783. unset($this->entityCacheByName[$this->entityCache[$id]->{$this->nameKey}]);
  784. unset($this->entityCache[$id]);
  785. }
  786. }
  787. else {
  788. $this->entityCache = array();
  789. $this->entityCacheByName = array();
  790. }
  791. }
  792. /**
  793. * Overridden to care about reverted entities.
  794. */
  795. public function delete($ids, DatabaseTransaction $transaction = NULL) {
  796. $entities = $ids ? $this->load($ids) : FALSE;
  797. if ($entities) {
  798. parent::delete($ids, $transaction);
  799. foreach ($entities as $id => $entity) {
  800. if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE)) {
  801. entity_defaults_rebuild(array($this->entityType));
  802. break;
  803. }
  804. }
  805. }
  806. }
  807. /**
  808. * Overridden to care about reverted bundle entities and to skip Rules.
  809. */
  810. public function invoke($hook, $entity) {
  811. if ($hook == 'delete') {
  812. // To ease figuring out whether this is a revert, make sure that the
  813. // entity status is updated in case the providing module has been
  814. // disabled.
  815. if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE) && !module_exists($entity->{$this->moduleKey})) {
  816. $entity->{$this->statusKey} = ENTITY_CUSTOM;
  817. }
  818. $is_revert = entity_has_status($this->entityType, $entity, ENTITY_IN_CODE);
  819. }
  820. if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) {
  821. $function($this->entityType, $entity);
  822. }
  823. if (isset($this->entityInfo['bundle of']) && $type = $this->entityInfo['bundle of']) {
  824. // Call field API bundle attachers for the entity we are a bundle of.
  825. if ($hook == 'insert') {
  826. field_attach_create_bundle($type, $entity->{$this->bundleKey});
  827. }
  828. elseif ($hook == 'delete' && !$is_revert) {
  829. field_attach_delete_bundle($type, $entity->{$this->bundleKey});
  830. }
  831. elseif ($hook == 'update' && $id = $entity->{$this->nameKey}) {
  832. if ($entity->original->{$this->bundleKey} != $entity->{$this->bundleKey}) {
  833. field_attach_rename_bundle($type, $entity->original->{$this->bundleKey}, $entity->{$this->bundleKey});
  834. }
  835. }
  836. }
  837. // Invoke the hook.
  838. module_invoke_all($this->entityType . '_' . $hook, $entity);
  839. // Invoke the respective entity level hook.
  840. if ($hook == 'presave' || $hook == 'insert' || $hook == 'update' || $hook == 'delete') {
  841. module_invoke_all('entity_' . $hook, $entity, $this->entityType);
  842. }
  843. }
  844. /**
  845. * Overridden to care exportables that are overridden.
  846. */
  847. public function save($entity, DatabaseTransaction $transaction = NULL) {
  848. // Preload $entity->original by name key if necessary.
  849. if (!empty($entity->{$this->nameKey}) && empty($entity->{$this->idKey}) && !isset($entity->original)) {
  850. $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->nameKey});
  851. }
  852. // Update the status for entities getting overridden.
  853. if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE) && empty($entity->is_rebuild)) {
  854. $entity->{$this->statusKey} |= ENTITY_CUSTOM;
  855. }
  856. return parent::save($entity, $transaction);
  857. }
  858. /**
  859. * Overridden.
  860. */
  861. public function export($entity, $prefix = '') {
  862. $vars = get_object_vars($entity);
  863. unset($vars[$this->statusKey], $vars[$this->moduleKey], $vars['is_new']);
  864. if ($this->nameKey != $this->idKey) {
  865. unset($vars[$this->idKey]);
  866. }
  867. return entity_var_json_export($vars, $prefix);
  868. }
  869. /**
  870. * Implements EntityAPIControllerInterface.
  871. */
  872. public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL) {
  873. $view = parent::view($entities, $view_mode, $langcode, $page);
  874. if ($this->nameKey != $this->idKey) {
  875. // Re-key the view array to be keyed by name.
  876. $return = array();
  877. foreach ($view[$this->entityType] as $id => $content) {
  878. $key = isset($content['#entity']->{$this->nameKey}) ? $content['#entity']->{$this->nameKey} : NULL;
  879. $return[$this->entityType][$key] = $content;
  880. }
  881. $view = $return;
  882. }
  883. return $view;
  884. }
  885. }