diff options
Diffstat (limited to 'includes')
-rw-r--r-- | includes/common.inc | 89 | ||||
-rw-r--r-- | includes/entity.inc | 294 | ||||
-rw-r--r-- | includes/file.inc | 29 |
3 files changed, 384 insertions, 28 deletions
diff --git a/includes/common.inc b/includes/common.inc index cabcf7dfa..bb4777dcc 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -5076,6 +5076,95 @@ function drupal_check_incompatibility($v, $current_version) { } /** + * Get the entity info array of an entity type. + * + * @see hook_entity_info() + * @see hook_entity_info_alter() + * + * @param $entity_type + * The entity type, e.g. node, for which the info shall be returned, or NULL + * to return an array with info about all types. + */ +function entity_get_info($entity_type = NULL) { + // We statically cache the information returned by hook_entity_info(). + $entity_info = &drupal_static(__FUNCTION__, array()); + + if (empty($entity_info)) { + if ($cache = cache_get('entity_info')) { + $entity_info = $cache->data; + } + else { + $entity_info = module_invoke_all('entity_info'); + // Merge in default values. + foreach ($entity_info as $name => $data) { + $entity_info[$name] += array( + 'fieldable' => FALSE, + 'controller class' => 'DrupalDefaultEntityController', + 'static cache' => TRUE, + 'load hook' => $name . '_load', + ); + } + // Let other modules alter the entity info. + drupal_alter('entity_info', $entity_info); + cache_set('entity_info', $entity_info); + } + } + + return empty($entity_type) ? $entity_info : $entity_info[$entity_type]; +} + +/** + * Load entities from the database. + * + * This function should be used whenever you need to load more than one entity + * from the database. The entities are loaded into memory and will not require + * database access if loaded again during the same page request. + * + * The actual loading is done through a class that has to implement the + * DrupalEntityController interface. By default, DrupalDefaultEntityController + * is used. Entity types can specify that a different class should be used by + * setting the 'controller class' key in hook_entity_info(). These classes can + * either implement the DrupalEntityController interface, or, most commonly, + * extend the DrupalDefaultEntityController class. See node_entity_info() and + * the NodeController in node.module as an example. + * + * @see hook_entity_info() + * @see DrupalEntityController + * @see DrupalDefaultEntityController + * + * @param $entity_type + * The entity type to load, e.g. node or user. + * @param $ids + * An array of entity IDs, or FALSE to load all entities. + * @param $conditions + * An array of conditions in the form 'field' => $value. + * @param $reset + * Whether to reset the internal cache for the requested entity type. + * + * @return + * An array of entity objects indexed by their ids. + */ +function entity_load($entity_type, $ids = array(), $conditions = array(), $reset = FALSE) { + if ($reset) { + entity_get_controller($entity_type)->resetCache(); + } + return entity_get_controller($entity_type)->load($ids, $conditions); +} + +/** + * Get the entity controller class for an entity type. + */ +function entity_get_controller($entity_type) { + $controllers = &drupal_static(__FUNCTION__, array()); + if (!isset($controllers[$entity_type])) { + $type_info = entity_get_info($entity_type); + $class = $type_info['controller class']; + $controllers[$entity_type] = new $class($entity_type); + } + return $controllers[$entity_type]; +} + +/** * Performs one or more XML-RPC request(s). * * @param $url 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; + } +} diff --git a/includes/file.inc b/includes/file.inc index 8047033be..aa9f425e9 100644 --- a/includes/file.inc +++ b/includes/file.inc @@ -432,34 +432,7 @@ function file_create_htaccess($directory, $private = TRUE) { * @see file_load() */ function file_load_multiple($fids = array(), $conditions = array()) { - // If they don't provide any criteria return nothing rather than all files. - if (!$fids && !$conditions) { - return array(); - } - $query = db_select('file', 'f')->fields('f'); - - // If the $fids array is populated, add those to the query. - if ($fids) { - $query->condition('f.fid', $fids, 'IN'); - } - - // If the conditions array is populated, add those to the query. - if ($conditions) { - foreach ($conditions as $field => $value) { - $query->condition('f.' . $field, $value); - } - } - $files = $query->execute()->fetchAllAssoc('fid'); - - // Invoke hook_file_load() on the terms loaded from the database - // and add them to the static cache. - if (!empty($files)) { - foreach (module_implements('file_load') as $module) { - $function = $module . '_file_load'; - $function($files); - } - } - return $files; + return entity_load('file', $fids, $conditions); } /** |