You are here

function language_hierarchy_query_language_hierarchy_limit_alter in Language Hierarchy 2.x

Same name and namespace in other branches
  1. 8 language_hierarchy.module \language_hierarchy_query_language_hierarchy_limit_alter()

Implements hook_query_TAG_alter().

The 'language_hierarchy_limit' tag can be used to limit results to only show the most specific translations for each base item, as if grouped by the base field on the base table.

The limiting is accomplished by joining in a correlated sub-query onto the same base table, but with the addition of relative language priorities. This is an expensive operation and will likely not be tenable on queries over tables with many records.

For this to work, there must be an array of metadata set on the query under the 'language_hierarchy_limit' key. That array must be keyed by the unaliased field, including the table alias (e.g. node_field_data.langcode), where the language codes are found. The inner array must include the following keys:

  • 'base_table', mapped to the unaliased name of the base table.
  • 'base_field', mapped to the unaliased name of the base item ID field (e.g. nid) on the base table.
  • 'lang_codes', mapped to an array of the language codes in the fallback chain (including the most specific).

File

./language_hierarchy.module, line 381
Add sublanguage handling functionality to Drupal.

Code

function language_hierarchy_query_language_hierarchy_limit_alter(AlterableInterface $query) {
  if (!$query instanceof SelectInterface) {
    return;
  }
  $lh_metadata = $query
    ->getMetaData('language_hierarchy_limit');
  $view = $query
    ->getMetaData('view');

  // Views plugins cannot set metadata directly on the query, but we support
  // pulling it from the view build info.
  if (!$lh_metadata) {
    if ($view instanceof ViewExecutable) {
      $lh_metadata = $view->build_info['language_hierarchy_limit'];
    }
  }
  if (!$lh_metadata) {
    return;
  }
  $database = \Drupal::database();
  foreach ($lh_metadata as $qualified_field => $metadata) {
    list($base_table_alias, $base_langcode_field) = explode('.', $qualified_field);

    // Use a unique alias for the sub-query's base table.
    $intended_alias = 'lhp_subquery_base';
    $sq_base_alias = $intended_alias;
    $count = 2;
    $tables = $query
      ->getTables();
    while (!empty($tables[$sq_base_alias])) {
      $sq_base_alias = $intended_alias . '_' . $count++;
    }
    $sub_query = $database
      ->select($metadata['base_table'], $sq_base_alias);
    $sub_query
      ->addField($sq_base_alias, $base_langcode_field, 'lhp_subquery_langcode');
    $lhp_sq_alias = $sub_query
      ->addJoin('INNER', 'language_hierarchy_priority', 'lhp_subquery', "{$sq_base_alias}.{$base_langcode_field} = %alias.langcode");

    // It is actually our parent query which handles the resolution of our
    // placeholders (because we inject it as a string).
    $lang_codes_placeholder = ':db_condition_placeholder_' . $query
      ->nextPlaceholder() . '[]';
    $sub_query
      ->where("{$sq_base_alias}.langcode IN ({$lang_codes_placeholder})");
    $base_field = $metadata['base_field'];
    $sub_query
      ->where("{$sq_base_alias}.{$base_field} = {$base_table_alias}.{$base_field}");
    $sub_query
      ->orderBy($lhp_sq_alias . '.priority', 'DESC');
    $sub_query
      ->range(0, 1);

    // MySQL does not support LIMIT ranges in correlated sub-queries within JOIN
    // or WHERE IN conditions. However, it does support them in correlated sub-
    // queries residing on the left-hand-side ON clause of another join. So we
    // do a join on a trivial "SELECT 1" subquery, the single-value of which
    // becomes our "language_priority.is_highest" flag.
    //
    // The Drupal Database API requires a table to be named so we cannot just
    // specify "SELECT 1". Tiresome - yes, but we want to follow the API to
    // guarantee cross-backend support. Hopefully the database engine is smart
    // enough to optimise this. We use our own table because we know it exists.

    /** @var \Drupal\Core\Database\Query\SelectInterface $join_flag_subquery */
    $join_flag_subquery = $database
      ->select('language_hierarchy_priority')
      ->range(0, 1);
    $join_flag_subquery
      ->addExpression('1', 'is_highest');

    // Putting a sub-query in an ON condition requires us to stringify the
    // query ourselves. There is precedent in core for this - see
    // \Drupal\views\Plugin\views\relationship\GroupwiseMax::leftQuery().
    $sub_query_string = (string) $sub_query;
    $flag_alias = $query
      ->addJoin('LEFT', $join_flag_subquery, 'language_priority', "({$sub_query_string}) = {$qualified_field}", [
      $lang_codes_placeholder => $metadata['lang_codes'],
    ]);

    // Don't exclude language neutral entities.
    $or_group = $query
      ->orConditionGroup()
      ->isNotNull("{$flag_alias}.is_highest")
      ->condition($qualified_field, LanguageInterface::LANGCODE_NOT_SPECIFIED);
    $query
      ->condition($or_group);
  }
}