Skip to content
22 changes: 21 additions & 1 deletion data/templates/default/interface.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
{{ include('components/element-header.html.twig') }}

{{ include('components/constants.html.twig') }}
{{ include('components/properties.html.twig') }}
{{ include('components/methods.html.twig') }}
{{ include('components/source-modal.html.twig') }}
</article>
{% endblock %}

{% block on_this_page %}
{% set constants = constants(node) %}
{% set properties = properties(node) %}
{% set methods = methods(node) %}

<section class="phpdocumentor-on-this-page__content">
Expand All @@ -25,10 +27,28 @@
<li class="phpdocumentor-on-this-page-section__title">Table Of Contents</li>
<li>
<ul class="phpdocumentor-list -clean">
{% if constants is not empty %}
<li><a href="{{ link(node) }}#toc-constants">Constants</a></li>
{% endif %}
{% if properties is not empty %}
<li><a href="{{ link(node) }}#toc-properties">Properties</a></li>
{% endif %}
{% if methods is not empty %}
<li><a href="{{ link(node) }}#toc-methods">Methods</a></li>
{% endif %}
</ul>
</li>
{% if properties is not empty %}
<li class="phpdocumentor-on-this-page-section__title">Properties</li>
<li>
<ul class="phpdocumentor-list -clean">
{% for property in properties|sortByVisibility %}
<li class="{% if property.deprecated %}-deprecated{% endif %}"><a href="{{ link(property) }}">${{ property.name }}</a></li>
{% endfor %}
</ul>
</li>
{% endif %}

{% if methods is not empty %}
<li class="phpdocumentor-on-this-page-section__title">Methods</li>
<li>
Expand All @@ -45,7 +65,7 @@
<li>
<ul class="phpdocumentor-list -clean">
{% for constant in constants|sortByVisibility %}
<li class="{% if constants.deprecated %}-deprecated{% endif %}"><a href="{{ link(constant) }}">{{ constant.name }}</a></li>
<li class="{% if constant.deprecated %}-deprecated{% endif %}"><a href="{{ link(constant) }}">{{ constant.name }}</a></li>
{% endfor %}
</ul>
</li>
Expand Down
8 changes: 8 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ parameters:

- '#Class phpDocumentor\\Guides\\Compiler\\DescriptorAwareCompilerContext extends \@final class phpDocumentor\\Guides\\Compiler\\CompilerContext#'

# HasProperties::getMagicProperties() recurses into a parent descriptor through the ChildInterface contract.
# InterfaceDescriptor uses the trait but does not implement ChildInterface (its own getParent() returns a
# Collection of parent interfaces), so that branch is unreachable in its context; PHPStan nonetheless observes
# the always-false instanceof while analysing the trait for InterfaceDescriptor.
-
message: '#Instanceof between phpDocumentor\\Descriptor\\Collection and phpDocumentor\\Descriptor\\InterfaceDescriptor will always evaluate to false\.#'
path: src/phpDocumentor/Descriptor/Traits/HasProperties.php

excludePaths:
#test data
- %currentWorkingDirectory%/tests/features/**/*.php
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
use phpDocumentor\Descriptor\InterfaceDescriptor;
use phpDocumentor\Descriptor\Interfaces\InterfaceInterface;
use phpDocumentor\Descriptor\MethodDescriptor;
use phpDocumentor\Descriptor\PropertyDescriptor;
use phpDocumentor\Reflection\Php\Constant;
use phpDocumentor\Reflection\Php\Interface_;
use phpDocumentor\Reflection\Php\Method;
use phpDocumentor\Reflection\Php\Property;

use function strlen;
use function substr;
Expand Down Expand Up @@ -51,6 +53,7 @@ public function buildDescriptor(object $data): InterfaceInterface

$this->assembleDocBlock($data->getDocBlock(), $interfaceDescriptor);
$this->addConstants($data->getConstants(), $interfaceDescriptor);
$this->addProperties($data->getProperties(), $interfaceDescriptor);
$this->addMethods($data->getMethods(), $interfaceDescriptor);

$interfaceParent = $interfaceDescriptor->getParent();
Expand Down Expand Up @@ -79,6 +82,24 @@ protected function addConstants(array $constants, InterfaceInterface $interfaceD
}
}

/**
* Registers the child properties with the generated Interface Descriptor.
*
* @param Property[] $properties
*/
protected function addProperties(array $properties, InterfaceInterface $interfaceDescriptor): void
{
foreach ($properties as $property) {
$propertyDescriptor = $this->getBuilder()->buildDescriptor($property, PropertyDescriptor::class);
if ($propertyDescriptor === null) {
continue;
}

$propertyDescriptor->setParent($interfaceDescriptor);
$interfaceDescriptor->getProperties()->set($propertyDescriptor->getName(), $propertyDescriptor);
}
}

/**
* Registers the child methods with the generated Interface Descriptor.
*
Expand Down
24 changes: 24 additions & 0 deletions src/phpDocumentor/Descriptor/InterfaceDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use phpDocumentor\Descriptor\Interfaces\ConstantInterface;
use phpDocumentor\Descriptor\Interfaces\InterfaceInterface;
use phpDocumentor\Descriptor\Interfaces\MethodInterface;
use phpDocumentor\Descriptor\Interfaces\PropertyInterface;
use phpDocumentor\Reflection\Fqsen;

/**
Expand All @@ -27,6 +28,7 @@
class InterfaceDescriptor extends DescriptorAbstract implements Interfaces\InterfaceInterface
{
use Traits\HasAttributes;
use Traits\HasProperties;

/** @var Collection<InterfaceInterface|Fqsen> $parents */
protected Collection $parents;
Expand Down Expand Up @@ -87,6 +89,24 @@ public function getInheritedConstants(): Collection
return $inheritedConstants;
}

/** @return Collection<PropertyInterface> */
public function getInheritedProperties(): Collection
{
$inheritedProperties = Collection::fromInterfaceString(PropertyInterface::class);

/** @var InterfaceInterface|Fqsen $parent */
foreach ($this->getParent() as $parent) {
if (! $parent instanceof Interfaces\InterfaceInterface) {
continue;
}

$inheritedProperties = $inheritedProperties->merge($parent->getProperties());
$inheritedProperties = $inheritedProperties->merge($parent->getInheritedProperties());
}

return $inheritedProperties;
}

public function setMethods(Collection $methods): void
{
$this->methods = $methods;
Expand Down Expand Up @@ -124,6 +144,10 @@ public function setPackage($package): void
$constant->setPackage($package);
}

foreach ($this->getProperties() as $property) {
$property->setPackage($package);
}

foreach ($this->getMethods() as $method) {
$method->setPackage($package);
}
Expand Down
17 changes: 17 additions & 0 deletions src/phpDocumentor/Descriptor/Interfaces/InterfaceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ public function getConstants(): Collection;
/** @return Collection<ConstantInterface> */
public function getInheritedConstants(): Collection;

/**
* Sets the properties associated with this interface.
*
* @param Collection<PropertyInterface> $properties
*/
public function setProperties(Collection $properties): void;

/**
* Returns the properties associated with this interface.
*
* @return Collection<PropertyInterface>
*/
public function getProperties(): Collection;

/** @return Collection<PropertyInterface> */
public function getInheritedProperties(): Collection;

/**
* Sets the methods belonging to this interface.
*
Expand Down
8 changes: 4 additions & 4 deletions src/phpDocumentor/Descriptor/PropertyDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class PropertyDescriptor extends DescriptorAbstract implements
use Traits\CanHaveADefaultValue;
use Traits\HasAttributes;

/** @var ClassInterface|TraitInterface|null $parent */
/** @var ClassInterface|TraitInterface|InterfaceInterface|null $parent */
protected ElementInterface|null $parent = null;

protected bool $static = false;
Expand All @@ -63,8 +63,8 @@ public function __construct()
*/
public function setParent($parent): void
{
/** @var ClassInterface|TraitInterface $parent */
Assert::isInstanceOfAny($parent, [ClassInterface::class, TraitInterface::class]);
/** @var ClassInterface|TraitInterface|InterfaceInterface $parent */
Assert::isInstanceOfAny($parent, [ClassInterface::class, TraitInterface::class, InterfaceInterface::class]);

$this->setFullyQualifiedStructuralElementName(
new Fqsen($parent->getFullyQualifiedStructuralElementName() . '::$' . $this->getName()),
Expand All @@ -73,7 +73,7 @@ public function setParent($parent): void
$this->parent = $parent;
}

/** @return ClassInterface|TraitInterface|null */
/** @return ClassInterface|TraitInterface|InterfaceInterface|null */
public function getParent(): ElementInterface|null
{
return $this->parent;
Expand Down
67 changes: 67 additions & 0 deletions tests/unit/phpDocumentor/Descriptor/InterfaceDescriptorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ public function testSettingAndGettingConstants(): void
$this->assertSame($mock, $this->fixture->getConstants());
}

public function testSettingAndGettingProperties(): void
{
$this->assertInstanceOf(Collection::class, $this->fixture->getProperties());

$mock = m::mock(Collection::class);

$this->fixture->setProperties($mock);

$this->assertSame($mock, $this->fixture->getProperties());
}

public function testSettingAndGettingMethods(): void
{
$this->assertInstanceOf(Collection::class, $this->fixture->getMethods());
Expand Down Expand Up @@ -168,6 +179,54 @@ public function testGetInheritedConstantsWithClassDescriptorParent(): void
$this->assertSame([$constantInGrandParent, $constantInParent], $result->getAll());
}

public function testGetInheritedPropertiesNoParent(): void
{
$descriptor = new InterfaceDescriptor();
$this->assertInstanceOf(Collection::class, $descriptor->getInheritedProperties());

$descriptor->setParent(new Collection());
$this->assertInstanceOf(Collection::class, $descriptor->getInheritedProperties());
}

public function testGetInheritedPropertiesWithParentInterface(): void
{
$propertyInParent = $this->givenPropertyWithName('property');
$propertyInGrandParent = $this->givenPropertyWithName('propertyInGrandParent');

$parentInterface = new InterfaceDescriptor();
$parentInterface->setProperties(new Collection([$propertyInParent]));

$grandParentInterface = new InterfaceDescriptor();
$grandParentInterface->setProperties(new Collection([$propertyInGrandParent]));

$parentInterface->setParent(new Collection([$grandParentInterface]));
$this->fixture->setParent(new Collection([$parentInterface]));

$result = $this->fixture->getInheritedProperties();

$this->assertInstanceOf(Collection::class, $result);
$this->assertSame([$propertyInGrandParent, $propertyInParent], $result->getAll());
}

public function testGetInheritedPropertiesWithMultipleParentInterfaces(): void
{
$propertyInFirstParent = $this->givenPropertyWithName('propertyInFirstParent');
$propertyInSecondParent = $this->givenPropertyWithName('propertyInSecondParent');

$firstParent = new InterfaceDescriptor();
$firstParent->setProperties(new Collection([$propertyInFirstParent]));

$secondParent = new InterfaceDescriptor();
$secondParent->setProperties(new Collection([$propertyInSecondParent]));

$this->fixture->setParent(new Collection([$firstParent, $secondParent]));

$result = $this->fixture->getInheritedProperties();

$this->assertInstanceOf(Collection::class, $result);
$this->assertSame([$propertyInSecondParent, $propertyInFirstParent], $result->getAll());
}

public function testRetrievingInheritedMethodsReturnsEmptyCollectionWithoutParent(): void
{
$inheritedMethods = $this->fixture->getInheritedMethods();
Expand Down Expand Up @@ -220,4 +279,12 @@ private function givenConstantWithName(string $name): ConstantDescriptor

return $constantInParent;
}

private function givenPropertyWithName(string $name): PropertyDescriptor
{
$property = new PropertyDescriptor();
$property->setName($name);

return $property;
}
}
Loading