party.primary_fields.inc in Party 7
Primary field related functions and callbacks.
File
includes/party.primary_fields.incView source
<?php
/**
* @file
* Primary field related functions and callbacks.
*/
/**
* Helper class for primary fields.
*
* Primary fields provide a means to cache data from potentially multiple data
* sets and sources into a single field on the party. This can then be used for
* searching, indexing etc.
*
* An example of this is the party's label. Parties with different hats may have
* different data sets, for example an individual and organisation. While the
* individual's name is stored in a name field on the individual profile, the
* organisation's name could be stored in a text field on the organisation
* profile. The label primary fields can be set to pull from both and will run
* through all the potential sources until a valid (non-NULL) value has been
* found.
*
* In addition, sources can use callbacks to deal with type conversion or
* rendering. For example, the User's UID has a callback that will convert a UID
* into the return from format_username().
*
* Primary fields are configured from the 'Manage fields' page for a party. Some
* properties are provided, such as label and email and other may be added,
* either by other modules or through the Field UI.
*
* To add a primary field through the Field UI, add a field of the desired type
* and use the 'Primary field' widget type. This will then allow you to select
* sources that match the target primary field's type.
*
* Modules can add primary fields using hook_entity_property_info(). Any
* property with the key 'party primary field' set to TRUE will appear on the
* party's Manage fields page for configuration. Properties used as primary
* fields must have a 'setter callback' defined or an Exception will be thrown.
*
* Modules wishing to configure primary fields on install should add their
* settings to the 'party_primary_fields' variable. See
* PartyPrimaryFields::$fields for the data structure used. Modules can also
* implement hook_party_primary_fields_fields_alter() to add primary fields from
* other sources or to override the configured settings for anything using the
* built in configuration.
*
* Sources can be added by declaring a property on the party or a data set via
* hook_entity_property_info(). It is advised that 'type' is set for data type
* matching. There will need to be a 'getter callback', otherwise an Exception
* will be thrown.
*
* Callbacks can be added to detected properties using
* hook_party_primary_fields_sources_alter(). See PartyPrimaryFields::$sources
* for the data structure.
*
* @see hook_party_primary_fields_fields_alter()
* @see hook_party_primary_fields_sources_alter()
* @see party_field_extra_fields()
*/
class PartyPrimaryFields {
/**
* Cache of primary fields.
* @var array
* An array of primary field information. Outer keys are target properties.
* Values are an array of sources each of which are an array of:
* - data_set: The data set name the property is on. 'party' is a special case
* which allows a party to refer to itself.
* - property: The property we are pulling from.
* - value: Optionally provide a sub-key of the property to use. The structure
* must be described with hook_entity_property_info().
* - callback: Optionally the key of a callback to use. The callback must be
* defined in the source's callbacks.
* - weight: The weight of this source. Lower weights are processed first.
*/
protected static $fields;
/**
* Cache of primary field sources.
* @var array
* An array of all the sources, grouped by data set. Outer keys are the data
* set name. Values are an array of:
* - All keys from self::$data_sets.
* - sources: An array of sources contained in this data set. Keys are
* an identifier (normally data_set_name:property[:value] who's values are
* arrays of:
* - label: The label of the property.
* - option label: The label and name of the property, suitable for options.
* - type: The data type as known by the entity property info.
* - field: Whether this is a field.
* - data_set: The data set this property is on.
* - property: The property on the data set entity.
* - value: The key of the structure of the property. If NULL, the property
* is taken as whole.
* - callbacks: Array of possible callbacks. Each one should have a unique
* key and it's value should be and array of:
* - label: Human readable label.
* - callback: A callable. It will receive the following arguments:
* - value: The value of the source field.
* - target: The name of the target property/field.
* - info: Metadata about the target property.
* - file: A file to include for the callback. This can either be a
* a string path relative to DRUPAL_ROOT or an array of (see
* module_load_include() arguments for key descriptions):
* - type
* - module
* - name
* A recommended location for callbacks is MODULE.party.inc.
* - type: The data type this callback returns.
*/
protected static $sources;
/**
* Cache of data set info including our spoofed party.
* @var array
* An array of data set info outer keyed by data set. Key/value pairs are:
* - set_name: The name of the data set. 'party' is a special case which
* allows a party to refer to itself.
* - entity type: The entity type of the data set.
* - entity bundle: The bundle of the data set entity.
* - label: The label of the data set.
* - option label: The label and name of the data set, suitable for options.
*/
protected static $data_sets;
/**
* Clears the field and source cache.
*/
public static function clearCaches() {
// Clear our cache.
cache_clear_all('party:primary_fields:', 'cache', TRUE);
// Reset our statics.
self::$fields = NULL;
self::$sources = NULL;
}
/**
* Get hold of primary field information.
*
* Primary fields are used to cache information from multiple potential data set
* sources into a single property (or field) on the party.
*
* The Primary Field widget allows you to create primary fields via the Field UI
* by adding fields to the party with the Primary Field widget.
*
* @param string $target
* Optionally provide a target property to retrieve the sources for.
*
* @return array
* If $target is NULL, outer keys are target properties. See
* PartyPrimaryFields::$primary_fields for a description of the structure.
*/
public static function getFields($target = NULL) {
if (!isset(self::$fields)) {
self::buildFields();
}
if ($target) {
return isset(self::$fields[$target]) ? self::$fields[$target] : array();
}
return self::$fields;
}
/**
* Get hold of primary field source information.
*
* @param string $data_set
* Optionally provide a data set retrieve the sources for.
*
* @return FALSE||array
* Returns source information or FALSE if the requested information is not
* available. See PartyPrimaryFields::$sources for the data structure. If
* $property is given this will be a single source. Otherwise if $data_set
* is given it will be an array of sources. If $data_set is NULL, the entire
* source array will be returned.
*/
public static function getSources($data_set = NULL) {
if (!isset(self::$sources)) {
self::buildSources();
}
// If no data set is given, return the whole lot.
if (!isset($data_set)) {
return self::$sources;
}
return isset(self::$sources[$data_set]) ? self::$sources[$data_set]['sources'] : FALSE;
}
/**
* Get hold of primary field sources by type.
*
* @param string|array $types
* Either a single type or an array of types we want suitable sources for.
* @param bool $all_types
* Indicate whether you want to check against the 'all types' key which
* includes callback types Defaults to TRUE.
*
* @return array
* An array of source information filtered by type. See
* PartyPrimaryFields::$sources for the data structure.
*/
public static function getSourcesByType($types, $all_types = TRUE) {
// Get our full list of sources.
$sources = self::getSources();
// Loop over our sources and remove items we don't want.
foreach ($sources as &$data_set) {
foreach ($data_set['sources'] as $key => $info) {
if (!self::sourceTypeMatch($types, $info, $all_types)) {
unset($data_set['sources'][$key]);
}
}
}
// Run our cleanup.
self::cleanSources($sources);
return $sources;
}
/**
* Get the info of a particular source.
*
* @param string|array $source
* Either a data set name or a partial source info array for the required
* source. See PartyPrimaryFields::$sources for the data structure.
* 'data_set' and 'property' are required.
* @param string $property
* Optionally provide a property to retrieve the source info for. $data_set
* is required if $property is given. Only used if $source is a data set
* string.
* @param string $value
* Optionally provide a value on a property to retrieve the source info for.
* $property is required if $value is given. Only used if $source is a data
* set string.
*
* @return FALSE||array
* The full source info for the given partial source or FALSE if not
* available.
*/
public static function getSource($source, $property = NULL, $value = NULL) {
if (is_string($source)) {
$source = array(
'data_set' => $source,
'property' => $property,
'value' => $value,
);
}
// Get hold of sources on the data set.
$sources = self::getSources($source['data_set']);
if (!$sources) {
return FALSE;
}
// Build our key for returning the source info.
$key = self::getSourceKey($source);
return isset($sources[$key]) ? $sources[$key] : FALSE;
}
/**
* Build a key for a given source.
*
* @param array $source_info
* The source info for the required source. See PartyPrimaryFields::$sources
* for the data structure. 'data_set' and 'property' are required.
* @param bool $ignore_value
* Optionally ignore the value key. This can be used to find the parent
* property of a source that uses a value key.
*
* @return string
* A key for the source property.
*/
public static function getSourceKey($source_info, $ignore_value = FALSE) {
// Get our information for the key in the right order.
$key_info = array(
'data_set' => $source_info['data_set'],
'property' => $source_info['property'],
);
if (!$ignore_value && $source_info['value']) {
$key_info['value'] = $source_info['value'];
}
// Build and return our key.
return implode(':', $key_info);
}
/**
* Retrieve property information for a specific entity/bundle.
*
* @param string $entity_type
* The entity type we want information for.
* @param string $bundle
* The bundle we want information for.
* @param string $property
* Optionally only return information for a specific proprety.
*
* @return FALSE|array
* FALSE if the requested information is not available. If $property is
* given, the metadata for that property, otherwise an array of property
* metadata keyed by property.
*/
public static function getPropertyInfo($entity_type, $bundle, $property = NULL) {
$entity = entity_create_stub_entity($entity_type, array(
0 => NULL,
2 => $bundle,
));
$wrapper = entity_metadata_wrapper($entity_type, $entity);
$info = array();
foreach (array_keys($wrapper
->getPropertyInfo()) as $key) {
$info[$key] = $wrapper
->getPropertyInfo($key);
}
if ($property) {
return isset($info[$property]) ? $info[$property] : FALSE;
}
else {
return $info;
}
}
/**
* Check whether the types match.
*
* @param string|array $types
* Either a single type or an array of types we want suitable sources for.
* @param array $source
* The source info for the source we are checking. See
* PartyPrimaryFields::$sources for the data structure.
* @param bool $all_types
* Indicate whether you want to check against the 'all types' key which
* includes callback types. Defaults to TRUE.
*
* @return bool
* Whether the source matches the given type(s).
*/
public static function sourceTypeMatch($types, $source, $all_types = TRUE) {
// Make sure $types is an array.
$types = (array) $types;
// If $all_types we want to check the 'all types' key which is an array.
if ($all_types) {
return (bool) array_intersect($types, $source['all types']);
}
else {
return in_array($source['type'], $types);
}
}
/**
* Get data set info including the spoofed party.
*
* @param string $data_set
* Optionally return the info for a single data set.
*
* @return FALSE|array
* Data set info array. If $data_set is not given, it is outer keyed by data
* set name. If it is set and it is not available, FALSE is returned.
*/
public static function getDataSetInfo($data_set = NULL) {
if (!isset(self::$data_sets)) {
self::$data_sets = array(
'party' => array(
'set_name' => 'party',
'label' => 'Party',
'entity type' => 'party',
'entity bundle' => 'party',
),
);
self::$data_sets += party_get_data_set_info();
}
if ($data_set) {
return isset(self::$data_sets[$data_set]) ? self::$data_sets[$data_set] : FALSE;
}
return self::$data_sets;
}
/**
* Execute a callback on a value.
*/
public static function executeCallback($value, $source, $target) {
// Check we want to execute callback.
if (isset($source['callback'])) {
// Get our source info and check the callback exists.
$source_info = self::getSource($source);
if (isset($source_info['callbacks'][$source['callback']])) {
// Get the callback info.
$callback = $source_info['callbacks'][$source['callback']];
// Load an include if necessary.
if (isset($callback['file'])) {
if (is_array($callback['file'])) {
module_load_include($callback['file']['type'], $callback['file']['module'], $callback['file']['name']);
}
else {
require_once DRUPAL_ROOT . '/' . $callback['file'];
}
}
// If the callback is valid, execute it.
if (is_callable($callback['callback'])) {
$target_info = self::getPropertyInfo('party', 'party', $target);
$value = call_user_func($callback['callback'], $value, $target, $target_info, $source_info);
}
}
}
return $value;
}
/**
* Set up a source form under the given element.
*
* This is typically called on a fieldset.
*
* @param array $element
* The container or fieldset element or form we are adding the source table
* to. In addition to it's normal keys, this element should have:
* - #target: The name of the target property.
* - #source_types: Optionally provide a filter for source types. If not
* provided it will be worked out from the given target property.
* - #parents: As this function does not deal with any submission of the
* user's input, #parents should be set to indicate where the submitted
* data should appear in $form_state['values']. If not set, array() will
* be used resulting in $form_state['values'] having 'sources' as a root
* key.
* @param array $form_state
* The form state.
* @param array $form
* The whole form.
*/
public static function sourceForm(&$element, &$form_state, &$form) {
// Set up our include and submission handler.
form_load_include($form_state, 'inc', 'party', 'party.admin');
$form['#submit'][] = 'party_primary_fields_clear_caches';
// Make sure the element has the required properties.
$element['#tree'] = TRUE;
if (!isset($element['#parents'])) {
$element['#parents'] = array();
}
// Get hold of info about the target.
if (!array_key_exists('#source_types', $element)) {
$target_info = self::getPropertyInfo('party', 'party', $element['#target']);
$element['#source_types'] = isset($target_info['type']) ? array(
$target_info['type'],
) : NULL;
}
elseif (is_string($element['#source_types'])) {
$element['#source_types'] = (array) $element['#source_types'];
}
// Get hold of the current state of the sources.
if (isset($form_state['values'])) {
$sources = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
}
if (!isset($sources) || !is_array($sources)) {
$sources = $element['#default_value'];
}
// Make sure the sources are sorted by weight.
uasort($sources, 'drupal_sort_weight');
// Construct our draggable table.
$element['table'] = array(
'#theme' => 'table',
'#header' => array(
'property_desc' => t('Property'),
'data_set_desc' => t('Data set'),
'callback' => t('Callback'),
'weight' => t('Weight'),
'remove' => t('Remove'),
),
'#empty' => t('No sources are selects.'),
'#attributes' => array(
'class' => array(
'field-ui-overview',
),
'id' => 'field-sources',
),
'#parents' => $element['#parents'],
'#pre_render' => array(
'party_primary_fields_source_table_pre_render',
),
);
drupal_add_tabledrag('field-sources', 'order', 'sibling', 'source-weight');
// Build our rows which will be put into the table after building.
foreach ($sources as $key => $source) {
// Get hold of our source info.
$data_set_info = self::getDataSetInfo($source['data_set']);
$source_info = self::getSource($source);
$property_key = self::getSourceKey($source);
$property_key = substr($property_key, strpos($property_key, ':') + 1);
$row = array(
'#element_validate' => array(
'party_primary_fields_source_table_source_cleanup',
),
'data_set' => array(
'#type' => 'value',
'#value' => $source['data_set'],
),
'property' => array(
'#type' => 'value',
'#value' => $source['property'],
),
'value' => array(
'#type' => 'value',
'#value' => $source['value'],
),
'property_desc' => array(
'#markup' => format_string('@label <small>[@name]</small>', array(
'@label' => $source_info['label'],
'@name' => $property_key,
)),
),
'data_set_desc' => array(
'#markup' => format_string('@label <small>[@name]</small>', array(
'@label' => $data_set_info['label'],
'@name' => $data_set_info['set_name'],
)),
),
'weight' => array(
'#type' => 'weight',
'#default_value' => $source['weight'],
'#delta' => 50,
'#title_display' => 'invisible',
'#title' => t('Weight for @label', array(
'@label' => $source_info['label'],
)),
'#attributes' => array(
'class' => array(
'source-weight',
),
),
),
'callback' => array(),
'remove' => array(
'#type' => 'submit',
'#value' => t('Remove'),
'#name' => 'remove_source:' . $key,
'#submit' => array(
'party_primary_fields_source_table_submit',
),
),
);
// Find any valid callbacks.
$callbacks = array();
if (!empty($source_info['callbacks'])) {
foreach ($source_info['callbacks'] as $key => $info) {
if (!isset($element['#source_types']) || in_array($info['type'], $element['#source_types'])) {
$callbacks[$key] = $info['label'];
}
}
}
// If we have valid callbacks, provide options.
$row['callback'] = array(
'#type' => 'select',
'#title' => t('Callback'),
'#title_display' => 'invisible',
'#options' => $callbacks,
'#default_value' => isset($source['callback']) ? $source['callback'] : NULL,
);
// If the source is valid on it's own, provide an empty option.
if (empty($callbacks) || !isset($element['#source_types']) || in_array($source_info['type'], $element['#source_types'])) {
$row['callback']['#empty_option'] = t('No callback');
}
$element['table'][$key] = $row;
}
// Find suitable sources.
$suitable_sources = PartyPrimaryFields::getSourcesByType($element['#source_types']);
// Build our options list.
$options = array();
foreach ($suitable_sources as $data_set) {
$set_options = array();
foreach ($data_set['sources'] as $source) {
$key = $source['data_set'] . ':' . $source['property'];
if (isset($source['value'])) {
$key .= ':' . $source['value'];
}
$set_options[$key] = $source['option label'];
}
$options[$data_set['option label']] = $set_options;
}
$element['add_source'] = array(
'#type' => 'container',
'#tree' => FALSE,
'#weight' => 99,
'#attributes' => array(
'class' => array(
'container-inline',
),
),
'add_source_property' => array(
'#type' => 'select',
'#title' => t('Source property'),
'#options' => $options,
'#empty_option' => ' - ' . t('Select property') . ' - ',
),
'add_source_submit' => array(
'#type' => 'submit',
'#value' => t('Add source'),
'#limit_validation_errors' => array(
array(
'add_source_property',
),
$element['#parents'],
),
'#validate' => array(
'party_primary_fields_source_table_validate',
),
'#submit' => array(
'party_primary_fields_source_table_submit',
),
),
);
}
/**
* Build the primary field information.
*
* @param bool $reset
* Whether the cache should be reset.
*/
protected static function buildFields($reset = FALSE) {
// If we are resetting, clear our static cache.
if ($reset) {
self::$fields = NULL;
}
else {
if ($cache = cache_get('party:primary_fields:fields')) {
self::$fields = $cache->data;
}
}
// If we have no information, build it.
if (!isset(self::$fields)) {
// Label and email are provided by custom UIs and stored in a variable which
// other modules could potentially add to.
self::$fields = variable_get('party_primary_fields', array());
// Retrieve from primary field widgets.
foreach (field_info_instances('party', 'party') as $instance) {
if ($instance['widget']['type'] == 'party_primary_field') {
self::$fields[$instance['field_name']] = $instance['widget']['settings']['sources'];
}
}
// Allow other modules to alter it.
drupal_alter('party_primary_fields_fields', self::$fields);
// Remove any invalid targets.
$party_property_info = self::getPropertyInfo('party', 'party');
foreach (array_keys(self::$fields) as $target) {
if (!isset($party_property_info[$target])) {
// Remove and log a watchdog warning.
watchdog('party', 'The target for the primary field %target does not exist.', array(
'%target' => $target,
), WATCHDOG_WARNING);
unset(self::$fields[$target]);
}
}
// Make sure the sources are valid and sorted by weight.
$valid_sources = self::getSources();
foreach (self::$fields as $target => &$sources) {
// If we don't have any sources, skip it all.
if (!is_array($sources)) {
unset(self::$fields[$target]);
continue;
}
// Remove any invalid sources.
foreach ($sources as $key => &$source) {
// Make sure our sources have all the keys.
$source += array(
'data_set' => NULL,
'property' => NULL,
'value' => NULL,
'callback' => NULL,
'weight' => 0,
);
// Check that the data set exists.
if (!isset($valid_sources[$source['data_set']])) {
watchdog('party', 'The source data set %data_set for the primary field %target does not exist.', array(
'%data_set' => $source['data_set'],
'%target' => $target,
), WATCHDOG_WARNING);
unset($sources[$key]);
continue;
}
// Check that the property exists.
$source_key = self::getSourceKey($source, TRUE);
if (!isset($valid_sources[$source['data_set']]['sources'][$source_key])) {
watchdog('party', 'The source property %key for the primary field %target does not exist.', array(
'%key' => $key,
'%target' => $target,
), WATCHDOG_WARNING);
unset($sources[$key]);
continue;
}
$source_key = self::getSourceKey($source);
if (!isset($valid_sources[$source['data_set']]['sources'][$source_key])) {
watchdog('party', 'The source property %key for the primary field %target does not exist. Falling back to %property.', array(
'%key' => $key,
'%target' => $target,
'%property' => $source['property'],
), WATCHDOG_WARNING);
$sources[$key]['value'] = NULL;
continue;
}
}
// Sort the sources.
uasort($sources, 'drupal_sort_weight');
}
}
// Store our information to the cache.
cache_set('party:primary_fields:fields', self::$fields);
}
/**
* Build the primary field source information.
*
* @param bool $reset
* Whether the cache should be reset.
*/
protected static function buildSources($reset = FALSE) {
// If we are resetting, clear our static cache.
if ($reset) {
self::$sources = NULL;
}
else {
if ($cache = cache_get('party:primary_fields:sources')) {
self::$sources = $cache->data;
}
}
// If we have no information, build it.
if (!isset(self::$sources)) {
$field_map = field_info_field_map();
self::$sources = array();
// Go over our data sets finding potential sources.
foreach (self::getDataSetInfo() as $data_set_name => $data_set) {
// Build our data set information.
self::$sources[$data_set_name] = array(
'set_name' => $data_set['set_name'],
'entity type' => $data_set['entity type'],
'entity bundle' => $data_set['entity bundle'],
'label' => $data_set['label'],
'option label' => "{$data_set['label']} ({$data_set['set_name']})",
'sources' => array(),
);
// Go over our property info to get the sources.
$property_info = self::getPropertyInfo($data_set['entity type'], $data_set['entity bundle']);
foreach ($property_info as $property => $info) {
// Build our source info.
$source_info = array(
'label' => $info['label'],
'option label' => "{$info['label']} ({$property})",
'type' => isset($info['type']) ? $info['type'] : NULL,
'field_type' => NULL,
'data_set' => $data_set_name,
'property' => $property,
'value' => NULL,
'callbacks' => array(),
'all types' => array(),
);
// If this is a field we store the field type.
if (!empty($info['field'])) {
if (isset($field_map[$property])) {
$source_info['field_type'] = $field_map[$property]['type'];
}
}
// Get the key and add it to the data set sources.
$key = self::getSourceKey($source_info);
self::$sources[$data_set_name]['sources'][$key] = $source_info;
// Process any values within this property.
if (isset($info['property info'])) {
foreach ($info['property info'] as $value => $value_info) {
// Build our source info.
$source_info = array(
'label' => "{$info['label']}: {$value_info['label']}",
'option label' => "{$info['label']}: {$value_info['label']} ({$property}:{$value})",
'type' => isset($value_info['type']) ? $value_info['type'] : NULL,
'field_type' => NULL,
'data_set' => $data_set_name,
'property' => $property,
'value' => $value,
'callbacks' => array(),
'all types' => array(),
);
// Get the key and add it to the data set sources.
$key = self::getSourceKey($source_info);
self::$sources[$data_set_name]['sources'][$key] = $source_info;
}
}
}
}
// Allow other modules to alter sources.
drupal_alter('party_primary_fields_sources', self::$sources);
// Run our cleanup.
self::cleanSources(self::$sources);
}
// Store our information to the cache.
cache_set('party:primary_fields:sources', self::$sources);
}
/**
* Clean up source information.
*
* Calculate the 'all types' key for properties and removing empty data sets.
*/
protected static function cleanSources(&$sources) {
foreach ($sources as $data_set_name => &$data_set) {
// Remove empty data sets.
if (empty($data_set['sources'])) {
unset($sources[$data_set_name]);
continue;
}
// Build a list of all types including callbacks.
foreach ($sources[$data_set_name]['sources'] as &$source) {
// Make sure our basic type is included.
$source['all types'][] = $source['type'];
// Add any callback types.
foreach ($source['callbacks'] as $callback) {
$source['all types'][] = $callback['type'];
}
// Filter the list and map it.
$source['all types'] = drupal_map_assoc(array_unique($source['all types']));
}
}
}
}
Classes
Name![]() |
Description |
---|---|
PartyPrimaryFields | Helper class for primary fields. |