Commit 803b8b39 authored by Dries's avatar Dries
Browse files

- Patch #443422 by yched, bjaspan | chx, merlinofchaos, Scott Reynolds, plach,...

- Patch #443422 by yched, bjaspan | chx, merlinofchaos, Scott Reynolds, plach, profix898, mattyoung: added support for pluggable 'per field' storage engine. Comes with documentation and tests.

The Field Attach API uses the Field Storage API to perform all "database access". Each Field Storage API hook function defines a primitive database operation such as read, write, or delete. The default field storage module, field_sql_storage.module, uses the local SQL database to implement these operations, but alternative field storage backends can choose to represent the data in SQL differently or use a completely different storage mechanism such as a cloud-based database.
parent 25299606
......@@ -70,10 +70,10 @@ function hook_field_extra_fields($bundle) {
/**
* @defgroup field_types Field Types API
* @{
* Define field types, widget types, and display formatter types.
* Define field types, widget types, display formatter types, storage types.
*
* The bulk of the Field Types API are related to field types. A field type
* represents a particular data storage type (integer, string, date, etc.) that
* represents a particular type of data (integer, string, date, etc.) that
* can be attached to a fieldable object. hook_field_info() defines the basic
* properties of a field type, and a variety of other field hooks are called by
* the Field Attach API to perform field-type-specific actions.
......@@ -97,6 +97,9 @@ function hook_field_extra_fields($bundle) {
* behavior of existing field types.
* @see hook_field_widget_info().
* @see hook_field_formatter_info().
*
* A third kind of pluggable handlers, storage backends, is defined by the
* @link field_storage Field Storage API @endlink.
*/
/**
......@@ -1096,6 +1099,45 @@ function hook_field_attach_delete_bundle($bundle, $instances) {
* @{
*/
/**
* Expose Field API storage backends.
*
* @return
* An array describing the storage backends implemented by the module.
* The keys are storage backend names. To avoid name clashes, storage backend
* names should be prefixed with the name of the module that exposes them.
* The values are arrays describing the storage backend, with the following
* key/value pairs:
* - label: The human-readable name of the storage backend.
* - description: A short description for the storage backend.
* - settings: An array whose keys are the names of the settings available
* for the storage backend, and whose values are the default values for
* those settings.
*/
function hook_field_storage_info() {
return array(
'field_sql_storage' => array(
'label' => t('Default SQL storage'),
'description' => t('Stores fields in the local SQL database, using per-field tables.'),
'settings' => array(),
),
);
}
/**
* Perform alterations on Field API storage types.
*
* @param $info
* Array of informations on storage types exposed by
* hook_field_field_storage_info() implementations.
*/
function hook_field_storage_info_alter(&$info) {
// Add a setting to a storage type.
$info['field_sql_storage']['settings'] += array(
'mymodule_additional_setting' => 'default value',
);
}
/**
* Load field data for a set of objects.
*
......@@ -1107,15 +1149,15 @@ function hook_field_attach_delete_bundle($bundle, $instances) {
* FIELD_LOAD_CURRENT to load the most recent revision for all
* fields, or FIELD_LOAD_REVISION to load the version indicated by
* each object.
* @param $skip_fields
* An array keyed by field ids whose data has already been loaded and
* therefore should not be loaded again. The values associated to these keys
* are not specified.
* @param $fields
* An array listing the fields to be loaded. The keys of the array are field
* ids, the values of the array are the object ids (or revision ids,
* depending on the $age parameter) to be loaded for each field.
* @return
* Loaded field values are added to $objects. Fields with no values should be
* set as an empty array.
*/
function hook_field_storage_load($obj_type, $objects, $age, $skip_fields) {
function hook_field_storage_load($obj_type, $objects, $age, $fields) {
}
/**
......@@ -1128,12 +1170,11 @@ function hook_field_storage_load($obj_type, $objects, $age, $skip_fields) {
* @param $op
* FIELD_STORAGE_UPDATE when updating an existing object,
* FIELD_STORAGE_INSERT when inserting a new object.
* @param $skip_fields
* An array keyed by field ids whose data has already been written and
* therefore should not be written again. The values associated to these keys
* are not specified.
* @param $fields
* An array listing the fields to be written. The keys and values of the
* array are field ids.
*/
function hook_field_storage_write($obj_type, $object, $op, $skip_fields) {
function hook_field_storage_write($obj_type, $object, $op, $fields) {
}
/**
......@@ -1143,8 +1184,11 @@ function hook_field_storage_write($obj_type, $object, $op, $skip_fields) {
* The entity type of object, such as 'node' or 'user'.
* @param $object
* The object on which to operate.
* @param $fields
* An array listing the fields to delete. The keys and values of the
* array are field ids.
*/
function hook_field_storage_delete($obj_type, $object) {
function hook_field_storage_delete($obj_type, $object, $fields) {
}
/**
......@@ -1159,8 +1203,11 @@ function hook_field_storage_delete($obj_type, $object) {
* The object on which to operate. The revision to delete is
* indicated by the object's revision id property, as identified by
* hook_fieldable_info() for $obj_type.
* @param $fields
* An array listing the fields to delete. The keys and values of the
* array are field ids.
*/
function hook_field_storage_delete_revision($obj_type, $object) {
function hook_field_storage_delete_revision($obj_type, $object, $fields) {
}
/**
......@@ -1185,26 +1232,6 @@ function hook_field_storage_delete_revision($obj_type, $object) {
function hook_field_storage_query($field_name, $conditions, $count, &$cursor = NULL, $age) {
}
/**
* Act on creation of a new bundle.
*
* @param $bundle
* The name of the bundle being created.
*/
function hook_field_storage_create_bundle($bundle) {
}
/**
* Act on a bundle being renamed.
*
* @param $bundle_old
* The old name of the bundle.
* @param $bundle_new
* The new name of the bundle.
*/
function hook_field_storage_rename_bundle($bundle_old, $bundle_new) {
}
/**
* Act on creation of a new field.
*
......@@ -1217,21 +1244,19 @@ function hook_field_storage_create_field($field) {
/**
* Act on deletion of a field.
*
* @param $field_name
* The name of the field being deleted.
* @param $field
* The field being deleted.
*/
function hook_field_storage_delete_field($field_name) {
function hook_field_storage_delete_field($field) {
}
/**
* Act on deletion of a field instance.
*
* @param $field_name
* The name of the field in the new instance.
* @param $bundle
* The name of the bundle in the new instance.
* @param $instance
* The instance being deleted.
*/
function hook_field_storage_delete_instance($field_name, $bundle) {
function hook_field_storage_delete_instance($instance) {
}
/**
......
......@@ -44,17 +44,16 @@ class FieldQueryException extends FieldException {}
* @{
* Implement a storage engine for Field API data.
*
* The Field Attach API uses the Field Storage API to perform all
* "database access". Each Field Storage API hook function defines a
* primitive database operation such as read, write, or delete. The
* default field storage module, field_sql_storage.module, uses the
* local SQL database to implement these operations, but alternative
* field storage engines can choose to represent the data in SQL
* differently or use a completely different storage mechanism such as
* a cloud-based database.
* The Field Attach API uses the Field Storage API to perform all "database
* access". Each Field Storage API hook function defines a primitive database
* operation such as read, write, or delete. The default field storage module,
* field_sql_storage.module, uses the local SQL database to implement these
* operations, but alternative field storage backends can choose to represent
* the data in SQL differently or use a completely different storage mechanism
* such as a cloud-based database.
*
* The Drupal system variable field_storage_module identifies the
* field storage module to use.
* Each field defines which storage backend it uses. The Drupal system variable
* 'field_default_storage' identifies the storage backend used by default.
*/
/**
......@@ -525,9 +524,8 @@ function field_attach_form($obj_type, $object, &$form, &$form_state, $langcode =
* - 'deleted': If TRUE, the function will operate on deleted fields
* as well as non-deleted fields. If unset or FALSE, only
* non-deleted fields are operated on.
* @returns
* Loaded field values are added to $objects. Fields with no values should be
* set as an empty array.
* @return
* Loaded field values are added to $objects.
*/
function field_attach_load($obj_type, $objects, $age = FIELD_LOAD_CURRENT, $options = array()) {
$load_current = $age == FIELD_LOAD_CURRENT;
......@@ -578,7 +576,7 @@ function field_attach_load($obj_type, $objects, $age = FIELD_LOAD_CURRENT, $opti
if ($queried_objects) {
// The invoke order is:
// - hook_field_attach_pre_load()
// - storage engine's hook_field_storage_load()
// - storage backend's hook_field_storage_load()
// - field-type module's hook_field_load()
// - hook_field_attach_load()
......@@ -590,9 +588,39 @@ function field_attach_load($obj_type, $objects, $age = FIELD_LOAD_CURRENT, $opti
$function($obj_type, $queried_objects, $age, $skip_fields, $options);
}
// Invoke the storage engine's hook_field_storage_load(): the field storage
// engine loads the rest.
module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_load', $obj_type, $queried_objects, $age, $skip_fields, $options);
// Collect the storage backends used by the remaining fields in the objects.
$storages = array();
foreach ($queried_objects as $obj) {
list($id, $vid, $bundle) = field_extract_ids($obj_type, $obj);
if ($options['deleted']) {
$instances = field_read_instances(array('bundle' => $bundle), array('include_deleted' => $options['deleted']));
}
else {
$instances = field_info_instances($bundle);
}
foreach ($instances as $instance) {
if (!isset($options['field_id']) || $options['field_id'] == $instance['field_id']) {
$field_name = $instance['field_name'];
$field_id = $instance['field_id'];
// Make sure all fields are present at least as empty arrays.
if (!isset($queried_objects[$id]->{$field_name})) {
$queried_objects[$id]->{$field_name} = array();
}
// Collect the storage backend if the field has not been loaded yet.
if (!isset($skip_fields[$field_id])) {
$field = field_info_field_by_id($field_id);
$storages[$field['storage']['type']][$field_id][] = $load_current ? $id : $vid;
}
}
}
}
// Invoke hook_field_storage_load() on the relevant storage backends.
foreach ($storages as $storage => $fields) {
$storage_info = field_info_storage_types($storage);
module_invoke($storage_info['module'], 'field_storage_load', $obj_type, $queried_objects, $age, $fields, $options);
}
// Invoke field-type module's hook_field_load().
_field_invoke_multiple('load', $obj_type, $queried_objects, $age, $options);
......@@ -791,6 +819,8 @@ function field_attach_insert($obj_type, $object) {
_field_invoke_default('insert', $obj_type, $object);
_field_invoke('insert', $obj_type, $object);
list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
// Let other modules act on inserting the object, accumulating saved
// fields along the way.
$skip_fields = array();
......@@ -799,10 +829,26 @@ function field_attach_insert($obj_type, $object) {
$function($obj_type, $object, $skip_fields);
}
// Field storage module saves any remaining unsaved fields.
module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_write', $obj_type, $object, FIELD_STORAGE_INSERT, $skip_fields);
// Collect the storage backends used by the remaining fields in the objects.
$storages = array();
foreach (field_info_instances($bundle) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$field_name = $field['field_name'];
if (!empty($object->$field_name)) {
// Collect the storage backend if the field has not been written yet.
if (!isset($skip_fields[$field_id])) {
$storages[$field['storage']['type']][$field_id] = $field_id;
}
}
}
// Field storage backends save any remaining unsaved fields.
foreach ($storages as $storage => $fields) {
$storage_info = field_info_storage_types($storage);
module_invoke($storage_info['module'], 'field_storage_write', $obj_type, $object, FIELD_STORAGE_INSERT, $fields);
}
list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
if ($cacheable) {
cache_clear_all("field:$obj_type:$id", 'cache_field');
}
......@@ -819,6 +865,8 @@ function field_attach_insert($obj_type, $object) {
function field_attach_update($obj_type, $object) {
_field_invoke('update', $obj_type, $object);
list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
// Let other modules act on updating the object, accumulating saved
// fields along the way.
$skip_fields = array();
......@@ -827,10 +875,30 @@ function field_attach_update($obj_type, $object) {
$function($obj_type, $object, $skip_fields);
}
// Field storage module saves any remaining unsaved fields.
module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_write', $obj_type, $object, FIELD_STORAGE_UPDATE, $skip_fields);
// Collect the storage backends used by the remaining fields in the objects.
$storages = array();
foreach (field_info_instances($bundle) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$field_name = $field['field_name'];
// Leave the field untouched if $object comes with no $field_name property,
// but empty the field if it comes as a NULL value or an empty array.
// Function property_exists() is slower, so we catch the more frequent
// cases where it's an empty array with the faster isset().
if (isset($object->$field_name) || property_exists($object, $field_name)) {
// Collect the storage backend if the field has not been written yet.
if (!isset($skip_fields[$field_id])) {
$storages[$field['storage']['type']][$field_id] = $field_id;
}
}
}
// Field storage backends save any remaining unsaved fields.
foreach ($storages as $storage => $fields) {
$storage_info = field_info_storage_types($storage);
module_invoke($storage_info['module'], 'field_storage_write', $obj_type, $object, FIELD_STORAGE_UPDATE, $fields);
}
list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
if ($cacheable) {
cache_clear_all("field:$obj_type:$id", 'cache_field');
}
......@@ -847,7 +915,22 @@ function field_attach_update($obj_type, $object) {
*/
function field_attach_delete($obj_type, $object) {
_field_invoke('delete', $obj_type, $object);
module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete', $obj_type, $object);
list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
// Collect the storage backends used by the fields in the objects.
$storages = array();
foreach (field_info_instances($bundle) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$storages[$field['storage']['type']][$field_id] = $field_id;
}
// Field storage backends delete their data.
foreach ($storages as $storage => $fields) {
$storage_info = field_info_storage_types($storage);
module_invoke($storage_info['module'], 'field_storage_delete', $obj_type, $object, $fields);
}
// Let other modules act on deleting the object.
foreach (module_implements('field_attach_delete') as $module) {
......@@ -855,7 +938,6 @@ function field_attach_delete($obj_type, $object) {
$function($obj_type, $object);
}
list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
if ($cacheable) {
cache_clear_all("field:$obj_type:$id", 'cache_field');
}
......@@ -872,7 +954,22 @@ function field_attach_delete($obj_type, $object) {
*/
function field_attach_delete_revision($obj_type, $object) {
_field_invoke('delete_revision', $obj_type, $object);
module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete_revision', $obj_type, $object);
list($id, $vid, $bundle, $cacheable) = field_extract_ids($obj_type, $object);
// Collect the storage backends used by the fields in the objects.
$storages = array();
foreach (field_info_instances($bundle) as $instance) {
$field = field_info_field_by_id($instance['field_id']);
$field_id = $field['id'];
$storages[$field['storage']['type']][$field_id] = $field_id;
}
// Field storage backends delete their data.
foreach ($storages as $storage => $fields) {
$storage_info = field_info_storage_types($storage);
module_invoke($storage_info['module'], 'field_storage_delete_revision', $obj_type, $object, $fields);
}
// Let other modules act on deleting the revision.
foreach (module_implements('field_attach_delete_revision') as $module) {
......@@ -977,7 +1074,8 @@ function field_attach_query($field_id, $conditions, $count, &$cursor = NULL, $ag
}
// If the request hasn't been handled, let the storage engine handle it.
if (!$skip_field) {
$function = variable_get('field_storage_module', 'field_sql_storage') . '_field_storage_query';
$field = field_info_field_by_id($field_id);
$function = $field['storage']['module'] . '_field_storage_query';
$results = $function($field_id, $conditions, $count, $cursor, $age);
}
......@@ -1194,8 +1292,6 @@ function field_attach_prepare_translation($node) {
* The name of the newly created bundle.
*/
function field_attach_create_bundle($bundle) {
module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_create_bundle', $bundle);
// Clear the cache.
field_cache_clear();
......@@ -1214,7 +1310,6 @@ function field_attach_create_bundle($bundle) {
* The new name of the bundle.
*/
function field_attach_rename_bundle($bundle_old, $bundle_new) {
module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_rename_bundle', $bundle_old, $bundle_new);
db_update('field_config_instance')
->fields(array('bundle' => $bundle_new))
->condition('bundle', $bundle_old)
......@@ -1243,12 +1338,15 @@ function field_attach_rename_bundle($bundle_old, $bundle_new) {
* The bundle to delete.
*/
function field_attach_delete_bundle($bundle) {
// Delete the instances themseves
// First, delete the instances themseves.
$instances = field_info_instances($bundle);
foreach ($instances as $instance) {
field_delete_instance($instance['field_name'], $bundle);
}
// Clear the cache.
field_cache_clear();
// Let other modules act on deleting the bundle.
foreach (module_implements('field_attach_delete_bundle') as $module) {
$function = $module . '_field_attach_delete_bundle';
......
......@@ -76,6 +76,20 @@
* - settings (array)
* A sub-array of key/value pairs of field-type-specific settings. Each
* field type module defines and documents its own field settings.
* - storage (array)
* A sub-array of key/value pairs identifying the storage backend to use for
* the for the field.
* - type (string)
* The storage backend used by the field. Storage backends are defined
* by modules that implement hook_field_storage_info().
* - module (string, read-only)
* The name of the module that implements the storage backend.
* - active (integer, read-only)
* TRUE if the module that implements the storage backend is currently
* enabled, FALSE otherwise.
* - settings (array)
* A sub-array of key/value pairs of settings. Each storage backend
* defines and documents its own settings.
*
* Field Instance objects are (currently) represented as an array of
* key/value pairs. The object properties are:
......@@ -196,6 +210,11 @@
* carefully, for it might seriously affect the site's performance.
* - settings: each omitted setting is given the default value defined in
* hook_field_info().
* - storage:
* - type: the storage backend specified in the 'field_default_storage'
* system variable.
* - settings: each omitted setting is given the default value specified in
* hook_field_storage_info().
* @return
* The $field structure with the id property filled in.
* @throw
......@@ -223,12 +242,6 @@ function field_create_field($field) {
array('%name' => $field['field_name'])));
}
// Check that the field type is known.
$field_type = field_info_field_types($field['type']);
if (!$field_type) {
throw new FieldException(t('Attempt to create a field of unknown type %type.', array('%type' => $field['type'])));
}
// Ensure the field name is unique over active and disabled fields.
// We do not care about deleted fields.
$prior_field = field_read_field($field['field_name'], array('include_inactive' => TRUE));
......@@ -253,22 +266,40 @@ function field_create_field($field) {
'translatable' => FALSE,
'locked' => FALSE,
'settings' => array(),
'storage' => array(),
'deleted' => 0,
);
// Check that the field type is known.
$field_type = field_info_field_types($field['type']);
if (!$field_type) {
throw new FieldException(t('Attempt to create a field of unknown type %type.', array('%type' => $field['type'])));
}
// Create all per-field-type properties (needed here as long as we have
// settings that impact column definitions).
$field['settings'] += field_info_field_settings($field['type']);
$field['module'] = $field_type['module'];
$field['active'] = 1;
$field['deleted'] = 0;
// Provide default storage.
$field['storage'] += array(
'type' => variable_get('field_storage_default', 'field_sql_storage'),
'settings' => array(),
);
// Check that the storage type is known.
$storage_type = field_info_storage_types($field['storage']['type']);
if (!$storage_type) {
throw new FieldException(t('Attempt to create a field with unknown storage type %type.', array('%type' => $field['storage']['type'])));
}
// Provide default storage settings.
$field['storage']['settings'] += field_info_storage_settings($field['storage']['type']);
$field['storage']['module'] = $storage_type['module'];
$field['storage']['active'] = 1;
// Collect storage information.
$schema = (array) module_invoke($field['module'], 'field_schema', $field);
$schema += array('columns' => array(), 'indexes' => array());
// 'columns' are hardcoded in the field type.
$field['columns'] = $schema['columns'];
// 'indexes' can be both hardcoded in the field type, and specified in the
// incoming $field definition.
$field += array(
......@@ -280,19 +311,32 @@ function field_create_field($field) {
// have its own column and is not automatically populated when the field is
// read.
$data = $field;
unset($data['columns'], $data['field_name'], $data['type'], $data['locked'], $data['module'], $data['cardinality'], $data['active'], $data['deleted']);
$field['data'] = $data;
unset($data['columns'], $data['field_name'], $data['type'], $data['active'], $data['module'], $data['storage_type'], $data['storage_active'], $data['storage_module'], $data['locked'], $data['cardinality'], $data['deleted']);
// Store the field and create the id.
drupal_write_record('field_config', $field);
$record = array(
'field_name' => $field['field_name'],
'type' => $field['type'],
'module' => $field['module'],
'active' => $field['active'],
'storage_type' => $field['storage']['type'],
'storage_module' => $field['storage']['module'],
'storage_active' => $field['storage']['active'],
'locked' => $field['locked'],
'data' => $data,
'cardinality' => $field['cardinality'],
'deleted' => $field['deleted'],
);
// The 'data' property is not part of the public field record.
unset($field['data']);
// Store the field and get the id back.
drupal_write_record('field_config', $record);
$field['id'] = $record['id'];
// Invoke hook_field_storage_create_field after the field is
// complete (e.g. it has its id).
try {
module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_create_field', $field);
// Invoke hook_field_storage_create_field after
// drupal_write_record() sets the field id.
module_invoke($storage_type['module'], 'field_storage_create_field', $field);
}
catch (Exception $e) {
// If storage creation failed, remove the field_config record before
......@@ -344,10 +388,13 @@ function field_update_field($field) {
$field += $prior_field;
$field['settings'] += $prior_field['settings'];
// Field type cannot be changed.
// Some updates are always disallowed.
if ($field['type'] != $prior_field['type']) {
throw new FieldException("Cannot change an existing field's type.");
}
if ($field['storage']['type'] != $prior_field['storage']['type']) {
throw new FieldException("Cannot change an existing field's storage type.");
}
// Collect the new storage information, since what is in
// $prior_field may no longer be right.
......@@ -442,7 +489,9 @@ function field_read_fields($params = array(), $include_additional = array()) {
$query->condition($key, $value);
}
if (!isset($include_additional['include_inactive']) || !$include_additional['include_inactive']) {
$query->condition('fc.active', 1);
$query
->condition('fc.active', 1)
->condition('fc.storage_active', 1);
}
$include_deleted = (isset($include_additional['include_deleted']) && $include_additional['include_deleted']);
if (!$include_deleted) {
......@@ -451,11 +500,20 @@ function field_read_fields($params = array(), $include_additional = array()) {
$fields = array();
$results = $query->execute();
foreach ($results as $field) {
// Extract serialized data.
$data = unserialize($field['data']);
unset($field['data']);
$field += $data;
foreach ($results as $record) {
$field = unserialize($record['data']);
$field['id'] = $record['id'];
$field['field_name'] = $record['field_name'];
$field['type'] = $record['type'];
$field['module'] = $record['module'];
$field['active'] = $record['active'];
$field['storage']['type'] = $record['storage_type'];
$field['storage']['module'] = $record['storage_module'];
$field['storage']['active'] = $record['storage_active'];