class DrupalDefaultEntityController

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.

Hierarchy

Expanded class hierarchy of DrupalDefaultEntityController

2 string references to 'DrupalDefaultEntityController'
EnableDisableTestCase::testEntityInfoCacheWatchdog in modules/system/system.test
Tests entity info cache after enabling a module with a dependency on an entity providing module.
entity_get_info in includes/common.inc
Get the entity info array of an entity type.

File

includes/entity.inc, line 48

View source
class DrupalDefaultEntityController implements DrupalEntityControllerInterface {
    
    /**
     * Static cache of entities, keyed by entity ID.
     *
     * @var array
     */
    protected $entityCache;
    
    /**
     * Entity type for this controller instance.
     *
     * @var string
     */
    protected $entityType;
    
    /**
     * Array of information about the entity.
     *
     * @var array
     *
     * @see entity_get_info()
     */
    protected $entityInfo;
    
    /**
     * Additional arguments to pass to hook_TYPE_load().
     *
     * Set before calling DrupalDefaultEntityController::attachLoad().
     *
     * @var array
     */
    protected $hookLoadArguments;
    
    /**
     * Name of the entity's ID field in the entity database table.
     *
     * @var string
     */
    protected $idKey;
    
    /**
     * Name of entity's revision database table field, if it supports revisions.
     *
     * Has the value FALSE if this entity does not use revisions.
     *
     * @var string
     */
    protected $revisionKey;
    
    /**
     * The table that stores revisions, if the entity supports revisions.
     *
     * @var string
     */
    protected $revisionTable;
    
    /**
     * Whether this entity type should use the static cache.
     *
     * Set by entity info.
     *
     * @var boolean
     */
    protected $cache;
    
    /**
     * Constructor: sets basic variables.
     *
     * @param $entityType
     *   The entity type for which the instance is created.
     */
    public function __construct($entityType) {
        $this->entityType = $entityType;
        $this->entityInfo = entity_get_info($entityType);
        $this->entityCache = array();
        $this->hookLoadArguments = array();
        $this->idKey = $this->entityInfo['entity keys']['id'];
        // Check if the entity type supports revisions.
        if (!empty($this->entityInfo['entity keys']['revision'])) {
            $this->revisionKey = $this->entityInfo['entity 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']);
    }
    
    /**
     * Implements DrupalEntityControllerInterface::resetCache().
     */
    public function resetCache(array $ids = NULL) {
        if (isset($ids)) {
            foreach ($ids as $id) {
                unset($this->entityCache[$id]);
            }
        }
        else {
            $this->entityCache = array();
        }
    }
    
    /**
     * Implements DrupalEntityControllerInterface::load().
     */
    public function load($ids = array(), $conditions = array()) {
        $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($conditions[$this->revisionKey])) {
            $revision_id = $conditions[$this->revisionKey];
            unset($conditions[$this->revisionKey]);
        }
        else {
            $revision_id = 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($ids) ? array_flip($ids) : FALSE;
        // Try to load entities from the static cache, if the entity type supports
        // static caching.
        if ($this->cache && !$revision_id) {
            $entities += $this->cacheGet($ids, $conditions);
            // If any entities were loaded, remove them from the ids still to load.
            if ($passed_ids) {
                $ids = array_keys(array_diff_key($passed_ids, $entities));
            }
        }
        // Ensure integer entity IDs are valid.
        if (!empty($ids)) {
            $this->cleanIds($ids);
        }
        // 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 ($ids === FALSE || $ids || $revision_id || $conditions && !$passed_ids) {
            // Build the query.
            $query = $this->buildQuery($ids, $conditions, $revision_id);
            $queried_entities = $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, $revision_id);
            $entities += $queried_entities;
        }
        if ($this->cache) {
            // Add entities to the cache if we are not loading a revision.
            if (!empty($queried_entities) && !$revision_id) {
                $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;
    }
    
    /**
     * Ensures integer entity IDs are valid.
     *
     * The identifier sanitization provided by this method has been introduced
     * as Drupal used to rely on the database to facilitate this, which worked
     * correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
     *
     * @param array $ids
     *   The entity IDs to verify. Non-integer IDs are removed from this array if
     *   the entity type requires IDs to be integers.
     */
    protected function cleanIds(&$ids) {
        $entity_info = entity_get_info($this->entityType);
        if (isset($entity_info['base table field types'])) {
            $id_type = $entity_info['base table field types'][$this->idKey];
            if ($id_type == 'serial' || $id_type == 'int') {
                $ids = array_filter($ids, array(
                    $this,
                    'filterId',
                ));
                $ids = array_map('intval', $ids);
            }
        }
    }
    
    /**
     * Callback for array_filter that removes non-integer IDs.
     */
    protected function filterId($id) {
        // ctype_digit() is used here instead of a strict comparison as sometimes
        // the id is passed as a string containing '0' which may represent a bug
        // elsewhere but would fail with a strict comparison.
        return is_numeric($id) && $id == (int) $id && ctype_digit((string) $id);
    }
    
    /**
     * Builds 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 CommentController::buildQuery() or TaxonomyTermController::buildQuery()
     * for examples.
     *
     * @param $ids
     *   An array of entity IDs, or FALSE to load all entities.
     * @param $conditions
     *   An array of conditions. Keys are field names on the entity's base table.
     *   Values will be compared for equality. All the comparisons will be ANDed
     *   together. This parameter is deprecated; use an EntityFieldQuery instead.
     * @param $revision_id
     *   The ID of the revision to load, or FALSE if this query is asking for the
     *   most current revision(s).
     *
     * @return SelectQuery
     *   A SelectQuery object for loading the entity.
     */
    protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
        $query = db_select($this->entityInfo['base table'], 'base');
        $query->addTag($this->entityType . '_load_multiple');
        if ($revision_id) {
            $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(
                ':revisionId' => $revision_id,
            ));
        }
        elseif ($this->revisionKey) {
            $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}");
        }
        // Add fields from the {entity} table.
        $entity_fields = $this->entityInfo['schema_fields_sql']['base table'];
        if ($this->revisionKey) {
            // Add all fields from the {entity_revision} table.
            $entity_revision_fields = drupal_map_assoc($this->entityInfo['schema_fields_sql']['revision table']);
            // The id field is provided by entity, so remove it.
            unset($entity_revision_fields[$this->idKey]);
            // 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]]);
                }
            }
            $query->fields('revision', $entity_revision_fields);
        }
        $query->fields('base', $entity_fields);
        if ($ids) {
            $query->condition("base.{$this->idKey}", $ids, 'IN');
        }
        if ($conditions) {
            foreach ($conditions as $field => $value) {
                $query->condition('base.' . $field, $value);
            }
        }
        return $query;
    }
    
    /**
     * Attaches data to entities upon loading.
     *
     * This will attach fields, if the entity is fieldable. It calls
     * hook_entity_load() for modules which need to add data to all entities.
     * 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.
     *
     * @param $queried_entities
     *   Associative array of query results, keyed on the entity ID.
     * @param $revision_id
     *   ID of the revision that was loaded, or FALSE if the most current revision
     *   was loaded.
     */
    protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
        // Attach fields.
        if ($this->entityInfo['fieldable']) {
            if ($revision_id) {
                field_attach_load_revision($this->entityType, $queried_entities);
            }
            else {
                field_attach_load($this->entityType, $queried_entities);
            }
        }
        // Call hook_entity_load().
        foreach (module_implements('entity_load') as $module) {
            $function = $module . '_entity_load';
            $function($queried_entities, $this->entityType);
        }
        // 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);
        }
    }
    
    /**
     * Gets 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.
     *
     * @return
     *   Array of entities from the entity cache.
     */
    protected function cacheGet($ids, $conditions = array()) {
        $entities = array();
        // Load any available entities from the internal cache.
        if (!empty($this->entityCache)) {
            if ($ids) {
                $entities += array_intersect_key($this->entityCache, array_flip($ids));
            }
            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) {
                // Iterate over all conditions and compare them to the entity
                // properties. We cannot use array_diff_assoc() here since the
                // conditions can be nested arrays, too.
                foreach ($conditions as $property_name => $condition) {
                    if (is_array($condition)) {
                        // Multiple condition values for one property are treated as OR
                        // operation: only if the value is not at all in the condition array
                        // we remove the entity.
                        if (!in_array($entity->{$property_name}, $condition)) {
                            unset($entities[$entity->{$this->idKey}]);
                            continue 2;
                        }
                    }
                    elseif ($condition != $entity->{$property_name}) {
                        unset($entities[$entity->{$this->idKey}]);
                        continue 2;
                    }
                }
            }
        }
        return $entities;
    }
    
    /**
     * Stores entities in the static entity cache.
     *
     * @param $entities
     *   Entities to store in the cache.
     */
    protected function cacheSet($entities) {
        $this->entityCache += $entities;
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title Overrides
DrupalDefaultEntityController::$cache protected property Whether this entity type should use the static cache.
DrupalDefaultEntityController::$entityCache protected property Static cache of entities, keyed by entity ID.
DrupalDefaultEntityController::$entityInfo protected property Array of information about the entity.
DrupalDefaultEntityController::$entityType protected property Entity type for this controller instance.
DrupalDefaultEntityController::$hookLoadArguments protected property Additional arguments to pass to hook_TYPE_load().
DrupalDefaultEntityController::$idKey protected property Name of the entity's ID field in the entity database table.
DrupalDefaultEntityController::$revisionKey protected property Name of entity's revision database table field, if it supports revisions.
DrupalDefaultEntityController::$revisionTable protected property The table that stores revisions, if the entity supports revisions.
DrupalDefaultEntityController::attachLoad protected function Attaches data to entities upon loading. 4
DrupalDefaultEntityController::buildQuery protected function Builds the query to load the entity. 4
DrupalDefaultEntityController::cacheGet protected function Gets entities from the static cache. 1
DrupalDefaultEntityController::cacheSet protected function Stores entities in the static entity cache.
DrupalDefaultEntityController::cleanIds protected function Ensures integer entity IDs are valid.
DrupalDefaultEntityController::filterId protected function Callback for array_filter that removes non-integer IDs.
DrupalDefaultEntityController::load public function Implements DrupalEntityControllerInterface::load(). Overrides DrupalEntityControllerInterface::load
DrupalDefaultEntityController::resetCache public function Implements DrupalEntityControllerInterface::resetCache(). Overrides DrupalEntityControllerInterface::resetCache
DrupalDefaultEntityController::__construct public function Constructor: sets basic variables.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.