api-8.x-1.x-dev/src/Entity/DocBlock.php

src/Entity/DocBlock.php
<?php

namespace Drupal\api\Entity;

use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\api\Entity\DocBlock\DocClassMember;
use Drupal\api\Entity\DocBlock\DocFile;
use Drupal\api\Entity\DocBlock\DocFunction;
use Drupal\api\Entity\DocBlock\DocNamespace;
use Drupal\api\Entity\DocBlock\DocOverride;
use Drupal\api\Entity\DocBlock\DocReference;
use Drupal\api\Formatter;
use Drupal\api\Interfaces\BranchInterface;
use Drupal\api\Interfaces\DocBlockInterface;
use Drupal\api\Traits\MatchingTrait;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Render\Attribute\FormElement;
use Drupal\Core\Render\Attribute\RenderElement;

/**
 * Defines the docblock entity class.
 *
 * @ContentEntityType(
 *   id = "docblock",
 *   label = @Translation("DocBlock"),
 *   label_collection = @Translation("DocBlocks"),
 *   handlers = {
 *     "access" = "Drupal\api\ApiAccessControlHandler",
 *     "views_data" = "Drupal\views\EntityViewsData",
 *     "storage_schema" = "Drupal\api\StorageSchema\ApiContentStorageSchema",
 *     "route_provider" = {
 *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
 *     }
 *   },
 *   base_table = "api_branch_docblock",
 *   admin_permission = "administer API reference",
 *   field_indexes = {
 *     "title",
 *     "object_name",
 *     "object_type",
 *     "file_name",
 *     "member_name",
 *     "namespace",
 *     "namespaced_name",
 *     "is_drupal"
 *   },
 *   entity_keys = {
 *     "id" = "id",
 *     "uuid" = "uuid"
 *   },
 *   links = {
 *     "canonical" = "/docblock/{docblock}"
 *   },
 * )
 */
class DocBlock extends ContentEntityBase implements DocBlockInterface {

  use MatchingTrait;
  use EntityChangedTrait;

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields = parent::baseFieldDefinitions($entity_type);

    $fields['title'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Title'))
      ->setDescription(t('Display title'))
      ->setRequired(TRUE);

    $fields['object_name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Object name'))
      ->setDescription(t('Name of object'))
      ->setRequired(TRUE);

    $fields['object_type'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Object type'))
      ->setDescription(t('Type of object: function, class, etc.'))
      ->setRequired(TRUE);

    $fields['file_name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('File name'))
      ->setDescription(t('Full path of file.'))
      ->setRequired(TRUE);

    $fields['member_name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Member name'))
      ->setDescription(t('For class members, the name without Class::'));

    $fields['documentation'] = BaseFieldDefinition::create('string_long')
      ->setLabel(t('Documentation'))
      ->setDescription(t('Full documentation'))
      ->setRequired(TRUE);

    $fields['summary'] = BaseFieldDefinition::create('string_long')
      ->setLabel(t('Summary'))
      ->setDescription(t('Short description from documentation'))
      ->setRequired(TRUE);

    $fields['code'] = BaseFieldDefinition::create('string_long')
      ->setLabel(t('Code'))
      ->setDescription(t('Formatted source code'))
      ->setRequired(TRUE);

    $fields['see'] = BaseFieldDefinition::create('string_long')
      ->setLabel(t('See'))
      ->setDescription(t('See also references'));

    $fields['var'] = BaseFieldDefinition::create('string_long')
      ->setLabel(t('Var'))
      ->setDescription(t('Variable type from var tag'));

    $fields['throws'] = BaseFieldDefinition::create('string_long')
      ->setLabel(t('Throws'))
      ->setDescription(t('Throws section'));

    $fields['deprecated'] = BaseFieldDefinition::create('string_long')
      ->setLabel(t('Deprecated'))
      ->setDescription(t('Deprecated descriptions'));

    $fields['namespace'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Namespace'))
      ->setDescription(t('Namespace, if any'));

    $fields['namespaced_name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Namespaced name'))
      ->setRequired(TRUE)
      ->setDescription(t('Fully-qualified namespaced name, starting with backslash'));

    $fields['modifiers'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Modifiers'))
      ->setDescription(t('Modifiers such as static, abstract, etc.'));

    $fields['start_line'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('Start line'))
      ->setDescription(t('Start line of the object in file'));

    $fields['is_drupal'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('Is Drupal'))
      ->setDescription(t('Whether this is or is not a Drupal object'));

    $fields['class'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Class'))
      ->setDescription(t('DocBlock ID of class this is part of'))
      ->setSetting('target_type', 'docblock')
      ->setSetting('handler', 'default')
      ->setCardinality(1);

    $fields['branch'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Branch'))
      ->setDescription(t('The identifier of the branch this documentation is part of.'))
      ->setSetting('target_type', 'branch')
      ->setSetting('handler', 'default')
      ->setRequired(TRUE)
      ->setCardinality(1);

    $fields['comments'] = BaseFieldDefinition::create('comment')
      ->setLabel(t('Comments'))
      ->setDescription(t('Comments for documentation objects.'))
      ->setSetting('comment_type', 'api_comment')
      ->setDefaultValueCallback('\Drupal\api\Entity\DocBlock::commentStatusValue');

    return $fields;
  }

  /**
   * {@inheritdoc}
   */
  public function toUrl($rel = 'canonical', array $options = []) {
    if ($rel == 'canonical') {
      $url = Formatter::objectUrl($this);
      if ($url) {
        return $url->setOptions($options);
      }
    }

    return parent::toUrl($rel, $options);
  }

  /**
   * Returns the current default value for the field.
   *
   * @return array
   *   Value for the field.
   */
  public static function commentStatusValue() {
    return [
      'status' => \Drupal::config('api.comments')->get('status') ?? CommentItemInterface::OPEN,
    ];
  }

  /**
   * Apply comments settings to all existing DocBlocks.
   */
  public static function applyCommentsStatusToAll() {
    $status = \Drupal::config('api.comments')->get('status');
    if (!is_null($status)) {
      // We don't need hooks, so mass update query.
      \Drupal::database()->update('api_branch_docblock')
        ->fields(['comments' => $status])
        ->execute();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getComments() {
    return $this->get('comments');
  }

  /**
   * {@inheritdoc}
   */
  public function label() {
    return $this->getTitle();
  }

  /**
   * {@inheritdoc}
   */
  public function getTitle() {
    return $this->get('title')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setTitle($title) {
    $this->set('title', $title);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getFileName() {
    return $this->get('file_name')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setFileName($file_name) {
    $this->set('file_name', $file_name);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getObjectName() {
    return $this->get('object_name')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setObjectName($object_name) {
    $this->set('object_name', $object_name);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getObjectType() {
    return $this->get('object_type')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setObjectType($object_type) {
    $this->set('object_type', $object_type);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getMemberName() {
    return $this->get('member_name')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setMemberName($member_name) {
    $this->set('member_name', $member_name);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocumentation() {
    return $this->get('documentation')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setDocumentation($documentation) {
    $this->set('documentation', $documentation);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getSummary() {
    return $this->get('summary')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setSummary($summary) {
    $this->set('summary', $summary);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getCode() {
    return $this->get('code')->value ?? '';
  }

  /**
   * {@inheritdoc}
   */
  public function setCode($code) {
    $this->set('code', $code);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getSee() {
    return $this->get('see')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setSee($see) {
    $this->set('see', $see);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getVar() {
    return $this->get('var')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setVar($var) {
    $this->set('var', $var);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getThrows() {
    return $this->get('throws')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setThrows($throws) {
    $this->set('throws', $throws);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getDeprecated() {
    return $this->get('deprecated')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setDeprecated($deprecated) {
    $this->set('deprecated', $deprecated);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getNamespace() {
    return $this->get('namespace')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setNamespace($namespace) {
    $this->set('namespace', $namespace);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getNamespacedName() {
    return $this->get('namespaced_name')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setNamespacedName($namespaced_name) {
    $this->set('namespaced_name', $namespaced_name);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getModifiers() {
    return $this->get('modifiers')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setModifiers($modifiers) {
    $this->set('modifiers', $modifiers);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getStartLine() {
    return $this->get('start_line')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setStartLine($start_line) {
    $this->set('start_line', $start_line);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getIsDrupal() {
    return $this->get('is_drupal')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function isDrupal() {
    return $this->getIsDrupal();
  }

  /**
   * {@inheritdoc}
   */
  public function setIsDrupal($is_drupal) {
    $this->set('is_drupal', $is_drupal);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getClass() {
    return $this->get('class')->referencedEntities()[0] ?? NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function setClass(DocBlockInterface $class) {
    $this->set('class', $class);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getBranch() {
    $referenced_entities = $this->get('branch')->referencedEntities();
    return $referenced_entities ? $referenced_entities[0] : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function setBranch(BranchInterface $branch) {
    $this->set('branch', $branch);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocFunction() {
    $function_ids = DocFunction::matches([], $this);
    if ($function_ids) {
      $function_id = array_shift($function_ids);
      return DocFunction::load($function_id);
    }

    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocFile() {
    $file_ids = DocFile::matches([], $this);
    if ($file_ids) {
      $file_id = array_shift($file_ids);
      return DocFile::load($file_id);
    }

    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocClassMembers() {
    $class_members = DocClassMember::matches([], $this);
    if ($class_members) {
      return DocClassMember::loadMultiple($class_members);
    }

    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocBlockClassMembers($member_name = NULL, $function_or_not_functions = NULL) {
    if ($this->getObjectType() !== 'class') {
      return NULL;
    }

    $members = [];

    $conditions = [
      'class' => $this->id(),
    ];
    if (!is_null($member_name)) {
      $conditions['member_name'] = $member_name;
    }
    if (!is_null($function_or_not_functions)) {
      $operator = ($function_or_not_functions) ? '=' : '<>';
      $conditions['object_type'] = [
        'operator' => $operator,
        'value' => 'function',
      ];
    }

    $direct_members = self::matches($conditions, $this->getBranch());
    if ($direct_members) {
      $members = self::loadMultiple($direct_members);
    }

    $class_members = $this->getDocClassMembers();
    if ($class_members) {
      foreach ($class_members as $class_member) {
        $include = TRUE;

        $name_used = (!empty($class_member->getMemberAlias())) ?
          $class_member->getMemberAlias() :
          $class_member->getDocBlock()->getMemberName();
        if (!is_null($member_name) && ($member_name != $name_used)) {
          $include = FALSE;
        }

        if (!is_null($function_or_not_functions)) {
          $type = $class_member->getDocBlock()->getObjectType();
          if ($type == 'function' && !$function_or_not_functions) {
            $include = FALSE;
          }
          elseif ($type != 'function' && $function_or_not_functions) {
            $include = FALSE;
          }
        }

        if ($include && !in_array($class_member->getDocBlock()->id(), $direct_members)) {
          $members[] = $class_member->getDocBlock();
        }
      }
    }

    return $members;
  }

  /**
   * Get matches by referencing docblocks.
   *
   * @param array $reference_ids
   *   Array of DocReference IDs.
   * @param \Drupal\api\Interfaces\BranchInterface $branch
   *   Branch where to look for results.
   * @param bool $extends_match
   *   Get the ID of the extends_docblock instead of the docblock property.
   *
   * @return int[]|null
   *   List of IDs or null.
   */
  protected static function getMatchesByReferences(array $reference_ids, ?BranchInterface $branch = NULL, $extends_match = FALSE) {
    if (empty($reference_ids)) {
      return NULL;
    }

    $references = DocReference::loadMultiple($reference_ids);
    $ids = [];
    foreach ($references as $reference) {
      if ($extends_match && $reference->getExtendsDocBlock()) {
        $ids[] = $reference->getExtendsDocBlock()->id();
      }
      elseif (!$extends_match && $reference->getDocBlock()) {
        $ids[] = $reference->getDocBlock()->id();
      }
    }

    if (empty($ids)) {
      return NULL;
    }

    // And now get the objects info.
    return self::matches([
      'id' => [
        'operator' => 'IN',
        'value' => $ids,
      ],
      'object_type' => [
        'operator' => 'IN',
        'value' => ['class', 'interface'],
      ],
    ], $branch, NULL, ['namespaced_name' => 'ASC']);
  }

  /**
   * {@inheritdoc}
   */
  public function getAncestors($type = 'class', $same_branch = FALSE) {
    $branch = ($same_branch) ? $this->getBranch() : NULL;

    // First get elements extending this object.
    $references = DocReference::matches([
      'docblock' => $this->id(),
      'object_type' => $type,
    ], $branch);

    return self::getMatchesByReferences($references, $branch, TRUE);
  }

  /**
   * {@inheritdoc}
   */
  public function getChildren($type = 'class', $same_branch = FALSE) {
    $branch = ($same_branch) ? $this->getBranch() : NULL;

    // First get elements extending this object.
    $references = DocReference::matches([
      'extends_docblock' => $this->id(),
      'object_type' => $type,
    ], $branch);

    return self::getMatchesByReferences($references, $branch);
  }

  /**
   * {@inheritdoc}
   */
  public function getDocOverrides() {
    $results = DocOverride::matches([], $this);
    return ($results) ? DocOverride::loadMultiple($results) : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocReferences($type = '', $extending_internal_only = TRUE, $name_match = NULL) {
    $conditions = [];
    if ($type) {
      if (is_array($type)) {
        $conditions['object_type'] = [
          'operator' => 'IN',
          'value' => $type,
        ];
      }
      else {
        $conditions['object_type'] = $type;
      }
    }
    if ($extending_internal_only) {
      $conditions['extends_docblock'] = [
        'operator' => '<>',
        'value' => '',
      ];
    }

    if (!is_null($name_match)) {
      $conditions['object_name'] = $name_match;
      $references = DocReference::matches($conditions, $this->getBranch());
    }
    else {
      $references = DocReference::matches($conditions, $this->getBranch(), $this);
    }

    if ($references) {
      return DocReference::loadMultiple($references);
    }

    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function toBasicArray() {
    return [
      'object_name' => $this->getObjectName(),
      'namespaced_name' => $this->getNamespacedName(),
      'title' => $this->getTitle(),
      'member_name' => $this->getMemberName(),
      'summary' => $this->getSummary(),
      'object_type' => $this->getObjectType(),
      'file_name' => $this->getFileName(),
      'url' => $this->toUrl('canonical', [
        'absolute' => TRUE,
      ])->toString(),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public static function preDelete(EntityStorageInterface $storage, array $entities) {
    parent::preDelete($storage, $entities);
    foreach ($entities as $entity) {
      /** @var \Drupal\api\Entity\DocBlock $entity */
      $entity->deleteRelated('docblock_file');
      $entity->deleteRelated('docblock_function');
      $entity->deleteRelated('docblock_namespace');
      $entity->deleteRelated('docblock_class_member');
      $entity->deleteRelated('docblock_class_member', 'class_docblock');
      $entity->deleteRelated('docblock_override');
      $entity->deleteRelated('docblock_override', 'overrides_docblock');
      $entity->deleteRelated('docblock_override', 'documented_in_docblock');
      $entity->deleteRelated('docblock_reference');
      $entity->deleteRelated('docblock_reference', 'extends_docblock');
    }
  }

  /**
   * {@inheritdoc}
   */
  public function deleteRelated($entity_type, $field_name = 'docblock') {
    // https://www.drupal.org/node/3051072
    $storage_handler = \Drupal::entityTypeManager()->getStorage($entity_type);
    $ids = $storage_handler->getQuery()
      ->accessCheck(FALSE)
      ->condition($field_name, $this->id())
      ->execute();
    foreach (array_chunk($ids, 50) as $chunk) {
      $entities = $storage_handler->loadMultiple($chunk);
      $storage_handler->delete($entities);
    }

    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public static function getFullList(BranchInterface $branch, ?array $range = NULL) {
    return self::matches([
      'object_type' => [
        'operator' => '<>',
        'value' => 'mainpage',
      ],
    ], $branch, $range, ['object_name' => 'ASC']);
  }

  /**
   * {@inheritdoc}
   */
  public static function getMainpage(BranchInterface $branch) {
    $result = self::matches([
      'object_type' => 'mainpage',
    ], $branch);
    return array_shift($result);
  }

  /**
   * {@inheritdoc}
   */
  public static function getListingTypes(BranchInterface $branch) {
    $results = [];

    // Make the conditions for each type of listing. The objective is to
    // count to see if there is at least one object of that type to list on the
    // corresponding listing page.
    $type_conditions = [
      'groups' => ['object_type' => 'group'],
      'classes' => [
        'object_type' => [
          'operator' => 'IN',
          'value' => ['class', 'interface', 'trait'],
        ],
        'class' => 0,
      ],
      'functions' => ['object_type' => 'function'],
      'constants' => ['object_type' => 'constant'],
      'globals' => ['object_type' => 'global'],
      'files' => ['object_type' => 'file'],
      'namespaces' => [
        'namespace' => [
          'operator' => '<>',
          'value' => '',
        ],
      ],
      'deprecated' => [
        'deprecated' => [
          'operator' => '<>',
          'value' => '',
        ],
      ],
      'services' => ['object_type' => 'service'],
      'elements' => ['object_type' => 'element'],
    ];

    foreach ($type_conditions as $type => $conditions) {
      if ($type === 'elements') {
        $conditions['range'] = ['limit' => 1];
        $res = DocReference::matches($conditions, $branch);
      }
      else {
        $res = self::matches($conditions, $branch, ['limit' => 1]);
      }
      $results[$type] = ($res && count($res));
    }

    return $results;
  }

  /**
   * {@inheritdoc}
   */
  public static function findFiles(BranchInterface $branch) {
    return self::matches([
      'object_type' => 'file',
    ], $branch);
  }

  /**
   * {@inheritdoc}
   */
  public static function findAttributes(DocBlockInterface $docBlock, BranchInterface $branch) {
    $conditions = [
      'object_type' => 'attribute',
      // This is completely arbitrary. The object type needs to be stored
      // somewhere and title is as good a place as any.
      'title' => $docBlock->getObjectType(),
      'member_name' => $docBlock->getMemberName(),
    ];
    if ($class = $docBlock->getClass()) {
      $conditions['class'] = $class->id();
    }
    return self::matches($conditions, $branch);
  }

  /**
   * {@inheritdoc}
   */
  public static function matches(array $conditions, ?BranchInterface $branch = NULL, ?array $range = NULL, ?array $sort = NULL) {
    /** @var \Drupal\Core\Entity\Query\QueryInterface $query */
    $query = \Drupal::entityQuery('docblock');
    if ($branch) {
      $query->condition('branch', $branch->id());
    }

    self::applyConditions($query, $conditions, $range, $sort);

    return $query->execute();
  }

  /**
   * {@inheritdoc}
   */
  public static function findByMemberName($member_name, array $branch_ids, $function_or_not_functions) {
    $operator = ($function_or_not_functions) ? '=' : '<>';
    $conditions = [
      'member_name' => $member_name,
      'branch' => [
        'operator' => 'IN',
        'value' => $branch_ids,
      ],
      'object_type' => [
        'operator' => $operator,
        'value' => 'function',
      ],
    ];
    return DocBlock::matches($conditions);
  }

  /**
   * {@inheritdoc}
   */
  public static function findClassesByNamespacedName($namespaced_name, array $branch_ids) {
    return DocBlock::matches([
      'object_type' => 'class',
      'namespaced_name' => $namespaced_name,
      'branch' => [
        'operator' => 'IN',
        'value' => $branch_ids,
      ],
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public static function searchByTitle($term, ?BranchInterface $branch = NULL, $limit = 10, $exact_match = FALSE) {
    $operator = ($exact_match) ? '=' : 'CONTAINS';
    return DocBlock::matches([
      'title' => [
        'operator' => $operator,
        'value' => $term,
      ],
    ], $branch, ['limit' => (int) $limit, 'offset' => 0], ['title' => 'ASC']);
  }

  /**
   * {@inheritdoc}
   */
  public static function findSimilar(DocBlockInterface $docBlock, $same_branch = TRUE) {
    $conditions = [];

    // Find objects of the same name within this branch.
    if ($docBlock->getObjectType() == 'file') {
      // For files, the object name includes the path, so match on the title.
      $conditions['title'] = $docBlock->getTitle();
    }
    else {
      // For other objects, match on the object name and find matching names
      // within the same branch.
      $conditions['object_name'] = $docBlock->getObjectName();
    }
    $conditions['object_type'] = $docBlock->getObjectType();
    $conditions['id'] = [
      'operator' => '<>',
      'value' => $docBlock->id(),
    ];

    if ($same_branch) {
      return DocBlock::matches($conditions, $docBlock->getBranch());
    }

    $other_branches = array_diff($docBlock->getBranch()->getProject()->getBranches(), [$docBlock->getBranch()->id()]);
    if (empty($other_branches)) {
      return NULL;
    }

    $conditions['branch'] = [
      'operator' => 'IN',
      'value' => $other_branches,
    ];

    return DocBlock::matches($conditions);

  }

  /**
   * {@inheritdoc}
   */
  public static function totalCount() {
    return (int) \Drupal::entityQuery('docblock')
      ->accessCheck(TRUE)
      ->count()
      ->execute();
  }

  /**
   * Insert or update docblock.
   *
   * @param array $docblock
   *   Docblock information.
   * @param \Drupal\api\Interfaces\BranchInterface $branch
   *   Branch where it belongs to.
   * @param array $processed_ids
   *   Already processed ids to avoid duplication.
   *
   * @return \Drupal\api\Interfaces\DocBlockInterface|null
   *   Created or updated entity or null.
   */
  protected static function upsert(array $docblock, BranchInterface $branch, array $processed_ids) {
    // Check if we have any existing records matching the criteria.
    $existing_ids = self::matches(
      [
        'object_name' => $docblock['object_name'],
        'file_name' => $docblock['file_name'],
        'object_type' => $docblock['object_type'],
      ],
      $branch
    );

    // Create or update now the main record.
    if (!empty($existing_ids)) {
      $id = reset($existing_ids);
      if (in_array($id, $processed_ids)) {
        \Drupal::logger('api')->warning(
          'Duplicate item found in file %file at line %line in %branch. Only first instance of %name is saved',
          [
            '%file' => $docblock['file_name'],
            '%line' => $docblock['start_line'],
            '%name' => $docblock['object_name'],
            '%branch' => $branch->getTitle(),
          ]
        );
        // Don't save this one.
        return NULL;
      }

      /** @var \Drupal\api\Entity\DocBlock $entity */
      $entity = self::load($id);
      foreach ($docblock as $field => $value) {
        if ($entity->hasField($field)) {
          $entity->set($field, $value);
        }
      }
      $entity->save();
    }
    else {
      $docblock['branch'] = $branch->id();
      $entity = self::create($docblock);
      $entity->save();
    }

    return $entity;
  }

  /**
   * Insert or update related information for a docblock.
   *
   * @param array $docblock
   *   Docblock array.
   * @param \Drupal\api\Interfaces\DocBlockInterface $entity
   *   DocBlock entity.
   * @param \Drupal\api\Interfaces\BranchInterface $branch
   *   Branch entity.
   * @param string $namespace
   *   Namespace where the info belongs to.
   * @param array $use_aliases
   *   Aliases used in this docblock.
   */
  protected static function upsertRelated(array $docblock, DocBlockInterface $entity, BranchInterface $branch, $namespace, array $use_aliases) {
    /** @var \Drupal\api\Parser $parser */
    $parser = \Drupal::service('api.parser');

    // Now let's go for all the related entities.
    switch ($docblock['object_type']) {
      case 'function':
        DocReference::deleteRelatedByType([
          'implementations',
        ], $entity, $branch);
        $entity->deleteRelated('docblock_function');

        $parser->addTextDefaults($docblock, 'docblock_function');
        /** @var \Drupal\api\Interfaces\DocBlock\DocFunctionInterface $docFunction */
        $docFunction = DocFunction::create();
        $docFunction
          ->setDocBlock($entity)
          ->setSignature($docblock['signature'])
          ->setParameters($docblock['parameters'])
          ->setReturnValue($docblock['return_value'])
          ->save();
        DocReference::replaceMultiple($docblock, $entity, $branch, $namespace, $use_aliases, FALSE);
        break;

      case 'service':
        DocReference::replaceMultiple($docblock, $entity, $branch, $namespace, $use_aliases, FALSE);
        break;

      case 'file':
        $ids = \Drupal::entityQuery('docblock_file')
          ->accessCheck(FALSE)
          ->condition('docblock', $entity->id())
          ->execute();
        /** @var \Drupal\api\Interfaces\DocBlock\DocFileInterface $docFile */
        if (!$ids || !($docFile = DocFile::load(reset($ids)))) {
          $docFile = DocFile::create();
        }
        $docFile
          ->setDocBlock($entity)
          ->setBasename($docblock['basename'])
          ->save();
        DocReference::replaceMultiple($docblock, $entity, $branch, $namespace, $use_aliases, TRUE);
        break;

      case 'interface':
      case 'class':
      case 'trait':
        DocReference::deleteRelatedByType([
          'class',
          'interface',
          'trait',
          'annotation_class',
          'annotation',
          'element',
          'implementations',
        ], $entity, $branch);
        DocNamespace::deleteRelatedByType([
          'trait_alias',
          'trait_precedence',
        ], $entity);

        foreach ($docblock['extends'] as $extend) {
          $refname = Formatter::fullClassname($extend, $namespace, $use_aliases);
          DocReference::createReference($branch, 'class', $refname, $entity);
        }
        foreach ($docblock['implements'] as $implement) {
          $refname = Formatter::fullClassname($implement, $namespace, $use_aliases);
          DocReference::createReference($branch, 'interface', $refname, $entity);
        }

        if (isset($docblock['references']['use_trait'])) {
          foreach ($docblock['references']['use_trait'] as $alias => $info) {
            $class = $info['class'];
            $refname = Formatter::fullClassname($class, $namespace, $use_aliases);
            $refalias = Formatter::fullClassname($alias, $namespace, $use_aliases);
            if ($refname != $refalias) {
              \Drupal::logger('api')->warning('Aliases for use statements for traits are not supported in %filename for alias %alias of %class in %project %branch', [
                '%filename' => $docblock['file_name'],
                '%alias' => $alias,
                '%class' => $class,
                '%project' => $branch->getProject()->getTitle(),
                '%branch' => $branch->getTitle(),
              ]);
            }
            DocReference::createReference($branch, 'trait', $refname, $entity);
            // If there are insteadof/alias details for this trait, save them
            // in the namespaces table (because it has the right columns).
            if (isset($info['details'])) {
              foreach ($info['details'] as $type => $list) {
                foreach ($list as $name => $item) {
                  if ($type == 'precedence') {
                    // This is an insteadof statement.
                    $name = Formatter::fullClassname($name, $namespace, $use_aliases);

                    /** @var \Drupal\api\Interfaces\DocBlock\DocNamespaceInterface $docNamespace */
                    $docNamespace = DocNamespace::create();
                    $docNamespace
                      ->setDocBlock($entity)
                      ->setObjectType('trait_' . $type)
                      ->setClassName($item)
                      ->setClassAlias($name)
                      ->save();
                  }
                  elseif (in_array($name, ['public', 'protected', 'private'])) {
                    \Drupal::logger('api')->warning('Trait inheritance that changes visibility is not supported in %filename for %item in %project %branch', [
                      '%filename' => $docblock['file_name'],
                      '%item' => $item,
                      '%project' => $branch->getProject()->getTitle(),
                      '%branch' => $branch->getTitle(),
                    ]);
                  }
                  else {
                    /** @var \Drupal\api\Interfaces\DocBlock\DocNamespaceInterface $docNamespace */
                    $docNamespace = DocNamespace::create();
                    $docNamespace
                      ->setDocBlock($entity)
                      ->setObjectType('trait_' . $type)
                      ->setClassName($refname . '::' . $item)
                      ->setClassAlias($name)
                      ->save();
                  }
                }
              }
            }
          }
        }
        if (isset($docblock['references']['annotation'])) {
          foreach ($docblock['references']['annotation'] as $class) {
            DocReference::createReference($branch, 'annotation', $class, $entity);
          }
        }
        if (isset($docblock['references']['element'])) {
          foreach ($docblock['references']['element'] as $element_type) {
            DocReference::createReference($branch, 'element', $element_type, $entity);
          }
        }
        if ($docblock['annotation_class']) {
          DocReference::createReference($branch, 'annotation_class', $docblock['object_name'], $entity);
        }
        break;

    }

    DocReference::deleteRelatedByType([
      'group',
    ], $entity, $branch);
    if (isset($docblock['groups'])) {
      foreach ($docblock['groups'] as $group_name) {
        DocReference::createReference($branch, 'group', $group_name, $entity);
      }
    }

    DocReference::deleteRelatedByType([
      'attribute',
    ], $entity, $branch);
    foreach ($docblock['php_attributes'] ?? [] as $attribute_class => $names) {
      $type = match (ltrim($attribute_class, '\\')) {
        FormElement::class, RenderElement::class => 'element',
        Hook::class => 'implementations',
        default => NULL,
      };
      if ($type) {
        array_map(fn ($name) => DocReference::createReference($branch, $type, $name, $entity), $names);
      }
      DocReference::createReference($branch, 'attribute', $attribute_class, $entity);
    }

  }

  /**
   * Update or insert new docblock item.
   *
   * Unlike the PHP/External branches, the $docblocks_array element contains
   * multiple docblock elements, so the logic here is far more complex.
   * We will be creating DocBlock elements as well as functions, members,
   * namespaces, etc.
   *
   * DocBlock will refer to Drupal entity.
   * docblock will refer to the results from parsing.
   *
   * @param array $docblocks_array
   *   Fields and values to insert/udpate.
   * @param \Drupal\api\Interfaces\BranchInterface $branch
   *   Branch where this item belongs to.
   *
   * @return bool
   *   Whether everything went as expected or not.
   */
  public static function createOrUpdate(array $docblocks_array, BranchInterface $branch) {
    /** @var \Drupal\api\Parser $parser */
    $parser = \Drupal::service('api.parser');

    $docblocks = $docblocks_array;
    if (empty($docblocks)) {
      return FALSE;
    }
    $nested_groups = [];
    $namespace = '';
    $use_aliases = [];
    $processed_ids = [];
    $class_ids = [];
    $attributes = [];

    $old_ids = self::findByFileName($docblocks[0]['file_name'], $branch);

    $docblocks = $parser->fileDocblockFirst($docblocks);

    // Take care of the (bad, but possible) case where the doc block for one
    // of the items in the file (class, function, etc.) has a @defgroup
    // or @mainpage in it, by separating the doc block from the item.
    $docblocks = $parser->splitByTags($docblocks, ['mainpage', 'defgroup']);

    foreach ($docblocks as $docblock) {
      // Do the heavy lifting for the docblock and then save if all went well.
      if ($parser->processDocblock($docblock, $namespace, $use_aliases, $nested_groups, $class_ids, $branch)) {
        $parser->addTextDefaults($docblock);

        $entity = self::upsert($docblock, $branch, $processed_ids);
        if (empty($entity)) {
          // This was already processed, so ignore the rest.
          continue;
        }

        // Keep track of class IDs.
        if (in_array($docblock['object_type'], ['class', 'interface', 'trait'])) {
          $class_ids[$docblock['object_name']] = $entity->id();
          // In the D7 version, we'd keep this in memory for a final
          // recalculation of class elements (members, overrides, etc), but
          // here we will just do a query on the database, so we don't need
          // to keep track of it.
        }

        // Add all related entities now.
        self::upsertRelated($docblock, $entity, $branch, $namespace, $use_aliases);

        // Mark this record as processed.
        $processed_ids[] = $entity->id();
      }
    }

    // Clean out all of the doc objects from this file that no longer exist.
    $old_ids = array_diff($old_ids, $processed_ids);
    if (count($old_ids)) {
      self::deleteMultiple($old_ids);
    }

    return TRUE;
  }

  /**
   * Deletes a list of given entities by their IDs.
   *
   * @param array $ids
   *   IDs to delete.
   */
  public static function deleteMultiple(array $ids) {
    // https://www.drupal.org/node/3051072
    $storage_handler = \Drupal::entityTypeManager()->getStorage('docblock');
    foreach (array_chunk($ids, 50) as $chunk) {
      $entities = $storage_handler->loadMultiple($chunk);
      $storage_handler->delete($entities);
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function findByFileName($file_name, BranchInterface $branch) {
    return self::matches(['file_name' => $file_name], $branch);
  }

  /**
   * {@inheritdoc}
   */
  public static function findByNameAndType($name, $type, BranchInterface $branch) {
    return self::matches([
      'object_name' => $name,
      'object_type' => $type,
    ], $branch);
  }

  /**
   * {@inheritdoc}
   */
  public static function getGroupListingPage($type, BranchInterface $branch) {
    $result = DocBlock::matches([
      'object_name' => 'listing_page_' . $type,
      'object_type' => 'group',
    ], $branch);
    return array_shift($result);
  }

  /**
   * {@inheritdoc}
   */
  public static function getHookFunctions(BranchInterface $branch) {
    $ids = self::matches([
      'object_type' => 'function',
      'object_name' => [
        'value' => 'hook_',
        'operator' => 'STARTS_WITH',
      ],
    ], $branch);
    return self::loadMultiple($ids);
  }

  /**
   * {@inheritdoc}
   */
  public static function findFileByFileName($file_name, BranchInterface $branch) {
    $cache = &drupal_static(__CLASS__ . __FUNCTION__, []);
    if (isset($cache[$file_name][$branch->id()])) {
      return $cache[$file_name][$branch->id()];
    }
    $result = self::matches([
      'file_name' => $file_name,
      'object_type' => 'file',
    ], $branch);

    return $cache[$file_name][$branch->id()] = array_shift($result);
  }

  /**
   * Find elements by group tag inside a branch.
   *
   * @param string $group
   *   Name of the group.
   * @param \Drupal\api\Interfaces\BranchInterface $branch
   *   Branch to search in.
   * @param string $exclude_this_file
   *   If filled in, ignore results in this file.
   *
   * @return int[]|null
   *   List of IDs or null if nothing found.
   */
  public static function findByDefgroup($group, BranchInterface $branch, $exclude_this_file = '') {
    $conditions = [
      'object_name' => $group,
      'object_type' => 'group',
    ];
    if (!empty($exclude_this_file)) {
      $conditions['file_name'] = [
        'operator' => '<>',
        'value' => $exclude_this_file,
      ];
    }
    return self::matches($conditions, $branch);
  }

  /**
   * {@inheritdoc}
   */
  public static function bestByClassByName($class_name, BranchInterface $branch) {
    if (!$branch) {
      return NULL;
    }

    // Check current branch.
    $found = self::matches([
      'namespaced_name' => $class_name,
    ], $branch);

    if (!$found) {
      // Check core branch.
      $core = Branch::findCoreBranch($branch);
      if ($core) {
        $found = self::matches([
          'namespaced_name' => $class_name,
        ], $core);
      }
    }

    if (!$found) {
      // See if there is a branch with matching core compatibility at least.
      $sameCoreBranches = Branch::sameCoreCompatibilityBranches($branch);
      if ($sameCoreBranches) {
        $found = self::matches([
          'namespaced_name' => $class_name,
          'branch' => [
            'operator' => 'IN',
            'value' => $sameCoreBranches,
          ],
        ], NULL);
      }
    }

    // Unique match.
    if ($found && count($found) == 1) {
      $id = array_shift($found);
      return self::load($id);
    }

    // Didn't find a unique match or didn't find a match at all.
    return NULL;
  }

}

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

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