Commit bf8e8219 authored by dawehner's avatar dawehner Committed by tim.plunkett

Issue #1779390 by dawehner, aspilicious: Refactor join plugins.

parent 4addda03
......@@ -88,8 +88,8 @@ function add_table($join = NULL, $alias = NULL) {
// Cycle through the joins. This isn't as error-safe as the normal
// ensure_path logic. Perhaps it should be.
$r_join = clone $join;
while ($r_join->left_table != $base_table) {
$r_join = HandlerBase::getTableJoin($r_join->left_table, $base_table);
while ($r_join->leftTable != $base_table) {
$r_join = HandlerBase::getTableJoin($r_join->leftTable, $base_table);
}
// If we found that there are tables in between, add the relationship.
if ($r_join->table != $join->table) {
......
......@@ -782,33 +782,33 @@ public static function getTimezone() {
public static function getTableJoin($table, $base_table) {
$data = views_fetch_data($table);
if (isset($data['table']['join'][$base_table])) {
$h = $data['table']['join'][$base_table];
if (!empty($h['join_id']) && class_exists($h['handler'])) {
$id = $h['join_id'];
$join_info = $data['table']['join'][$base_table];
if (!empty($join_info['join_id'])) {
$id = $join_info['join_id'];
}
else {
$id = 'standard';
}
$handler = views_get_plugin('join', $id);
// Fill in some easy defaults
$handler->definition = $h;
if (empty($handler->definition['table'])) {
$handler->definition['table'] = $table;
$configuration = $join_info;
// Fill in some easy defaults.
if (empty($configuration['table'])) {
$configuration['table'] = $table;
}
// If this is empty, it's a direct link.
if (empty($handler->definition['left_table'])) {
$handler->definition['left_table'] = $base_table;
if (empty($configuration['left_table'])) {
$configuration['left_table'] = $base_table;
}
if (isset($h['arguments'])) {
call_user_func_array(array(&$handler, 'construct'), $h['arguments']);
}
else {
$handler->construct();
if (isset($join_info['arguments'])) {
foreach ($join_info['arguments'] as $key => $argument) {
$configuration[$key] = $argument;
}
}
return $handler;
$join = drupal_container()->get('plugin.manager.views.join')->createInstance($id, $configuration);
return $join;
}
}
......
......@@ -823,7 +823,7 @@ function summary_name_field() {
$j = HandlerBase::getTableJoin($this->name_table, $this->table);
if ($j) {
$join = clone $j;
$join->left_table = $this->tableAlias;
$join->leftTable = $this->tableAlias;
$this->name_table_alias = $this->query->add_table($this->name_table, $this->relationship, $join);
}
}
......
......@@ -6,104 +6,170 @@
*/
namespace Drupal\views\Plugin\views\join;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
/**
* @defgroup views_join_handlers Views join handlers
* @{
* Handlers to tell Views how to join tables together.
*
* Here is an example how to join from table one to example two so it produces
* the following sql:
* @code
* INNER JOIN {two} ON one.field_a = two.field_b
* @code.
* The required php code for this kind of functionality is the following:
* @code
* $configuration = array(
* 'table' => 'two',
* 'field' => 'field_b',
* 'left_table' => 'one',
* 'left_field' => 'field_a',
* 'operator' => '='
* );
* $join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard', $configuration);
*
* Here is how you do complex joins:
*
* @code
* class JoinComplex extends Join {
* // PHP 4 doesn't call constructors of the base class automatically from a
* // constructor of a derived class. It is your responsibility to propagate
* // the call to constructors upstream where appropriate.
* function construct($table, $left_table, $left_field, $field, $extra = array(), $type = 'LEFT') {
* parent::construct($table, $left_table, $left_field, $field, $extra, $type);
* }
*
* function build_join($select_query, $table, $view_query) {
* class JoinComplex extends JoinPluginBase {
* public function buildJoin($select_query, $table, $view_query) {
* // Add an additional hardcoded condition to the query.
* $this->extra = 'foo.bar = baz.boing';
* parent::build_join($select_query, $table, $view_query);
* parent::buildJoin($select_query, $table, $view_query);
* }
* }
* @endcode
*/
/**
* A function class to represent a join and create the SQL necessary
* to implement the join.
* Represents a join and creates the SQL necessary to implement the join.
*
* This is the Delegation pattern. If we had PHP5 exclusively, we would
* declare this an interface.
* @todo It might make sense to create an interface for joins.
*
* Extensions of this class can be used to create more interesting joins.
*
* join definition
* - table: table to join (right table)
* - field: field to join on (right field)
* - left_table: The table we join to
* - left_field: The field we join to
* - type: either LEFT (default) or INNER
* - extra: An array of extra conditions on the join. Each condition is
* either a string that's directly added, or an array of items:
* - - table: If not set, current table; if NULL, no table. If you specify a
* table in cached definition, Views will try to load from an existing
* alias. If you use realtime joins, it works better.
* - - field: Field or formula
* in formulas we can reference the right table by using %alias
* @see SelectQueryInterface::addJoin()
* - - operator: defaults to =
* - - value: Must be set. If an array, operator will be defaulted to IN.
* - - numeric: If true, the value will not be surrounded in quotes.
* - - extra type: How all the extras will be combined. Either AND or OR. Defaults to AND.
*/
class JoinPluginBase {
class JoinPluginBase extends PluginBase {
var $table = NULL;
/**
* The table to join (right table).
*
* @var string
*/
public $table;
var $left_table = NULL;
/**
* The field to join on (right field).
*
* @var string
*/
public $field;
var $left_field = NULL;
/**
* The table we join to.
*
* @var string
*/
public $leftTable;
var $field = NULL;
/**
* The field we join to.
*
* @var string
*/
public $leftField;
var $extra = NULL;
/**
* An array of extra conditions on the join.
*
* Each condition is either a string that's directly added, or an array of
* items:
* - table(optional): If not set, current table; if NULL, no table. If you
* specify a table in cached configuration, Views will try to load from an
* existing alias. If you use realtime joins, it works better.
* - field(optional): Field or formula. In formulas we can reference the
* right table by using %alias.
* - operator(optional): The operator used, Defaults to "=".
* - value: Must be set. If an array, operator will be defaulted to IN.
* - numeric: If true, the value will not be surrounded in quotes.
*
* @see SelectQueryInterface::addJoin()
*
* @var array
*/
public $extra;
var $type = NULL;
/**
* The join type, so for example LEFT (default) or INNER.
*
* @var string
*/
public $type;
var $definition = array();
/**
* The configuration array passed by initJoin.
*
* @var array
*
* @see Drupal\views\Plugin\views\join\JoinPluginBase::initJoin()
*/
public $configuration = array();
/**
* Construct the Drupal\views\Join object.
* How all the extras will be combined. Either AND or OR.
*
* @var string
*/
function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT') {
$this->extra_type = 'AND';
if (!empty($table)) {
$this->table = $table;
$this->left_table = $left_table;
$this->left_field = $left_field;
$this->field = $field;
$this->extra = $extra;
$this->type = strtoupper($type);
public $extraOperator;
/**
* Defines whether a join has been adjusted.
*
* Views updates the join object to set the table alias instead of the table
* name. Once views has changed the alias it sets the adjusted value so it
* does not have to be updated anymore. If you create your own join object
* you should set the adjusted in the definition array to TRUE if you already
* know the table alias.
*
* @var bool
*
* @see Drupal\views\Plugin\HandlerBase::getTableJoin()
* @see Drupal\views\Plugin\views\query\Sql::adjust_join()
* @see Drupal\views\Plugin\views\relationship\RelationshipPluginBase::query()
*/
public $adjusted;
/**
* Constructs a Drupal\views\Plugin\views\join\JoinPluginBase object.
*/
public function __construct(array $configuration, $plugin_id, DiscoveryInterface $discovery) {
parent::__construct($configuration, $plugin_id, $discovery);
// Merge in some default values.
$configuration += array(
'type' => 'LEFT',
'extra_operator' => 'AND'
);
$this->configuration = $configuration;
if (!empty($configuration['table'])) {
$this->table = $configuration['table'];
}
elseif (!empty($this->definition)) {
// if no arguments, construct from definition.
// These four must exist or it will throw notices.
$this->table = $this->definition['table'];
$this->left_table = $this->definition['left_table'];
$this->left_field = $this->definition['left_field'];
$this->field = $this->definition['field'];
if (!empty($this->definition['extra'])) {
$this->extra = $this->definition['extra'];
}
if (!empty($this->definition['extra type'])) {
$this->extra_type = strtoupper($this->definition['extra type']);
}
$this->type = !empty($this->definition['type']) ? strtoupper($this->definition['type']) : 'LEFT';
$this->leftTable = $configuration['left_table'];
$this->leftField = $configuration['left_field'];
$this->field = $configuration['field'];
if (!empty($configuration['extra'])) {
$this->extra = $configuration['extra'];
}
if (isset($configuration['adjusted'])) {
$this->extra = $configuration['adjusted'];
}
$this->extraOperator = strtoupper($configuration['extra_operator']);
$this->type = $configuration['type'];
}
/**
......@@ -118,21 +184,21 @@ function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field
* @param $view_query
* The source query, implementation of views_plugin_query.
*/
function build_join($select_query, $table, $view_query) {
if (empty($this->definition['table formula'])) {
public function buildJoin($select_query, $table, $view_query) {
if (empty($this->configuration['table formula'])) {
$right_table = $this->table;
}
else {
$right_table = $this->definition['table formula'];
$right_table = $this->configuration['table formula'];
}
if ($this->left_table) {
$left = $view_query->get_table_info($this->left_table);
$left_field = "$left[alias].$this->left_field";
if ($this->leftTable) {
$left = $view_query->get_table_info($this->leftTable);
$left_field = "$left[alias].$this->leftField";
}
else {
// This can be used if left_field is a formula or something. It should be used only *very* rarely.
$left_field = $this->left_field;
$left_field = $this->leftField;
}
$condition = "$left_field = $table[alias].$this->field";
......@@ -199,7 +265,7 @@ function build_join($select_query, $table, $view_query) {
$condition .= ' AND ' . array_shift($extras);
}
else {
$condition .= ' AND (' . implode(' ' . $this->extra_type . ' ', $extras) . ')';
$condition .= ' AND (' . implode(' ' . $this->extraOperator . ' ', $extras) . ')';
}
}
}
......
......@@ -40,7 +40,7 @@ function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field
* @return
*
*/
function build_join($select_query, $table, $view_query) {
public function buildJoin($select_query, $table, $view_query) {
if (empty($this->definition['table formula'])) {
$right_table = "{" . $this->table . "}";
}
......@@ -92,7 +92,7 @@ function build_join($select_query, $table, $view_query) {
$condition .= ' AND ' . array_shift($extras);
}
else {
$condition .= ' AND (' . implode(' ' . $this->extra_type . ' ', $extras) . ')';
$condition .= ' AND (' . implode(' ' . $this->extraOperator . ' ', $extras) . ')';
}
}
}
......
......@@ -611,8 +611,8 @@ function ensure_path($table, $relationship = NULL, $join = NULL, $traced = array
// Does a table along this path exist?
if (isset($this->tables[$relationship][$table]) ||
($join && $join->left_table == $relationship) ||
($join && $join->left_table == $this->relationships[$relationship]['table'])) {
($join && $join->leftTable == $relationship) ||
($join && $join->leftTable == $this->relationships[$relationship]['table'])) {
// Make sure that we're linking to the correct table for our relationship.
foreach (array_reverse($add) as $table => $path_join) {
......@@ -622,20 +622,20 @@ function ensure_path($table, $relationship = NULL, $join = NULL, $traced = array
}
// Have we been this way?
if (isset($traced[$join->left_table])) {
if (isset($traced[$join->leftTable])) {
// we looped. Broked.
return FALSE;
}
// Do we have to add this table?
$left_join = $this->get_join_data($join->left_table, $this->relationships[$relationship]['base']);
if (!isset($this->tables[$relationship][$join->left_table])) {
$add[$join->left_table] = $left_join;
$left_join = $this->get_join_data($join->leftTable, $this->relationships[$relationship]['base']);
if (!isset($this->tables[$relationship][$join->leftTable])) {
$add[$join->leftTable] = $left_join;
}
// Keep looking.
$traced[$join->left_table] = TRUE;
return $this->ensure_path($join->left_table, $relationship, $left_join, $traced, $add);
$traced[$join->leftTable] = TRUE;
return $this->ensure_path($join->leftTable, $relationship, $left_join, $traced, $add);
}
/**
......@@ -660,23 +660,23 @@ function adjust_join($join, $relationship) {
$join = clone $join;
// Do we need to try to ensure a path?
if ($join->left_table != $this->relationships[$relationship]['table'] &&
$join->left_table != $this->relationships[$relationship]['base'] &&
!isset($this->tables[$relationship][$join->left_table]['alias'])) {
$this->ensure_table($join->left_table, $relationship);
if ($join->leftTable != $this->relationships[$relationship]['table'] &&
$join->leftTable != $this->relationships[$relationship]['base'] &&
!isset($this->tables[$relationship][$join->leftTable]['alias'])) {
$this->ensure_table($join->leftTable, $relationship);
}
// First, if this is our link point/anchor table, just use the relationship
if ($join->left_table == $this->relationships[$relationship]['table']) {
$join->left_table = $relationship;
if ($join->leftTable == $this->relationships[$relationship]['table']) {
$join->leftTable = $relationship;
}
// then, try the base alias.
elseif (isset($this->tables[$relationship][$join->left_table]['alias'])) {
$join->left_table = $this->tables[$relationship][$join->left_table]['alias'];
elseif (isset($this->tables[$relationship][$join->leftTable]['alias'])) {
$join->leftTable = $this->tables[$relationship][$join->leftTable]['alias'];
}
// But if we're already looking at an alias, use that instead.
elseif (isset($this->table_queue[$relationship]['alias'])) {
$join->left_table = $this->table_queue[$relationship]['alias'];
$join->leftTable = $this->table_queue[$relationship]['alias'];
}
}
......@@ -1304,7 +1304,7 @@ public function query($get_count = FALSE) {
// Add all the tables to the query via joins. We assume all LEFT joins.
foreach ($this->table_queue as $table) {
if (is_object($table['join'])) {
$table['join']->build_join($query, $table, $this);
$table['join']->buildJoin($query, $table, $this);
}
}
......
......@@ -349,6 +349,7 @@ public function query() {
$def['field'] = $base_field;
$def['left_table'] = $this->tableAlias;
$def['left_field'] = $this->field;
$def['adjusted'] = TRUE;
if (!empty($this->options['required'])) {
$def['type'] = 'INNER';
}
......@@ -376,11 +377,7 @@ public function query() {
else {
$id = 'subquery';
}
$join = views_get_plugin('join', $id);
$join->definition = $def;
$join->construct();
$join->adjusted = TRUE;
$join = drupal_container()->get('plugin.manager.views.join')->createInstance($id, $def);
// use a short alias for this:
$alias = $def['table'] . '_' . $this->table;
......
......@@ -126,6 +126,7 @@ public function query() {
$def['field'] = $base_field;
$def['left_table'] = $this->tableAlias;
$def['left_field'] = $this->realField;
$def['adjusted'] = TRUE;
if (!empty($this->options['required'])) {
$def['type'] = 'INNER';
}
......@@ -140,12 +141,7 @@ public function query() {
else {
$id = 'standard';
}
$join = views_get_plugin('join', $id);
$join->definition = $def;
$join->options = $this->options;
$join->construct();
$join->adjusted = TRUE;
$join = drupal_container()->get('plugin.manager.views.join')->createInstance($id, $def);
// use a short alias for this:
$alias = $def['table'] . '_' . $this->table;
......
......@@ -46,8 +46,14 @@ public function query() {
$max_depth = isset($this->definition['max depth']) ? $this->definition['max depth'] : MENU_MAX_DEPTH;
for ($i = 1; $i <= $max_depth; ++$i) {
if ($this->options['sort_within_level']) {
$join = views_get_plugin('join', 'standard');
$join->construct('menu_links', $this->tableAlias, $this->field . $i, 'mlid');
$definition = array(
'table' => 'menu_links',
'field' => 'mlid',
'left_table' => $this->tableAlias,
'left_field' => $this->field . $i
);
$join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard', $definition);
$menu_links = $this->query->add_table('menu_links', NULL, $join);
$this->query->add_orderby($menu_links, 'weight', $this->options['order']);
$this->query->add_orderby($menu_links, 'link_title', $this->options['order']);
......
......@@ -16,6 +16,13 @@
*/
class JoinTest extends PluginTestBase {
/**
* A plugin manager which handlers the instances of joins.
*
* @var Drupal\views\Plugin\Type\ViewsPluginManager
*/
protected $manager;
public static function getInfo() {
return array(
'name' => 'Join',
......@@ -24,36 +31,43 @@ public static function getInfo() {
);
}
protected function setUp() {
parent::setUp();
// Add a join plugin manager which can be used in all of the tests.
$this->manager = drupal_container()->get('plugin.manager.views.join');
}
/**
* Tests an example join plugin.
*/
public function testExamplePlugin() {
$join = drupal_container()->get('plugin.manager.views.join')->createInstance('join_test');
$this->assertTrue($join instanceof JoinTestPlugin, 'The correct join class got loaded.');
// Setup a simple join and test the result sql.
$view = views_get_view('frontpage');
$view->initDisplay();
$view->initQuery();
$definition = array(
$configuration = array(
'left_table' => 'node',
'left_field' => 'uid',
'table' => 'users',
'field' => 'uid',
);
$join->definition = $definition;
$join->construct();
$join = $this->manager->createInstance('join_test', $configuration);
$this->assertTrue($join instanceof JoinTestPlugin, 'The correct join class got loaded.');
$rand_int = rand(0, 1000);
$join->setJoinValue($rand_int);
$query = db_select('node');
$table = array('alias' => 'users');
$join->build_join($query, $table, $view->query);
$join->buildJoin($query, $table, $view->query);
$tables = $query->getTables();
$join_info = $tables['users'];
debug($join_info);
$this->assertTrue(strpos($join_info['condition'], "node.uid = $rand_int") !== FALSE, 'Make sure that the custom join plugin can extend the join base and alter the result.');
}
......@@ -61,8 +75,6 @@ public function testExamplePlugin() {
* Tests the join plugin base.
*/
public function testBasePlugin() {
$join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard');
$this->assertTrue($join instanceof JoinPluginBase, 'The correct join class got loaded.');
// Setup a simple join and test the result sql.
$view = views_get_view('frontpage');
......@@ -71,56 +83,51 @@ public function testBasePlugin() {
// First define a simple join without an extra condition.
// Set the various options on the join object.
$definition = array(
$configuration = array(
'left_table' => 'node',
'left_field' => 'uid',
'table' => 'users',
'field' => 'uid',
);
$join->definition = $definition;
$join->construct();
$join = $this->manager->createInstance('standard', $configuration);
$this->assertTrue($join instanceof JoinPluginBase, 'The correct join class got loaded.');
// Build the actual join values and read them back from the dbtng query
// object.
$query = db_select('node');
$table = array('alias' => 'users');
$join->build_join($query, $table, $view->query);
$join->buildJoin($query, $table, $view->query);
$tables = $query->getTables();
$join_info = $tables['users'];
$this->assertEqual($join_info['join type'], 'LEFT', 'Make sure the default join type is LEFT');
$this->assertEqual($join_info['table'], $definition['table']);
$this->assertEqual($join_info['table'], $configuration['table']);
$this->assertEqual($join_info['alias'], 'users');
$this->assertEqual($join_info['condition'], 'node.uid = users.uid');
// Set a different alias and make sure table info is as expected.
$join = drupal_container()->get('plugin.manager.views.join')->createInstance('standard');
$join->definition = $definition;
$join->construct();
$join = $this->manager->createInstance('standard', $configuration);
$table = array('alias' => 'users1');
$join->build_join($query, $table, $view->query);