From 56d33c68c943302b86d8419daabea27fe27c59a7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 18 May 2026 16:19:34 +0000
Subject: [PATCH 1/4] Initial plan
From f839dbf476136280ecc53c0379308c7c8d1f4d86 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 18 May 2026 16:27:54 +0000
Subject: [PATCH 2/4] Fix PHP 8.5 observer deprecation handling
Agent-Logs-Url: https://github.com/voku/HtmlMin/sessions/1a74d871-5b13-42d4-b172-ee87a4776089
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
.github/workflows/ci.yml | 15 ++++++++++----
infection.json5 | 11 ++++++++++
phpstan.neon | 1 -
src/voku/helper/HtmlMin.php | 2 +-
tests/HtmlMinTest.php | 41 +++++++++++++++++++++++++++++++++++++
5 files changed, 64 insertions(+), 6 deletions(-)
create mode 100644 infection.json5
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c3d691c..179fb1d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,7 +42,7 @@ jobs:
php-version: ${{ matrix.php }}
coverage: xdebug
extensions: zip
- tools: composer
+ tools: composer, infection
- name: Determine composer cache directory
id: composer-cache
@@ -57,8 +57,8 @@ jobs:
- name: Install dependencies
run: |
- if [[ "${{ matrix.php }}" == "8.1" ]]; then
- composer require phpstan/phpstan --no-update
+ if [[ "${{ matrix.php }}" == "8.5" ]]; then
+ composer require --dev phpstan/phpstan --no-update
fi;
if [[ "${{ matrix.composer }}" == "lowest" ]]; then
@@ -78,10 +78,17 @@ jobs:
- name: Run phpstan
continue-on-error: true
- if: ${{ matrix.php == '8.1' }}
+ if: ${{ matrix.php == '8.5' }}
run: |
php vendor/bin/phpstan analyse
+ - name: Run infection
+ if: ${{ matrix.php == '8.5' }}
+ env:
+ XDEBUG_MODE: coverage
+ run: |
+ infection --threads=max --min-msi=0 --min-covered-msi=0 --only-covering-test-cases --no-progress
+
- name: Upload coverage results to Coveralls
continue-on-error: true
env:
diff --git a/infection.json5 b/infection.json5
new file mode 100644
index 0000000..0bac9c9
--- /dev/null
+++ b/infection.json5
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://infection.github.io/schema.json",
+ "source": {
+ "directories": [
+ "src"
+ ]
+ },
+ "logs": {
+ "summary": "build/logs/infection-summary.log"
+ }
+}
diff --git a/phpstan.neon b/phpstan.neon
index b58c3b4..5d10cdb 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,5 +1,4 @@
parameters:
level: 8
- checkMissingIterableValueType: false
paths:
- %currentWorkingDirectory%/src/
diff --git a/src/voku/helper/HtmlMin.php b/src/voku/helper/HtmlMin.php
index 42b66f8..53f5cb1 100644
--- a/src/voku/helper/HtmlMin.php
+++ b/src/voku/helper/HtmlMin.php
@@ -375,7 +375,7 @@ public function __construct()
*/
public function attachObserverToTheDomLoop(HtmlMinDomObserverInterface $observer)
{
- $this->domLoopObservers[$observer] = $observer;
+ $this->domLoopObservers->offsetSet($observer, $observer);
}
/**
diff --git a/tests/HtmlMinTest.php b/tests/HtmlMinTest.php
index bb69e2b..f67c627 100644
--- a/tests/HtmlMinTest.php
+++ b/tests/HtmlMinTest.php
@@ -21,6 +21,47 @@ public function testEmptyResult()
static::assertSame('', (new HtmlMin())->minify(''));
}
+ public function testAttachObserverToTheDomLoopDoesNotTriggerDeprecation()
+ {
+ $observer = new class implements \voku\helper\HtmlMinDomObserverInterface {
+ /** @var int */
+ public $beforeCalls = 0;
+
+ /** @var int */
+ public $afterCalls = 0;
+
+ public function domElementBeforeMinification(\voku\helper\SimpleHtmlDomInterface $element, \voku\helper\HtmlMinInterface $htmlMin)
+ {
+ ++$this->beforeCalls;
+ }
+
+ public function domElementAfterMinification(\voku\helper\SimpleHtmlDomInterface $element, \voku\helper\HtmlMinInterface $htmlMin)
+ {
+ ++$this->afterCalls;
+ }
+ };
+
+ \set_error_handler(static function (int $severity, string $message, string $file, int $line): bool {
+ if (($severity & \E_DEPRECATED) !== 0) {
+ throw new \ErrorException($message, 0, $severity, $file, $line);
+ }
+
+ return false;
+ });
+
+ try {
+ $minifier = new HtmlMin();
+ $minifier->attachObserverToTheDomLoop($observer);
+
+ static::assertSame('
test
', $minifier->minify('test
'));
+ } finally {
+ \restore_error_handler();
+ }
+
+ static::assertGreaterThan(0, $observer->beforeCalls);
+ static::assertGreaterThan(0, $observer->afterCalls);
+ }
+
/**
* @return array
*/
From 021bcb52f72e391939943809f2cd0d76d7225c7f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 18 May 2026 17:38:59 +0000
Subject: [PATCH 3/4] Integrate Infection PHPStan analysis in CI
Agent-Logs-Url: https://github.com/voku/HtmlMin/sessions/c4646820-ee2c-4ae4-99b6-2d713d8aece6
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
.github/workflows/ci.yml | 3 +-
infection.json5 | 1 +
src/voku/helper/HtmlMin.php | 60 ++++++++++---------
.../HtmlMinDomObserverOptimizeAttributes.php | 2 +-
src/voku/helper/HtmlMinInterface.php | 2 +-
tests/HtmlMinTest.php | 16 +++++
6 files changed, 53 insertions(+), 31 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 179fb1d..2d401f0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -58,7 +58,7 @@ jobs:
- name: Install dependencies
run: |
if [[ "${{ matrix.php }}" == "8.5" ]]; then
- composer require --dev phpstan/phpstan --no-update
+ composer require --dev phpstan/phpstan:^2.1 --no-update
fi;
if [[ "${{ matrix.composer }}" == "lowest" ]]; then
@@ -77,7 +77,6 @@ jobs:
php vendor/bin/phpunit -c phpunit.xml --coverage-clover=build/logs/clover.xml
- name: Run phpstan
- continue-on-error: true
if: ${{ matrix.php == '8.5' }}
run: |
php vendor/bin/phpstan analyse
diff --git a/infection.json5 b/infection.json5
index 0bac9c9..5b91d70 100644
--- a/infection.json5
+++ b/infection.json5
@@ -1,5 +1,6 @@
{
"$schema": "https://infection.github.io/schema.json",
+ "staticAnalysisTool": "phpstan",
"source": {
"directories": [
"src"
diff --git a/src/voku/helper/HtmlMin.php b/src/voku/helper/HtmlMin.php
index 53f5cb1..8d81699 100644
--- a/src/voku/helper/HtmlMin.php
+++ b/src/voku/helper/HtmlMin.php
@@ -25,17 +25,6 @@ class HtmlMin implements HtmlMinInterface
*/
private static $regExSpace = "/[[:space:]]{2,}|[\r\n]/u";
- /**
- * @var string[]
- *
- * @psalm-var list
- */
- private static $optional_end_tags = [
- 'html',
- 'head',
- 'body',
- ];
-
/**
* @var string[]
*
@@ -91,7 +80,7 @@ class HtmlMin implements HtmlMinInterface
];
/**
- * @var array
+ * @var array
*/
private static $booleanAttributes = [
'allowfullscreen' => '',
@@ -138,7 +127,7 @@ class HtmlMin implements HtmlMinInterface
];
/**
- * @var array
+ * @var list
*/
private static $skipTagsForRemoveWhitespace = [
'code',
@@ -149,7 +138,7 @@ class HtmlMin implements HtmlMinInterface
];
/**
- * @var array
+ * @var array
*/
private $protectedChildNodes = [];
@@ -1230,7 +1219,7 @@ private function domNodeOpeningTagOptional(\DOMNode $node): bool
}
return !\in_array(
- $firstChild->tagName,
+ $firstChild->nodeName,
[
'meta',
'link',
@@ -1586,7 +1575,7 @@ private function minifyJsonString(string $json): string
}
/**
- * @return array
+ * @return string[]
*/
public function getDomainsToRemoveHttpPrefixFromAttributes(): array
{
@@ -2266,9 +2255,9 @@ private function protectTagHelper(HtmlDomParser $dom, string $selector): HtmlDom
}
$parentNode = $element->parentNode();
- if ($parentNode !== null && $parentNode->nodeValue !== null) {
+ if ($parentNode !== null && $parentNode->getNode()->nodeValue !== null) {
$this->protectedChildNodes[$this->protected_tags_counter] = $parentNode->innerHtml();
- $parentNode->nodeValue = '<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $this->protected_tags_counter . '">' . $this->protectedChildNodesHelper . '>';
+ $parentNode->getNode()->nodeValue = '<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $this->protected_tags_counter . '">' . $this->protectedChildNodesHelper . '>';
}
++$this->protected_tags_counter;
@@ -2301,7 +2290,7 @@ private function protectTags(HtmlDomParser $dom): HtmlDomParser
}
}
- $innerHtml = $element->innerhtml;
+ $innerHtml = $element->innerHtml();
// On PHP < 8.0 the simplevokubroken-hash mechanism restores content
// (including surrounding newlines/spaces) AFTER fixHtmlOutput's trim
@@ -2338,7 +2327,8 @@ private function protectTags(HtmlDomParser $dom): HtmlDomParser
) {
$originalInnerHtml = $innerHtml;
try {
- $innerHtml = \JShrink\Minifier::minify($innerHtml);
+ $minifiedInnerHtml = \JShrink\Minifier::minify($innerHtml);
+ $innerHtml = \is_string($minifiedInnerHtml) ? $minifiedInnerHtml : $originalInnerHtml;
} catch (\Exception $e) {
$innerHtml = $originalInnerHtml;
}
@@ -2411,6 +2401,9 @@ private function removeComments(HtmlDomParser $dom): HtmlDomParser
foreach ($dom->findMulti('//comment()') as $commentWrapper) {
$comment = $commentWrapper->getNode();
$val = $comment->nodeValue;
+ if ($val === null) {
+ continue;
+ }
if (\strpos($val, '[') === false) {
$parentNode = $comment->parentNode;
if ($parentNode !== null) {
@@ -2449,6 +2442,9 @@ private function removeCommentsOnlyFromHtmlString(string $html): string
foreach ($dom->findMulti('//comment()') as $commentWrapper) {
$comment = $commentWrapper->getNode();
$commentValue = $comment->nodeValue;
+ if ($commentValue === null) {
+ continue;
+ }
if (
$this->isConditionalComment($commentValue)
||
@@ -2508,7 +2504,7 @@ private function removeWhitespaceAroundTags(SimpleHtmlDomInterface $element)
/**
* Callback function for preg_replace_callback use.
*
- * @param array $matches PREG matches
+ * @param array $matches PREG matches
*
* @return string
*/
@@ -2516,7 +2512,7 @@ private function restoreProtectedHtml($matches): string
{
\preg_match('/=["\']*(?\d+)/', $matches['attributes'], $matchesInner);
- return $this->protectedChildNodes[$matchesInner['id']] ?? '';
+ return isset($matchesInner['id']) ? ($this->protectedChildNodes[(int) $matchesInner['id']] ?? '') : '';
}
/**
@@ -2574,6 +2570,10 @@ private function sumUpWhitespace(HtmlDomParser $dom): HtmlDomParser
continue;
}
+ if ($text_node->nodeValue === null) {
+ continue;
+ }
+
$nodeValueTmp = \preg_replace(self::$regExSpace, ' ', $text_node->nodeValue);
if ($nodeValueTmp !== null) {
$text_node->nodeValue = $nodeValueTmp;
@@ -2600,38 +2600,44 @@ public function useKeepBrokenHtml(bool $keepBrokenHtml): self
}
/**
- * @param string[] $templateLogicSyntaxInSpecialScriptTags
+ * @param array $templateLogicSyntaxInSpecialScriptTags
*
* @return HtmlMin
*/
public function overwriteTemplateLogicSyntaxInSpecialScriptTags(array $templateLogicSyntaxInSpecialScriptTags): self
{
+ $validatedTemplateLogicSyntaxInSpecialScriptTags = [];
foreach ($templateLogicSyntaxInSpecialScriptTags as $tmp) {
if (!\is_string($tmp)) {
throw new \InvalidArgumentException('setTemplateLogicSyntaxInSpecialScriptTags only allows string[]');
}
+
+ $validatedTemplateLogicSyntaxInSpecialScriptTags[] = $tmp;
}
- $this->templateLogicSyntaxInSpecialScriptTags = $templateLogicSyntaxInSpecialScriptTags;
+ $this->templateLogicSyntaxInSpecialScriptTags = $validatedTemplateLogicSyntaxInSpecialScriptTags;
return $this;
}
/**
- * @param string[] $specialScriptTags
+ * @param array $specialScriptTags
*
- * @return HtmlDomParser
+ * @return HtmlMin
*/
public function overwriteSpecialScriptTags(array $specialScriptTags): self
{
+ $validatedSpecialScriptTags = [];
foreach ($specialScriptTags as $tag) {
if (!\is_string($tag)) {
throw new \InvalidArgumentException('SpecialScriptTags only allows string[]');
}
+
+ $validatedSpecialScriptTags[] = $tag;
}
- $this->specialScriptTags = $specialScriptTags;
+ $this->specialScriptTags = $validatedSpecialScriptTags;
return $this;
}
diff --git a/src/voku/helper/HtmlMinDomObserverOptimizeAttributes.php b/src/voku/helper/HtmlMinDomObserverOptimizeAttributes.php
index edfa2da..e94ff1f 100644
--- a/src/voku/helper/HtmlMinDomObserverOptimizeAttributes.php
+++ b/src/voku/helper/HtmlMinDomObserverOptimizeAttributes.php
@@ -162,7 +162,7 @@ public function domElementAfterMinification(SimpleHtmlDomInterface $element, Htm
* @param string $tag
* @param string $attrName
* @param string $attrValue
- * @param array $allAttr
+ * @param array $allAttr
* @param HtmlMinInterface $htmlMin
*
* @return bool
diff --git a/src/voku/helper/HtmlMinInterface.php b/src/voku/helper/HtmlMinInterface.php
index 7a0cfe9..56a3833 100644
--- a/src/voku/helper/HtmlMinInterface.php
+++ b/src/voku/helper/HtmlMinInterface.php
@@ -163,7 +163,7 @@ public function isXHTML(): bool;
public function getLocalDomains(): array;
/**
- * @return array
+ * @return string[]
*/
public function getDomainsToRemoveHttpPrefixFromAttributes(): array;
}
diff --git a/tests/HtmlMinTest.php b/tests/HtmlMinTest.php
index f67c627..f26a323 100644
--- a/tests/HtmlMinTest.php
+++ b/tests/HtmlMinTest.php
@@ -62,6 +62,22 @@ public function domElementAfterMinification(\voku\helper\SimpleHtmlDomInterface
static::assertGreaterThan(0, $observer->afterCalls);
}
+ public function testOverwriteTemplateLogicSyntaxInSpecialScriptTagsRejectsNonStringValues()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('setTemplateLogicSyntaxInSpecialScriptTags only allows string[]');
+
+ (new HtmlMin())->overwriteTemplateLogicSyntaxInSpecialScriptTags(['{%', 123]);
+ }
+
+ public function testOverwriteSpecialScriptTagsRejectsNonStringValues()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('SpecialScriptTags only allows string[]');
+
+ (new HtmlMin())->overwriteSpecialScriptTags(['text/html', 123]);
+ }
+
/**
* @return array
*/
From f94d6681c7f3680b9defb69f894542fc38c9e7b6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 18 May 2026 18:26:53 +0000
Subject: [PATCH 4/4] Align HtmlMin exception messages with method names
Agent-Logs-Url: https://github.com/voku/HtmlMin/sessions/6d4cb1d3-82e6-4d7b-a893-c981c65a02af
Co-authored-by: voku <264695+voku@users.noreply.github.com>
---
src/voku/helper/HtmlMin.php | 4 ++--
tests/HtmlMinTest.php | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/voku/helper/HtmlMin.php b/src/voku/helper/HtmlMin.php
index 8d81699..b93e29a 100644
--- a/src/voku/helper/HtmlMin.php
+++ b/src/voku/helper/HtmlMin.php
@@ -2609,7 +2609,7 @@ public function overwriteTemplateLogicSyntaxInSpecialScriptTags(array $templateL
$validatedTemplateLogicSyntaxInSpecialScriptTags = [];
foreach ($templateLogicSyntaxInSpecialScriptTags as $tmp) {
if (!\is_string($tmp)) {
- throw new \InvalidArgumentException('setTemplateLogicSyntaxInSpecialScriptTags only allows string[]');
+ throw new \InvalidArgumentException('overwriteTemplateLogicSyntaxInSpecialScriptTags only allows string[]');
}
$validatedTemplateLogicSyntaxInSpecialScriptTags[] = $tmp;
@@ -2631,7 +2631,7 @@ public function overwriteSpecialScriptTags(array $specialScriptTags): self
$validatedSpecialScriptTags = [];
foreach ($specialScriptTags as $tag) {
if (!\is_string($tag)) {
- throw new \InvalidArgumentException('SpecialScriptTags only allows string[]');
+ throw new \InvalidArgumentException('overwriteSpecialScriptTags only allows string[]');
}
$validatedSpecialScriptTags[] = $tag;
diff --git a/tests/HtmlMinTest.php b/tests/HtmlMinTest.php
index f26a323..adb58d2 100644
--- a/tests/HtmlMinTest.php
+++ b/tests/HtmlMinTest.php
@@ -65,7 +65,7 @@ public function domElementAfterMinification(\voku\helper\SimpleHtmlDomInterface
public function testOverwriteTemplateLogicSyntaxInSpecialScriptTagsRejectsNonStringValues()
{
$this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage('setTemplateLogicSyntaxInSpecialScriptTags only allows string[]');
+ $this->expectExceptionMessage('overwriteTemplateLogicSyntaxInSpecialScriptTags only allows string[]');
(new HtmlMin())->overwriteTemplateLogicSyntaxInSpecialScriptTags(['{%', 123]);
}
@@ -73,7 +73,7 @@ public function testOverwriteTemplateLogicSyntaxInSpecialScriptTagsRejectsNonStr
public function testOverwriteSpecialScriptTagsRejectsNonStringValues()
{
$this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage('SpecialScriptTags only allows string[]');
+ $this->expectExceptionMessage('overwriteSpecialScriptTags only allows string[]');
(new HtmlMin())->overwriteSpecialScriptTags(['text/html', 123]);
}