api-8.x-1.x-dev/src/Entity/DocBlock/DocReference.php
src/Entity/DocBlock/DocReference.php
<?php
namespace Drupal\api\Entity\DocBlock;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\api\Entity\Branch;
use Drupal\api\Entity\DocBlock;
use Drupal\api\Entity\PhpDocumentation;
use Drupal\api\ExtendedQueries;
use Drupal\api\Formatter;
use Drupal\api\Interfaces\BranchInterface;
use Drupal\api\Interfaces\DocBlock\DocReferenceInterface;
use Drupal\api\Interfaces\DocBlockInterface;
use Drupal\api\Parser;
use Drupal\api\Traits\MatchingTrait;
/**
* Defines the docblock reference entity class.
*
* @ContentEntityType(
* id = "docblock_reference",
* label = @Translation("DocBlock Reference"),
* label_collection = @Translation("DocBlock references"),
* 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_reference",
* admin_permission = "administer API reference",
* field_indexes = {
* "object_name",
* "object_type"
* },
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid"
* },
* )
*/
class DocReference extends ContentEntityBase implements DocReferenceInterface {
use MatchingTrait;
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['object_name'] = BaseFieldDefinition::create('string')
->setLabel(t('Object name'));
$fields['object_type'] = BaseFieldDefinition::create('string')
->setLabel(t('Object type'))
->setRequired(TRUE);
$fields['docblock'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('DocBlock'))
->setDescription(t('DocBlock element'))
->setSetting('target_type', 'docblock')
->setSetting('handler', 'default')
->setCardinality(1)
->setRequired(TRUE);
$fields['extends_docblock'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Extends DocBlock'))
->setDescription(t('If this is a class or interface inheritance, the computed ID value'))
->setSetting('target_type', 'docblock')
->setSetting('handler', 'default')
->setCardinality(1);
$fields['branch'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Branch'))
->setDescription(t('Branch where this is documented'))
->setSetting('target_type', 'branch')
->setSetting('handler', 'default')
->setCardinality(1)
->setRequired(TRUE);
return $fields;
}
/**
* {@inheritdoc}
*/
public function getBranch() {
return $this->get('branch')->referencedEntities()[0];
}
/**
* {@inheritdoc}
*/
public function setBranch(BranchInterface $branch) {
$this->set('branch', $branch);
return $this;
}
/**
* {@inheritdoc}
*/
public function getDocBlock() {
return $this->get('docblock')->referencedEntities()[0];
}
/**
* {@inheritdoc}
*/
public function setDocBlock(DocBlockInterface $docblock) {
$this->set('docblock', $docblock);
return $this;
}
/**
* {@inheritdoc}
*/
public function getExtendsDocBlock() {
$docblock = $this->get('extends_docblock')->referencedEntities();
return !empty($docblock[0]) ? $docblock[0] : NULL;
}
/**
* {@inheritdoc}
*/
public function setExtendsDocBlock(DocBlockInterface $docblock) {
$this->set('extends_docblock', $docblock);
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;
}
/**
* Remove any references for the entity and branch.
*
* @param \Drupal\api\Interfaces\DocBlockInterface $entity
* Entity where references are linked to.
* @param \Drupal\api\Interfaces\BranchInterface $branch
* Branch where references are linked to.
* @param bool $do_namespaces
* Do namespaces too or not.
*/
public static function removeReferences(DocBlockInterface $entity, BranchInterface $branch, bool $do_namespaces) {
// Remove any existing references.
self::deleteRelatedByType([
'function',
'potential hook',
'potential fieldhook',
'potential entityhook',
'potential userhook',
'potential theme',
'potential element',
'potential alter',
'potential callback',
'potential file',
'constant',
'member-parent',
'member-self',
'member',
'member-class',
'yaml string',
'service_tag',
'service_class',
], $entity, $branch);
if ($do_namespaces) {
DocNamespace::deleteRelatedByType([
'namespace',
'use_alias',
], $entity);
}
}
/**
* Add references for the entity and branch.
*
* @param \Drupal\api\Interfaces\DocBlockInterface $entity
* Entity where references are linked to.
* @param \Drupal\api\Interfaces\BranchInterface $branch
* Branch where references are linked to.
* @param array $references
* References to add.
* @param bool $do_namespaces
* Do namespaces too or not.
* @param string $namespace
* Namespace for the references.
* @param array $use_aliases
* Aliases for the reference.
* @param string $shortname
* Shortname for the references.
*/
public static function addReferences(DocBlockInterface $entity, BranchInterface $branch, array $references, $do_namespaces, $namespace, array $use_aliases, $shortname) {
foreach ($references as $type => $list) {
switch ($type) {
case 'namespace':
if ($do_namespaces) {
// In this case, $list is actually a string containing the namespace
// for a file, not a list.
/** @var \Drupal\api\Interfaces\DocBlock\DocNamespaceInterface $docNamespace */
$docNamespace = DocNamespace::create();
$docNamespace
->setDocBlock($entity)
->setObjectType($type)
->setClassName($list)
->setClassAlias('')
->save();
}
break;
case 'use_alias':
if ($do_namespaces) {
// Save all of the use aliases.
foreach ($list as $alias => $class) {
/** @var \Drupal\api\Interfaces\DocBlock\DocNamespaceInterface $docNamespace */
$docNamespace = DocNamespace::create();
$docNamespace
->setDocBlock($entity)
->setObjectType($type)
->setClassName($class)
->setClassAlias($alias)
->save();
}
}
break;
case 'member-class':
// Don't save a reference to the item itself.
unset($list[$shortname]);
foreach ($list as $call) {
// These are references to ClassName::method(). Make sure
// they are fully namespaced.
$call = Formatter::fullClassname($call, $namespace, $use_aliases);
self::createReference($branch, $type, $call, $entity);
}
break;
case 'potential callback':
case 'function':
case 'member-self':
// Don't save a reference to the item itself.
unset($list[$shortname]);
foreach ($list as $call) {
// If the name contains a backslash, and the first occurrence is not
// at the beginning, make sure it starts with a backslash
// so it is a fully-namespaced reference.
$pos = strpos($call, '\\');
if ($pos !== FALSE && $pos !== 0) {
$call = '\\' . $call;
}
self::createReference($branch, $type, $call, $entity);
}
break;
default:
foreach ($list as $call) {
// If the name contains a backslash, and the first occurrence is not
// at the beginning, make sure it starts with a backslash
// so it is a fully-namespaced reference.
$pos = strpos($call, '\\');
if ($pos !== FALSE && $pos !== 0) {
$call = '\\' . $call;
}
self::createReference($branch, $type, $call, $entity);
}
}
}
}
/**
* {@inheritdoc}
*/
public static function replaceMultiple(array $docblock, DocBlockInterface $entity, BranchInterface $branch, $namespace, array $use_aliases, $do_namespaces) {
$references = $docblock['references'];
$name = $docblock['object_name'];
// Find the name without class prefix, if any.
$shortname = $name;
if (($pos = strpos($name, '::')) !== FALSE && $pos > 1) {
$shortname = substr($name, $pos + 2);
}
// Remove and re-create the references.
self::removeReferences($entity, $branch, $do_namespaces);
self::addReferences($entity, $branch, $references, $do_namespaces, $namespace, $use_aliases, $shortname);
}
/**
* {@inheritdoc}
*/
public static function createReference(BranchInterface $branch, $type, $name, DocBlockInterface $docBlock) {
// Don't make references to built-in PHP functions.
$is_php_function = FALSE;
if ($type == 'function') {
$is_php_function = count(PhpDocumentation::findByName($name));
}
// Avoid trying to save really long object names.
$name = mb_substr($name, 0, 127);
$name = Formatter::validateEncoding($name);
if (!$is_php_function) {
/** @var \Drupal\api\Interfaces\DocBlock\DocReferenceInterface $reference */
$reference = self::create();
$reference
->setObjectName($name)
->setObjectType($type)
->setDocBlock($docBlock)
->setBranch($branch)
->save();
return $reference;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public static function deleteRelatedByType(array $types, DocBlockInterface $entity, BranchInterface $branch) {
// https://www.drupal.org/node/3051072
$storage_handler = \Drupal::entityTypeManager()->getStorage('docblock_reference');
$ids = self::matches([
'object_type' => [
'operator' => 'IN',
'value' => $types,
],
], $branch, $entity);
foreach (array_chunk($ids, 50) as $chunk) {
$entities = $storage_handler->loadMultiple($chunk);
$storage_handler->delete($entities);
}
}
/**
* Update members information related to the members given.
*
* @param array $members
* Members information.
* @param int $class_id
* ID of the class.
*/
protected static function updateMembers(array &$members, $class_id) {
/** @var \Drupal\api\Interfaces\DocBlockInterface $docBlock */
$classDocBlock = DocBlock::load($class_id);
if ($classDocBlock) {
// Delete all references.
$classDocBlock->deleteRelated('docblock_class_member', 'class_docblock');
// And crate new ones.
$processed = [];
foreach ($members as $list) {
foreach ($list as $member) {
$member_alias = ($member['alias'] != $member['member_name']) ?
$member['alias'] :
NULL;
$processed_key = $member_alias . '-' . $member['docblock'] . '-' . $classDocBlock->id();
if (empty($processed[$processed_key])) {
/** @var \Drupal\api\Interfaces\DocBlock\DocClassMemberInterface $classMember */
$classMember = DocClassMember::create();
$classMember
->setDocBlock(DocBlock::load($member['docblock']))
->setClassDocBlock($classDocBlock)
->setMemberAlias($member_alias)
->save();
$processed[$processed_key] = TRUE;
}
}
}
}
}
/**
* Update override information related to the members given.
*
* @param array $members
* Members information.
*/
protected static function updateOverrides(array &$members) {
// Save the overrides info, and if we are getting documentation from an
// inherited member, update the summary on the main object. This is only
// done for the direct members of this class.
foreach ($members as $list) {
foreach ($list as $member) {
if ($member['direct_member']) {
/** @var \Drupal\api\Interfaces\DocBlockInterface $docBlock */
$docBlock = DocBlock::load($member['docblock']);
if ($member['documented_in_docblock'] != $member['docblock']) {
$docBlock->setSummary($member['summary'])->save();
}
// Delete the old information and insert new.
$docBlock->deleteRelated('docblock_override');
/** @var \Drupal\api\Interfaces\DocBlock\DocOverrideInterface $override */
$override = DocOverride::create();
$override->setDocBlock($docBlock);
if (!empty($member['overrides_docblock'])) {
$override->setOverridesDocBlock(DocBlock::load($member['overrides_docblock']));
}
if (!empty($member['documented_in_docblock'])) {
$override->setDocumentedInDocBlock(DocBlock::load($member['documented_in_docblock']));
}
$override->save();
}
}
}
}
/**
* Finds the trait aliases for a given class.
*
* @param \Drupal\api\Interfaces\DocBlockInterface $docBlock
* Class object.
*
* @return array
* List of trait aliases
*/
protected static function findTraitAliases(DocBlockInterface $docBlock) {
$results = DocNamespace::matches([
'object_type' => 'trait_alias',
], $docBlock);
$return = [];
if (!empty($results)) {
foreach ($results as $id) {
$item = DocNamespace::load($id);
$alias = $item->getClassAlias();
$name = $item->getClassName();
$pos = strpos($name, '::');
if ($pos >= 1) {
$class = substr($name, 0, $pos);
$member = substr($name, $pos + 2);
$return[$class][$member] = $alias;
}
}
}
return $return;
}
/**
* Finds the trait precedences for a given class.
*
* @param \Drupal\api\Interfaces\DocBlockInterface $docBlock
* Class object.
*
* @return array
* List of trait precedences
*/
protected static function findTraitPrecedence(DocBlockInterface $docBlock) {
$results = DocNamespace::matches([
'object_type' => 'trait_precedence',
], $docBlock);
$omit = [];
if (!empty($results)) {
foreach ($results as $id) {
$item = DocNamespace::load($id);
$class = $item->getClassAlias();
$method = $item->getClassName();
$omit[$class][] = $method;
}
}
return $omit;
}
/**
* Calculates a bare list of this class's direct members.
*
* @param \Drupal\api\Interfaces\DocBlockInterface $docBlock
* DocBlock object.
* @param array $member_info
* Member information.
*
* @return array
* Array with added members information from the given object.
*/
protected static function makeBareMemberList(DocBlockInterface $docBlock, array &$member_info) {
$members = [
'function' => [],
'property' => [],
'constant' => [],
];
foreach ($member_info[$docBlock->id()] as $type => $list) {
foreach ($list as $member_did) {
$item = ExtendedQueries::loadExtendedArray($member_did, $type);
if (!$item) {
continue;
}
$has_docs = Formatter::elementHasDocumentation($item);
$member_name = $item['member_name'];
$members[$type][$member_name] = [
'docblock' => $item['id'],
'overrides_docblock' => NULL,
'documented_in_docblock' => ($has_docs) ? $item['id'] : NULL,
'summary' => $item['summary'],
'alias' => $member_name,
'member_name' => $member_name,
'direct_member' => TRUE,
];
}
}
return $members;
}
/**
* Calculates class member information for a class, including inheritance.
*
* Calculate the full list of class members, including inherited, and figure
* out what each one is overriding and where each one is documented.
*
* @param int $class_id
* Documentation ID of the class to calculate.
* @param array $parent_info
* Array of collected parent information.
* @param array $member_info
* Array of collected direct member information.
* @param array $cache_array
* Caching array as we will most likely encounter a class multiple times.
*
* @return array[]
* List of calculated members.
*/
protected static function calculateClassMembers($class_id, array &$parent_info, array &$member_info, array &$cache_array) {
$members = [
'function' => [],
'property' => [],
'constant' => [],
];
if (!$class_id) {
return $members;
}
if (!isset($member_info[$class_id])) {
$member_info[$class_id] = [
'function' => [],
];
}
// See if we already calculated members for this class, during another
// class traversal.
if (isset($cache_array[$class_id])) {
return $cache_array[$class_id];
}
/** @var \Drupal\api\Interfaces\DocBlockInterface $docBlock */
$docBlock = DocBlock::load($class_id);
// @todo Maybe a reference wasn't created yet.
if (!$docBlock) {
return $members;
}
// Add this class's direct members to the list, and put it in the
// cache for now, to avoid loops.
$members = self::makeBareMemberList($docBlock, $member_info);
$cache_array[$class_id] = $members;
// Add in the parents.
if (!isset($parent_info[$class_id])) {
return $members;
}
// See if there are any aliases or insteadof statements for trait members.
$aliases = self::findTraitAliases($docBlock);
$omit = self::findTraitPrecedence($docBlock);
foreach ($parent_info[$class_id] as $parent_name => $parent) {
$parent_members = self::calculateClassMembers($parent, $parent_info, $member_info, $cache_array);
self::memberListMerge(
$members,
$parent_members,
$aliases[$parent_name] ?? [],
$omit[$parent_name] ?? []
);
}
// Save in the cache and return.
$cache_array[$class_id] = $members;
return $members;
}
/**
* Merges a member list with parent member list.
*
* @param array $members
* List of member information, modified by reference.
* @param array $parent_members
* List of parent member information to merge in.
* @param array $aliases
* List of aliases for member names (functions only).
* @param array $omit
* List of methods in parent class to omit due to insteadof statements.
*/
protected static function memberListMerge(array &$members, array &$parent_members, array $aliases, array $omit) {
foreach ($parent_members as $type => $new_type_list) {
foreach ($new_type_list as $member_name => $info) {
if (in_array($member_name, $omit)) {
continue;
}
$alias = (isset($aliases[$member_name]) && $type == 'function') ?
$aliases[$member_name] :
$member_name;
if (isset($members[$type][$alias])) {
// We already knew about this member. Save override info, but only
// for direct members.
if ($members[$type][$alias]['direct_member']) {
if (!$members[$type][$alias]['overrides_docblock']) {
// We just found what the known member is overriding.
$members[$type][$alias]['overrides_docblock'] = $info['docblock'];
}
if (!$members[$type][$alias]['documented_in_docblock']) {
// The old member didn't have documentation, maybe this one does.
$members[$type][$alias]['documented_in_docblock'] = $info['documented_in_docblock'];
}
if (!$members[$type][$alias]['summary']) {
// The old member didn't have a summary, maybe this one does.
$members[$type][$alias]['summary'] = $info['summary'];
}
}
}
else {
// It's a new member inherited from a parent, add it in.
$info['alias'] = $alias;
$info['direct_member'] = FALSE;
$members[$type][$alias] = $info;
}
}
}
}
/**
* Updates several DocEntities based on the parameters given.
*
* @param int $class_id
* ID of the referencing DocBlock.
* @param array $parent_info
* List of class parents.
* @param array $member_info
* List of class members.
*/
public static function updateClassReferenceInfo($class_id, array &$parent_info, array &$member_info) {
if (!$class_id) {
return;
}
$cache_array = [];
$members = self::calculateClassMembers($class_id, $parent_info, $member_info, $cache_array);
self::updateOverrides($members);
self::updateMembers($members, $class_id);
ExtendedQueries::updateCalculatedReferences($member_info, $class_id);
}
/**
* Get 'service_tags' DocReference IDs for given DocBlock IDs.
*
* @param array $ids
* List of DocBlock IDs.
*
* @return int[]|null
* List of matching DocReference IDs or null.
*/
public static function getServiceTags(array $ids) {
return self::matches([
'object_type' => 'service_tag',
'docblock' => [
'operator' => 'IN',
'value' => $ids,
],
]);
}
/**
* Get 'class' DocReference IDs for given DocBlock IDs.
*
* @param \Drupal\api\Interfaces\BranchInterface $branch
* Branch to check in.
* @param \Drupal\api\Interfaces\DocBlockInterface $docBlock
* Docblock to check in.
*
* @return int[]|null
* List of matching DocReference IDs or null.
*/
public static function getClassReference(?BranchInterface $branch = NULL, ?DocBlockInterface $docBlock = NULL) {
return self::matches([
'object_type' => 'class',
], $branch, $docBlock);
}
/**
* {@inheritdoc}
*/
public static function matches(array $conditions, ?BranchInterface $branch = NULL, ?DocBlockInterface $docBlock = NULL) {
/** @var \Drupal\Core\Entity\Query\QueryInterface $query */
$query = \Drupal::entityQuery('docblock_reference');
if ($branch) {
$query->condition('branch', $branch->id());
}
if ($docBlock) {
$query->condition('docblock', $docBlock->id());
}
$range = NULL;
if (!empty($conditions['range'])) {
$range = $conditions['range'];
unset($conditions['range']);
}
$sort = NULL;
if (!empty($conditions['sort'])) {
$sort = $conditions['sort'];
unset($conditions['sort']);
}
self::applyConditions($query, $conditions, $range, $sort);
return $query->execute();
}
/**
* Fills the inherited elements array and keeps track of classes_changed.
*
* @param array $classes_changed
* Classes changed.
* @param array $classes_todo
* Classes to do.
* @param array $classes_added
* Classes added.
* @param \Drupal\api\Interfaces\DocBlockInterface $classDocBlock
* Class DocBlock entity.
* @param \Drupal\api\Interfaces\BranchInterface $branch
* Branch where all this info belongs to.
*/
protected static function fillClassInheritedArray(array &$classes_changed, array &$classes_todo, array &$classes_added, DocBlockInterface $classDocBlock, BranchInterface $branch) {
// If this is one of the "changed" classes, see if anything else inherits
// from this class. If so, we need to check it too. This could be a class
// that has already computed this as its extends_docblock, or a class in the
// same core compatibility that extends a class by this namespaced name
// but maybe didn't know the DID yet. Don't worry about the possible
// edge case of a class that had found another one previously and this one
// would now be better.
if (in_array($classDocBlock->id(), $classes_changed)) {
$conditions = [
'object_type' => [
'operator' => 'IN',
'value' => ['class', 'interface', 'trait'],
],
'or' => [
'extends_docblock' => $classDocBlock->id(),
'object_name' => $classDocBlock->getNamespacedName(),
],
];
$sameCoreBranches = Branch::sameCoreCompatibilityBranches($branch);
if ($sameCoreBranches) {
$conditions['branch'] = [
'operator' => 'IN',
'value' => $sameCoreBranches,
];
}
$results = self::matches($conditions, NULL, NULL);
if ($results) {
foreach ($results as $entity_id) {
$entity = self::load($entity_id);
if (!$entity->getDocBlock()) {
continue;
}
$reference_docblock_id = $entity->getDocBlock()->id();
if (!isset($classes_added[$reference_docblock_id])) {
$classes_added[$reference_docblock_id] = $reference_docblock_id;
if (!in_array($reference_docblock_id, $classes_todo)) {
$classes_todo[] = $reference_docblock_id;
}
if (!in_array($reference_docblock_id, $classes_changed)) {
$classes_changed[] = $reference_docblock_id;
}
}
}
}
}
}
/**
* Fills the class_parents array and keeps track of classes_changed.
*
* @param array $class_parents
* Parents array.
* @param array $classes_changed
* Classes changed.
* @param array $classes_todo
* Classes to do.
* @param array $classes_added
* Classes added.
* @param \Drupal\api\Interfaces\DocBlockInterface $classDocBlock
* Class DocBlock entity.
* @param \Drupal\api\Interfaces\BranchInterface $branch
* Branch where all this info belongs to.
*/
protected static function fillClassParentsArray(array &$class_parents, array &$classes_changed, array &$classes_todo, array &$classes_added, DocBlockInterface $classDocBlock, ?BranchInterface $branch = NULL) {
// See if this class extends or inherits anything else, or uses traits,
// and if so, calculate the IDs of the "parent" classes, traits, or
// interfaces. Do this in the right order for inheritance: traits
// trump class extends, and class extends trump interfaces.
$parents = [];
$parents_to_check = [];
$types = ['trait', 'class', 'interface'];
foreach ($types as $type) {
$parent_ids = self::matches(['object_type' => $type], $branch, $classDocBlock);
if ($parent_ids) {
$parents_to_check = array_merge($parents_to_check, $parent_ids);
}
}
foreach ($parents_to_check as $parent) {
/** @var \Drupal\api\Interfaces\DocBlock\DocReferenceInterface $parent_reference */
$parent_reference = self::load($parent);
// Figure out the ID of this parent.
/** @var \Drupal\api\Interfaces\DocBlockInterface $bestClassDocBlock */
$bestClassDocBlock = DocBlock::bestByClassByName(
$parent_reference->getObjectName(),
$parent_reference->getBranch()
);
if ($bestClassDocBlock) {
$parents[$parent_reference->getObjectName()] = $bestClassDocBlock->id();
// See if it's different from what we already had.
$currentExtendsDocBlock = $parent_reference->getExtendsDocBlock();
if (
!$currentExtendsDocBlock ||
($bestClassDocBlock->id() != $currentExtendsDocBlock->id())
) {
// Update the reference storage record with this new ID.
$parent_reference
->setExtendsDocBlock($bestClassDocBlock)
->save();
// This class changed.
if (!in_array($classDocBlock->id(), $classes_changed)) {
$classes_changed[] = $classDocBlock->id();
}
}
// If this class's parent is already marked as "changed", then this
// class needs to be marked as "changed" also.
if (
in_array($bestClassDocBlock->id(), $classes_changed) &&
!in_array($classDocBlock->id(), $classes_changed)
) {
$classes_changed[] = $classDocBlock->id();
}
// We also need to get the members of parent classes, so add it to the
// to do list.
if (($bestClassDocBlock->id()) && !isset($classes_added[$bestClassDocBlock->id()])) {
if (!in_array($bestClassDocBlock->id(), $classes_todo)) {
$classes_todo[] = $bestClassDocBlock->id();
}
$classes_added[$bestClassDocBlock->id()] = $bestClassDocBlock->id();
}
}
}
$class_parents[$classDocBlock->id()] = $parents;
}
/**
* Fills the class_members array with relevant information.
*
* @param array $class_members
* Array to fill.
* @param \Drupal\api\Interfaces\DocBlockInterface $classDocBlock
* Class DocBlock entity.
* @param \Drupal\api\Interfaces\BranchInterface $branch
* Branch where all this info belongs to.
*/
protected static function fillClassMembersArray(array &$class_members, DocBlockInterface $classDocBlock, ?BranchInterface $branch = NULL) {
// We're going to need the direct members of this class. Since methods,
// properties, and constants can share names, do each separately.
$direct_members_ids = DocBlock::matches([
'object_type' => [
'operator' => 'IN',
'value' => ['function', 'property', 'constant'],
],
'class' => $classDocBlock->id(),
], $branch);
if (!empty($direct_members_ids)) {
$direct_members = [
'function' => [],
'property' => [],
'constant' => [],
];
foreach ($direct_members_ids as $direct_member_id) {
$item = DocBlock::load($direct_member_id);
$direct_members[$item->getObjectType()][$item->getMemberName()] = $item->id();
}
$class_members[$classDocBlock->id()] = $direct_members;
}
}
/**
* {@inheritdoc}
*/
public static function classRelations(BranchInterface $branch) {
$counter = NULL;
$memory_cache = \Drupal::service('entity.memory_cache');
$queue = \Drupal::service('queue')->get('api_parse_queue');
$queue_operations = $branch->getProject()->isCore();
// Use if you are trying to debug long-running jobs that might take minutes
// to start showing anything. If you set this to TRUE you will see the
// progress and the total count. Note that unit tests will fail if you leave
// it as TRUE if "beStrictAboutOutputDuringTests" value is set to "true".
$progress_output_debug = FALSE;
// Figure out which classes and interfaces we need to update.
$classes_touched = DocBlock::matches([
'object_type' => [
'operator' => 'IN',
'value' => ['class', 'interface', 'trait'],
],
], $branch);
// For each of these classes, and any that extend/implement them, we need
// to recompute:
// - The IDs for extended/implemented classes/interfaces.
// - IDs for classes that extend/implement this class/interface.
// - For traits, the traits and classes that use them.
// - The {api_branch_docblock_class_members} table, which figures out all
// class members including inherited members.
// - The {api_branch_docblock_override} table, which figures out for a class
// member if it is overriding another class's member, and where it is
// documented.
// - The "computed-member" information in {api_branch_docblock_reference}
// table.
// So, the first step is to recompute class inheritance. This is stored by
// name in {api_branch_docblock_reference}, and we need to find the actual
// IDs of the classes. Do this one by one, since we need to compute some
// other stuff anyway as we go.
$classes_todo = array_values($classes_touched);
$classes_changed = $classes_touched;
// Avoid infinite loops by keeping track of which classes we've already
// checked.
$classes_added = $classes_touched;
// Try to reduce memory from early on.
unset($classes_touched);
gc_collect_cycles();
// Keep track of the IDs of classes that extend/implement others, and the
// members of each class.
$class_parents = [];
$class_members = [];
if ($progress_output_debug) {
$count = count($classes_todo);
$counter = 0;
echo "\n\nGathering inheritance information (total count $count):\n";
}
// Use while-loop as the $classes_todo array might grow while processing
// the entries.
$todo_done = [];
while ($class_id = array_pop($classes_todo)) {
if (!isset($todo_done[$class_id])) {
/** @var \Drupal\api\Interfaces\DocBlockInterface $classDocBlock */
$classDocBlock = DocBlock::load($class_id);
self::fillClassMembersArray($class_members, $classDocBlock);
self::fillClassParentsArray($class_parents, $classes_changed, $classes_todo, $classes_added, $classDocBlock);
self::fillClassInheritedArray($classes_changed, $classes_todo, $classes_added, $classDocBlock, $branch);
$todo_done[$class_id] = TRUE;
// Reset the cache in order to free memory as we progress.
$memory_cache->deleteAll();
}
if ($progress_output_debug) {
$counter++;
echo "$counter...";
}
}
// Try to reduce memory before a new loop as we no longer need these.
unset($classes_todo, $classes_added, $todo_done);
gc_collect_cycles();
if (count($classes_changed) > 5000) {
// Anything this big should be queued.
// For reference, core 10.1.x has around 15000.
$queue_operations = TRUE;
}
if ($queue_operations) {
$temp_store = \Drupal::service('tempstore.private')->get(Parser::QUEUE_PARSE);
$temp_store->set('api:class_members:' . $branch->id(), $class_members);
$temp_store->set('api:class_parents:' . $branch->id(), $class_parents);
}
if ($progress_output_debug) {
$count = count($classes_changed);
$counter = 0;
echo "\n\nUpdating class references (total count $count):\n";
}
// OK, at this point we have a list of all the classes that we need to
// update member, override, and class parent information for. And we have
// in hand a list of the parent IDs and the direct members for each one.
// So go through this list and redo the members and overrides tables.
$references_done = [];
foreach ($classes_changed as $cid) {
if (!isset($references_done[$cid])) {
if ($queue_operations) {
// Core branches are by far the biggest compared to the others so
// queue the operations in favour of smaller queue items.
$info = [
'branch_id' => $branch->id(),
'branch_type' => $branch->getEntityTypeId(),
'action' => 'class_references_information',
'data' => Json::encode([
'cid' => $cid,
]),
];
$queue->createItem($info);
}
else {
// Other branches can be processed straight away.
self::updateClassReferenceInfo($cid, $class_parents, $class_members);
$memory_cache->deleteAll();
}
$references_done[$cid] = TRUE;
}
if ($progress_output_debug) {
$counter++;
echo $counter . ($queue_operations ? ' - queued' : '') . "...";
}
}
if ($progress_output_debug) {
echo "\n\n";
}
// Try to reduce memory before a new loop.
unset($references_done);
gc_collect_cycles();
if ($queue_operations) {
// Queue this one, so it's run after all the others.
$info = [
'branch_id' => $branch->id(),
'branch_type' => $branch->getEntityTypeId(),
'action' => 'direct_method_references',
'data' => Json::encode([
'classes_changed' => $classes_changed,
]),
];
$queue->createItem($info);
}
else {
self::directMethodReferences($classes_changed, $class_members);
}
}
/**
* Calculate computed member references for member parent calls.
*
* @param array $classes_changed
* Ids of classes to process.
* @param array $class_members
* Member information for classes.
*/
public static function directMethodReferences(array &$classes_changed, array &$class_members): void {
// Now that all the class members have been updated, calculate
// computed-member references for member-parent calls in the references
// table. These are cases where ChildClass::foo() calls parent::bar(),
// and we need to figure out the full name of the parent member. Do this
// for all class methods in $classes_changed.
$direct_methods_done = [];
foreach ($classes_changed as $cid) {
if (!isset($direct_methods_done[$cid])) {
if (isset($class_members[$cid]) && count($class_members[$cid]['function'])) {
$direct_methods = array_values($class_members[$cid]['function']);
ExtendedQueries::createNewReferences($direct_methods);
}
$direct_methods_done[$cid] = TRUE;
}
}
}
}
