Skip to content
55 changes: 45 additions & 10 deletions stubs/dom.stub
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

class DOMDocument
class DOMDocument extends DOMNode
{

/** @var DOMDocumentType|null */
Expand Down Expand Up @@ -29,7 +29,16 @@ class DOMDocument

class DOMNode
{
/**
* @readonly
* @var DOMNamedNodeMap<DOMAttr>|null
*/
public $attributes;

/**
* @phpstan-assert-if-true DOMNamedNodeMap<DOMAttr> $this->attributes
*/
public function hasAttributes(): bool {}
}

class DOMElement extends DOMNode
Expand All @@ -38,6 +47,12 @@ class DOMElement extends DOMNode
/** @var DOMDocument */
public $ownerDocument;

/**
* @readonly
* @var DOMNamedNodeMap<DOMAttr>
*/
public $attributes;

/**
* @param string $name
* @return DOMNodeList<DOMElement>
Expand All @@ -50,7 +65,6 @@ class DOMElement extends DOMNode
* @return DOMNodeList<DOMElement>
*/
public function getElementsByTagNameNS ($namespaceURI, $localName) {}

}

/**
Expand Down Expand Up @@ -82,47 +96,47 @@ class DOMXPath

}

class DOMAttr
class DOMAttr extends DOMNode
{

/** @var DOMDocument */
public $ownerDocument;

}

class DOMCharacterData
class DOMCharacterData extends DOMNode
{

/** @var DOMDocument */
public $ownerDocument;

}

class DOMDocumentType
class DOMDocumentType extends DOMNode
{

/** @var DOMDocument */
public $ownerDocument;

}

class DOMEntity
class DOMEntity extends DOMNode
{

/** @var DOMDocument */
public $ownerDocument;

}

class DOMNotation
class DOMNotation extends DOMNode
{

/** @var DOMDocument */
public $ownerDocument;

}

class DOMProcessingInstruction
class DOMProcessingInstruction extends DOMNode
{

/** @var DOMDocument */
Expand All @@ -141,14 +155,35 @@ class DOMProcessingInstruction
}

/**
* @template-covariant TNode as DOMNode
* @implements Traversable<string, TNode>
* @implements IteratorAggregate<int, TNode>
* @property-read int $length
*/
class DOMNamedNodeMap
class DOMNamedNodeMap implements Traversable, IteratorAggregate, Countable
{

/**
* @return (TNode | null)
*/
public function getNamedItem(string $qualifiedName)
{
}
/**
* @return (TNode | null)
*/
public function getNamedItemNS(?string $namespace, string $localName)
{
}
/**
* @return (TNode | null)
*/
public function item(int $index)
{
}
}

class DOMText
class DOMText extends DOMCharacterData
{

/** @var string */
Expand Down
48 changes: 48 additions & 0 deletions tests/PHPStan/Analyser/nsrt/DOMLegacyNamedNodeNap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types = 1);

namespace DOMLegacyNamedNodeMap;

use DOMAttr;
use DOMElement;
use DOMNode;
use DOMNamedNodeMap;
use function PHPStan\Testing\assertType;

class Foo
{
public function basic_node(DOMNode $node): void {
if ($node->hasAttributes()) {
assertType('DOMNamedNodeMap<DOMAttr>', $node->attributes);
} else {
assertType('null', $node->attributes);
}
}

public function element_node(DOMElement $element): void
{
assertType('DOMNamedNodeMap<DOMAttr>', $element->attributes);
if ($element->hasAttribute('class')) {
$attribute = $element->getAttributeNode('class');
assertType(DOMAttr::class, $attribute);
assertType('string', $attribute->value);
} else {
$attribute = $element->getAttributeNode('class');
assertType('false', $attribute);
}
}

public function element_node_attribute_fetch_via_attributes_property(DOMElement $element): void
{
assertType('DOMNamedNodeMap<DOMAttr>', $element->attributes);
if ($element->hasAttribute('class')) {
$attribute = $element->attributes->getNamedItem('class');
if ($attribute === null) {
return;
}
assertType(DOMAttr::class, $attribute);
assertType('string', $attribute->value);
}
}
}
8 changes: 8 additions & 0 deletions tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -957,4 +957,12 @@ public function testTraitMixin(): void
$this->analyse([__DIR__ . '/data/trait-mixin.php'], []);
}

public function testDomExtensionLegacyTemplateNodes(): void
{
$this->checkThisOnly = false;
$this->checkUnionTypes = true;
$this->checkDynamicProperties = true;
$this->analyse([__DIR__ . '/data/dom-legacy-ext-template-nodes.php'], []);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace DOMNodeStubsAccessProperties;

function basic_node(\DOMNode $node): void {
var_dump($node->attributes);
}

function element_node(\DOMElement $element): void
{
if ($element->hasAttribute('class')) {
$attribute = $element->getAttributeNode('class');
echo $attribute->value;
}
}

function element_node_attribute_fetch_via_attributes_property(\DOMElement $element): void
{
$attribute = $element->attributes->getNamedItem('class');
if ($attribute === null) {
return;
}
echo $attribute->value;
}

function element_node_attribute_fetch_via_getAttributeNode(\DOMElement $element): void
{
$attribute = $element->getAttributeNode('class');
if ($attribute === null) {
return;
}
echo $attribute->value;
}
Loading