X-Git-Url: http://www.aleph1.co.uk/gitweb/?a=blobdiff_plain;f=web%2Fcore%2Fmodules%2Fviews%2Fsrc%2FPlugin%2Fviews%2Frelationship%2FGroupwiseMax.php;fp=web%2Fcore%2Fmodules%2Fviews%2Fsrc%2FPlugin%2Fviews%2Frelationship%2FGroupwiseMax.php;h=1db8da7f72c70b3fef9302240ec435dcb6c49d0a;hb=a2bd1bf0c2c1f1a17d188f4dc0726a45494cefae;hp=0000000000000000000000000000000000000000;hpb=57c063afa3f66b07c4bbddc2d6129a96d90f0aad;p=yaffs-website diff --git a/web/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php b/web/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php new file mode 100644 index 000000000..1db8da7f7 --- /dev/null +++ b/web/core/modules/views/src/Plugin/views/relationship/GroupwiseMax.php @@ -0,0 +1,387 @@ + NULL]; + // Descending more useful. + $options['subquery_order'] = ['default' => 'DESC']; + $options['subquery_regenerate'] = ['default' => FALSE]; + $options['subquery_view'] = ['default' => FALSE]; + $options['subquery_namespace'] = ['default' => FALSE]; + + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + // Get the sorts that apply to our base. + $sorts = Views::viewsDataHelper()->fetchFields($this->definition['base'], 'sort'); + $sort_options = []; + foreach ($sorts as $sort_id => $sort) { + $sort_options[$sort_id] = "$sort[group]: $sort[title]"; + } + $base_table_data = Views::viewsData()->get($this->definition['base']); + + // Extends the relationship's basic options, allowing the user to pick a + // sort and an order for it. + $form['subquery_sort'] = [ + '#type' => 'select', + '#title' => $this->t('Representative sort criteria'), + // Provide the base field as sane default sort option. + '#default_value' => !empty($this->options['subquery_sort']) ? $this->options['subquery_sort'] : $this->definition['base'] . '.' . $base_table_data['table']['base']['field'], + '#options' => $sort_options, + '#description' => $this->t("The sort criteria is applied to the data brought in by the relationship to determine how a representative item is obtained for each row. For example, to show the most recent node for each user, pick 'Content: Updated date'."), + ]; + + $form['subquery_order'] = [ + '#type' => 'radios', + '#title' => $this->t('Representative sort order'), + '#description' => $this->t("The ordering to use for the sort criteria selected above."), + '#options' => ['ASC' => $this->t('Ascending'), 'DESC' => $this->t('Descending')], + '#default_value' => $this->options['subquery_order'], + ]; + + $form['subquery_namespace'] = [ + '#type' => 'textfield', + '#title' => $this->t('Subquery namespace'), + '#description' => $this->t('Advanced. Enter a namespace for the subquery used by this relationship.'), + '#default_value' => $this->options['subquery_namespace'], + ]; + + + // WIP: This stuff doesn't work yet: namespacing issues. + // A list of suitable views to pick one as the subview. + $views = ['' => '- None -']; + foreach (Views::getAllViews() as $view) { + // Only get views that are suitable: + // - base must the base that our relationship joins towards + // - must have fields. + if ($view->get('base_table') == $this->definition['base'] && !empty($view->getDisplay('default')['display_options']['fields'])) { + // TODO: check the field is the correct sort? + // or let users hang themselves at this stage and check later? + $views[$view->id()] = $view->id(); + } + } + + $form['subquery_view'] = [ + '#type' => 'select', + '#title' => $this->t('Representative view'), + '#default_value' => $this->options['subquery_view'], + '#options' => $views, + '#description' => $this->t('Advanced. Use another view to generate the relationship subquery. This allows you to use filtering and more than one sort. If you pick a view here, the sort options above are ignored. Your view must have the ID of its base as its only field, and should have some kind of sorting.'), + ]; + + $form['subquery_regenerate'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Generate subquery each time view is run'), + '#default_value' => $this->options['subquery_regenerate'], + '#description' => $this->t('Will re-generate the subquery for this relationship every time the view is run, instead of only when these options are saved. Use for testing if you are making changes elsewhere. WARNING: seriously impairs performance.'), + ]; + } + + /** + * Helper function to create a pseudo view. + * + * We use this to obtain our subquery SQL. + */ + protected function getTemporaryView() { + $view = View::create(['base_table' => $this->definition['base']]); + $view->addDisplay('default'); + return $view->getExecutable(); + } + + /** + * When the form is submitted, make sure to clear the subquery string cache. + */ + public function submitOptionsForm(&$form, FormStateInterface $form_state) { + $cid = 'views_relationship_groupwise_max:' . $this->view->storage->id() . ':' . $this->view->current_display . ':' . $this->options['id']; + \Drupal::cache('data')->delete($cid); + } + + /** + * Generate a subquery given the user options, as set in the options. + * + * These are passed in rather than picked up from the object because we + * generate the subquery when the options are saved, rather than when the view + * is run. This saves considerable time. + * + * @param $options + * An array of options: + * - subquery_sort: the id of a views sort. + * - subquery_order: either ASC or DESC. + * + * @return string + * The subquery SQL string, ready for use in the main query. + */ + protected function leftQuery($options) { + // Either load another view, or create one on the fly. + if ($options['subquery_view']) { + $temp_view = Views::getView($options['subquery_view']); + // Remove all fields from default display + unset($temp_view->display['default']['display_options']['fields']); + } + else { + // Create a new view object on the fly, which we use to generate a query + // object and then get the SQL we need for the subquery. + $temp_view = $this->getTemporaryView(); + + // Add the sort from the options to the default display. + // This is broken, in that the sort order field also gets added as a + // select field. See https://www.drupal.org/node/844910. + // We work around this further down. + $sort = $options['subquery_sort']; + list($sort_table, $sort_field) = explode('.', $sort); + $sort_options = ['order' => $options['subquery_order']]; + $temp_view->addHandler('default', 'sort', $sort_table, $sort_field, $sort_options); + } + + // Get the namespace string. + $temp_view->namespace = (!empty($options['subquery_namespace'])) ? '_' . $options['subquery_namespace'] : '_INNER'; + $this->subquery_namespace = (!empty($options['subquery_namespace'])) ? '_' . $options['subquery_namespace'] : 'INNER'; + + // The value we add here does nothing, but doing this adds the right tables + // and puts in a WHERE clause with a placeholder we can grab later. + $temp_view->args[] = '**CORRELATED**'; + + // Add the base table ID field. + $temp_view->addHandler('default', 'field', $this->definition['base'], $this->definition['field']); + + $relationship_id = NULL; + // Add the used relationship for the subjoin, if defined. + if (isset($this->definition['relationship'])) { + list($relationship_table, $relationship_field) = explode(':', $this->definition['relationship']); + $relationship_id = $temp_view->addHandler('default', 'relationship', $relationship_table, $relationship_field); + } + $temp_item_options = ['relationship' => $relationship_id]; + + // Add the correct argument for our relationship's base + // ie the 'how to get back to base' argument. + // The relationship definition tells us which one to use. + $temp_view->addHandler('default', 'argument', $this->definition['argument table'], $this->definition['argument field'], $temp_item_options); + + // Build the view. The creates the query object and produces the query + // string but does not run any queries. + $temp_view->build(); + + // Now take the SelectQuery object the View has built and massage it + // somewhat so we can get the SQL query from it. + $subquery = $temp_view->build_info['query']; + + // Workaround until https://www.drupal.org/node/844910 is fixed: + // Remove all fields from the SELECT except the base id. + $fields = &$subquery->getFields(); + foreach (array_keys($fields) as $field_name) { + // The base id for this subquery is stored in our definition. + if ($field_name != $this->definition['field']) { + unset($fields[$field_name]); + } + } + + // Make every alias in the subquery safe within the outer query by + // appending a namespace to it, '_inner' by default. + $tables = &$subquery->getTables(); + foreach (array_keys($tables) as $table_name) { + $tables[$table_name]['alias'] .= $this->subquery_namespace; + // Namespace the join on every table. + if (isset($tables[$table_name]['condition'])) { + $tables[$table_name]['condition'] = $this->conditionNamespace($tables[$table_name]['condition']); + } + } + // Namespace fields. + foreach (array_keys($fields) as $field_name) { + $fields[$field_name]['table'] .= $this->subquery_namespace; + $fields[$field_name]['alias'] .= $this->subquery_namespace; + } + // Namespace conditions. + $where = &$subquery->conditions(); + $this->alterSubqueryCondition($subquery, $where); + // Not sure why, but our sort order clause doesn't have a table. + // TODO: the call to addHandler() above to add the sort handler is probably + // wrong -- needs attention from someone who understands it. + // In the meantime, this works, but with a leap of faith. + $orders = &$subquery->getOrderBy(); + foreach ($orders as $order_key => $order) { + // But if we're using a whole view, we don't know what we have! + if ($options['subquery_view']) { + list($sort_table, $sort_field) = explode('.', $order_key); + } + $orders[$sort_table . $this->subquery_namespace . '.' . $sort_field] = $order; + unset($orders[$order_key]); + } + + // The query we get doesn't include the LIMIT, so add it here. + $subquery->range(0, 1); + + // Extract the SQL the temporary view built. + $subquery_sql = $subquery->__toString(); + + // Replace the placeholder with the outer, correlated field. + // Eg, change the placeholder ':users_uid' into the outer field 'users.uid'. + // We have to work directly with the SQL, because putting a name of a field + // into a SelectQuery that it does not recognize (because it's outer) just + // makes it treat it as a string. + $outer_placeholder = ':' . str_replace('.', '_', $this->definition['outer field']); + $subquery_sql = str_replace($outer_placeholder, $this->definition['outer field'], $subquery_sql); + + return $subquery_sql; + } + + /** + * Recursive helper to add a namespace to conditions. + * + * Similar to _views_query_tag_alter_condition(). + * + * (Though why is the condition we get in a simple query 3 levels deep???) + */ + protected function alterSubqueryCondition(AlterableInterface $query, &$conditions) { + foreach ($conditions as $condition_id => &$condition) { + // Skip the #conjunction element. + if (is_numeric($condition_id)) { + if (is_string($condition['field'])) { + $condition['field'] = $this->conditionNamespace($condition['field']); + } + elseif (is_object($condition['field'])) { + $sub_conditions = &$condition['field']->conditions(); + $this->alterSubqueryCondition($query, $sub_conditions); + } + } + } + } + + /** + * Helper function to namespace query pieces. + * + * Turns 'foo.bar' into '"foo_NAMESPACE".bar'. + * PostgreSQL doesn't support mixed-cased identifiers unless quoted, so we + * need to quote each single part to prevent from query exceptions. + */ + protected function conditionNamespace($string) { + $parts = explode(' = ', $string); + foreach ($parts as &$part) { + if (strpos($part, '.') !== FALSE) { + $part = '"' . str_replace('.', $this->subquery_namespace . '".', $part); + } + } + + return implode(' = ', $parts); + } + + /** + * {@inheritdoc} + */ + public function query() { + // Figure out what base table this relationship brings to the party. + $table_data = Views::viewsData()->get($this->definition['base']); + $base_field = empty($this->definition['base field']) ? $table_data['table']['base']['field'] : $this->definition['base field']; + + $this->ensureMyTable(); + + $def = $this->definition; + $def['table'] = $this->definition['base']; + $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'; + } + + if ($this->options['subquery_regenerate']) { + // For testing only, regenerate the subquery each time. + $def['left_query'] = $this->leftQuery($this->options); + } + else { + // Get the stored subquery SQL string. + $cid = 'views_relationship_groupwise_max:' . $this->view->storage->id() . ':' . $this->view->current_display . ':' . $this->options['id']; + $cache = \Drupal::cache('data')->get($cid); + if (isset($cache->data)) { + $def['left_query'] = $cache->data; + } + else { + $def['left_query'] = $this->leftQuery($this->options); + \Drupal::cache('data')->set($cid, $def['left_query']); + } + } + + if (!empty($def['join_id'])) { + $id = $def['join_id']; + } + else { + $id = 'subquery'; + } + $join = Views::pluginManager('join')->createInstance($id, $def); + + // use a short alias for this: + $alias = $def['table'] . '_' . $this->table; + + $this->alias = $this->query->addRelationship($alias, $join, $this->definition['base'], $this->relationship); + } + +}