diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index 329cf16..940d605 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -1647,11 +1647,14 @@ function hook_field_storage_details_alter(&$details, $field) { * loaded. */ function hook_field_storage_load($entity_type, $entities, $age, $fields, $options) { - $field_info = field_info_field_by_ids(); $load_current = $age == FIELD_LOAD_CURRENT; foreach ($fields as $field_id => $ids) { - $field = $field_info[$field_id]; + // By the time hook_field_storage_load() runs, the relevant fields have + // been populated in the static cache, so calling field_info_field_by_id() + // on each field individually is more efficient than loading all fields in + // memory upfront with field_info_field_by_ids(). + $field = field_info_field_by_id($field_id); $field_name = $field['field_name']; $table = $load_current ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field); diff --git a/core/modules/field/field.attach.inc b/core/modules/field/field.attach.inc index 9291725..3875eb2 100644 --- a/core/modules/field/field.attach.inc +++ b/core/modules/field/field.attach.inc @@ -281,7 +281,6 @@ function _field_invoke_multiple($op, $entity_type, $entities, &$a = NULL, &$b = 'language' => NULL, ); $options += $default_options; - $field_info = field_info_field_by_ids(); $fields = array(); $grouped_instances = array(); @@ -305,7 +304,7 @@ function _field_invoke_multiple($op, $entity_type, $entities, &$a = NULL, &$b = foreach ($instances as $instance) { $field_id = $instance['field_id']; $field_name = $instance['field_name']; - $field = $field_info[$field_id]; + $field = field_info_field_by_id($field_id); $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; if (function_exists($function)) { // Add the field to the list of fields to invoke the hook on. @@ -612,7 +611,6 @@ function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcod * non-deleted fields are operated on. */ function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $options = array()) { - $field_info = field_info_field_by_ids(); $load_current = $age == FIELD_LOAD_CURRENT; // Merge default options. @@ -690,7 +688,7 @@ function field_attach_load($entity_type, $entities, $age = FIELD_LOAD_CURRENT, $ } // Collect the storage backend if the field has not been loaded yet. if (!isset($skip_fields[$field_id])) { - $field = $field_info[$field_id]; + $field = field_info_field_by_id($field_id); $storages[$field['storage']['type']][$field_id][] = $load_current ? $id : $vid; } } diff --git a/core/modules/field/field.crud.inc b/core/modules/field/field.crud.inc index 6df3235..7eab359 100644 --- a/core/modules/field/field.crud.inc +++ b/core/modules/field/field.crud.inc @@ -319,7 +319,11 @@ function field_read_field($field_name, $include_additional = array()) { * Reads in fields that match an array of conditions. * * @param array $params - * An array of conditions to match against. + * An array of conditions to match against. Keys are columns from the + * 'field_config' table, values are conditions to match. Additionally, + * conditions on the 'entity_type' and 'bundle' columns from the + * 'field_config_instance' table are supported (select fields having an + * instance on a given bundle). * @param array $include_additional * The default behavior of this function is to not return fields that * are inactive or have been deleted. Setting @@ -337,8 +341,18 @@ function field_read_fields($params = array(), $include_additional = array()) { // Turn the conditions into a query. foreach ($params as $key => $value) { + // Allow filtering on the 'entity_type' and 'bundle' columns of the + // field_config_instance table. + if ($key == 'entity_type' || $key == 'bundle') { + if (empty($fci_join)) { + $fci_join = $query->join('field_config_instance', 'fci', 'fc.id = fci.field_id'); + } + $key = 'fci.' . $key; + } + $query->condition($key, $value); } + if (!isset($include_additional['include_inactive']) || !$include_additional['include_inactive']) { $query ->condition('fc.active', 1) diff --git a/core/modules/field/field.info.inc b/core/modules/field/field.info.inc index 5a99034..8510454 100644 --- a/core/modules/field/field.info.inc +++ b/core/modules/field/field.info.inc @@ -6,6 +6,730 @@ */ /** + * Provides field and instance definitions for the current runtime environment. + * + * This class holds static and persistent caches of the collected data. The + * methods retrieving all fields or all instances across all bundles do not read + * from the caches (and do not populate the caches either). As such, they should + * be used sparingly. + * + * The persistent cache uses one cache entry per bundle, storing both fields and + * instances. Fields used in multiple bundles are thus replicated in several + * cache entries (the static cache). Cache entries are loaded for bundles as a + * whole, optimizing for the most common pattern of iterating over the instances + * of a bundle (and then probably accessing each corresponding field) rather + * than accessing a single one. The specific caching strategy and + * interdependencies between fields and instances make the DrupalCacheArray + * class unfit here. + */ +class FieldInfo { + + /** + * Flag indicating a read or write operation on the static cache. + */ + const CACHE_STATIC = 'static'; + + /** + * Flag indicating a read or write operation on the persistent cache. + */ + const CACHE_PERSISTENT = 'persistent'; + + /** + * Lightweight map of fields across entity types and bundles. + * + * @var array + */ + protected $fieldMap; + + /** + * List of $field structures keyed by ID. Includes deleted fields. + * + * @var array + */ + protected $fieldsById; + + /** + * Mapping of field names to the ID of the corresponding non-deleted field. + * + * @var array + */ + protected $fieldIdsByName; + + /** + * Contents of bundles ($instance definitions and extra fields). + * + * @var array + */ + protected $bundleInfo; + + /** + * Constructs a FieldInfo object. + */ + function __construct() { + $this->init(); + } + + /** + * Clears the static and persistent caches. + */ + public function flush() { + $this->init(); + cache('field')->deletePrefix('info_fields:'); + } + + /** + * Initializes the static cache. + */ + protected function init() { + $this->fieldMap = array(); + $this->fieldsById = array(); + $this->fieldIdsByName = array(); + $this->bundleInfo = array(); + } + + /** + * Retrieves the field map from cache. + * + * @param $cache + * The type of cache. Either self::CACHE_STATIC or self::CACHE_PERSISTENT. + * + * @return + * The field map, as returned by getFieldMap(), if present in the specified + * cache, else NULL. + */ + protected function cacheGetFieldMap($cache) { + switch ($cache) { + case self::CACHE_STATIC: + if ($this->fieldMap) { + return $this->fieldMap; + } + break; + + case self::CACHE_PERSISTENT: + if ($cached = cache('field')->get('field_map')) { + return $cached->data; + } + break; + } + } + + /** + * Stores the field map in cache. + * + * @param $cache + * The type of cache. Either self::CACHE_STATIC or self::CACHE_PERSISTENT. + * @param $map + * The field map, as returned by getFieldMap(). + */ + protected function cacheSetFieldMap($cache, $map) { + switch ($cache) { + case self::CACHE_STATIC: + $this->fieldMap = $map; + break; + + case self::CACHE_PERSISTENT: + cache('field')->set('field_map', $map); + break; + } + } + + /** + * Retrieves a field definition from cache. + * + * @param $cache + * This parameter is only present for consistency and is not actually used. + * Only self::CACHE_STATIC is implemented, fields only enter the persistent + * cache as part of a full bundle. + * @param $field_name + * The field name. + * + * @return + * The field definition, if present in the static cache, else NULL. + */ + protected function cacheGetField($cache, $field_name) { + if (isset($this->fieldIdsByName[$field_name])) { + return $this->fieldsById[$this->fieldIdsByName[$field_name]]; + } + } + + /** + * Retrieves a field definition from the static cache + * + * @param $cache + * This parameter is only present for consistency and is not actually used. + * Only self::CACHE_STATIC is implemented, fields only enter the persistent + * cache as part of a full bundle. + * @param $field_id + * The field ID. + * + * @return + * The field definition, if present in the static cache, else NULL. + */ + protected function cacheGetFieldbyId($cache, $field_id) { + if (isset($this->fieldsById[$field_id])) { + return $this->fieldsById[$field_id]; + } + } + + /** + * Stores a field definition in the static cache. + * + * @param $cache + * This parameter is only present for consistency and is not actually used. + * Only self::CACHE_STATIC is implemented, fields only enter the persistent + * cache as part of a full bundle. + * @param $field + * The field definition. + */ + protected function cacheSetField($cache, $field) { + $this->fieldsById[$field['id']] = $field; + // Allow the retrieval of non-deleted fields by their name. + if (!$field['deleted']) { + $this->fieldIdsByName[$field['field_name']] = $field['id']; + } + } + + /** + * Retrieves the contents of a bundle from the static cache. + * + * @param $cache + * The type of cache. Either self::CACHE_STATIC or self::CACHE_PERSISTENT. + * @param $entity_type + * The entity type. + * @param $bundle + * The bundle name. + * + * @return + * The contents of the bundle, as returned by getBundleInfo(), if present in + * the static cache, else NULL. + */ + protected function cacheGetBundleInfo($cache, $entity_type, $bundle) { + switch ($cache) { + case self::CACHE_STATIC: + if (isset($this->bundleInfo[$entity_type][$bundle])) { + return $this->bundleInfo[$entity_type][$bundle]; + } + break; + + case self::CACHE_PERSISTENT: + if ($cached = cache('field')->get("bundle_info:$entity_type:$bundle")) { + return $cached->data; + } + break; + } + } + + /** + * Stores the contents of a bundle in the static cache. + * + * @param $cache + * The type of cache. Either self::CACHE_STATIC or self::CACHE_PERSISTENT. + * @param $entity_type + * The entity type. + * @param $bundle + * The bundle name. + * @param $info + * The contents of the bundle, as returned by getBundleInfo() + */ + protected function cacheSetBundleInfo($cache, $entity_type, $bundle, $info) { + switch ($cache) { + case self::CACHE_STATIC: + $this->bundleInfo[$entity_type][$bundle] = $info; + break; + + case self::CACHE_PERSISTENT: + cache('field')->set("bundle_info:$entity_type:$bundle", $info); + break; + } + } + + /** + * Collects a lightweight map of fields across bundles. + * + * @return + * An array keyed by field name. Each value is an array with entity + * types as keys and the array of bundle names as values. + */ + protected function getFieldMap() { + if ($map = $this->cacheGetFieldMap(self::CACHE_STATIC)) { + return $map; + } + + if ($map = $this->cacheGetFieldMap(self::CACHE_PERSISTENT)) { + $this->cacheSetFieldMap(self::CACHE_STATIC, $map); + return $map; + } + + $map = array(); + + $query = db_select('field_config_instance', 'fci'); + $query->join('field_config', 'fc', 'fc.id = fci.field_id'); + $query->fields('fci', array('field_name', 'entity_type', 'bundle')) + ->condition('fc.active', 1) + ->condition('fc.storage_active', 1) + ->condition('fc.deleted', 0) + ->condition('fci.deleted', 0); + foreach ($query->execute() as $row) { + $map[$row->field_name][$row->entity_type][] = $row->bundle; + } + + $this->cacheSetFieldMap(self::CACHE_STATIC, $map); + $this->cacheSetFieldMap(self::CACHE_PERSISTENT, $map); + + return $map; + } + + /** + * Returns all active, non-deleted fields. + * + * This method does not read from nor populate the static and persistent + * caches. + * + * @return + * An array of field definitions, keyed by field name. + */ + public function getFields() { + $fields = array(); + foreach (field_read_fields() as $field) { + if (!$field['deleted']) { + $fields[$field['field_name']] = $this->prepareField($field); + } + } + return $fields; + } + + /** + * Returns all active fields, including deleted ones. + * + * This method does not read from nor populate the static and persistent + * caches. + * + * @return + * An array of field definitions, keyed by field ID. + */ + public function getFieldsById() { + $fields = array(); + foreach (field_read_fields(array(), array('include_deleted' => TRUE)) as $field) { + $fields[$field['id']] = $this->prepareField($field); + } + return $fields; + } + + /** + * Returns a field definition from a field name. + * + * This method only retrieves active, non-deleted fields. + * + * @param $field_name + * The field name. + * + * @return + * The field definition, or NULL if no field was found. + */ + public function getField($field_name) { + if ($field = $this->cacheGetField(self::CACHE_STATIC, $field_name)) { + return $field; + } + + if ($field = field_read_field(array('field_name' => $field_name))) { + $field = $this->prepareField($field); + + // Save in the static cache (fields only enter the persistent cache as + // part of a full bundle). + $this->cachesetField(self::CACHE_STATIC, $field); + + return $field; + } + } + + /** + * Returns a field definition from a field ID. + * + * This method only retrieves active fields, deleted or not. + * + * @param $field_id + * The field ID. + * + * @return + * The field definition, or NULL if no field was found. + */ + public function getFieldById($field_id) { + if ($field = $this->cacheGetFieldById(self::CACHE_STATIC, $field_id)) { + return $field; + } + + if ($fields = field_read_fields(array('id' => $field_id), array('include_deleted' => TRUE))) { + $field = current($fields); + $field = $this->prepareField($field); + + // Save in the static cache (fields only enter the persistent cache as + // part of a full bundle). + $this->cachesetField(self::CACHE_STATIC, $field); + + return $field; + } + } + + /** + * Retrieves all active, non-deleted instances definitions. + * + * This method does not read from nor populate the static and persistent + * caches. + * + * @param $entity_type + * (optional) The entity type. + * + * @return + * If $entity_type is not set, all instances keyed by entity type and bundle + * name. If $entity_type is set, all instances for that entity type, keyed + * by bundle name. + */ + public function getInstances($entity_type = NULL) { + $instances = array(); + + // We need the field type for each instance. Fetch that directly from the + // database rather than loading all field definitions in memory with + // field_read_fields(). + $field_types = db_query("SELECT field_name, type FROM {field_config} WHERE active = 1 AND storage_active = 1 AND deleted = 0")->fetchAllKeyed(); + + // Collect and prepare instances. + $params = isset($entity_type) ? array('entity_type' => $entity_type) : array(); + foreach (field_read_instances($params) as $instance) { + $instance = $this->prepareInstance($instance, $field_types[$instance['field_name']]); + $instances[$instance['entity_type']][$instance['bundle']][$instance['field_name']] = $instance; + } + + if (isset($entity_type)) { + return isset($instances[$entity_type]) ? $instances[$entity_type] : array(); + } + else { + return $instances; + } + } + + /** + * Retrieves the instances and extra fields for a bundle. + * + * The function also populates the correpsonding field definitions in the + * static cache. + * + * @param $entity_type + * The entity type. + * @param $bundle + * The bundle name. + * + * @return + * An array with the following key/value pairs: + * - instances: The list of instance definitions. + * - extra_fields: The list extra fields. + */ + public function getBundleInfo($entity_type, $bundle) { + if ($info = $this->cacheGetBundleInfo(self::CACHE_STATIC, $entity_type, $bundle)) { + return $info; + } + + if ($info = $this->cacheGetBundleInfo(self::CACHE_PERSISTENT, $entity_type, $bundle)) { + // Cache hit: Extract the field definitions and store them in memory. + foreach ($info['fields'] as $field) { + $this->cacheSetField(self::CACHE_STATIC, $field); + } + unset($info['fields']); + + $this->cacheSetBundleInfo(self::CACHE_STATIC, $entity_type, $bundle, $info); + return $info; + } + + // Cache miss: collect the information from the database. + + $info = array( + 'instances' => array(), + 'extra_fields' => array(), + ); + + // Pre-fetch the fields in the bundle. + $params = array('entity_type' => $entity_type, 'bundle' => $bundle); + $fields = field_read_fields($params, array('include_deleted' => 1)); + + // This iterates on non-deleted instances, so deleted fields are kept + // out of the persistent caches. + foreach (field_read_instances($params) as $instance) { + $field = $fields[$instance['field_id']]; + + $instance = $this->prepareInstance($instance, $field['type']); + $info['instances'][$field['field_name']] = $instance; + + // If the field is not in our global static list yet, add it. + if (!$this->cacheGetFieldById(self::CACHE_STATIC, $field['id'])) { + $field = $this->prepareField($field); + $this->cacheSetField(self::CACHE_STATIC, $field); + } + } + + // Populate 'extra_fields'. Note: given the current shape of + // hook_field_extra_fields(), we have no other way than collecting extra + // fields on all bundles. + $extra = module_invoke_all('field_extra_fields'); + drupal_alter('field_extra_fields', $extra); + // Merge in saved settings. + if (isset($extra[$entity_type][$bundle])) { + $info['extra_fields'] = $this->prepareExtraFields($extra[$entity_type][$bundle], $entity_type, $bundle); + } + + // The method might get called on invalid entity types or bundles, for + // which we do not want to pollute our caches. However, there might be + // edge cases where the entity type and bundle are valid but still unknown + // by a stale entity_get_info(). Thus on unknown entity types or bundles, + // we do check the corresponding field instances, but do not cache the + // result. + $entity_info = entity_get_info($entity_type); + if (isset($entity_info['bundles'][$bundle])) { + $this->cacheSetBundleInfo(self::CACHE_STATIC, $entity_type, $bundle, $info); + + // The persistent cache additionally contains the definitions of the + // fields involved in the bundle. + $cache = $info + array('fields' => array()); + foreach ($info['instances'] as $instance) { + $cache['fields'][] = $this->cacheGetFieldbyId(self::CACHE_STATIC, $instance['field_id']); + } + $this->cacheSetBundleInfo(self::CACHE_PERSISTENT, $entity_type, $bundle, $cache); + } + + return $info; + } + + /** + * Prepares a field definition for the current run-time context. + * + * @param $field + * The raw field structure as read from the database. + * + * @return + * The field definition completed for the current runtime context. + */ + public function prepareField($field) { + // Make sure all expected field settings are present. + $field['settings'] += field_info_field_settings($field['type']); + $field['storage']['settings'] += field_info_storage_settings($field['storage']['type']); + + // Add storage details. + $details = (array) module_invoke($field['storage']['module'], 'field_storage_details', $field); + drupal_alter('field_storage_details', $details, $field, $instance); + $field['storage']['details'] = $details; + + // Populate the list of bundles using the field.. + $field['bundles'] = array(); + if (!$field['deleted']) { + $map = $this->getFieldMap(); + if (isset($map[$field['field_name']])) { + $field['bundles'] = $map[$field['field_name']]; + } + } + + return $field; + } + + /** + * Prepares an instance definition for the current run-time context. + * + * @param $instance + * The raw instance structure as read from the database. + * @param $field_type + * The field type. + * + * @return + * The field instance array completed for the current runtime context. + */ + public function prepareInstance($instance, $field_type) { + // Make sure all expected instance settings are present. + $instance['settings'] += field_info_instance_settings($field_type); + + // Set a default value for the instance. + if (field_behaviors_widget('default value', $instance) == FIELD_BEHAVIOR_DEFAULT && !isset($instance['default_value'])) { + $instance['default_value'] = NULL; + } + + // Prepare widget settings. + $instance['widget'] = $this->prepareInstanceWidget($instance['widget'], $field_type); + + // Prepare display settings. + foreach ($instance['display'] as $view_mode => $display) { + $instance['display'][$view_mode] = $this->prepareInstanceDisplay($display, $field_type); + } + + // Fall back to 'hidden' for view modes configured to use custom display + // settings, and for which the instance has no explicit settings. + $entity_info = entity_get_info($instance['entity_type']); + $view_modes = array_merge(array('default'), array_keys($entity_info['view modes'])); + $view_mode_settings = field_view_mode_settings($instance['entity_type'], $instance['bundle']); + foreach ($view_modes as $view_mode) { + if ($view_mode == 'default' || !empty($view_mode_settings[$view_mode]['custom_settings'])) { + if (!isset($instance['display'][$view_mode])) { + $instance['display'][$view_mode] = array( + 'type' => 'hidden', + 'label' => 'above', + 'settings' => array(), + 'weight' => 0, + ); + } + } + } + + return $instance; + } + + /** + * Prepares widget properties for the current run-time context. + * + * @param $widget + * Widget specifications as found in $instance['widget']. + * @param $field_type + * The field type. + * + * @return + * The widget properties completed for the current runtime context. + */ + public function prepareInstanceWidget($widget, $field_type) { + $field_type_info = field_info_field_types($field_type); + + // Fill in default values. + $widget += array( + 'type' => $field_type_info['default_widget'], + 'settings' => array(), + 'weight' => 0, + ); + + $widget_type_info = field_info_widget_types($widget['type']); + // Fall back to default formatter if formatter type is not available. + if (!$widget_type_info) { + $widget['type'] = $field_type_info['default_widget']; + $widget_type_info = field_info_widget_types($widget['type']); + } + $widget['module'] = $widget_type_info['module']; + // Fill in default settings for the widget. + $widget['settings'] += field_info_widget_settings($widget['type']); + + return $widget; + } + + /** + * Adapts display specifications to the current run-time context. + * + * @param $display + * Display specifications as found in $instance['display']['a_view_mode']. + * @param $field_type + * The field type. + * + * @return + * The display properties completed for the current runtime context. + */ + public function prepareInstanceDisplay($display, $field_type) { + $field_type_info = field_info_field_types($field_type); + + // Fill in default values. + $display += array( + 'label' => 'above', + 'type' => $field_type_info['default_formatter'], + 'settings' => array(), + 'weight' => 0, + ); + if ($display['type'] != 'hidden') { + $formatter_type_info = field_info_formatter_types($display['type']); + // Fall back to default formatter if formatter type is not available. + if (!$formatter_type_info) { + $display['type'] = $field_type_info['default_formatter']; + $formatter_type_info = field_info_formatter_types($display['type']); + } + $display['module'] = $formatter_type_info['module']; + // Fill in default settings for the formatter. + $display['settings'] += field_info_formatter_settings($display['type']); + } + + return $display; + } + + /** + * Prepares 'extra fields' for the current run-time context. + * + * @param $extra_fields + * The array of extra fields, as collected in hook_field_extra_fields(). + * @param $entity_type + * The entity type. + * @param $bundle + * The bundle name. + * + * @return + * The list of extra fields completed for the current runtime context. + */ + public function prepareExtraFields($extra_fields, $entity_type, $bundle) { + $entity_type_info = entity_get_info($entity_type); + $bundle_settings = field_bundle_settings($entity_type, $bundle); + $extra_fields += array('form' => array(), 'display' => array()); + + $result = array(); + // Extra fields in forms. + foreach ($extra_fields['form'] as $name => $field_data) { + $settings = isset($bundle_settings['extra_fields']['form'][$name]) ? $bundle_settings['extra_fields']['form'][$name] : array(); + if (isset($settings['weight'])) { + $field_data['weight'] = $settings['weight']; + } + $result['form'][$name] = $field_data; + } + + // Extra fields in displayed entities. + $data = $extra_fields['display']; + foreach ($extra_fields['display'] as $name => $field_data) { + $settings = isset($bundle_settings['extra_fields']['display'][$name]) ? $bundle_settings['extra_fields']['display'][$name] : array(); + $view_modes = array_merge(array('default'), array_keys($entity_type_info['view modes'])); + foreach ($view_modes as $view_mode) { + if (isset($settings[$view_mode])) { + $field_data['display'][$view_mode] = $settings[$view_mode]; + } + else { + $field_data['display'][$view_mode] = array( + 'weight' => $field_data['weight'], + 'visible' => TRUE, + ); + } + } + unset($field_data['weight']); + $result['display'][$name] = $field_data; + } + + return $result; + } +} + +/** + * Retrieves the FieldInfo object for the current request. + * + * @return + * An instance of the FieldInfo class. + */ +function _field_info_field_cache() { + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + + if (!isset($drupal_static_fast)) { + $drupal_static_fast['field_info_field_cache'] = &drupal_static(__FUNCTION__); + } + $info = &$drupal_static_fast['field_info_field_cache']; + + if (!isset($info)) { + $info = new FieldInfo(); + } + + return $info; +} + +/** + * Resets the cached data about existing fields and instances. + */ +function _field_info_field_reset() { + $cache = _field_info_field_cache(); + $cache->flush(); +} + +/** * @defgroup field_info Field Info API * @{ * Obtain information about Field API configuration. @@ -34,7 +758,7 @@ function field_info_cache_clear() { entity_info_cache_clear(); _field_info_collate_types_reset(); - _field_info_collate_fields_reset(); + _field_info_field_reset(); } /** @@ -167,289 +891,6 @@ function _field_info_collate_types_reset() { } /** - * Collates all information on existing fields and instances. - * - * @return - * An associative array containing: - * - fields: Array of existing fields, keyed by field ID. This element - * lists deleted and non-deleted fields, but not inactive ones. - * Each field has an additional element, 'bundles', which is an array - * of all non-deleted instances of that field. - * - field_ids: Array of field IDs, keyed by field name. This element - * only lists non-deleted, active fields. - * - instances: Array of existing instances, keyed by entity type, bundle - * name and field name. This element only lists non-deleted instances - * whose field is active. - * - * @see _field_info_collate_fields_reset() - */ -function _field_info_collate_fields() { - // Use the advanced drupal_static() pattern, since this is called very often. - static $drupal_static_fast; - - if (!isset($drupal_static_fast)) { - $drupal_static_fast['field_info_collate_fields'] = &drupal_static(__FUNCTION__); - } - $info = &$drupal_static_fast['field_info_collate_fields']; - - if (!isset($info)) { - if ($cached = cache('field')->get('field_info_fields')) { - $info = $cached->data; - } - else { - $definitions = array( - 'field_ids' => field_read_fields(array(), array('include_deleted' => 1)), - 'instances' => field_read_instances(), - ); - - // Populate 'fields' with all fields, keyed by ID. - $info['fields'] = array(); - foreach ($definitions['field_ids'] as $key => $field) { - $info['fields'][$key] = $definitions['field_ids'][$key] = _field_info_prepare_field($field); - } - - // Build an array of field IDs for non-deleted fields, keyed by name. - $info['field_ids'] = array(); - foreach ($info['fields'] as $key => $field) { - if (!$field['deleted']) { - $info['field_ids'][$field['field_name']] = $key; - } - } - - // Populate 'instances'. Only non-deleted instances are considered. - $info['instances'] = array(); - foreach (field_info_bundles() as $entity_type => $bundles) { - foreach ($bundles as $bundle => $bundle_info) { - $info['instances'][$entity_type][$bundle] = array(); - } - } - foreach ($definitions['instances'] as $instance) { - $field = $info['fields'][$instance['field_id']]; - $instance = _field_info_prepare_instance($instance, $field); - $info['instances'][$instance['entity_type']][$instance['bundle']][$instance['field_name']] = $instance; - // Enrich field definitions with the list of bundles where they have - // instances. NOTE: Deleted fields in $info['field_ids'] are not - // enriched because all of their instances are deleted, too, and - // are thus not in $definitions['instances']. - $info['fields'][$instance['field_id']]['bundles'][$instance['entity_type']][] = $instance['bundle']; - } - - // Populate 'extra_fields'. - $extra = module_invoke_all('field_extra_fields'); - drupal_alter('field_extra_fields', $extra); - // Merge in saved settings. - foreach ($extra as $entity_type => $bundles) { - foreach ($bundles as $bundle => $extra_fields) { - $extra_fields = _field_info_prepare_extra_fields($extra_fields, $entity_type, $bundle); - $info['extra_fields'][$entity_type][$bundle] = $extra_fields; - } - } - - cache('field')->set('field_info_fields', $info); - } - } - - return $info; -} - -/** - * Clear collated information on existing fields and instances. - */ -function _field_info_collate_fields_reset() { - drupal_static_reset('_field_info_collate_fields'); - cache('field')->delete('field_info_fields'); -} - -/** - * Prepares a field definition for the current run-time context. - * - * Since the field was last saved or updated, new field settings can be - * expected. - * - * @param $field - * The raw field structure as read from the database. - */ -function _field_info_prepare_field($field) { - // Make sure all expected field settings are present. - $field['settings'] += field_info_field_settings($field['type']); - $field['storage']['settings'] += field_info_storage_settings($field['storage']['type']); - - // Add storage details. - $details = (array) module_invoke($field['storage']['module'], 'field_storage_details', $field); - drupal_alter('field_storage_details', $details, $field, $instance); - $field['storage']['details'] = $details; - - // Initialize the 'bundles' list. - $field['bundles'] = array(); - - return $field; -} - -/** - * Prepares an instance definition for the current run-time context. - * - * Since the instance was last saved or updated, a number of things might have - * changed: widgets or formatters disabled, new settings expected, new view - * modes added... - * - * @param $instance - * The raw instance structure as read from the database. - * @param $field - * The field structure for the instance. - * - * @return - * Field instance array. - */ -function _field_info_prepare_instance($instance, $field) { - // Make sure all expected instance settings are present. - $instance['settings'] += field_info_instance_settings($field['type']); - - // Set a default value for the instance. - if (field_behaviors_widget('default value', $instance) == FIELD_BEHAVIOR_DEFAULT && !isset($instance['default_value'])) { - $instance['default_value'] = NULL; - } - - $instance['widget'] = _field_info_prepare_instance_widget($field, $instance['widget']); - - foreach ($instance['display'] as $view_mode => $display) { - $instance['display'][$view_mode] = _field_info_prepare_instance_display($field, $display); - } - - // Fallback to 'hidden' for view modes configured to use custom display - // settings, and for which the instance has no explicit settings. - $entity_info = entity_get_info($instance['entity_type']); - $view_modes = array_merge(array('default'), array_keys($entity_info['view modes'])); - $view_mode_settings = field_view_mode_settings($instance['entity_type'], $instance['bundle']); - foreach ($view_modes as $view_mode) { - if ($view_mode == 'default' || !empty($view_mode_settings[$view_mode]['custom_settings'])) { - if (!isset($instance['display'][$view_mode])) { - $instance['display'][$view_mode] = array( - 'type' => 'hidden', - 'label' => 'above', - 'settings' => array(), - 'weight' => 0, - ); - } - } - } - - return $instance; -} - -/** - * Adapts display specifications to the current run-time context. - * - * @param $field - * The field structure for the instance. - * @param $display - * Display specifications as found in - * $instance['display']['some_view_mode']. - */ -function _field_info_prepare_instance_display($field, $display) { - $field_type = field_info_field_types($field['type']); - - // Fill in default values. - $display += array( - 'label' => 'above', - 'type' => $field_type['default_formatter'], - 'settings' => array(), - 'weight' => 0, - ); - if ($display['type'] != 'hidden') { - $formatter_type = field_info_formatter_types($display['type']); - // Fallback to default formatter if formatter type is not available. - if (!$formatter_type) { - $display['type'] = $field_type['default_formatter']; - $formatter_type = field_info_formatter_types($display['type']); - } - $display['module'] = $formatter_type['module']; - // Fill in default settings for the formatter. - $display['settings'] += field_info_formatter_settings($display['type']); - } - - return $display; -} - -/** - * Prepares widget specifications for the current run-time context. - * - * @param $field - * The field structure for the instance. - * @param $widget - * Widget specifications as found in $instance['widget']. - */ -function _field_info_prepare_instance_widget($field, $widget) { - $field_type = field_info_field_types($field['type']); - - // Fill in default values. - $widget += array( - 'type' => $field_type['default_widget'], - 'settings' => array(), - 'weight' => 0, - ); - - $widget_type = field_info_widget_types($widget['type']); - // Fallback to default formatter if formatter type is not available. - if (!$widget_type) { - $widget['type'] = $field_type['default_widget']; - $widget_type = field_info_widget_types($widget['type']); - } - $widget['module'] = $widget_type['module']; - // Fill in default settings for the widget. - $widget['settings'] += field_info_widget_settings($widget['type']); - - return $widget; -} - -/** - * Prepares 'extra fields' for the current run-time context. - * - * @param $extra_fields - * The array of extra fields, as collected in hook_field_extra_fields(). - * @param $entity_type - * The entity type. - * @param $bundle - * The bundle name. - */ -function _field_info_prepare_extra_fields($extra_fields, $entity_type, $bundle) { - $entity_type_info = entity_get_info($entity_type); - $bundle_settings = field_bundle_settings($entity_type, $bundle); - $extra_fields += array('form' => array(), 'display' => array()); - - $result = array(); - // Extra fields in forms. - foreach ($extra_fields['form'] as $name => $field_data) { - $settings = isset($bundle_settings['extra_fields']['form'][$name]) ? $bundle_settings['extra_fields']['form'][$name] : array(); - if (isset($settings['weight'])) { - $field_data['weight'] = $settings['weight']; - } - $result['form'][$name] = $field_data; - } - - // Extra fields in displayed entities. - $data = $extra_fields['display']; - foreach ($extra_fields['display'] as $name => $field_data) { - $settings = isset($bundle_settings['extra_fields']['display'][$name]) ? $bundle_settings['extra_fields']['display'][$name] : array(); - $view_modes = array_merge(array('default'), array_keys($entity_type_info['view modes'])); - foreach ($view_modes as $view_mode) { - if (isset($settings[$view_mode])) { - $field_data['display'][$view_mode] = $settings[$view_mode]; - } - else { - $field_data['display'][$view_mode] = array( - 'weight' => $field_data['weight'], - 'visible' => TRUE, - ); - } - } - unset($field_data['weight']); - $result['display'][$name] = $field_data; - } - - return $result; -} - -/** * Determines the behavior of a widget with respect to an operation. * * @param $op @@ -596,20 +1037,19 @@ function field_info_bundles($entity_type = NULL) { /** * Returns all field definitions. * + * Warning: this function does not read from cached data, and the returned array + * can be costly memory-wise. When iterating over the fields present in a given + * bundle after a call to field_info_instances($entity_type, $bundle), it is + * recommended to use field_info_field() on each individual field instead. + * * @return * An array of field definitions, keyed by field name. Each field has an * additional property, 'bundles', which is an array of all the bundles to * which this field belongs keyed by entity type. */ function field_info_fields() { - $fields = array(); - $info = _field_info_collate_fields(); - foreach ($info['fields'] as $key => $field) { - if (!$field['deleted']) { - $fields[$field['field_name']] = $field; - } - } - return $fields; + $cache = _field_info_field_cache(); + return $cache->getFields(); } /** @@ -628,10 +1068,8 @@ function field_info_fields() { * @see field_info_field_by_id() */ function field_info_field($field_name) { - $info = _field_info_collate_fields(); - if (isset($info['field_ids'][$field_name])) { - return $info['fields'][$info['field_ids'][$field_name]]; - } + $cache = _field_info_field_cache(); + return $cache->getField($field_name); } /** @@ -649,17 +1087,17 @@ function field_info_field($field_name) { * @see field_info_field() */ function field_info_field_by_id($field_id) { - $info = _field_info_collate_fields(); - if (isset($info['fields'][$field_id])) { - return $info['fields'][$field_id]; - } + $cache = _field_info_field_cache(); + return $cache->getFieldById($field_id); } /** * Returns the same data as field_info_field_by_id() for every field. * - * This function is typically used when handling all fields of some entities - * to avoid thousands of calls to field_info_field_by_id(). + * Warning: this function does not read from cached data, and the returned array + * can be costly memory-wise. When iterating over the fields present in a given + * bundle after a call to field_info_instances($entity_type, $bundle), it is + * recommended to use field_info_field_by_id() on each individual field instead. * * @return * An array, each key is a field ID and the values are field arrays as @@ -670,17 +1108,23 @@ function field_info_field_by_id($field_id) { * @see field_info_field_by_id() */ function field_info_field_by_ids() { - $info = _field_info_collate_fields(); - return $info['fields']; + $cache = _field_info_field_cache(); + return $cache->getFieldsById(); } /** * Retrieves information about field instances. * + * When retrieving the instances of a specific bundle (i.e. when both + * $entity_type and $bundle_name are provided, the function also populates a + * static chache with the corresponding field definitions, allowing a fast + * retrieval of field_info_field() later in the request. + * * @param $entity_type - * The entity type for which to return instances. + * (optional) The entity type for which to return instances. * @param $bundle_name - * The bundle name for which to return instances. + * (optional) The bundle name for which to return instances. If $entity_type + * is NULL, the $bundle_name parameter is ignored. * * @return * If $entity_type is not set, return all instances keyed by entity type and @@ -689,22 +1133,26 @@ function field_info_field_by_ids() { * all instances for that bundle. */ function field_info_instances($entity_type = NULL, $bundle_name = NULL) { - $info = _field_info_collate_fields(); + $cache = _field_info_field_cache(); + if (!isset($entity_type)) { - return $info['instances']; + return $cache->getInstances(); } if (!isset($bundle_name)) { - return $info['instances'][$entity_type]; + return $cache->getInstances($entity_type); } - if (isset($info['instances'][$entity_type][$bundle_name])) { - return $info['instances'][$entity_type][$bundle_name]; - } - return array(); + + $info = $cache->getBundleInfo($entity_type, $bundle_name); + return $info['instances']; } /** * Returns an array of instance data for a specific field and bundle. * + * The function populates a static cache with all fields and instances used in + * the bundle, allowing a fast retrieval of field_info_field() or + * field_info_instance() later in the request. + * * @param $entity_type * The entity type for the instance. * @param $field_name @@ -713,9 +1161,10 @@ function field_info_instances($entity_type = NULL, $bundle_name = NULL) { * The bundle name for the instance. */ function field_info_instance($entity_type, $field_name, $bundle_name) { - $info = _field_info_collate_fields(); - if (isset($info['instances'][$entity_type][$bundle_name][$field_name])) { - return $info['instances'][$entity_type][$bundle_name][$field_name]; + $cache = _field_info_field_cache(); + $info = $cache->getBundleInfo($entity_type, $bundle_name); + if (isset($info['instances'][$field_name])) { + return $info['instances'][$field_name]; } } @@ -773,9 +1222,11 @@ function field_info_instance($entity_type, $field_name, $bundle_name) { * The array of pseudo-field elements in the bundle. */ function field_info_extra_fields($entity_type, $bundle, $context) { - $info = _field_info_collate_fields(); - if (isset($info['extra_fields'][$entity_type][$bundle][$context])) { - return $info['extra_fields'][$entity_type][$bundle][$context]; + $cache = _field_info_field_cache(); + $info = $cache->getBundleInfo($entity_type, $bundle); + + if (isset($info['extra_fields'][$context])) { + return $info['extra_fields'][$context]; } return array(); } diff --git a/core/modules/field/field.module b/core/modules/field/field.module index 2d31efa..485b3f6 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -892,7 +892,8 @@ function field_view_field($entity_type, $entity, $field_name, $display = array() if ($field = field_info_field($field_name)) { if (is_array($display)) { // When using custom display settings, fill in default values. - $display = _field_info_prepare_instance_display($field, $display); + $cache = _field_info_field_cache(); + $display = $cache->prepareInstanceDisplay($display, $field["type"]); } // Hook invocations are done through the _field_invoke() functions in diff --git a/core/modules/field/modules/field_sql_storage/field_sql_storage.module b/core/modules/field/modules/field_sql_storage/field_sql_storage.module index 92d244a..097c625 100644 --- a/core/modules/field/modules/field_sql_storage/field_sql_storage.module +++ b/core/modules/field/modules/field_sql_storage/field_sql_storage.module @@ -324,11 +324,14 @@ function field_sql_storage_field_storage_delete_field($field) { * Implements hook_field_storage_load(). */ function field_sql_storage_field_storage_load($entity_type, $entities, $age, $fields, $options) { - $field_info = field_info_field_by_ids(); $load_current = $age == FIELD_LOAD_CURRENT; foreach ($fields as $field_id => $ids) { - $field = $field_info[$field_id]; + // By the time hook_field_storage_load() runs, the relevant fields have + // been populated in the static cache, so calling field_info_field_by_id() + // on each field individually is more efficient than loading all fields in + // memory upfront with field_info_field_by_ids(). + $field = field_info_field_by_id($field_id); $field_name = $field['field_name']; $table = $load_current ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field); diff --git a/core/modules/field_ui/field_ui.module b/core/modules/field_ui/field_ui.module index ab807dc..a00cf7c 100644 --- a/core/modules/field_ui/field_ui.module +++ b/core/modules/field_ui/field_ui.module @@ -344,7 +344,10 @@ function field_ui_inactive_instances($entity_type, $bundle_name = NULL) { } $params['entity_type'] = $entity_type; - $active_instances = field_info_instances($entity_type); + $active_instances = field_info_instances($entity_type, $bundle_name); + if (!empty($bundle_name)) { + $active_instances = array($bundle_name => $active_instances); + } $all_instances = field_read_instances($params, array('include_inactive' => TRUE)); foreach ($all_instances as $instance) { if (!isset($active_instances[$instance['bundle']][$instance['field_name']])) { diff --git a/core/modules/field_ui/field_ui.test b/core/modules/field_ui/field_ui.test index adfd900..4b3babc 100644 --- a/core/modules/field_ui/field_ui.test +++ b/core/modules/field_ui/field_ui.test @@ -298,7 +298,7 @@ class FieldUIManageFieldsTestCase extends FieldUITestCase { */ function assertFieldSettings($bundle, $field_name, $string = 'dummy test string', $entity_type = 'node') { // Reset the fields info. - _field_info_collate_fields_reset(); + _field_info_field_reset(); // Assert field settings. $field = field_info_field($field_name); $this->assertTrue($field['settings']['test_field_setting'] == $string, t('Field settings were found.')); @@ -389,7 +389,7 @@ class FieldUIManageFieldsTestCase extends FieldUITestCase { $this->fieldUIDeleteField($bundle_path1, $this->field_name, $this->field_label, $this->type); // Reset the fields info. - _field_info_collate_fields_reset(); + _field_info_field_reset(); // Check that the field instance was deleted. $this->assertNull(field_info_instance('node', $this->field_name, $this->type), t('Field instance was deleted.')); // Check that the field was not deleted @@ -399,7 +399,7 @@ class FieldUIManageFieldsTestCase extends FieldUITestCase { $this->fieldUIDeleteField($bundle_path2, $this->field_name, $this->field_label, $type_name2); // Reset the fields info. - _field_info_collate_fields_reset(); + _field_info_field_reset(); // Check that the field instance was deleted. $this->assertNull(field_info_instance('node', $this->field_name, $type_name2), t('Field instance was deleted.')); // Check that the field was deleted too.