diff --git a/features.admin.inc b/features.admin.inc index 5de989b..15ef2df 100644 --- a/features.admin.inc +++ b/features.admin.inc @@ -267,12 +267,15 @@ function _features_export_form_components(&$form, &$form_state) { module_load_include('inc', 'features', 'features.export'); $feature_export = _features_export_generate($export, $form_state, $feature); - $feature_export = features_export_prepare($feature_export, $feature->name); + $feature_export = features_export_prepare($feature_export, $feature->name, TRUE); $info = features_export_info($feature_export); drupal_add_js(array('features' => array('info' => $info)), 'setting'); } + // determine any components that are deprecated + $deprecated = features_get_deprecated($export['components']); + $sections = array('included', 'detected', 'added'); foreach ($export['components'] as $component => $component_info) { $label = (isset($component_info['name']) ? @@ -287,42 +290,72 @@ function _features_export_form_components(&$form, &$form_state) { if ($count + count($component_info['options']['sources']) > 0) { - $form['export'][$component] = array( - '#markup' => '', - '#tree' => TRUE, + if (!empty($deprecated[$component])) { + // only show deprecated component if it has some exports + if (!empty($component_info['options']['included'])) { + $form['export'][$component] = array( + '#markup' => '', + '#tree' => TRUE, + ); + + $form['export'][$component]['deprecated'] = array( + '#type' => 'fieldset', + '#title' => $label . " (" . t('DEPRECATED') . ")", + '#tree' => TRUE, + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#attributes' => array('class' => array('features-export-component')), + ); + $list = ' '; + foreach ($component_info['options']['included'] as $key) { + $list .= "$key"; + } + $form['export'][$component]['deprecated']['selected'] = array( + '#prefix' => "
", + '#markup' => $list, + '#suffix' => "
", + ); + } + } + else { + $form['export'][$component] = array( + '#markup' => '', + '#tree' => TRUE, + ); + + $form['export'][$component]['sources'] = array( + '#type' => 'fieldset', + '#title' => $label, + '#tree' => TRUE, + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#attributes' => array('class' => array('features-export-component')), + '#prefix' => "
", ); - $form['export'][$component]['sources'] = array( - '#type' => 'fieldset', - '#title' => $label, - '#tree' => TRUE, - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#attributes' => array('class' => array('features-export-component')), - '#prefix' => "
", - ); - $form['export'][$component]['sources']['selected'] = array( - '#type' => 'checkboxes', - '#id' => "edit-sources-$component_name", - '#options' => features_dom_encode_options($component_info['options']['sources']), - '#default_value' => features_dom_encode_options($component_info['selected']['sources'], FALSE), - '#attributes' => array( - 'class' => array('component-select'), - ), - ); - - foreach ($sections as $section) { - $form['export'][$component][$section] = array( + $form['export'][$component]['sources']['selected'] = array( '#type' => 'checkboxes', - '#options' => !empty($component_info['options'][$section]) ? - features_dom_encode_options($component_info['options'][$section]) : array(), - '#default_value' => !empty($component_info['selected'][$section]) ? - features_dom_encode_options($component_info['selected'][$section], FALSE) : array(), - '#attributes' => array('class' => array('component-' . $section)), + '#id' => "edit-sources-$component_name", + '#options' => features_dom_encode_options($component_info['options']['sources']), + '#default_value' => features_dom_encode_options($component_info['selected']['sources'], FALSE), + '#attributes' => array( + 'class' => array('component-select'), + ), ); + + foreach ($sections as $section) { + $form['export'][$component][$section] = array( + '#type' => 'checkboxes', + '#options' => !empty($component_info['options'][$section]) ? + features_dom_encode_options($component_info['options'][$section]) : array(), + '#default_value' => !empty($component_info['selected'][$section]) ? + features_dom_encode_options($component_info['selected'][$section], FALSE) : array(), + '#attributes' => array('class' => array('component-' . $section)), + ); + } + $form['export'][$component][$sections[0]]['#prefix'] = + "
"; + $form['export'][$component][$sections[count($sections)-1]]['#suffix'] = '
'; } - $form['export'][$component][$sections[0]]['#prefix'] = - "
"; - $form['export'][$component][$sections[count($sections)-1]]['#suffix'] = '
'; } } $form['export']['features_legend'] = array( diff --git a/features.export.inc b/features.export.inc index 37f772c..6718793 100644 --- a/features.export.inc +++ b/features.export.inc @@ -170,6 +170,13 @@ function _features_export_maximize_dependencies($dependencies, $module_name = '' /** * Prepare a feature export array into a finalized info array. + * @param $export + * An exported feature definition. + * @param $module_name + * The name of the module to be exported. + * @param $reset + * Boolean flag for resetting the module cache. Only set to true when + * doing a final export for delivery. */ function features_export_prepare($export, $module_name, $reset = FALSE) { $existing = features_get_modules($module_name, $reset); @@ -186,9 +193,16 @@ function features_export_prepare($export, $module_name, $reset = FALSE) { $defaults = $existing ? $existing->info : array('core' => '7.x', 'package' => 'Features'); $export = array_merge($defaults, $export); + $deprecated = features_get_deprecated(); // Cleanup info array foreach ($export['features'] as $component => $data) { - $export['features'][$component] = array_keys($data); + // if performing the final export, do not export deprecated components + if ($reset && !empty($deprecated[$component])) { + unset($export['features'][$component]); + } + else { + $export['features'][$component] = array_keys($data); + } } if (isset($export['dependencies'])) { $export['dependencies'] = array_values($export['dependencies']); @@ -252,9 +266,14 @@ function features_export_render($export, $module_name, $reset = FALSE) { // Generate hook code $component_hooks = features_export_render_hooks($export, $module_name, $reset); $components = features_get_components(); + $deprecated = features_get_deprecated($components); // Group component code into their respective files foreach ($component_hooks as $component => $hooks) { + if ($reset && !empty($deprecated[$component])) { + // skip deprecated components on final export + continue; + } $file = array('name' => 'features'); if (isset($components[$component]['default_file'])) { switch ($components[$component]['default_file']) { diff --git a/features.module b/features.module index d9ce8d6..c8c13e0 100644 --- a/features.module +++ b/features.module @@ -954,3 +954,23 @@ function features_hook_info() { ); return array_fill_keys($hooks, array('group' => 'features')); } + +/** + * Returns an array of deprecated components + * Rather than deprecating the component directly, we look for other components + * that supersedes the component + * @param $components + * The array of components (component_info) from features_get_components typically. + */ +function features_get_deprecated($components = array()) { + if (empty($components)) { + $components = features_get_components(); + } + $deprecated = array(); + foreach ($components as $component => $component_info) { + if (!empty($component_info['supersedes'])) { + $deprecated[$component_info['supersedes']] = $component_info['supersedes']; + } + } + return $deprecated; +} diff --git a/includes/features.field.inc b/includes/features.field.inc index c396668..18dc6d3 100644 --- a/includes/features.field.inc +++ b/includes/features.field.inc @@ -6,14 +6,340 @@ function field_features_api() { return array( 'field' => array( + // this is deprecated by field_base and field_instance + // but retained for compatibility with older exports 'name' => t('Fields'), 'default_hook' => 'field_default_fields', 'default_file' => FEATURES_DEFAULTS_INCLUDED, 'feature_source' => TRUE, + ), + 'field_base' => array( + 'name' => t('Field Bases'), + 'default_hook' => 'field_default_field_bases', + 'default_file' => FEATURES_DEFAULTS_INCLUDED, + 'feature_source' => TRUE, + 'supersedes' => 'field', + ), + 'field_instance' => array( + 'name' => t('Field Instances'), + 'default_hook' => 'field_default_field_instances', + 'default_file' => FEATURES_DEFAULTS_INCLUDED, + 'feature_source' => TRUE, + 'supersedes' => 'field', ) ); } +/** + * Implements hook_features_export_options(). + */ +function field_base_features_export_options() { + $options = array(); + $fields = field_info_fields(); + foreach ($fields as $field_name => $field) { + $options[$field_name] = $field_name; + } + return $options; +} + +/** + * Implements hook_features_export_options(). + */ +function field_instance_features_export_options() { + $options = array(); + $instances = field_info_instances(); + foreach ($instances as $entity_type => $bundles) { + foreach ($bundles as $bundle => $fields) { + foreach ($fields as $field) { + $identifier = "{$entity_type}-{$bundle}-{$field['field_name']}"; + $options[$identifier] = $identifier; + } + } + } + return $options; +} + +/** + * Implements hook_features_export(). + */ +function field_base_features_export($data, &$export, $module_name = '') { + $pipe = array(); + $map = features_get_default_map('field_base'); + + // The field_default_field_bases() hook integration is provided by the + // features module so we need to add it as a dependency. + $export['dependencies']['features'] = 'features'; + + foreach ($data as $identifier) { + if ($base = features_field_base_load($identifier)) { + // If this field is already provided by another module, remove the field + // and add the other module as a dependency. + if (isset($map[$identifier]) && $map[$identifier] != $module_name) { + if (isset($export['features']['field_base'][$identifier])) { + unset($export['features']['field_base'][$identifier]); + } + $module = $map[$identifier]; + $export['dependencies'][$module] = $module; + } + // If the field has not yet been exported, add it + else { + $export['features']['field_base'][$identifier] = $identifier; + $export['dependencies'][$base['module']] = $base['module']; + if ($base['storage']['type'] != variable_get('field_storage_default', 'field_sql_storage')) { + $export['dependencies'][$base['storage']['module']] = $base['storage']['module']; + } + // If taxonomy field, add in the vocabulary + if ($base['type'] == 'taxonomy_term_reference' && !empty($base['settings']['allowed_values'])) { + foreach ($base['settings']['allowed_values'] as $allowed_values) { + if (!empty($allowed_values['vocabulary'])) { + $pipe['taxonomy'][] = $allowed_values['vocabulary']; + } + } + } + } + } + } + return $pipe; +} + +/** + * Implements hook_features_export(). + */ +function field_instance_features_export($data, &$export, $module_name = '') { + $pipe = array('field_base' => array()); + $map = features_get_default_map('field_instance'); + + // The field_default_field_instances() hook integration is provided by the + // features module so we need to add it as a dependency. + $export['dependencies']['features'] = 'features'; + + foreach ($data as $identifier) { + if ($instance = features_field_instance_load($identifier)) { + // If this field is already provided by another module, remove the field + // and add the other module as a dependency. + if (isset($map[$identifier]) && $map[$identifier] != $module_name) { + if (isset($export['features']['field_instance'][$identifier])) { + unset($export['features']['field_instance'][$identifier]); + } + $module = $map[$identifier]; + $export['dependencies'][$module] = $module; + } + // If the field has not yet been exported, add it + else { + $export['features']['field_instance'][$identifier] = $identifier; + $export['dependencies'][$instance['widget']['module']] = $instance['widget']['module']; + foreach ($instance['display'] as $key => $display) { + if (isset($display['module'])) { + $export['dependencies'][$display['module']] = $display['module']; + // @TODO: handle the pipe to image styles + } + } + $pipe['field_base'][] = $instance['field_name']; + } + } + } + return $pipe; +} + +/** + * Implements hook_features_export_render(). + */ +function field_base_features_export_render($module, $data, $export = NULL) { + $translatables = $code = array(); + $code[] = ' $field_bases = array();'; + $code[] = ''; + foreach ($data as $identifier) { + if ($field = features_field_base_load($identifier)) { + unset($field['columns']); + // unset($field['locked']); + // Only remove the 'storage' declaration if the field is using the default + // storage type. + if ($field['storage']['type'] == variable_get('field_storage_default', 'field_sql_storage')) { + unset($field['storage']); + } + // If we still have a storage declaration here it means that a non-default + // storage type was altered into to the field definition. And noone would + // never need to change the 'details' key, so don't render it. + if (isset($field['storage']['details'])) { + unset($field['storage']['details']); + } + + _field_instance_features_export_sort($field); + $field_export = features_var_export($field, ' '); + $field_identifier = features_var_export($identifier); + $code[] = " // Exported field_base: {$field_identifier}"; + $code[] = " \$field_bases[{$field_identifier}] = {$field_export};"; + $code[] = ""; + } + } + $code[] = ' return $field_bases;'; + $code = implode("\n", $code); + return array('field_default_field_bases' => $code); +} + +/** + * Implements hook_features_export_render(). + */ +function field_instance_features_export_render($module, $data, $export = NULL) { + $translatables = $code = array(); + + $code[] = ' $field_instances = array();'; + $code[] = ''; + + foreach ($data as $identifier) { + if ($instance = features_field_instance_load($identifier)) { + _field_instance_features_export_sort($instance); + $field_export = features_var_export($instance, ' '); + $instance_identifier = features_var_export($identifier); + $code[] = " // Exported field_instance: {$instance_identifier}"; + $code[] = " \$field_instances[{$instance_identifier}] = {$field_export};"; + $code[] = ""; + + if (!empty($instance['label'])) { + $translatables[] = $instance['label']; + } + if (!empty($instance['description'])) { + $translatables[] = $instance['description']; + } + } + } + if (!empty($translatables)) { + $code[] = features_translatables_export($translatables, ' '); + } + $code[] = ' return $field_instances;'; + $code = implode("\n", $code); + return array('field_default_field_instances' => $code); +} + +// Helper to enforce consistency in field export arrays. +function _field_instance_features_export_sort(&$field, $sort = TRUE) { + + // Some arrays are not sorted to preserve order (for example allowed_values). + static $sort_blacklist = array( + 'allowed_values', + 'format_handlers', + ); + + if ($sort) { + ksort($field); + } + foreach ($field as $k => $v) { + if (is_array($v)) { + _field_instance_features_export_sort($field[$k], !in_array($k, $sort_blacklist)); + } + } +} + +/** + * Implements hook_features_revert(). + */ +function field_base_features_revert($module) { + field_base_features_rebuild($module); +} + +/** + * Implements hook_features_revert(). + */ +function field_instance_features_revert($module) { + field_instance_features_rebuild($module); +} + +/** + * Implements of hook_features_rebuild(). + * Rebuilds fields from code defaults. + */ +function field_base_features_rebuild($module) { + if ($fields = features_get_default('field_base', $module)) { + field_info_cache_clear(); + + // Load all the existing field bases up-front so that we don't + // have to rebuild the cache all the time. + $existing_fields = field_info_fields(); + + foreach ($fields as $field) { + // Create or update field. + if (isset($existing_fields[$field['field_name']])) { + $existing_field = $existing_fields[$field['field_name']]; + if ($field + $existing_field != $existing_field) { + field_update_field($field); + } + } + else { + field_create_field($field); + $existing_fields[$field['field_name']] = $field; + } + variable_set('menu_rebuild_needed', TRUE); + } + } +} + +/** + * Implements of hook_features_rebuild(). + * Rebuilds field instances from code defaults. + */ +function field_instance_features_rebuild($module) { + if ($instances = features_get_default('field_instance', $module)) { + field_info_cache_clear(); + + // Load all the existing instances up-front so that we don't + // have to rebuild the cache all the time. + $existing_instances = field_info_instances(); + + foreach ($instances as $field_instance) { + // If the field base information does not exist yet, cancel out. + if (!field_info_field($field_instance['field_name'])) { + continue; + } + + // Create or update field instance. + if (isset($existing_instances[$field_instance['entity_type']][$field_instance['bundle']][$field_instance['field_name']])) { + $existing_instance = $existing_instances[$field_instance['entity_type']][$field_instance['bundle']][$field_instance['field_name']]; + if ($field_instance + $existing_instance != $existing_instance) { + field_update_instance($field_instance); + } + } + else { + field_create_instance($field_instance); + $existing_instances[$field_instance['entity_type']][$field_instance['bundle']][$field_instance['field_name']] = $field_instance; + } + } + + if ($instances) { + variable_set('menu_rebuild_needed', TRUE); + } + } +} + +/** + * Load a field base configuration by a field_name identifier. + */ +function features_field_base_load($field_name) { + if ($field_info = field_info_field($field_name)) { + unset($field_info['id']); + unset($field_info['bundles']); + return $field_info; + } + return FALSE; +} + +/** + * Load a field's instance configuration by an entity_type-bundle-field_name + * identifier. + */ +function features_field_instance_load($identifier) { + list($entity_type, $bundle, $field_name) = explode('-', $identifier); + if ($instance_info = field_info_instance($entity_type, $field_name, $bundle)) { + unset($instance_info['id']); + unset($instance_info['field_id']); + return $instance_info; + } + return FALSE; +} + +/* ----- DEPRECATED FIELD EXPORT ----- + * keep this code for backward compatibility with older exports + * until v3.x + */ /** * Implements hook_features_export_options(). diff --git a/includes/features.node.inc b/includes/features.node.inc index af35e25..7c45558 100644 --- a/includes/features.node.inc +++ b/includes/features.node.inc @@ -44,6 +44,7 @@ function node_features_export($data, &$export, $module_name = '') { $fields = field_info_instances('node', $type); foreach ($fields as $name => $field) { $pipe['field'][] = "node-{$field['bundle']}-{$field['field_name']}"; + $pipe['field_instance'][] = "node-{$field['bundle']}-{$field['field_name']}"; } } } diff --git a/includes/features.taxonomy.inc b/includes/features.taxonomy.inc index 5ac4ec5..a7c85cd 100644 --- a/includes/features.taxonomy.inc +++ b/includes/features.taxonomy.inc @@ -49,6 +49,7 @@ function taxonomy_features_export($data, &$export, $module_name = '') { $fields = field_info_instances('taxonomy_term', $machine_name); foreach ($fields as $name => $field) { $pipe['field'][] = "taxonomy_term-{$field['bundle']}-{$field['field_name']}"; + $pipe['field_instance'][] = "taxonomy_term-{$field['bundle']}-{$field['field_name']}"; } } }