Skip to content

Commit fefc2a8

Browse files
Extract methods to reduce code duplication and handle error in DOMDocument::saveXML() for #1092
1 parent 194f273 commit fefc2a8

File tree

8 files changed

+111
-85
lines changed

8 files changed

+111
-85
lines changed

ChangeLog-11.0.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
44

5+
## [11.0.11] - 2025-MM-DD
6+
7+
### Fixed
8+
9+
* [#1092](https://github.com/sebastianbergmann/php-code-coverage/issues/1092): Error in `DOMDocument::saveXML()` is not handled
10+
511
## [11.0.11] - 2025-08-27
612

713
### Changed
@@ -87,6 +93,7 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
8793
* This component now requires PHP-Parser 5
8894
* This component is no longer supported on PHP 8.1
8995

96+
[11.0.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.11...11.0
9097
[11.0.11]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.10...11.0.11
9198
[11.0.10]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.9...11.0.10
9299
[11.0.9]: https://github.com/sebastianbergmann/php-code-coverage/compare/11.0.8...11.0.9

src/Report/Clover.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,31 @@
1010
namespace SebastianBergmann\CodeCoverage\Report;
1111

1212
use function count;
13-
use function dirname;
14-
use function file_put_contents;
1513
use function is_string;
1614
use function ksort;
1715
use function max;
1816
use function range;
19-
use function str_contains;
2017
use function time;
2118
use DOMDocument;
2219
use SebastianBergmann\CodeCoverage\CodeCoverage;
2320
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
2421
use SebastianBergmann\CodeCoverage\Node\File;
2522
use SebastianBergmann\CodeCoverage\Util\Filesystem;
23+
use SebastianBergmann\CodeCoverage\Util\Xml;
2624

2725
final class Clover
2826
{
2927
/**
28+
* @param null|non-empty-string $target
29+
* @param null|non-empty-string $name
30+
*
3031
* @throws WriteOperationFailedException
3132
*/
3233
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
3334
{
3435
$time = (string) time();
3536

36-
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
37-
$xmlDocument->formatOutput = true;
37+
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
3838

3939
$xmlCoverage = $xmlDocument->createElement('coverage');
4040
$xmlCoverage->setAttribute('generated', $time);
@@ -214,16 +214,10 @@ public function process(CodeCoverage $coverage, ?string $target = null, ?string
214214
$xmlMetrics->setAttribute('coveredelements', (string) ($report->numberOfTestedMethods() + $report->numberOfExecutedLines() + $report->numberOfExecutedBranches()));
215215
$xmlProject->appendChild($xmlMetrics);
216216

217-
$buffer = $xmlDocument->saveXML();
217+
$buffer = Xml::asString($xmlDocument);
218218

219219
if ($target !== null) {
220-
if (!str_contains($target, '://')) {
221-
Filesystem::createDirectory(dirname($target));
222-
}
223-
224-
if (@file_put_contents($target, $buffer) === false) {
225-
throw new WriteOperationFailedException($target);
226-
}
220+
Filesystem::write($target, $buffer);
227221
}
228222

229223
return $buffer;

src/Report/Cobertura.php

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@
1212
use const DIRECTORY_SEPARATOR;
1313
use function basename;
1414
use function count;
15-
use function dirname;
16-
use function file_put_contents;
1715
use function preg_match;
1816
use function range;
19-
use function str_contains;
2017
use function str_replace;
2118
use function time;
2219
use DOMImplementation;
2320
use SebastianBergmann\CodeCoverage\CodeCoverage;
2421
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
2522
use SebastianBergmann\CodeCoverage\Node\File;
2623
use SebastianBergmann\CodeCoverage\Util\Filesystem;
24+
use SebastianBergmann\CodeCoverage\Util\Xml;
2725

2826
final class Cobertura
2927
{
3028
/**
29+
* @param null|non-empty-string $target
30+
*
3131
* @throws WriteOperationFailedException
3232
*/
3333
public function process(CodeCoverage $coverage, ?string $target = null): string
@@ -44,10 +44,9 @@ public function process(CodeCoverage $coverage, ?string $target = null): string
4444
'http://cobertura.sourceforge.net/xml/coverage-04.dtd',
4545
);
4646

47-
$document = $implementation->createDocument('', '', $documentType);
48-
$document->xmlVersion = '1.0';
49-
$document->encoding = 'UTF-8';
50-
$document->formatOutput = true;
47+
$document = $implementation->createDocument('', '', $documentType);
48+
$document->xmlVersion = '1.0';
49+
$document->encoding = 'UTF-8';
5150

5251
$coverageElement = $document->createElement('coverage');
5352

@@ -289,16 +288,10 @@ public function process(CodeCoverage $coverage, ?string $target = null): string
289288

290289
$coverageElement->setAttribute('complexity', (string) $complexity);
291290

292-
$buffer = $document->saveXML();
291+
$buffer = Xml::asString($document);
293292

294293
if ($target !== null) {
295-
if (!str_contains($target, '://')) {
296-
Filesystem::createDirectory(dirname($target));
297-
}
298-
299-
if (@file_put_contents($target, $buffer) === false) {
300-
throw new WriteOperationFailedException($target);
301-
}
294+
Filesystem::write($target, $buffer);
302295
}
303296

304297
return $buffer;

src/Report/Crap4j.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,15 @@
1010
namespace SebastianBergmann\CodeCoverage\Report;
1111

1212
use function date;
13-
use function dirname;
14-
use function file_put_contents;
1513
use function htmlspecialchars;
1614
use function is_string;
1715
use function round;
18-
use function str_contains;
1916
use DOMDocument;
2017
use SebastianBergmann\CodeCoverage\CodeCoverage;
2118
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
2219
use SebastianBergmann\CodeCoverage\Node\File;
2320
use SebastianBergmann\CodeCoverage\Util\Filesystem;
21+
use SebastianBergmann\CodeCoverage\Util\Xml;
2422

2523
final class Crap4j
2624
{
@@ -32,12 +30,14 @@ public function __construct(int $threshold = 30)
3230
}
3331

3432
/**
33+
* @param null|non-empty-string $target
34+
* @param null|non-empty-string $name
35+
*
3536
* @throws WriteOperationFailedException
3637
*/
3738
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
3839
{
39-
$document = new DOMDocument('1.0', 'UTF-8');
40-
$document->formatOutput = true;
40+
$document = new DOMDocument('1.0', 'UTF-8');
4141

4242
$root = $document->createElement('crap_result');
4343
$document->appendChild($root);
@@ -119,16 +119,10 @@ public function process(CodeCoverage $coverage, ?string $target = null, ?string
119119
$root->appendChild($stats);
120120
$root->appendChild($methodsNode);
121121

122-
$buffer = $document->saveXML();
122+
$buffer = Xml::asString($document);
123123

124124
if ($target !== null) {
125-
if (!str_contains($target, '://')) {
126-
Filesystem::createDirectory(dirname($target));
127-
}
128-
129-
if (@file_put_contents($target, $buffer) === false) {
130-
throw new WriteOperationFailedException($target);
131-
}
125+
Filesystem::write($target, $buffer);
132126
}
133127

134128
return $buffer;

src/Report/PHP.php

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@
1010
namespace SebastianBergmann\CodeCoverage\Report;
1111

1212
use const PHP_EOL;
13-
use function dirname;
14-
use function file_put_contents;
1513
use function serialize;
16-
use function str_contains;
1714
use SebastianBergmann\CodeCoverage\CodeCoverage;
1815
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
1916
use SebastianBergmann\CodeCoverage\Util\Filesystem;
2017

2118
final class PHP
2219
{
20+
/**
21+
* @param null|non-empty-string $target
22+
*
23+
* @throws WriteOperationFailedException
24+
*/
2325
public function process(CodeCoverage $coverage, ?string $target = null): string
2426
{
2527
$coverage->clearCache();
@@ -28,13 +30,7 @@ public function process(CodeCoverage $coverage, ?string $target = null): string
2830
return \unserialize(<<<'END_OF_COVERAGE_SERIALIZATION'" . PHP_EOL . serialize($coverage) . PHP_EOL . 'END_OF_COVERAGE_SERIALIZATION' . PHP_EOL . ');';
2931

3032
if ($target !== null) {
31-
if (!str_contains($target, '://')) {
32-
Filesystem::createDirectory(dirname($target));
33-
}
34-
35-
if (@file_put_contents($target, $buffer) === false) {
36-
throw new WriteOperationFailedException($target);
37-
}
33+
Filesystem::write($target, $buffer);
3834
}
3935

4036
return $buffer;

src/Report/Xml/Facade.php

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,13 @@
1010
namespace SebastianBergmann\CodeCoverage\Report\Xml;
1111

1212
use const DIRECTORY_SEPARATOR;
13-
use const PHP_EOL;
1413
use function count;
1514
use function dirname;
1615
use function file_get_contents;
17-
use function file_put_contents;
1816
use function is_array;
1917
use function is_dir;
2018
use function is_file;
2119
use function is_writable;
22-
use function libxml_clear_errors;
23-
use function libxml_get_errors;
24-
use function libxml_use_internal_errors;
2520
use function sprintf;
2621
use function strlen;
2722
use function substr;
@@ -33,7 +28,9 @@
3328
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
3429
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
3530
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
31+
use SebastianBergmann\CodeCoverage\Util\Filesystem;
3632
use SebastianBergmann\CodeCoverage\Util\Filesystem as DirectoryUtil;
33+
use SebastianBergmann\CodeCoverage\Util\Xml;
3734
use SebastianBergmann\CodeCoverage\Version;
3835
use SebastianBergmann\CodeCoverage\XmlException;
3936
use SebastianBergmann\Environment\Runtime;
@@ -269,36 +266,8 @@ private function saveDocument(DOMDocument $document, string $name): void
269266
{
270267
$filename = sprintf('%s/%s.xml', $this->targetDirectory(), $name);
271268

272-
$document->formatOutput = true;
273-
$document->preserveWhiteSpace = false;
274269
$this->initTargetDirectory(dirname($filename));
275270

276-
file_put_contents($filename, $this->documentAsString($document));
277-
}
278-
279-
/**
280-
* @throws XmlException
281-
*
282-
* @see https://bugs.php.net/bug.php?id=79191
283-
*/
284-
private function documentAsString(DOMDocument $document): string
285-
{
286-
$xmlErrorHandling = libxml_use_internal_errors(true);
287-
$xml = $document->saveXML();
288-
289-
if ($xml === false) {
290-
$message = 'Unable to generate the XML';
291-
292-
foreach (libxml_get_errors() as $error) {
293-
$message .= PHP_EOL . $error->message;
294-
}
295-
296-
throw new XmlException($message);
297-
}
298-
299-
libxml_clear_errors();
300-
libxml_use_internal_errors($xmlErrorHandling);
301-
302-
return $xml;
271+
Filesystem::write($filename, Xml::asString($document));
303272
}
304273
}

src/Util/Filesystem.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
*/
1010
namespace SebastianBergmann\CodeCoverage\Util;
1111

12+
use function dirname;
13+
use function file_put_contents;
1214
use function is_dir;
1315
use function mkdir;
1416
use function sprintf;
17+
use function str_contains;
18+
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
1519

1620
/**
1721
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
@@ -34,4 +38,20 @@ public static function createDirectory(string $directory): void
3438
);
3539
}
3640
}
41+
42+
/**
43+
* @param non-empty-string $target
44+
*
45+
* @throws WriteOperationFailedException
46+
*/
47+
public static function write(string $target, string $buffer): void
48+
{
49+
if (!str_contains($target, '://')) {
50+
self::createDirectory(dirname($target));
51+
}
52+
53+
if (@file_put_contents($target, $buffer) === false) {
54+
throw new WriteOperationFailedException($target);
55+
}
56+
}
3757
}

src/Util/Xml.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of phpunit/php-code-coverage.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace SebastianBergmann\CodeCoverage\Util;
11+
12+
use const PHP_EOL;
13+
use function libxml_clear_errors;
14+
use function libxml_get_errors;
15+
use function libxml_use_internal_errors;
16+
use DOMDocument;
17+
use SebastianBergmann\CodeCoverage\XmlException;
18+
19+
/**
20+
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
21+
*/
22+
final readonly class Xml
23+
{
24+
/**
25+
* @throws XmlException
26+
*
27+
* @see https://bugs.php.net/bug.php?id=79191
28+
*/
29+
public static function asString(DOMDocument $document): string
30+
{
31+
$xmlErrorHandling = libxml_use_internal_errors(true);
32+
33+
$document->formatOutput = true;
34+
$document->preserveWhiteSpace = false;
35+
36+
$buffer = $document->saveXML();
37+
38+
if ($buffer === false) {
39+
$message = 'Unable to generate the XML';
40+
41+
foreach (libxml_get_errors() as $error) {
42+
$message .= PHP_EOL . $error->message;
43+
}
44+
45+
throw new XmlException($message);
46+
}
47+
48+
libxml_clear_errors();
49+
libxml_use_internal_errors($xmlErrorHandling);
50+
51+
return $buffer;
52+
}
53+
}

0 commit comments

Comments
 (0)