-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PHP 8.2 Improvements #533
PHP 8.2 Improvements #533
Changes from all commits
b491d83
525e918
167da1d
05c7cc0
a25c899
9d17198
419c696
6d3dfea
6e6a8ae
e72abc9
c9183d9
cef1d12
ca8d06b
76809be
7a81724
3873095
b9ce62b
3d6cf89
5f3ecf0
4130385
f5b41e7
d535106
ec7430d
012bef9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
/vendor/ | ||
composer.lock | ||
.phpunit.result.cache |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -108,6 +108,13 @@ final class Document extends DOMDocument | |
*/ | ||
const XPATH_INLINE_STYLE_ATTRIBUTES_QUERY = './/@style'; | ||
|
||
/** | ||
* Associative array for lazily-created, cached properties for the document. | ||
* | ||
* @var array | ||
*/ | ||
private $properties = []; | ||
|
||
/** | ||
* Associative array of options to configure the behavior of the DOM document abstraction. | ||
* | ||
|
@@ -142,13 +149,6 @@ final class Document extends DOMDocument | |
*/ | ||
private $cssMaxByteCountEnforced = -1; | ||
|
||
/** | ||
* Resource hint manager to manage resource hint <link> tags in the <head>. | ||
* | ||
* @var LinkManager|null | ||
*/ | ||
private $links; | ||
|
||
/** | ||
* List of document filter class names. | ||
* | ||
|
@@ -314,7 +314,7 @@ public static function fromNode(DOMNode &$node) | |
private function reset() | ||
{ | ||
// Drop references to old DOM document. | ||
unset($this->xpath, $this->head, $this->body); | ||
unset($this->properties['xpath'], $this->properties['head'], $this->properties['body']); | ||
|
||
// Reference of the document itself doesn't change here, but might need to change in the future. | ||
return $this; | ||
|
@@ -587,6 +587,8 @@ private function normalizeDocumentStructure($content) | |
|
||
/** | ||
* Normalize the structure of the document if it was already provided as a DOM. | ||
* | ||
* Warning: This method may not use any magic getters for html, head, or body. | ||
*/ | ||
public function normalizeDomStructure() | ||
{ | ||
|
@@ -618,8 +620,8 @@ public function normalizeDomStructure() | |
throw FailedToRetrieveRequiredDomElement::forHeadElement($head); | ||
} | ||
|
||
$this->head = $head; | ||
$html->appendChild($this->head); | ||
$this->properties['head'] = $head; | ||
$html->appendChild($head); | ||
|
||
if ($oldDocumentElement->nodeName === Tag::BODY) { | ||
$body = $oldDocumentElement; | ||
|
@@ -634,23 +636,23 @@ public function normalizeDomStructure() | |
throw FailedToRetrieveRequiredDomElement::forBodyElement($body); | ||
} | ||
|
||
$this->body = $body; | ||
$html->appendChild($this->body); | ||
$this->properties['body'] = $body; | ||
$html->appendChild($body); | ||
|
||
if ($oldDocumentElement !== $this->body && $oldDocumentElement !== $this->head) { | ||
$this->body->appendChild($oldDocumentElement); | ||
if ($oldDocumentElement !== $body && $oldDocumentElement !== $this->head) { | ||
$body->appendChild($oldDocumentElement); | ||
} | ||
} else { | ||
$head = $this->getElementsByTagName(Tag::HEAD)->item(0); | ||
if (!$head) { | ||
$this->head = $this->createElement(Tag::HEAD); | ||
$this->documentElement->insertBefore($this->head, $this->documentElement->firstChild); | ||
$this->properties['head'] = $this->createElement(Tag::HEAD); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not continue to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for the sake of performance? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I ran into some issues when I originally tried that, but I might have been doing something wrong. I can give it another try 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just tested again. Even on PHP 8.1 I am getting lots of Even when doing something seemingly simple like this: // Throws warning
$this->html = $html;
return $this->html;
// Works
$this->html = $html;
return $html;
// Also works
$this->properties['html'] = $html;
return $this->properties['html']; I also just analyzed the
No such issues happen with the current There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the issue at question is this note:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In other words, if a caller is trying to access |
||
$this->documentElement->insertBefore($this->properties['head'], $this->documentElement->firstChild); | ||
} | ||
|
||
$body = $this->getElementsByTagName(Tag::BODY)->item(0); | ||
if (!$body) { | ||
$this->body = $this->createElement(Tag::BODY); | ||
$this->documentElement->appendChild($this->body); | ||
$this->properties['body'] = $this->createElement(Tag::BODY); | ||
$this->documentElement->appendChild($this->properties['body']); | ||
} | ||
} | ||
|
||
|
@@ -660,15 +662,20 @@ public function normalizeDomStructure() | |
|
||
/** | ||
* Move invalid head nodes back to the body. | ||
* | ||
* Warning: This method may not use any magic getters for html, head, or body. | ||
*/ | ||
private function moveInvalidHeadNodesToBody() | ||
{ | ||
// Walking backwards makes it easier to move elements in the expected order. | ||
$node = $this->head->lastChild; | ||
$node = $this->properties['head']->lastChild; | ||
while ($node) { | ||
$nextSibling = $node->previousSibling; | ||
if (! $this->isValidHeadNode($node)) { | ||
$this->body->insertBefore($this->head->removeChild($node), $this->body->firstChild); | ||
if (!$this->isValidHeadNode($node)) { | ||
$this->properties['body']->insertBefore( | ||
$this->properties['head']->removeChild($node), | ||
$this->properties['body']->firstChild | ||
); | ||
} | ||
$node = $nextSibling; | ||
} | ||
|
@@ -681,12 +688,14 @@ private function moveInvalidHeadNodesToBody() | |
* the </body> not valid in AMP, but trailing elements after </html> will get wrapped in additional <html> elements. | ||
* While comment nodes would be allowed in AMP, everything is moved regardless so that source stack comments will | ||
* retain their relative position with the element nodes they annotate. | ||
* | ||
* Warning: This method may not use any magic getters for html, head, or body. | ||
*/ | ||
private function movePostBodyNodesToBody() | ||
{ | ||
// Move nodes (likely comments) from after the </body>. | ||
while ($this->body->nextSibling) { | ||
$this->body->appendChild($this->body->nextSibling); | ||
while ($this->properties['body']->nextSibling) { | ||
$this->properties['body']->appendChild($this->properties['body']->nextSibling); | ||
} | ||
|
||
// Move nodes from after the </html>. | ||
|
@@ -695,18 +704,20 @@ private function movePostBodyNodesToBody() | |
if ($nextSibling instanceof Element && Tag::HTML === $nextSibling->nodeName) { | ||
// Handle trailing elements getting wrapped in implicit duplicate <html>. | ||
while ($nextSibling->firstChild) { | ||
$this->body->appendChild($nextSibling->firstChild); | ||
$this->properties['body']->appendChild($nextSibling->firstChild); | ||
} | ||
$nextSibling->parentNode->removeChild($nextSibling); // Discard now-empty implicit <html>. | ||
} else { | ||
$this->body->appendChild($this->documentElement->nextSibling); | ||
$this->properties['body']->appendChild($this->documentElement->nextSibling); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Determine whether a node can be in the head. | ||
* | ||
* Warning: This method may not use any magic getters for html, head, or body. | ||
* | ||
* @link https://github.com/ampproject/amphtml/blob/445d6e3be8a5063e2738c6f90fdcd57f2b6208be/validator/engine/htmlparser.js#L83-L100 | ||
* @link https://www.w3.org/TR/html5/document-metadata.html | ||
* | ||
|
@@ -787,7 +798,7 @@ public function addAmpCustomStyle($style) | |
} | ||
|
||
$this->ampCustomStyle->textContent = $newStyle; | ||
$this->ampCustomStyleByteCount = $newByteCount; | ||
$this->properties['ampCustomStyleByteCount'] = $newByteCount; | ||
} | ||
|
||
/** | ||
|
@@ -818,6 +829,29 @@ public function getRemainingCustomCssSpace() | |
); | ||
} | ||
|
||
/** | ||
* Get the array of allowed keys of lazily-created, cached properties. | ||
* The array index is the key and the array value is the key's default value. | ||
* | ||
* @return array Array of allowed keys. | ||
*/ | ||
protected function getAllowedKeys() | ||
{ | ||
return [ | ||
'xpath', | ||
Tag::HTML, | ||
Tag::HEAD, | ||
Tag::BODY, | ||
Attribute::CHARSET, | ||
Attribute::VIEWPORT, | ||
'ampElements', | ||
'ampCustomStyle', | ||
'ampCustomStyleByteCount', | ||
'inlineStyleByteCount', | ||
'links', | ||
]; | ||
} | ||
|
||
/** | ||
* Magic getter to implement lazily-created, cached properties for the document. | ||
* | ||
|
@@ -828,8 +862,8 @@ public function __get($name) | |
{ | ||
switch ($name) { | ||
case 'xpath': | ||
$this->xpath = new DOMXPath($this); | ||
return $this->xpath; | ||
$this->properties['xpath'] = new DOMXPath($this); | ||
return $this->properties['xpath']; | ||
case Tag::HTML: | ||
$html = $this->getElementsByTagName(Tag::HTML)->item(0); | ||
|
||
|
@@ -843,8 +877,8 @@ public function __get($name) | |
throw FailedToRetrieveRequiredDomElement::forHtmlElement($html); | ||
} | ||
|
||
$this->html = $html; | ||
return $this->html; | ||
$this->properties['html'] = $html; | ||
return $this->properties['html']; | ||
case Tag::HEAD: | ||
$head = $this->getElementsByTagName(Tag::HEAD)->item(0); | ||
|
||
|
@@ -858,8 +892,8 @@ public function __get($name) | |
throw FailedToRetrieveRequiredDomElement::forHeadElement($head); | ||
} | ||
|
||
$this->head = $head; | ||
return $this->head; | ||
$this->properties['head'] = $head; | ||
return $this->properties['head']; | ||
case Tag::BODY: | ||
$body = $this->getElementsByTagName(Tag::BODY)->item(0); | ||
|
||
|
@@ -873,8 +907,8 @@ public function __get($name) | |
throw FailedToRetrieveRequiredDomElement::forBodyElement($body); | ||
} | ||
|
||
$this->body = $body; | ||
return $this->body; | ||
$this->properties['body'] = $body; | ||
return $this->properties['body']; | ||
case Attribute::CHARSET: | ||
// This is not cached as it could potentially be requested too early, before the viewport was added, and | ||
// the cache would then store null without rechecking later on after the viewport has been added. | ||
|
@@ -915,53 +949,87 @@ public function __get($name) | |
$this->head->appendChild($ampCustomStyle); | ||
} | ||
|
||
$this->ampCustomStyle = $ampCustomStyle; | ||
$this->properties['ampCustomStyle'] = $ampCustomStyle; | ||
|
||
return $this->ampCustomStyle; | ||
return $this->properties['ampCustomStyle']; | ||
|
||
case 'ampCustomStyleByteCount': | ||
if (!isset($this->ampCustomStyle)) { | ||
if (!isset($this->properties['ampCustomStyle'])) { | ||
$ampCustomStyle = $this->xpath->query(self::XPATH_AMP_CUSTOM_STYLE_QUERY, $this->head)->item(0); | ||
if (!$ampCustomStyle instanceof Element) { | ||
return 0; | ||
} else { | ||
$this->ampCustomStyle = $ampCustomStyle; | ||
} | ||
|
||
$this->properties['ampCustomStyle'] = $ampCustomStyle; | ||
} | ||
|
||
if (!isset($this->ampCustomStyleByteCount)) { | ||
$this->ampCustomStyleByteCount = strlen($this->ampCustomStyle->textContent); | ||
if (!isset($this->properties['ampCustomStyleByteCount'])) { | ||
$this->properties['ampCustomStyleByteCount'] = | ||
strlen($this->properties['ampCustomStyle']->textContent); | ||
} | ||
|
||
return $this->ampCustomStyleByteCount; | ||
return $this->properties['ampCustomStyleByteCount']; | ||
|
||
case 'inlineStyleByteCount': | ||
if (!isset($this->inlineStyleByteCount)) { | ||
$this->inlineStyleByteCount = 0; | ||
if (!isset($this->properties['inlineStyleByteCount'])) { | ||
$this->properties['inlineStyleByteCount'] = 0; | ||
$attributes = $this->xpath->query( | ||
self::XPATH_INLINE_STYLE_ATTRIBUTES_QUERY, | ||
$this->documentElement | ||
); | ||
foreach ($attributes as $attribute) { | ||
$this->inlineStyleByteCount += strlen($attribute->textContent); | ||
$this->properties['inlineStyleByteCount'] += strlen($attribute->textContent); | ||
} | ||
} | ||
|
||
return $this->inlineStyleByteCount; | ||
return $this->properties['inlineStyleByteCount']; | ||
|
||
case 'links': | ||
if (! isset($this->links)) { | ||
$this->links = new LinkManager($this); | ||
if (! isset($this->properties['links'])) { | ||
$this->properties['links'] = new LinkManager($this); | ||
} | ||
|
||
return $this->links; | ||
return $this->properties['links']; | ||
} | ||
|
||
// Mimic regular PHP behavior for missing notices. | ||
trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, E_USER_NOTICE); | ||
return null; | ||
} | ||
|
||
/** | ||
* Magic setter to implement lazily-created, cached properties for the document. | ||
* | ||
* @param string $name Name of the property to set. | ||
* @param mixed $value Value of the property. | ||
*/ | ||
public function __set($name, $value) | ||
{ | ||
if (!in_array($name, $this->getAllowedKeys(), true)) { | ||
// Mimic regular PHP behavior for missing notices. | ||
trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, E_USER_NOTICE); | ||
return; | ||
} | ||
|
||
$this->properties[$name] = $value; | ||
} | ||
|
||
/** | ||
* Magic callback for lazily-created, cached properties for the document. | ||
* | ||
* @param string $name Name of the property to set. | ||
*/ | ||
public function __isset($name) | ||
{ | ||
if (!in_array($name, $this->getAllowedKeys(), true)) { | ||
// Mimic regular PHP behavior for missing notices. | ||
trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, E_USER_NOTICE); | ||
return false; | ||
} | ||
|
||
return isset($this->properties[$name]); | ||
} | ||
|
||
/** | ||
* Make sure we properly reinitialize on clone. | ||
* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized a potential large simplification here. Why if each of the properties were instead just declared as null?
The phpdoc notwithstanding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you did this in 525e918 but then reverted. Did you try
private
instead ofpublic
? This would model what is done with the$links
property already.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see the problem with that approach. It's that private access in the class won't cause the getter to be invoked. I think I have a way around that, and that is to manually invoke
__get()
for methods onDocument
. I'm going to give it a try, though the commit may not be pushed until Monday.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bah. Doing this makes PHPStan blow up with a lot of "Access to private property" errors.