Commit 25427b01 authored by fago's avatar fago

#1266036 patch by drunken monkey, fago, thegreat: add Views base tables,...

 #1266036 patch by drunken monkey, fago, thegreat: add Views base tables, fields and relationships based upon data selection on entity objects.
parent 21c15b65
......@@ -401,6 +401,28 @@ function entity_hook_field_info() {
);
}
/**
* Alter the handlers used by the data selection tables provided by this module.
*
* @param array $field_handlers
* An array of the field handler classes to use for specific types. The keys
* are the types, mapped to their respective classes. Contained types are:
* - All primitive types known by the entity API (see
* hook_entity_property_info()).
* - options: Special type for fields having an options list.
* - field: Special type for Field API fields.
* - entity: Special type for entity-valued fields.
* - relationship: Views relationship handler to use for relationships.
* Values for all specific entity types can be additionally added.
*
* @see entity_views_field_definition()
* @see entity_views_get_field_handlers()
*/
function hook_entity_views_field_handlers_alter(array &$field_handlers) {
$field_handlers['duration'] = 'example_duration_handler';
$field_handlers['node'] = 'example_node_handler';
}
/**
* @} End of "addtogroup hooks".
*/
name = Entity API
description = Enables modules to work with any entity type and to provide entities.
core = 7.x
files[] = views/plugins/entity_plugin_row_entity_view.inc
files[] = includes/entity.controller.inc
files[] = includes/entity.inc
files[] = includes/entity.ui.inc
files[] = includes/entity.wrapper.inc
files[] = entity.features.inc
files[] = entity.info.inc
files[] = entity.rules.inc
files[] = entity.test
files[] = includes/entity.inc
files[] = includes/entity.controller.inc
files[] = includes/entity.ui.inc
files[] = includes/entity.wrapper.inc
files[] = views/handlers/entity_views_field_handler_helper.inc
files[] = views/handlers/entity_views_handler_field_boolean.inc
files[] = views/handlers/entity_views_handler_field_date.inc
files[] = views/handlers/entity_views_handler_field_duration.inc
files[] = views/handlers/entity_views_handler_field_entity.inc
files[] = views/handlers/entity_views_handler_field_field.inc
files[] = views/handlers/entity_views_handler_field_numeric.inc
files[] = views/handlers/entity_views_handler_field_options.inc
files[] = views/handlers/entity_views_handler_field_text.inc
files[] = views/handlers/entity_views_handler_field_uri.inc
files[] = views/handlers/entity_views_handler_relationship_by_bundle.inc
files[] = views/handlers/entity_views_handler_relationship.inc
files[] = views/plugins/entity_plugin_row_entity_view.inc
......@@ -10,12 +10,12 @@
/**
* Get the entity property info array of an entity type.
*
* @see hook_entity_property_info()
* @see hook_entity_property_info_alter()
*
* @param $entity_type
* The entity type, e.g. node, for which the info shall be returned, or NULL
* to return an array with info about all types.
*
* @see hook_entity_property_info()
* @see hook_entity_property_info_alter()
*/
function entity_get_property_info($entity_type = NULL) {
// Use the advanced drupal_static() pattern, since this is called very often.
......@@ -41,6 +41,21 @@ function entity_get_property_info($entity_type = NULL) {
return empty($entity_type) ? $info : (isset($info[$entity_type]) ? $info[$entity_type] : array());
}
/**
* Returns the default information for an entity property.
*
* @return
* An array of optional property information keys mapped to their defaults.
*
* @see hook_entity_property_info()
*/
function entity_property_info_defaults() {
return array(
'type' => 'text',
'getter callback' => 'entity_property_verbatim_get',
);
}
/**
* Gets an array of info about all properties of a given entity type.
*
......@@ -310,6 +325,22 @@ function entity_property_list_extract_type($type) {
return FALSE;
}
/**
* Extracts the innermost type for a type string like list<list<date>>.
*
* @param $type
* The type to examine.
*
* @return
* For list types, the innermost type. The type itself otherwise.
*/
function entity_property_extract_innermost_type($type) {
while (strpos($type, 'list<') === 0 && $type[strlen($type)-1] == '>') {
$type = substr($type, 5, -1);
}
return $type;
}
/**
* Gets the property just as it is set in the data.
*/
......
......@@ -14,10 +14,12 @@
* - hook_entity_info() specifies a 'module' key, and the module does not
* implement hook_views_data().
*
* @see entity_crud_hook_entity_info().
* @see entity_crud_hook_entity_info()
* @see entity_views_table_definition()
*/
function entity_views_data() {
$data = array();
foreach (entity_crud_get_info() as $type => $info) {
// Provide default integration with the basic controller class if we know
// the module providing the entity and it does not provide views integration.
......@@ -35,9 +37,224 @@ function entity_views_data() {
}
}
// Add tables based upon data selection "queries" for all entity types.
foreach (entity_get_info() as $type => $info) {
$table = entity_views_table_definition($type);
if ($table) {
$data['entity_' . $type] = $table;
}
}
return $data;
}
/**
* Helper function for getting data selection based entity Views table definitions.
*
* This creates extra tables for each entity type that are not associated with a
* query plugin (and thus are not base tables) and just rely on the entities to
* retrieve the displayed data. To obtain the entities corresponding to a
* certain result set, the field handlers defined on the table use a generic
* interface defined for query plugins that are based on entity handling, and
* which is described in the entity_views_example_query class.
*
* These tables are called "data selection tables".
*
* Other modules providing Views integration with new query plugins that are
* based on entities can then use these tables as a base for their own tables
* (by directly using this method and modifying the returned table) and/or by
* specifying relationships to them. The tables returned here already specify
* relationships to each other wherever an entity contains a reference to
* another (e.g., the node author constructs a relationship from nodes to
* users).
*
* As filtering and other query manipulation is potentially more plugin-specific
* than the display, only field handlers and relationships are provided with
* these tables. By providing a add_selector_orderby() method, the query plugin
* can, however, support click-sorting for the field handlers in these tables.
*
* For a detailed discussion see http://drupal.org/node/1266036
*
* For example use see the Search API views module in the Search API project:
* http://drupal.org/project/search_api
*
* @param $type
* The entity type whose table definition should be returned.
*
* @return
* An array containing the data selection Views table definition for the
* entity type.
*
* @see entity_views_field_definition()
*/
function entity_views_table_definition($type) {
// As other modules might want to copy these tables as a base for their own
// Views integration, we statically cache the tables to save some time.
$tables = &drupal_static(__FUNCTION__, array());
if (!isset($tables[$type])) {
$info = entity_get_info($type);
$tables[$type]['table'] = array(
'group' => $info['label'],
'entity type' => $type,
);
foreach (entity_get_all_property_info($type) as $key => $property) {
entity_views_field_definition($key, $property, $tables[$type]);
}
}
return $tables[$type];
}
/**
* Helper function for adding a Views field definition to data selection based Views tables.
*
* @param $field
* The data selector of the field to add. E.g. "title" would derive the node
* title property, "body:summary" the node body's summary.
* @param array $property_info
* The property information for which to create a field definition.
* @param array $table
* The table into which the definition should be inserted.
* @param $title_prefix
* Internal use only.
*
* @see entity_views_table_definition()
*/
function entity_views_field_definition($field, array $property_info, array &$table, $title_prefix = '') {
$additional = array();
$additional_field = array();
// Create a valid Views field identifier (no colons, etc.). Keep the original
// data selector as real field though.
$key = _entity_views_field_identifier($field, $table);
if ($key != $field) {
$additional['real field'] = $field;
}
$field_name = EntityFieldHandlerHelper::get_selector_field_name($field);
$field_handlers = entity_views_get_field_handlers();
$property_info += entity_property_info_defaults();
$type = entity_property_extract_innermost_type($property_info['type']);
$title = $title_prefix . $property_info['label'];
if ($info = entity_get_info($type)) {
$additional_field['entity type'] = $type;
$additional['relationship'] = array(
'handler' => $field_handlers['relationship'],
'base' => 'entity_' . $type,
'base field' => $info['entity keys']['id'],
'relationship field' => $field,
'label' => $title,
);
if ($property_info['type'] != $type) {
// This is a list of entities, so we should mark the relationship as such.
$additional['relationship']['multiple'] = TRUE;
}
// Implementers of the field handlers alter hook could add handlers for
// specific entity types.
if (!isset($field_handlers[$type])) {
$type = 'entity';
}
}
elseif (!empty($property_info['field'])) {
$type = 'field';
// Views' Field API field handler needs some extra definitions to work.
$additional_field['field_name'] = $field_name;
$additional_field['entity_tables'] = array();
$additional_field['entity type'] = $table['table']['entity type'];
$additional_field['is revision'] = FALSE;
}
// Copied from EntityMetadataWrapper::optionsList()
elseif (isset($property_info['options list']) && is_callable($property_info['options list'])) {
// If this is a nested property, we need to get rid of all prefixes first.
$type = 'options';
$additional_field['options callback'] = array(
'function' => $property_info['options list'],
'info' => $property_info,
);
}
elseif ($type == 'decimal') {
$additional_field['float'] = TRUE;
}
if (isset($field_handlers[$type])) {
$table += array($key => array());
$table[$key] += array(
'title' => $title,
'help' => empty($property_info['description']) ? t('(No information available)') : $property_info['description'],
'field' => array(),
);
$table[$key]['field'] += array(
'handler' => $field_handlers[$type],
'type' => $property_info['type'],
);
$table[$key] += $additional;
$table[$key]['field'] += $additional_field;
}
if (!empty($property_info['property info'])) {
foreach ($property_info['property info'] as $nested_key => $nested_property) {
entity_views_field_definition($field . ':' . $nested_key, $nested_property, $table, $title . ' » ');
}
}
}
/**
* @return array
* The handlers to use for the data selection based Views tables.
*
* @see hook_entity_views_field_handlers_alter()
*/
function entity_views_get_field_handlers() {
$field_handlers = drupal_static(__FUNCTION__);
if (!isset($field_handlers)) {
// Field handlers for the entity tables, by type.
$field_handlers = array(
'text' => 'entity_views_handler_field_text',
'token' => 'entity_views_handler_field_text',
'integer' => 'entity_views_handler_field_numeric',
'decimal' => 'entity_views_handler_field_numeric',
'date' => 'entity_views_handler_field_date',
'duration' => 'entity_views_handler_field_duration',
'boolean' => 'entity_views_handler_field_boolean',
'uri' => 'entity_views_handler_field_uri',
'options' => 'entity_views_handler_field_options',
'field' => 'entity_views_handler_field_field',
'entity' => 'entity_views_handler_field_entity',
'relationship' => 'entity_views_handler_relationship',
);
drupal_alter('entity_views_field_handlers', $field_handlers);
}
return $field_handlers;
}
/**
* Helper function for creating valid Views field identifiers out of data selectors.
*
* Uses $table to test whether the identifier is already used, and also
* recognizes if a definition for the same field is already present and returns
* that definition's identifier.
*
* @return string
* A valid Views field identifier that is not yet used as a key in $table.
*/
function _entity_views_field_identifier($field, array $table) {
$key = $base = preg_replace('/[^a-zA-Z0-9]+/S', '_', $field);
$i = 0;
// The condition checks whether this sanitized field identifier is already
// used for another field in this table (and whether the identifier is
// "table", which can never be used).
// If $table[$key] is set, the identifier is already used, but this might be
// already for the same field. To test that, we need the original field name,
// which is either $table[$key]['real field'], if set, or $key. If this
// original field name is equal to $field, we can use that key. Otherwise, we
// append numeric suffixes until we reach an unused key.
while ($key == 'table' || (isset($table[$key]) && (isset($table[$key]['real field']) ? $table[$key]['real field'] : $key) != $field)) {
$key = $base . '_' . ++$i;
}
return $key;
}
/**
* Implements hook_views_plugins().
*/
......@@ -102,6 +319,7 @@ class EntityDefaultViewsController {
'title' => drupal_ucfirst($this->info['label']),
'help' => isset($this->info['description']) ? $this->info['description'] : '',
);
$data[$table]['table']['entity type'] = $this->type;
$data[$table] += $this->schema_fields();
// Add in any reverse-relationships which have been determined.
......
<?php
/**
* @file
* Contains an example for a Views query plugin that could use the data selection tables.
*/
/**
* Describes the additional methods looked for on a query plugin if data selection based tables or fields are used.
*
* Only get_result_entities() needs to be present, so results can be retrieved.
* The other methods are optional.
*
* If the table does not contain entities, however, the get_result_wrappers()
* method is necessary, too. If this is the case and there are no relations to
* entity tables, the get_result_entities() method is not needed.
*
* @see entity_views_table_definition()
*/
abstract class entity_views_example_query extends views_plugin_query {
/**
* Add a sort to the query.
*
* This is used to add a sort based on an Entity API data selector instead
* of a field alias.
*
* This method has to be present if click-sorting on fields should be allowed
* for some fields using the default Entity API field handlers.
*
* @param $selector
* The field to sort on, as an Entity API data selector.
* @param $order
* The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'.
*/
public abstract function add_selector_orderby($selector, $order = 'ASC');
/**
* Returns the according entity objects for the given query results.
*
* This is compatible to the get_result_entities() method used by Views.
*
* The method is responsible for resolving the relationship and returning the
* entity objects for that relationship. The helper methods
* EntityFieldHandlerHelper::construct_property_selector() and
* EntityFieldHandlerHelper::extract_property_multiple() can be used to do
* this.
*
* @param $results
* The results of the query, as returned by this query plugin.
* @param $relationship
* (optional) A relationship for which the entities should be returned.
* @param $field
* (optional) The field for which the entity should be returned. This is
* only needed in case a field is derived via a referenced entity without
* using a relationship. For example, if the node's field "author:name" is
* used, the user entity would be returned instead of the node entity.
*
* @return
* A numerically indexed array containing two items: the entity type of
* entities returned by this method; and the array of entities, keyed by the
* same indexes as the results.
*
* @see EntityFieldHandlerHelper::extract_property_multiple()
*/
public abstract function get_result_entities($results, $relationship = NULL, $field = NULL);
/**
* Returns the according metadata wrappers for the given query results.
*
* This can be used if no entities for the results can be given, but entity
* metadata wrappers can be constructed for them.
*
* @param $results
* The results of the query, as returned by this query plugin.
* @param $relationship
* (optional) A relationship for which the wrappers should be returned.
* @param $field
* (optional) The field of which a wrapper should be returned.
*
* @return
* A numerically indexed array containing two items: the data type of
* the wrappers returned by this method; and the array of retrieved
* EntityMetadataWrapper objects, keyed by the same indexes as the results.
*/
public abstract function get_result_wrappers($results, $relationship = NULL, $field = NULL);
}
<?php
/**
* @file
* Contains the EntityFieldHandlerHelper class.
*/
/**
* Helper class containing static implementations of common field handler methods.
*
* Used by the data selection entity field handlers to avoid code duplication.
*
* @see entity_views_table_definition()
*/
class EntityFieldHandlerHelper {
/**
* Provide appropriate default options for a handler.
*/
public static function option_definition($handler) {
if (entity_property_list_extract_type($handler->definition['type'])) {
$options['list']['contains']['mode'] = array('default' => 'collapse');
$options['list']['contains']['separator'] = array('default' => ', ');
}
$options['link_to_entity'] = array('default' => FALSE);
return $options;
}
/**
* Provide an appropriate default option form for a handler.
*/
public static function options_form($handler, &$form, &$form_state) {
if (entity_property_list_extract_type($handler->definition['type'])) {
$form['list']['mode'] = array(
'#type' => 'select',
'#title' => t('List handling'),
'#options' => array(
'collapse' => t('Concatenate values using a seperator'),
'first' => t('Show first (if present)'),
'count' => t('Show item count'),
),
'#default_value' => $handler->options['list']['mode'],
);
$form['list']['separator'] = array(
'#type' => 'textfield',
'#title' => t('List seperator'),
'#default_value' => $handler->options['list']['separator'],
'#dependency' => array('edit-options-list-mode' => array('collapse')),
);
}
$form['link_to_entity'] = array(
'#type' => 'checkbox',
'#title' => t('Link this field to its entity'),
'#description' => t("When using this, you should not set any other link on the field."),
'#default_value' => $handler->options['link_to_entity'],
);
}
/**
* Add the field for the entity ID (if necessary).
*/
public static function query($handler) {
// Some of the parent handlers might require this.
$handler->field_alias = $handler->real_field;
$handler->base_field = self::get_selector_field_name($handler->real_field);
}
/**
* Extracts the innermost field name from a data selector.
*
* @param $selector
* The data selector.
*
* @return
* The last component of the data selector.
*/
public static function get_selector_field_name($selector) {
return ltrim(substr($selector, strrpos($selector, ':')), ':');
}
/**
* Adds a click-sort to the query.
*
* @param $order
* Either 'ASC' or 'DESC'.
*/
public static function click_sort($handler, $order) {
// The normal orderby() method for this usually won't work here. So we need
// query plugins to provide their own method for this.
if (method_exists($handler->query, 'add_selector_orderby')) {
$selector = self::construct_property_selector($handler, TRUE);
$handler->query->add_selector_orderby($selector, $order);
}
}
/**
* Load the entities for all rows that are about to be displayed.
*
* Automatically takes care of relationships, including data selection
* relationships.
*/
public static function pre_render($handler, &$values, $load_always = FALSE) {
if (empty($values)) {
return;
}
if (!$load_always && empty($handler->options['link_to_entity'])) {
// Check whether we even need to load the entities.
$selector = self::construct_property_selector($handler, TRUE);
$load = FALSE;
foreach ($values as $row) {
if (empty($row->_entity_properties) || !array_key_exists($selector, $row->_entity_properties)) {
$load = TRUE;
break;
}
}
if (!$load) {
return;
}
}
if (method_exists($handler->query, 'get_result_wrappers')) {
list($handler->entity_type, $handler->wrappers) = $handler->query->get_result_wrappers($values, $handler->relationship, $handler->real_field);
}
else {
list($handler->entity_type, $entities) = $handler->query->get_result_entities($values, $handler->relationship, $handler->real_field);
$handler->wrappers = array();
foreach ($entities as $id => $entity) {
$handler->wrappers[$id] = entity_metadata_wrapper($handler->entity_type, $entity);
}
}
}
/**
* Return an Entity API data selector for the given handler's relationship.
*
* A data selector is a concatenation of properties which should be followed
* to arrive at a desired property that may be nested in related entities or
* structures. The separate properties are herein concatenated with colons.
*
* For instance, a data selector of "author:roles" would mean to first
* access the "author" property of the given wrapper, and then for this new
* wrapper to access and return the "roles" property.
*
* Lists of entities are handled automatically by always returning only the
* first entity.
*
* @param $handler
* The handler for which to construct the selector.
* @param $complete
* If TRUE, the complete selector for the field is returned, not just the
* one for its parent. Defaults to FALSE.
*
* @return
* An Entity API data selector for the given handler's relationship.
*/
public static function construct_property_selector($handler, $complete = FALSE) {
$return = '';
if ($handler->relationship) {
$current_handler = $handler;
$view = $current_handler->view;
while (!empty($current_handler->relationship) && !empty($view->relationship[$current_handler->relationship])) {
$current_handler = $view->relationship[$current_handler->relationship];
$return = $current_handler->real_field . ($return ? ":$return" : '');
}
}
if ($complete) {
$return .= ($return ? ':' : '') . $handler->real_field;
}
elseif ($pos = strrpos($handler->real_field, ':')) {
// If we have a selector as the real_field, append this to the returned
// relationship selector.
$return .= ($return ? ':' : '') . substr($handler->real_field, 0, $pos);
}
return $return;
}
/**
* Extracts data from several metadata wrappers based on a data selector.
*
* All metadata wrappers passed to this function have to be based on the exact
* same property information. The data will be returned wrapped by one or more
* metadata wrappers.
*
* Can be used in query plugins for the get_result_entities() and
* get_result_wrappers() methods.
*
* @param array $wrappers
* The EntityMetadataWrapper objects from which to extract data.
* @param $selector
* The selector specifying the data to extract.
*
* @return array
* An array with numeric indices, containing the type of the extracted
* wrappers in the first element. The second element of the array contains
* the extracted property value(s) for each wrapper, keyed to the same key
* that was used for the respecive wrapper in $wrappers. All extracted
* properties are returned as metadata wrappers.
*/
public static function extract_property_multiple(array $wrappers, $selector) {
$parts = explode(':', $selector, 2);
$name = $parts[0];
$results = array();
$entities = array();
$type = '';
foreach ($wrappers as $i => $wrapper) {
try {
$property = $wrapper->$name;
$type = $property->type();
if ($property instanceof EntityDrupalWrapper) {
// Remember the entity IDs to later load all at once (so as to
// properly utilize multiple load functionality).
$id = $property->getIdentifier();
$entities[$type][$i] = $id;
}
elseif ($property instanceof EntityStructureWrapper) {
$results[$i] = $property;
}
elseif ($property instanceof EntityListWrapper) {
foreach ($property as $item) {
$results[$i] = $item;
$type = $item->type();
break;
}
}
// Do nothing in case it cannot be applied.
}
catch (EntityMetadataWrapperException $e) {
// Skip single empty properties.
}
}
if ($entities) {
// Map back the loaded entities back to the results array.
foreach ($entities as $type => $id_map) {
$loaded = entity_load($type, $id_map);
foreach ($id_map as $i => $id) {
if (isset($loaded[$id])) {
$results[$i] = entity_metadata_wrapper($type, $loaded[$id]);
}
}
}
}
// If there are no further parts in the selector, we are done now.
if (empty($parts[1])) {
return array($type, $results);
}
return self::extract_property_multiple($results, $parts[1]);
}
/**
* Get the value of a certain data selector.
*
* Uses $values->_entity_properties to look for already extracted properties.
*
* @param $handler
* The field handler for which to return a value.
* @param $values
* The values for the current row retrieved from the Views query, as an
* object.
* @param $field
* The field to extract. If no value is given, the field of the given
* handler is used instead. The special "entity object" value can be used to
* get the base entity instead of a special field.
* @param $default
* The value to return if the entity or field are not present.
*/
public static function get_value($handler, $values, $field = NULL, $default = NULL) {
// There is a value cache on each handler so parent handlers rendering a
// single field value from a list will get the single value, not the whole
// list.
if (!isset($field) && isset($handler->current_value)) {
return $handler->current_value;