summaryrefslogtreecommitdiff
path: root/includes/entity.inc
diff options
context:
space:
mode:
Diffstat (limited to 'includes/entity.inc')
-rw-r--r--includes/entity.inc294
1 files changed, 294 insertions, 0 deletions
diff --git a/includes/entity.inc b/includes/entity.inc
new file mode 100644
index 000000000..e50cf6fb3
--- /dev/null
+++ b/includes/entity.inc
@@ -0,0 +1,294 @@
+<?php
+// $Id$
+
+/**
+ * Interface for entity controller classes.
+ *
+ * All entity controller classes specified via the 'controller class' key
+ * returned by hook_entity_info() or hook_entity_info_alter() have to implement
+ * this interface.
+ *
+ * Most simple, SQL-based entity controllers will do better by extending
+ * DrupalDefaultEntityController instead of implementing this interface
+ * directly.
+ */
+interface DrupalEntityControllerInterface {
+ /**
+ * Constructor.
+ *
+ * @param $entityType
+ * The entity type for which the instance is created.
+ */
+ public function __construct($entityType);
+
+ /**
+ * Reset the internal, static entity cache.
+ */
+ public function resetCache();
+
+ /**
+ * Load one or more entities.
+ *
+ * @param $ids
+ * An array of entity IDs, or FALSE to load all entities.
+ * @param $conditions
+ * An array of conditions in the form 'field' => $value.
+ *
+ * @return
+ * An array of entity objects indexed by their ids.
+ */
+ public function load($ids = array(), $conditions = array());
+}
+
+/**
+ * Default implementation of DrupalEntityControllerInterface.
+ *
+ * This class can be used as-is by most simple entity types. Entity types
+ * requiring special handling can extend the class.
+ */
+class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
+
+ protected $entityCache;
+ protected $entityType;
+ protected $entityInfo;
+ protected $hookLoadArguments;
+ protected $idKey;
+ protected $revisionKey;
+ protected $revisionTable;
+ protected $query;
+
+ /**
+ * Constructor. Set basic variables.
+ */
+ public function __construct($entityType) {
+ $this->entityType = $entityType;
+ $this->entityInfo = entity_get_info($entityType);
+ $this->entityCache = array();
+ $this->hookLoadArguments = array();
+ $this->idKey = $this->entityInfo['object keys']['id'];
+
+ // Check if the entity type supports revisions.
+ if (isset($this->entityInfo['object keys']['revision'])) {
+ $this->revisionKey = $this->entityInfo['object keys']['revision'];
+ $this->revisionTable = $this->entityInfo['revision table'];
+ }
+ else {
+ $this->revisionKey = FALSE;
+ }
+
+ // Check if the entity type supports static caching of loaded entities.
+ $this->cache = !empty($this->entityInfo['static cache']);
+ }
+
+ public function resetCache() {
+ $this->entityCache = array();
+ }
+
+ public function load($ids = array(), $conditions = array()) {
+ $this->ids = $ids;
+ $this->conditions = $conditions;
+
+ $entities = array();
+
+ // Revisions are not statically cached, and require a different query to
+ // other conditions, so separate the revision id into its own variable.
+ if ($this->revisionKey && isset($this->conditions[$this->revisionKey])) {
+ $this->revisionId = $this->conditions[$this->revisionKey];
+ unset($this->conditions[$this->revisionKey]);
+ }
+ else {
+ $this->revisionId = FALSE;
+ }
+
+
+ // Create a new variable which is either a prepared version of the $ids
+ // array for later comparison with the entity cache, or FALSE if no $ids
+ // were passed. The $ids array is reduced as items are loaded from cache,
+ // and we need to know if it's empty for this reason to avoid querying the
+ // database when all requested entities are loaded from cache.
+ $passed_ids = !empty($this->ids) ? array_flip($this->ids) : FALSE;
+ // Try to load entities from the static cache, if the entity type supports
+ // static caching.
+ if ($this->cache) {
+ $entities += $this->cacheGet($this->ids, $this->conditions);
+ // If any entities were loaded, remove them from the ids still to load.
+ if ($passed_ids) {
+ $this->ids = array_keys(array_diff_key($passed_ids, $entities));
+ }
+ }
+
+ // Load any remaining entities from the database. This is the case if $ids
+ // is set to FALSE (so we load all entities), if there are any ids left to
+ // load, if loading a revision, or if $conditions was passed without $ids.
+ if ($this->ids === FALSE || $this->ids || $this->revisionId || ($this->conditions && !$passed_ids)) {
+ // Build the query.
+ $this->buildQuery();
+ $queried_entities = $this->query
+ ->execute()
+ ->fetchAllAssoc($this->idKey);
+ }
+
+ // Pass all entities loaded from the database through $this->attachLoad(),
+ // which attaches fields (if supported by the entity type) and calls the
+ // entity type specific load callback, for example hook_node_load().
+ if (!empty($queried_entities)) {
+ $this->attachLoad($queried_entities);
+ $entities += $queried_entities;
+ }
+
+ if ($this->cache) {
+ // Add entities to the cache if we are not loading a revision.
+ if (!empty($queried_entities) && !$this->revisionId) {
+ $this->cacheSet($queried_entities);
+ }
+ // Ensure that the returned array is ordered the same as the original
+ // $ids array if this was passed in and remove any invalid ids.
+ if ($passed_ids) {
+ // Remove any invalid ids from the array.
+ $passed_ids = array_intersect_key($passed_ids, $entities);
+ foreach ($entities as $entity) {
+ $passed_ids[$entity->{$this->idKey}] = $entity;
+ }
+ $entities = $passed_ids;
+ }
+ }
+
+ return $entities;
+ }
+
+ /**
+ * Build the query to load the entity.
+ *
+ * This has full revision support. For entities requiring special queries,
+ * the class can be extended, and the default query can be constructed by
+ * calling parent::buildQuery(). This is usually necessary when the object
+ * being loaded needs to be augmented with additional data from another
+ * table, such as loading node type into comments or vocabulary machine name
+ * into terms, however it can also support $conditions on different tables.
+ * See NodeController::buildQuery() or TaxonomyTermController::buildQuery()
+ * for examples.
+ */
+ protected function buildQuery() {
+ $this->query = db_select($this->entityInfo['base table'], 'base');
+
+ $this->query->addTag($this->entityType . '_load_multiple');
+
+ if ($this->revisionId) {
+ $this->query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(':revisionId' => $this->revisionId));
+ }
+ elseif ($this->revisionKey) {
+ $this->query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
+ }
+
+ // Add fields from the {entity} table.
+ $entity_fields = drupal_schema_fields_sql($this->entityInfo['base table']);
+
+ if ($this->revisionKey) {
+ // Add all fields from the {entity_revision} table.
+ $entity_revision_fields = drupal_map_assoc(drupal_schema_fields_sql($this->revisionTable));
+ // The id field is provided by entity, so remove it.
+ unset($entity_revision_fields[$this->idKey]);
+
+ // Change timestamp to revision_timestamp, and revision uid to
+ // revision_uid before adding them to the query.
+ // TODO: This is node specific and has to be moved into NodeController.
+ unset($entity_revision_fields['timestamp']);
+ $this->query->addField('revision', 'timestamp', 'revision_timestamp');
+ unset($entity_revision_fields['uid']);
+ $this->query->addField('revision', 'uid', 'revision_uid');
+
+ // Remove all fields from the base table that are also fields by the same
+ // name in the revision table.
+ $entity_field_keys = array_flip($entity_fields);
+ foreach ($entity_revision_fields as $key => $name) {
+ if (isset($entity_field_keys[$name])) {
+ unset($entity_fields[$entity_field_keys[$name]]);
+ }
+ }
+ $this->query->fields('revision', $entity_revision_fields);
+ }
+
+ $this->query->fields('base', $entity_fields);
+
+ if ($this->ids) {
+ $this->query->condition("base.{$this->idKey}", $this->ids, 'IN');
+ }
+ if ($this->conditions) {
+ foreach ($this->conditions as $field => $value) {
+ $this->query->condition('base.' . $field, $value);
+ }
+ }
+ }
+
+ /**
+ * Attach data to entities upon loading.
+ *
+ * This will attach fields, if the entity is fieldable. It also calls
+ * hook_TYPE_load() on the loaded entities. For example
+ * hook_node_load() or hook_user_load(). If your hook_TYPE_load()
+ * expects special parameters apart from the queried entities, you can set
+ * $this->hookLoadArguments prior to calling the method.
+ * See NodeController::attachLoad() for an example.
+ */
+ protected function attachLoad(&$queried_entities) {
+ // Attach fields.
+ if ($this->entityInfo['fieldable']) {
+ if ($this->revisionId) {
+ field_attach_load_revision($this->entityType, $queried_entities);
+ }
+ else {
+ field_attach_load($this->entityType, $queried_entities);
+ }
+ }
+
+ // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are
+ // always the queried entities, followed by additional arguments set in
+ // $this->hookLoadArguments.
+ $args = array_merge(array($queried_entities), $this->hookLoadArguments);
+ foreach (module_implements($this->entityInfo['load hook']) as $module) {
+ call_user_func_array($module . '_' . $this->entityInfo['load hook'], $args);
+ }
+ }
+
+ /**
+ * Get entities from the static cache.
+ *
+ * @param $ids
+ * If not empty, return entities that match these IDs.
+ * @param $conditions
+ * If set, return entities that match all of these conditions.
+ */
+ protected function cacheGet($ids, $conditions = array()) {
+ $entities = array();
+ // Load any available entities from the internal cache.
+ if (!empty($this->entityCache) && !$this->revisionId) {
+ if ($ids) {
+ $entities += array_intersect_key($this->entityCache, array_flip($ids));
+ }
+ // If loading entities only by conditions, fetch all available entities
+ // from the cache. Entities which don't match are removed later.
+ elseif ($conditions) {
+ $entities = $this->entityCache;
+ }
+ }
+
+ // Exclude any entities loaded from cache if they don't match $conditions.
+ // This ensures the same behavior whether loading from memory or database.
+ if ($conditions) {
+ foreach ($entities as $entity) {
+ $entity_values = (array) $entity;
+ if (array_diff_assoc($conditions, $entity_values)) {
+ unset($entities[$entity->{$this->idKey}]);
+ }
+ }
+ }
+ return $entities;
+ }
+
+ /**
+ * Store entities in the static entity cache.
+ */
+ protected function cacheSet($entities) {
+ $this->entityCache += $entities;
+ }
+}