api-8.x-1.x-dev/src/ExtendedQueries.php

src/ExtendedQueries.php
<?php

namespace Drupal\api;

use Drupal\Core\Link;
use Drupal\api\Entity\DocBlock;
use Drupal\api\Interfaces\BranchInterface;
use Drupal\api\Interfaces\DocBlockInterface;

/**
 * Class containing more complex queries.
 *
 * Falling back into SQL queries for performance reasons, and query complexity.
 *
 * These queries are usually used within the context of an entity, but can be
 * run in isolation. They use a more traditional query builder approach and the
 * return data is not entities, it's usually objects with a series of defined
 * fields.
 *
 * They might also contain some Drupalisms within their logic and in general, it
 * will be logic not linked directly to any entity but a combination of some.
 */
class ExtendedQueries {

  /**
   * Return the name of the table use in the database per entity.
   *
   * @param string $entity_id
   *   Machine name of the entity.
   *
   * @return string|string[]|null
   *   Single table name if parameter given, all map if no parameter given
   *   or null if nothing found.
   */
  public static function entityToTable($entity_id = NULL) {
    $map = [
      'project' => 'api_project',
      'branch' => 'api_branch',
      'docblock' => 'api_branch_docblock',
      'docblock_file' => 'api_branch_docblock_file',
      'docblock_function' => 'api_branch_docblock_function',
      'docblock_class_member' => 'api_branch_docblock_class_member',
      'docblock_namespace' => 'api_branch_docblock_namespace',
      'docblock_override' => 'api_branch_docblock_override',
      'docblock_reference' => 'api_branch_docblock_reference',
      'docblock_reference_count' => 'api_branch_docblock_reference_count',
      'external_branch' => 'api_external_branch',
      'external_documentation' => 'api_external_branch_documentation',
      'php_branch' => 'api_php_branch',
      'php_documentation' => 'api_php_branch_documentation',
    ];

    if (!is_null($entity_id)) {
      return $map[$entity_id] ?? NULL;
    }

    return $map;
  }

  /**
   * Returns the database connection service object.
   *
   * @return \Drupal\Core\Database\Connection
   *   Database connection object.
   */
  protected static function connection() {
    return \Drupal::service('database');
  }

  /**
   * Loads a bare documentation object, with or without overrides.
   *
   * @param int $id
   *   Documentation ID.
   * @param string $type
   *   Object type.
   * @param bool $with_overrides
   *   Include overrides or not.
   *
   * @return array|null
   *   Documentation object array, or NULL if not found.
   */
  public static function loadExtendedArray($id, $type = '', $with_overrides = FALSE) {
    if (!$id) {
      return NULL;
    }

    if ($with_overrides) {
      return self::loadExtendedWithOverrides($id);
    }

    $docblock_table = self::entityToTable('docblock');
    $function_table = self::entityToTable('docblock_function');
    $file_table = self::entityToTable('docblock_file');
    $branch_table = self::entityToTable('branch');

    $query = self::connection()->select($docblock_table, 'ad');
    $query->fields('ad');
    $query->condition('ad.id', $id);
    if ($type == 'function') {
      $query->leftJoin($function_table, 'afunc', 'afunc.docblock = ad.id');
      $query->fields('afunc', ['signature', 'parameters', 'return_value']);
    }
    elseif ($type == 'file') {
      $query->leftJoin($file_table, 'afile', 'afile.docblock = ad.id');
      $query->fields('afile', ['basename']);
    }
    $query->leftJoin($branch_table, 'b', 'ad.branch = b.id');
    $query->fields('b', ['project', 'core_compatibility']);

    $query->leftJoin($docblock_table, 'adfile', "adfile.file_name = ad.file_name AND adfile.object_type = 'file' AND adfile.branch = ad.branch");
    $query->addField('adfile', 'id', 'file_id');
    $query = $query->range(0, 1);

    $result = $query->execute()->fetchObject();

    return !empty($result) ? (array) $result : NULL;
  }

  /**
   * Loads a documentation object.
   *
   * @param string|int $object_name_or_id
   *   The string object name or integer documentation ID to load.
   * @param \Drupal\api\Interfaces\BranchInterface|null $branch
   *   Branch object. Ignored if $object_name_or_id is an integer.
   * @param string|array|null $object_type
   *   A string type, or array of strings: class, interface, function, etc.
   *   Can be omitted if $object_name_or_id is an integer.
   * @param string|null $file_name
   *   Name of the file the object is in (if needed). Ignored if
   *   $object_name_or_id is an integer.
   *
   * @return object|null
   *   Object with information about the matching documentation, or NULL if it
   *   does not exist.
   */
  public static function loadExtendedWithOverrides($object_name_or_id, ?BranchInterface $branch = NULL, $object_type = '', $file_name = NULL) {
    if (!is_array($object_type)) {
      $object_type = [$object_type];
    }

    $docblock_table = self::entityToTable('docblock');
    $function_table = self::entityToTable('docblock_function');
    $file_table = self::entityToTable('docblock_file');
    $override_table = self::entityToTable('docblock_override');

    // We don't have a cached value. Prepare the query.
    // Set up the basic query.
    $query = self::connection()->select($docblock_table, 'ad');
    $ao = $query->leftJoin($override_table, 'ao', 'ao.docblock = ad.id');
    $query
      ->fields('ad')
      ->fields($ao, ['documented_in_docblock', 'overrides_docblock']);

    if (is_numeric($object_name_or_id)) {
      // Quick query on just the documentation ID.
      $query->condition('ad.id', $object_name_or_id);
    }
    else {
      $query
        ->condition('ad.object_type', $object_type, 'IN')
        ->condition('ad.branch', $branch->id())
        ->condition('ad.object_name', $object_name_or_id);
      if (!is_null($file_name)) {
        $query->condition('ad.file_name', $file_name);
      }
    }

    // Set up extra fields, depending on the object type.
    if (in_array('function', $object_type)) {
      $afunc = $query->leftJoin($function_table, 'afunc', 'afunc.docblock = ad.id');
      $query->fields($afunc, ['signature', 'parameters', 'return_value']);
    }
    elseif (in_array('file', $object_type)) {
      $afile = $query->leftJoin($file_table, 'afile', 'afile.docblock = ad.id');
      $query->fields($afile, ['basename']);
    }

    // Set up field for the file's doc ID.
    $query->leftJoin($docblock_table, 'adfile', "adfile.file_name = ad.file_name AND adfile.object_type = 'file' AND adfile.branch = ad.branch");
    $query->addField('adfile', 'id', 'file_docblock');

    // Execute the query, and store in cache if it's successful.
    $query = $query->range(0, 1);
    $result = $query->execute();
    $members = [
      'documentation',
      'parameters',
      'return_value',
      'see',
      'deprecated',
      'throws',
      'var',
    ];
    foreach ($result as $result_object) {
      // Grab documentation from documented parent.
      if (!empty($result_object->documented_in_docblock) && $result_object->documented_in_docblock !== $result_object->id) {
        $documented_object = self::loadExtendedWithOverrides((int) $result_object->documented_in_docblock, $branch, $object_type);
        if (isset($documented_object)) {
          foreach ($members as $member) {
            if (isset($documented_object->$member)) {
              $result_object->$member = $documented_object->$member;
            }
          }
        }
      }

      return $result_object;
    }

    return NULL;
  }

  /**
   * Finds matches for an object name in a branch.
   *
   * @param string $name
   *   Name to match (text found in the code or documentation).
   * @param string $namespaced_name
   *   Fully namespaced name to look for. Only used when $type is 'function',
   *   'function_or_constant', or 'class'. In these cases, the function tries to
   *   find matches of the namespaced name first, and if that fails, then it
   *   tries again with the plain name.
   * @param string $type
   *   Type of object to match (see api_link_name() for options).
   * @param \Drupal\api\Interfaces\BranchInterface|null $branch
   *   Object representing the branch to search. If NULL, use core compatibility
   *   instead.
   * @param string $core_compatibility
   *   If $branch is NULL, search all branches with this core compatibility.
   * @param array|null $external_branch_ids
   *   If set, instead of looking in core branches, look in API reference
   *   branches that have an ID in this array.
   *
   * @return array
   *   With results and additional information about the matches.
   */
  public static function findMatchesAdvanced($name, $namespaced_name, $type, ?BranchInterface $branch = NULL, $core_compatibility = '', $external_branch_ids = NULL) {
    $connection = self::connection();
    $branch_table = self::entityToTable('branch');
    $docblock_table = self::entityToTable('docblock');
    $external_documentation_table = self::entityToTable('external_documentation');
    $docblock_reference_table = self::entityToTable('docblock_reference');
    $docblock_file_table = self::entityToTable('docblock_file');
    $match_name_field = '';

    // Build a query to find the matches.
    if ($external_branch_ids) {
      // This will not work for 'yaml_string' references.
      if ($type == 'yaml_string') {
        return [];
      }
      $using_external_branch = TRUE;
      $query = $connection->select($external_documentation_table, 'ad')
        ->fields('ad')
        ->condition('ad.external_branch', $external_branch_ids, 'IN');
    }
    else {
      $using_external_branch = FALSE;
      $query = $connection->select($docblock_table, 'ad')
        ->fields('ad', [
          'id',
          'branch',
          'object_name',
          'title',
          'object_type',
          'summary',
          'file_name',
        ]);
      $query->innerJoin($branch_table, 'b', 'ad.branch = b.id');
      $query->fields('b', ['slug', 'preferred', 'project']);
      if ($branch) {
        $query->condition('ad.branch', $branch->id());
      }
      else {
        $query->condition('b.core_compatibility', $core_compatibility);
      }
    }

    // Deal with namespaces and annotations.
    if ($type == 'annotation') {
      // We are looking for an annotation class.
      $query->innerJoin($docblock_reference_table, 'r_annotation', 'ad.id = r_annotation.docblock');
      $query->condition('r_annotation.object_type', 'annotation_class');
      $query->condition('ad.object_type', 'class');
    }

    $search_name_field = NULL;
    $original_name = '';
    if (in_array($type, ['function', 'function_or_constant', 'class'])) {
      $original_name = $name;
      $name = $namespaced_name;
      $query->addField('ad', 'namespaced_name', 'match_name');
      $match_name_field = 'ad.namespaced_name';
      $query->addField('ad', 'object_name', 'search_name');
      $search_name_field = 'ad.object_name';
    }
    elseif ($type != 'theme' && $type != 'file' && $type != 'yaml_string' && $type != 'element') {
      $query->addField('ad', 'object_name', 'match_name');
      $match_name_field = 'ad.object_name';
      $search_name_field = NULL;
    }

    // Figure out what potential names we should match on.
    $potential_names = [$name];
    $prefer_shorter = FALSE;
    $prefer_earlier = FALSE;

    if ($type == 'hook') {
      $potential_names = [
        'hook_' . $name,
        'hook_entity_' . $name,
        'hook_entity_bundle_' . $name,
        'hook_field_' . $name,
        'field_default_' . $name,
        'hook_user_' . $name,
        'hook_node_' . $name,
      ];
      $prefer_earlier = TRUE;
      $query->condition('ad.object_type', 'function');
    }
    elseif ($type == 'fieldhook') {
      $potential_names = [
        'hook_field_' . $name,
        'field_default_' . $name,
      ];
      $prefer_earlier = TRUE;
      $query->condition('ad.object_type', 'function');
    }
    elseif ($type == 'entityhook') {
      $potential_names = [
        'hook_entity_' . $name,
        'hook_entity_bundle_' . $name,
        'hook_node_' . $name,
      ];
      $prefer_earlier = TRUE;
      $query->condition('ad.object_type', 'function');
    }
    elseif ($type == 'userhook') {
      $potential_names = [
        'hook_user_' . $name,
      ];
      $prefer_earlier = TRUE;
      $query->condition('ad.object_type', 'function');
    }
    elseif ($type == 'alter hook') {
      $potential_names = ['hook_' . $name . '_alter'];
      $query->condition('ad.object_type', 'function');
    }
    elseif ($type == 'theme') {
      $potential_names = [];
      // Potential matches are the whole theme call, or with stripped off pieces
      // separated by __. And we look for template files preferably over
      // functions.
      $prefer_shorter = TRUE;
      $hook_elements = explode('__', $name);
      while (count($hook_elements) > 0) {
        $hook = implode('__', $hook_elements);
        $potential_names[] = str_replace('_', '-', $hook) . '.html.twig';
        $potential_names[] = str_replace('_', '-', $hook) . '.tpl.php';
        $potential_names[] = 'theme_' . $hook;
        array_pop($hook_elements);
      }
      // Because this needs to match theme files, match on object title (which
      // is the file base name).
      $query->condition('ad.object_type', ['file', 'function'], 'IN');
      $query->addField('ad', 'title', 'match_name');
      $match_name_field = 'ad.title';
    }
    elseif ($type == 'element') {
      // The string is the machine name of an element, and we want to link
      // to the element class.
      $query->innerJoin($docblock_reference_table, 'ars', "ad.id = ars.docblock AND ars.object_type = 'element'");
      $query->addField('ars', 'object_name', 'match_name');
      $match_name_field = 'ars.object_name';
      $search_name_field = NULL;
    }
    elseif ($type == 'function') {
      $query->condition('ad.object_type', 'function');
    }
    elseif ($type == 'service') {
      $query->condition('ad.object_type', 'service');
    }
    elseif ($type == 'global') {
      $query->condition('ad.object_type', 'global');
    }
    elseif ($type == 'function_or_constant') {
      $query->condition('ad.object_type', [
        'function',
        'constant',
        'class',
        'interface',
        'trait',
      ], 'IN');
    }
    elseif ($type == 'file') {
      // For files other than HTML type, the title is the basename of the file.
      // For HTML files, the title is taken from the HTML title element. So,
      // if we are matching in an API reference branch, try to match on the
      // title field, which is what we have. If we are matching in a regular
      // branch, join to the file table and match on the basename field.
      if ($using_external_branch) {
        $query->condition('ad.object_type', 'file');
        $query->addField('ad', 'title', 'match_name');
        $match_name_field = 'ad.title';
      }
      else {
        $query->leftJoin($docblock_file_table, 'af', 'ad.id = af.docblock');
        $query->addField('af', 'basename', 'match_name');
        $match_name_field = 'af.basename';
      }
    }
    elseif ($type == 'constant') {
      $query->condition('ad.object_type', 'constant');
    }
    elseif ($type == 'class') {
      $query->condition('ad.object_type', ['class', 'interface', 'trait'], 'IN');
    }
    elseif ($type == 'group') {
      $query->condition('ad.object_type', 'group');
    }
    elseif ($type == 'yaml_string') {
      // This is a bit of a different case. Here, we have a string and we are
      // trying to see if it was defined as a top-level key in a YAML services
      // or routing file. This would be stored in the {api_reference_storage}
      // table.
      $query->innerJoin($docblock_reference_table, 'ar', 'ad.id = ar.docblock');
      $query->addField('ar', 'object_name', 'match_name');
      $query->condition('ar.object_type', 'yaml string');
      $match_name_field = 'ar.object_name';
      $search_name_field = NULL;
    }

    // Execute the query and make an array of matches, making sure to only
    // keep the highest-priority matches.
    if ($match_name_field) {
      $query->condition($match_name_field, $potential_names, 'IN');
    }

    $results = $query->execute()->fetchAll();
    return [
      'results' => $results,
      'potential_names' => $potential_names,
      'search_name_field' => $search_name_field,
      'prefer_shorter' => $prefer_shorter,
      'prefer_earlier' => $prefer_earlier,
      'using_external_branch' => $using_external_branch,
      'original_name' => $original_name,
    ];
  }

  /**
   * Finds matches for a class and member names given.
   *
   * @param string $class_name
   *   Name of the class.
   * @param string $member_name
   *   Name of the member.
   * @param \Drupal\api\Interfaces\BranchInterface|null $branch
   *   Branch object.
   * @param string|null $core_compatibility
   *   Core compatibility if branch is not given.
   *
   * @return object[]|null
   *   Resulting records.
   */
  public static function findMatchingMembersAdvanced($class_name, $member_name, ?BranchInterface $branch = NULL, $core_compatibility = NULL) {
    $connection = self::connection();
    $branch_table = self::entityToTable('branch');
    $docblock_table = self::entityToTable('docblock');
    $docblock_class_member_table = self::entityToTable('docblock_class_member');

    // Make a query to find the class.
    $query = $connection->select($docblock_table, 'ad_class');
    $query->innerJoin($branch_table, 'b', 'ad_class.branch = b.id');
    $query->fields('b', ['slug', 'preferred', 'project']);
    if ($branch) {
      $query->condition('ad_class.branch', $branch->id());
    }
    else {
      $query->condition('b.core_compatibility', $core_compatibility);
    }
    $query->condition('ad_class.namespaced_name', $class_name);

    // Join to find the members.
    $query->innerJoin($docblock_class_member_table, 'am', 'ad_class.id = am.class_docblock');

    // Join to find info about the member name.
    $query->innerJoin($docblock_table, 'ad_member', 'am.docblock = ad_member.id');

    $condition = $connection->condition('OR')
      ->condition('am.member_alias', $member_name)
      ->condition('ad_member.member_name', $member_name);
    $query->condition($condition);

    $query->addField('am', 'member_alias', 'member1');
    $query->addField('ad_member', 'member_name', 'member2');
    $query->addField('ad_class', 'namespaced_name', 'classname');
    $query->fields('ad_member', [
      'id',
      'branch',
      'object_name',
      'title',
      'object_type',
      'summary',
      'file_name',
    ]);

    return $query->execute()->fetchAll();
  }

  /**
   * Gets an array of all the plain functions belonging to a branch.
   *
   * @param \Drupal\api\Interfaces\BranchInterface $branch
   *   Branch to check data from.
   *
   * @return array
   *   Function dump information.
   */
  public static function getFunctionDumpByBranch(BranchInterface $branch) {
    $docblock_table = self::entityToTable('docblock');
    $functions_table = self::entityToTable('docblock_function');

    $query = self::connection()->select($docblock_table, 'd')
      ->fields('d', ['summary'])
      ->orderBy('d.title');
    $query->innerJoin($functions_table, 'f', 'd.id = f.docblock');
    $query->fields('f', ['signature']);
    $query->condition('d.branch', $branch->id())
      ->condition('d.class', 0)
      ->condition('d.object_type', 'function');
    $result = $query->execute();

    $output = [];
    foreach ($result as $object) {
      // Make sure the summary is free of HTML tags and newlines.
      $summary = $object->summary;
      $summary = $summary ? strip_tags($summary) : '';
      $summary = preg_replace('|\s+|', ' ', $summary);

      $output[] = [
        'signature' => $object->signature,
        'summary' => $summary,
      ];
    }

    return $output;
  }

  /**
   * Calculates all new reference counts for a given branch.
   *
   * @param \Drupal\api\Interfaces\BranchInterface $branch
   *   Branch to check counts.
   */
  public static function calculateReferenceCounts(BranchInterface $branch) {
    // Creating thousands of entities will take a long time and this table can
    // (and should) be recreated at any time without too much cost. Also,
    // making the calculations, joins, etc via entities will not scale.
    $connection = self::connection();
    $reference_count_table = self::entityToTable('docblock_reference_count');
    $namespace_table = self::entityToTable('docblock_namespace');
    $reference_table = self::entityToTable('docblock_reference');
    $override_table = self::entityToTable('docblock_override');
    $docblock_table = self::entityToTable('docblock');

    // Remove existing reference counts for this branch.
    $connection->delete($reference_count_table)
      ->condition('branch', $branch->id())
      ->execute();

    // Calculate use counts for classes, with fully-qualified namespaced class
    // name. The entries in $namespace_table have the namespace declarations,
    // which are missing the initial \ character.
    $select = $connection->select($namespace_table, 'an');
    $select->leftJoin($docblock_table, 'ad', 'an.docblock = ad.id');
    $select->addExpression("CONCAT('\\\\', an.class_name)", 'object_name');
    $select->addExpression($branch->id(), 'branch');
    $select->addExpression("'use'", 'reference_type');
    $select->addExpression('COUNT(*)', 'reference_count');
    $select->addExpression('UUID()', 'uuid');
    $select
      ->condition('an.object_type', 'use_alias')
      ->condition('ad.branch', $branch->id())
      ->groupBy('an.class_name');
    $connection->insert($reference_count_table)
      ->from($select)
      ->execute();

    // Calculate call counts for functions and methods, including usage of
    // constants, and extend/implements for classes/interfaces. For simple
    // functions, these will not have namespaces on them. For class members and
    // extend/implements of classes, they will have namespaces.
    $select = $connection->select($reference_table, 'ars');
    $select->addField('ars', 'object_name', 'object_name');
    $select->addExpression($branch->id(), 'branch');
    $select->addExpression("'call'", 'reference_type');
    $select->addExpression('COUNT(*)', 'reference_count');
    $select->addExpression('UUID()', 'uuid');
    $select
      ->condition('ars.object_type', [
        'function',
        'constant',
        'member-class',
        'computed-member',
        'class',
        'interface',
      ], 'IN')
      ->condition('ars.branch', $branch->id())
      ->groupBy('ars.object_name');
    $connection->insert($reference_count_table)
      ->from($select)
      ->execute();

    // Calculate string reference counts. Some strings have namespaces, and some
    // don't, similar to the 'call' reference count above.
    $select = $connection->select($reference_table, 'ars');
    $select->addField('ars', 'object_name', 'object_name');
    $select->addExpression($branch->id(), 'branch');
    $select->addExpression("'string'", 'reference_type');
    $select->addExpression('COUNT(*)', 'reference_count');
    $select->addExpression('UUID()', 'uuid');
    $select
      ->condition('ars.object_type', [
        'potential callback',
        'potential file',
        'service_class',
        'yaml string',
      ], 'IN')
      ->condition('ars.branch', $branch->id())
      ->groupBy('ars.object_name');
    $connection->insert($reference_count_table)
      ->from($select)
      ->execute();

    // Calculate override counts for methods and other class members. For
    // disambiguation, store the namespaced name of the class::member.
    $select = $connection->select($override_table, 'ao');
    $select->leftJoin($docblock_table, 'ad', 'ao.docblock = ad.id');
    $select->leftJoin($docblock_table, 'ado', 'ao.overrides_docblock = ado.id');
    // Object name is the name of the overridden method.
    $select->addExpression('ado.namespaced_name', 'object_name');
    $select->addExpression($branch->id(), 'branch');
    $select->addExpression("'override'", 'reference_type');
    $select->addExpression('COUNT(*)', 'reference_count');
    $select->addExpression('UUID()', 'uuid');
    $select
      // Some records in $override_table are just about where the
      // documentation is. Filter these out.
      ->condition('ao.overrides_docblock', 0, '<>')
      // Some overrides may be in other branches. Filter these out too.
      ->condition('ad.branch', $branch->id())
      ->condition('ado.branch', $branch->id())
      ->groupBy('ado.namespaced_name');
    $connection->insert($reference_count_table)
      ->from($select)
      ->execute();
  }

  /**
   * Updates calculated references based on the member information given.
   *
   * @param array $member_info
   *   Member information.
   * @param int $class_id
   *   ID of the class.
   */
  public static function updateCalculatedReferences(array &$member_info, $class_id) {
    $connection = self::connection();
    $reference_table = self::entityToTable('docblock_reference');
    $docblock_table = self::entityToTable('docblock');
    $class_members_table = self::entityToTable('docblock_class_member');

    // Update the calculated member reference storage for this class, first
    // deleting any existing computed-member entries. These are for when
    // you have self::foo() or parent::bar() calls inside a method. We will
    // delete all computed-member entries, and recalculate the self:: ones here;
    // the parent:: ones are recalculated in api_shutdown() because we do not
    // necessarily have the parent class member information in the database
    // at this point.
    if (isset($member_info[$class_id]) && count($member_info[$class_id]['function'])) {
      // Delete all previous computed-member entries for calls from direct
      // methods of this class.
      $direct_methods = array_values($member_info[$class_id]['function']);
      $connection->delete($reference_table)
        ->condition('object_type', 'computed-member')
        ->condition('docblock', $direct_methods, 'IN')
        ->execute();

      // Calculate computed-member entries for member-self references. We're
      // taking an entry that says something like ThisClass::foo() calls
      // self::bar(), and trying to calculate the fully namespaced name of
      // self::bar(), which might be ThisClass::bar() or SomeParentClass::bar().
      $select = $connection->select($reference_table, 'r')
        ->condition('r.object_type', 'member-self')
        ->condition('r.docblock', $direct_methods, 'IN');
      // This joins to the documentation record of the calling method.
      $select->innerJoin($docblock_table, 'd', 'r.docblock = d.id');
      // This links to the now-updated member list of the class the calling
      // method is in.
      $select->innerJoin($class_members_table, 'm', 'd.class = m.class_docblock');
      // We're looking for a method whose name is r.object_name. It is either:
      // - m.member_alias (if it came from a trait with an alias)
      // - m.member_alias is NULL and it's the member name in the member's
      //   documentation record.
      $select->innerJoin($docblock_table, 'dm', 'm.docblock = dm.id');
      $and = $connection->condition('AND')
        ->where('dm.member_name = r.object_name')
        ->isNull('m.member_alias');
      $or = $connection->condition('OR')
        ->condition($and)
        ->where('m.member_alias = r.object_name');
      $select->condition($or);
      $select->condition('dm.object_type', 'function');

      // Now make up the needed fields for the api_branch_docblock_reference
      // records, and insert them into the table.
      $select->distinct();
      $select->addField('dm', 'namespaced_name', 'object_name');
      $select->addField('r', 'branch', 'branch');
      $select->addField('r', 'docblock', 'docblock');
      // Note: SelectQuery adds expressions to the query at the end of the field
      // list.
      $select->addExpression("'computed-member'", 'object_type');
      $select->addExpression('UUID()', 'uuid');

      $connection->insert($reference_table)
        ->fields(['object_name', 'branch', 'docblock', 'object_type', 'uuid'])
        ->from($select)
        ->execute();
    }
  }

  /**
   * Enters new records based on a complex query of direct methods.
   *
   * @param array $direct_methods
   *   List of direct methods.
   */
  public static function createNewReferences(array &$direct_methods) {
    $connection = self::connection();
    $reference_table = self::entityToTable('docblock_reference');
    $docblock_table = self::entityToTable('docblock');
    $class_members_table = self::entityToTable('docblock_class_member');

    $select = $connection->select($reference_table, 'r')
      ->condition('r.object_type', 'member-parent')
      ->condition('r.docblock', $direct_methods, 'IN');
    // This joins to the documentation record of the calling method.
    $select->innerJoin($docblock_table, 'cd', 'r.docblock = cd.id');
    // This finds the parent (extends) class record.
    $select->innerJoin($reference_table, 'e', 'e.docblock = cd.class');
    $select
      ->condition('e.object_type', 'class')
      ->isNotNull('e.extends_docblock');
    // This finds the members of the parent class.
    $select->innerJoin($class_members_table, 'pm', 'pm.class_docblock = e.extends_docblock');
    // We're looking for a method whose name is r.object_name. It is either:
    // - pm.member_alias (if it came from a trait with an alias)
    // - pm.member_alias is NULL and it's the member name in the member's
    //   documentation record.
    $select->innerJoin($docblock_table, 'dm', 'pm.docblock = dm.id');
    $and = $connection->condition('AND')
      ->where('dm.member_name = r.object_name')
      ->isNull('pm.member_alias');
    $and2 = $connection->condition('AND')
      ->where('pm.member_alias = r.object_name')
      ->isNotNull('pm.member_alias');
    $or = $connection->condition('OR')
      ->condition($and)
      ->condition($and2);
    $select->condition($or);
    $select->condition('dm.object_type', 'function');

    // Now make up the needed fields for the api_reference_storage records,
    // and insert them into the table.
    $select->distinct();
    $select->addField('dm', 'namespaced_name', 'object_name');
    $select->addField('r', 'branch', 'branch');
    $select->addField('r', 'docblock', 'docblock');
    // Note: SelectQuery adds expressions to the query at the end of the field
    // list.
    $select->addExpression("'computed-member'", 'object_type');
    $select->addExpression('UUID()', 'uuid');

    $connection->insert($reference_table)
      ->fields(['object_name', 'branch', 'docblock', 'object_type', 'uuid'])
      ->from($select)
      ->execute();
  }

  /**
   * Counts or lists references to a function or file.
   *
   * @param \Drupal\api\Interfaces\DocBlockInterface $docBlock
   *   DocBlock object.
   * @param \Drupal\api\Interfaces\BranchInterface $branch
   *   Object representing the branch to check.
   * @param string $type
   *   Type of reference. One of:
   *   - 'calls': $name is the name of a function, find function calls.
   *   - 'implementations': $name is the name of a hook, find implementations.
   *   - 'invokes': $name is the name of a hook, find invocations.
   *   - 'theme_invokes': $name is the name of a theme function or file, find
   *     theme calls.
   *   - 'element_invokes': $name is the name of an element, find #type uses of
   *     this element.
   *   - 'theme_references': $name is the name of a theme function or file, find
   *     string references to the theme hook name.
   *   - 'references': $name is the name of a function, find string references.
   *   - 'overrides': $exclude_id is the ID of a method, find overriding ones.
   *   - 'constants': $name is the name of a constant, find uses.
   *   - 'uses': $name is the fully-namespaced name of a class, find files that
   *     have use declarations.
   *   - 'annotations': $exclude_id is the ID of an annotation class. Find
   *     classes using it as annotation.
   *   - 'yml_config': $name is a YML config file name, find string references
   *     to the file name without .yml.
   *   - 'yml_keys': $name is a YML config file name, and $exclude_id is its
   *     documentation ID. Find references to keys in this file.
   *   - 'services': $name is a class, find services that reference it.
   *   - 'use': $name is a service, find functions that reference its name.
   * @param bool $count
   *   If TRUE (default), return a count. If FALSE, return an array.
   * @param int|null $exclude_id
   *   (optional) Document ID to exclude from the query, or to use as the ID for
   *   overrides and element_invokes.
   * @param int $limit
   *   (optional) Limit the number of references returned to this amount.
   * @param bool $is_drupal
   *   (optional) If set to FALSE, and $type is a Drupal-specific type (related
   *   to themes, hooks, or YAML), just return an empty list or zero count.
   *
   * @return int|array
   *   The number of references or an array of references.
   */
  public static function findReferences(DocBlockInterface $docBlock, BranchInterface $branch, $type, $count = TRUE, $exclude_id = 0, $limit = 0, $is_drupal = TRUE) {
    // Early return: if this is a Drupal type and $is_drupal is FALSE.
    $drupal_types = [
      'implementations',
      'theme_invokes',
      'element_invokes',
      'theme_references',
      'invokes',
      'yml_config',
      'yml_keys',
    ];
    if (!$is_drupal && in_array($type, $drupal_types)) {
      return ($count) ? 0 : [];
    }

    $name = ($docBlock->getClass() && ($type == 'calls' || $type == 'constants')) ?
      $docBlock->getNamespacedName() :
      $docBlock->getTitle();
    // In a few cases, we want to look for either the base name or the
    // namespaced name.
    $altname = $docBlock->getNamespacedName();
    if ($type == 'uses' || $type == 'services') {
      $name = $altname;
    }

    $references_type = 'potential callback';
    $group = FALSE;
    if ($type == 'yml_config') {
      // Verify this is a *.yml file. If not, just return.
      if (strpos($name, '.yml') != strlen($name) - 4) {
        return ($count) ? 0 : [];
      }
      // Strip off the .yml and look for string references.
      $name = substr($name, 0, strlen($name) - 4);
      $type = 'references';
      $references_type = 'potential file';
    }

    $connection = self::connection();
    $docblock_table = self::entityToTable('docblock');
    $docblock_reference_table = self::entityToTable('docblock_reference');
    $docblock_override_table = self::entityToTable('docblock_override');
    $docblock_namespace_table = self::entityToTable('docblock_namespace');

    $matches = [];
    $branch_id = $branch->id();
    $base_query = NULL;

    if ($type == 'calls' || $type == 'constants') {
      // Use reference storage to find functions that call this one, or
      // use this constant.
      $base_query = $connection->select($docblock_reference_table, 'r');
      $d = $base_query->leftJoin($docblock_table, 'd', 'r.docblock = d.id');
      $base_query->condition('d.branch', $branch_id)
        ->condition('d.id', $exclude_id, '<>')
        ->condition('r.object_type', [
          'function',
          'constant',
          'member-class',
          'computed-member',
        ], 'IN')
        ->condition('r.object_name', $name);
    }
    elseif ($type == 'overrides') {
      // Use overrides storage to find overrides of the ID passed as "exclude".
      $base_query = $connection->select($docblock_override_table, 'o');
      $d = $base_query->leftJoin($docblock_table, 'd', 'o.docblock = d.id');
      $base_query->condition('d.branch', $branch_id)
        ->condition('o.overrides_docblock', $exclude_id);
    }
    elseif ($type == 'references') {
      // Use reference storage to find functions that use this name as a string.
      $base_query = $connection->select($docblock_reference_table, 'r');
      $d = $base_query->innerJoin($docblock_table, 'd', 'r.docblock = d.id');
      $base_query->condition($d . '.branch', $branch_id)
        ->condition($d . '.id', $exclude_id, '<>')
        ->condition('r.object_type', $references_type);
      if ($altname) {
        $base_query->condition('r.object_name', [$name, $altname], 'IN');
      }
      else {
        $base_query->condition('r.object_name', $name);
      }
    }
    elseif ($type == 'use') {
      // Use reference storage to find functions that reference a service. It
      // could have a . in it, so it is either stored as 'potential file' or
      // 'potential callback'.
      $base_query = $connection->select($docblock_reference_table, 'r');
      $d = $base_query->innerJoin($docblock_table, 'd', 'r.docblock = d.id');
      $base_query->condition($d . '.branch', $branch_id)
        ->condition($d . '.id', $exclude_id, '<>')
        ->condition('r.object_type', ['potential file', 'potential callback'], 'IN');
      $base_query->condition('r.object_name', $name);
    }
    if ($type == 'services') {
      // Use reference storage to find services using this class.
      $base_query = $connection->select($docblock_reference_table, 'r');
      $d = $base_query->leftJoin($docblock_table, 'd', 'r.docblock = d.id');
      $base_query->condition('d.branch', $branch_id)
        ->condition('d.id', $exclude_id, '<>')
        ->condition('r.object_type', ['service_class'], 'IN')
        ->condition('r.object_name', $name);
    }
    elseif ($type == 'annotations') {
      // In this case, $exclude_id is the documentation ID of a class,
      // presumably with an 'annotation_class' reference in the table. Find
      // other classes that annotated with this same class name, which will
      // have 'annotation' references.
      $base_query = $connection->select($docblock_reference_table, 'r_annotation')
        ->condition('r_annotation.docblock', $exclude_id)
        ->condition('r_annotation.object_type', 'annotation_class');
      $base_query->innerJoin($docblock_reference_table, 'r_uses', "r_annotation.object_name = r_uses.object_name AND r_uses.object_type = 'annotation'");
      $base_query->innerJoin($docblock_table, 'd', 'r_uses.docblock = d.id');
      $base_query
        ->condition('d.branch', $branch_id);
      $group = TRUE;
    }
    elseif ($type == 'yml_keys') {
      // In this case, $exclude_id is the documentation ID of a file. We
      // want to find the string keys that file has in reference storage, and
      // then match those with 'potential callback' or 'potential file'
      // references.
      $base_query = $connection->select($docblock_reference_table, 'r');
      $base_query->innerJoin($docblock_table, 'd', 'r.docblock = d.id');
      $base_query
        ->condition('d.branch', $branch_id)
        ->condition('r.object_type', ['potential callback', 'potential file'], 'IN');
      $base_query->innerJoin($docblock_reference_table, 'rstrings', 'r.object_name = rstrings.object_name');
      $base_query
        ->condition('rstrings.docblock', $exclude_id)
        ->condition('rstrings.object_type', 'yaml string');
      // A given function/method might have multiple matching YML keys, so we
      // need to group this query.
      $group = TRUE;
    }
    elseif ($type == 'uses') {
      // Use namespace storage to find files with use declarations for this
      // class. Note that $name probably starts with a backslash, while use
      // declarations do not.
      $name = substr(Formatter::fullClassname($name), 1);
      $base_query = $connection->select($docblock_namespace_table, 'n');
      $d = $base_query->leftJoin($docblock_table, 'd', 'n.docblock = d.id');
      $base_query->condition('d.branch', $branch_id)
        ->condition('d.id', $exclude_id, '<>')
        ->condition('n.object_type', 'use_alias')
        ->condition('n.class_name', $name);
    }
    elseif ($type == 'implementations') {
      // Use pattern matching to find functions that look like implementations
      // of this one. i.e. something_hookname, where "something" doesn't start
      // with an underscore. Note that LIKE uses _ as "match any one character",
      // so _ has to be escaped in this query. Limit this to Drupal functions.
      if (strpos($name, 'hook_') === 0) {
        $hook_name = substr($name, 5);
        // If the hook has an UPPER_CASE_SECTION, allow that to match anything,
        // for cases like hook_update_N() and hook_form_FORM_ID_alter(). Note
        // that the upper-case section could be in the middle or end.
        $hook_name = preg_replace('/_[A-Z][A-Z_]*_/', '\_%\_', $hook_name);
        $hook_name = preg_replace('/_[A-Z][A-Z_]*$/', '\_%', $hook_name);
        $base_query = $connection->select($docblock_table, 'd');
        $base_query->leftJoin($docblock_reference_table, 'r', 'r.docblock = d.id AND r.object_type = :type AND r.object_name = :hook', [
          ':type' => $type,
          ':hook' => $hook_name,
        ]);
        $procedural_conditions = $base_query->andConditionGroup();
        $procedural_conditions
          ->condition('d.object_name', '%\_' . $hook_name, 'LIKE')
          ->condition('d.object_name', '\_%', 'NOT LIKE')
          ->condition('d.object_name', 'hook\_%', 'NOT LIKE')
          ->condition('d.object_type', 'function')
          ->condition('d.branch', $branch_id)
          ->condition('d.id', $exclude_id, '<>')
          ->condition('d.is_drupal', 1);
        $all_conditions = $base_query->orConditionGroup()
          ->condition($procedural_conditions)
          ->isNotNull('r.id');
        $base_query->condition($all_conditions);
      }
    }
    elseif ($type == 'theme_invokes' || $type == 'theme_references') {
      // Use reference storage to find functions that call this theme, or
      // string references to this theme. Limit this to Drupal functions on the
      // calls.
      $theme_name = '';
      if (strpos($name, 'theme_') === 0) {
        // It's presumably a theme function.
        $theme_name = substr($name, 6);
      }
      elseif (strpos($name, '.tpl.php') == strlen($name) - 8) {
        // It's presumably a theme template file.
        $name = basename($name);
        $theme_name = str_replace('-', '_', substr($name, 0, strlen($name) - 8));
      }
      elseif (preg_match('|^(.*)\.[^.]*\.twig$|', $name, $matches)) {
        // It's a *.*.twig file.
        $name = basename($name);
        $theme_name = str_replace('-', '_', basename($matches[1]));
      }

      if (strlen($theme_name) > 0) {
        // We could get calls to things like theme('name') or theme('name__sub')
        // (or the corresponding strings) recorded in the references. Match
        // either. Note _ escaping for LIKE queries.
        $base_query = $connection->select($docblock_reference_table, 'r');
        $d = $base_query->innerJoin($docblock_table, 'd', 'r.docblock = d.id');
        $base_query->condition($d . '.branch', $branch_id)
          ->condition($d . '.id', $exclude_id, '<>')
          ->condition('d.is_drupal', 1)
          ->condition(
            $connection->condition('OR')
              ->condition('r.object_name', $theme_name)
              ->condition('r.object_name', $theme_name . '\_\_%', 'LIKE')
          );
        if ($type == 'theme_invokes') {
          $base_query->condition('r.object_type', 'potential theme');
        }
        else {
          $base_query->condition('r.object_type', 'potential callback');
        }
      }
    }
    elseif ($type == 'invokes') {
      // Use reference storage to find functions that invoke this hook.
      // The reference storage holds the string that was actually found inside
      // the invoking function. So $name could be one of:
      // - "hook_$string",
      // - "hook_field_$string"
      // - "field_default_$string"
      // - "hook_user_$string"
      // - "hook_entity_$string"
      // - "hook_entity_bundle_$string"
      // - "hook_node_$string"
      // - "hook_$string_alter"
      // For these, {reference_storage} has object_type of 'potential hook',
      // 'potential fieldhook', etc. So, we need to build a query that will find
      // these matches between $string (field object_name) and $name (passed
      // into this function). And only do this for Drupal functions.
      $or_clauses = $connection->condition('OR');
      $found = FALSE;

      if (strpos($name, 'hook_') === 0) {
        $hook_name = substr($name, 5);
        $or_clauses->condition(
          $connection->condition('AND')
            ->condition('r.object_type', 'potential hook')
            ->condition('r.object_name', $hook_name)
        );
        $found = TRUE;
        if (strpos($hook_name, 'user_') === 0) {
          $sub_hook_name = substr($hook_name, 5);
          $or_clauses->condition(
            $connection->condition('AND')
              ->condition('r.object_type', 'potential userhook')
              ->condition('r.object_name', $sub_hook_name)
          );
        }
        elseif (strpos($hook_name, 'entity_') === 0) {
          $sub_hook_name = substr($hook_name, 7);
          $or_clauses->condition(
            $connection->condition('AND')
              ->condition('r.object_type', 'potential entityhook')
              ->condition('r.object_name', $sub_hook_name)
          );
          if (strpos($sub_hook_name, 'bundle_') === 0) {
            $sub_sub_hook_name = substr($sub_hook_name, 7);
            $or_clauses->condition(
              $connection->condition('AND')
                ->condition('r.object_type', 'potential entityhook')
                ->condition('r.object_name', $sub_sub_hook_name)
            );
          }
        }
        elseif (strpos($hook_name, 'field_') === 0) {
          $sub_hook_name = substr($hook_name, 6);
          $or_clauses->condition(
            $connection->condition('AND')
              ->condition('r.object_type', 'potential fieldhook')
              ->condition('r.object_name', $sub_hook_name)
          );
        }
        elseif (strpos($hook_name, 'node_') === 0) {
          $sub_hook_name = substr($hook_name, 5);
          $or_clauses->condition(
            $connection->condition('AND')
              ->condition('r.object_type', 'potential entityhook')
              ->condition('r.object_name', $sub_hook_name)
          );
        }
        elseif (strrpos($hook_name, '_alter') === strlen($hook_name) - 6) {
          $sub_hook_name = substr($hook_name, 0, strlen($hook_name) - 6);
          $or_clauses->condition(
            $connection->condition('AND')
              ->condition('r.object_type', 'potential alter')
              ->condition('r.object_name', $sub_hook_name)
          );
        }
      }
      elseif (strpos($name, 'field_default_') === 0) {
        $hook_name = substr($name, 14);
        $or_clauses->condition(
          $connection->condition('AND')
            ->condition('r.object_type', 'potential fieldhook')
            ->condition('r.object_name', $hook_name)
        );
        $found = TRUE;
      }

      // If we found at least one match, run this query we've built.
      if ($found) {
        $base_query = $connection->select($docblock_reference_table, 'r');
        $d = $base_query->innerJoin($docblock_table, 'd', 'r.docblock = d.id');
        $base_query->condition('d.branch', $branch_id)
          ->condition('d.id', $exclude_id, '<>')
          ->condition('d.is_drupal', 1)
          ->condition($or_clauses);
      }
    }
    elseif ($type == 'element_invokes') {
      // $exclude_id is the ID of a potential element class. This must have
      // an 'element' reference in reference storage. Then we're looking for
      // 'potential element' references to that element machine name.
      $base_query = $connection->select($docblock_reference_table, 'r')
        ->condition('r.docblock', $exclude_id)
        ->condition('r.object_type', 'element');
      $base_query->innerJoin($docblock_reference_table, 'r2', 'r.object_name = r2.object_name AND r.branch = r2.branch');
      $base_query->condition('r2.object_type', 'potential element');
      $d = $base_query->innerJoin($docblock_table, 'd', 'r2.docblock = d.id');
    }

    // See if we built a query to execute.
    if (is_null($base_query)) {
      return ($count) ? 0 : [];
    }

    // Execute the query.
    if ($count) {
      // We're looking for a count here.
      if ($group) {
        $base_query->addExpression('COUNT(DISTINCT d.id)', 'num');
      }
      else {
        $base_query->addExpression('COUNT(d.id)', 'num');
      }
      return $base_query->execute()->fetchField();
    }

    // If we get here, we want to return a list.
    if ($group) {
      $base_query->groupBy('d.id');
    }
    $base_query->fields('d',
      [
        'id',
        'branch',
        'object_name',
        'title',
        'summary',
        'file_name',
        'object_type',
        'class',
        'is_drupal',
      ])
      ->orderBy('d.title', 'ASC');
    if ($limit > 0) {
      $base_query->range(0, $limit);
    }

    $result = $base_query->execute()->fetchAll();
    $list = [];
    foreach ($result as $object) {
      $docBlock = DocBlock::load($object->id);
      $list[] = [
        'function' => Link::fromTextAndUrl($docBlock->getTitle(), Formatter::objectUrl($docBlock))->toString(),
        'file' => Formatter::linkFile($docBlock),
        'description' => Formatter::linkDocumentation($docBlock->getSummary(), $branch, NULL, ($docBlock->getClass() ? $docBlock->getClass()->id() : NULL), FALSE, FALSE, $docBlock->isDrupal()),
      ];
    }

    return $list;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc