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);
}
This diff is collapsed.
<?php
/**
* @file
* Contains the entity_views_handler_field_boolean class.
*/
/**
* A handler to provide proper displays for booleans.
*
* Overrides the default Views handler to retrieve the data from an entity via
* data selection.
*
* This handler may only be used in conjunction with data selection based Views
* tables or other base tables using a query plugin that supports data
* selection.
*
* @see entity_views_field_definition()
* @ingroup views_field_handlers
*/
class entity_views_handler_field_boolean extends views_handler_field_boolean {
/**
* Stores the entity type which is loaded by this field.
*/
public $entity_type;
/**
* Stores the result entities' metadata wrappers.
*/
public $wrappers = array();
/**
* The base name of the field, without data selector.
*/
public $base_field;
/**
* Stores the current value when rendering list fields.
*/
public $current_value;
/**
* Overridden to add the field for the entity ID (if necessary).
*/
public function query() {
EntityFieldHandlerHelper::query($this);
}
/**
* Adds a click-sort to the query.
*/
public function click_sort($order) {
EntityFieldHandlerHelper::click_sort($this, $order);
}
/**
* Load the entities for all rows that are about to be displayed.
*/
public function pre_render(&$values) {
parent::pre_render($values);
EntityFieldHandlerHelper::pre_render($this, $values);
}
/**
* Overridden to use a metadata wrapper.
*/
public function get_value($values, $field = NULL) {
return EntityFieldHandlerHelper::get_value($this, $values, $field);
}
/**
* Provide options for this handler.
*/
public function option_definition() {
return parent::option_definition() + EntityFieldHandlerHelper::option_definition($this);
}
/**
* Provide a options form for this handler.
*/
public function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
EntityFieldHandlerHelper::options_form($this, $form, $form_state);
}
/**
* Render the field.
*
* @param $values
* The values retrieved from the database.
*/
public function render($values) {
return EntityFieldHandlerHelper::render($this, $values);
}
/**
* Render a single field value.
*/
public function render_single_value($value, $values) {
return parent::render($values);
}
}
<?php
/**
* @file
* Contains the entity_views_handler_field_date class.
*/
/**
* A handler to provide proper displays for dates.
*
* Overrides the default Views handler to retrieve the data from an entity via
* data selection.
*
* This handler may only be used in conjunction with data selection based Views
* tables or other base tables using a query plugin that supports data
* selection.
*
* @see entity_views_field_definition()
* @ingroup views_field_handlers
*/
class entity_views_handler_field_date extends views_handler_field_date {
/**
* Stores the entity type which is loaded by this field.
*/
public $entity_type;
/**
* Stores the result entities' metadata wrappers.
*/
public $wrappers = array();
/**
* The base name of the field, without data selector.
*/
public $base_field;
/**
* Stores the current value when rendering list fields.
*/
public $current_value;
/**
* Overridden to add the field for the entity ID (if necessary).
*/
public function query() {
EntityFieldHandlerHelper::query($this);
}
/**
* Adds a click-sort to the query.
*/
public function click_sort($order) {
EntityFieldHandlerHelper::click_sort($this, $order);
}
/**
* Load the entities for all rows that are about to be displayed.
*/
public function pre_render(&$values) {
parent::pre_render($values);
EntityFieldHandlerHelper::pre_render($this, $values);
}
/**
* Overridden to use a metadata wrapper.
*/
public function get_value($values, $field = NULL) {
return EntityFieldHandlerHelper::get_value($this, $values, $field);
}
/**
* Provide options for this handler.
*/
public function option_definition() {
return parent::option_definition() + EntityFieldHandlerHelper::option_definition($this);
}
/**
* Provide a options form for this handler.
*/
public function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
EntityFieldHandlerHelper::options_form($this, $form, $form_state);
}
/**
* Render the field.
*
* @param $values
* The values retrieved from the database.
*/
public function render($values) {
return EntityFieldHandlerHelper::render($this, $values);
}
/**
* Render a single field value.
*/
public function render_single_value($value, $values) {
return parent::render($values);
}
}
<?php
/**
* @file
* Contains the entity_views_handler_field_duration class.
*/
/**
* A handler to provide proper displays for duration properties retrieved via data selection.
*
* This handler may only be used in conjunction with data selection based Views
* tables or other base tables using a query plugin that supports data
* selection.
*
* @see entity_views_field_definition()
* @ingroup views_field_handlers
*/
class entity_views_handler_field_duration extends views_handler_field {
/**
* Stores the entity type which is loaded by this field.
*/
public $entity_type;
/**
* Stores the result entities' metadata wrappers.
*/
public $wrappers = array();
/**
* The base name of the field, without data selector.
*/
public $base_field;
/**
* Stores the current value when rendering list fields.
*/
public $current_value;
/**
* Overridden to add the field for the entity ID (if necessary).
*/
public function query() {
EntityFieldHandlerHelper::query($this);
}
/**
* Adds a click-sort to the query.
*/
public function click_sort($order) {
EntityFieldHandlerHelper::click_sort($this, $order);
}
/**
* Load the entities for all rows that are about to be displayed.
*/
public function pre_render(&$values) {
parent::pre_render($values);
EntityFieldHandlerHelper::pre_render($this, $values);
}
/**
* Overridden to use a metadata wrapper.
*/
public function get_value($values, $field = NULL) {
return EntityFieldHandlerHelper::get_value($this, $values, $field);
}
public function option_definition() {
$options = parent::option_definition();
$options += EntityFieldHandlerHelper::option_definition($this);
$options['format_interval'] = array('default' => TRUE);
$options['granularity'] = array('default' => 2);
$options['prefix'] = array('default' => '', 'translatable' => TRUE);