Skip to content

Commit 356e873

Browse files
committed
refactor: extracted common XML parsing logic
1 parent ea799b1 commit 356e873

15 files changed

Lines changed: 188 additions & 64 deletions

src/Config/ConfigParser.php

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@
44

55
namespace DocbookCS\Config;
66

7+
use DocbookCS\Xml\XmlParser;
8+
79
final class ConfigParser
810
{
911
private const string NAMESPACE_URI = 'https://php.github.io/docbook-cs/config';
1012

13+
public function __construct(private readonly XmlParser $xmlParser = new XmlParser())
14+
{
15+
}
16+
1117
/**
1218
* @throws ConfigParserException if the file cannot be read or contains invalid XML.
1319
* @throws \InvalidArgumentException if a SniffEntry is constructed with an invalid class name.
@@ -19,21 +25,13 @@ public function parseFile(string $filePath): ConfigData
1925
}
2026

2127
$basePath = dirname(realpath($filePath) ?: '');
28+
$xml = $this->xmlParser->parseElement(file_get_contents($filePath) ?: '');
2229

23-
$previousUseErrors = libxml_use_internal_errors(true);
24-
$xml = simplexml_load_string(file_get_contents($filePath) ?: '');
25-
$errors = libxml_get_errors();
26-
libxml_clear_errors();
27-
libxml_use_internal_errors($previousUseErrors);
28-
29-
if ($xml === false) {
30-
$message = $errors !== []
31-
? $errors[0]->message
32-
: 'Unknown parse error'; // @codeCoverageIgnore
33-
throw ConfigParserException::invalidXml($filePath, trim($message));
30+
if ($xml instanceof \LibXMLError) {
31+
throw ConfigParserException::invalidXml($filePath, trim($xml->message));
3432
}
3533

36-
return $this->parse($xml, $basePath);
34+
return $this->createConfigData($xml, $basePath);
3735
}
3836

3937
/**
@@ -42,27 +40,20 @@ public function parseFile(string $filePath): ConfigData
4240
*/
4341
public function parseString(string $xmlContent, string $basePath): ConfigData
4442
{
45-
$previousUseErrors = libxml_use_internal_errors(true);
46-
$xml = simplexml_load_string($xmlContent);
47-
$errors = libxml_get_errors();
48-
libxml_clear_errors();
49-
libxml_use_internal_errors($previousUseErrors);
50-
51-
if ($xml === false) {
52-
$message = $errors !== []
53-
? $errors[0]->message
54-
: 'Unknown parse error'; // @codeCoverageIgnore
55-
throw ConfigParserException::invalidXml('(string)', trim($message));
43+
$xml = $this->xmlParser->parseElement($xmlContent);
44+
45+
if ($xml instanceof \LibXMLError) {
46+
throw ConfigParserException::invalidXml('(string)', trim($xml->message));
5647
}
5748

58-
return $this->parse($xml, $basePath);
49+
return $this->createConfigData($xml, $basePath);
5950
}
6051

6152
/**
6253
* @throws ConfigParserException if the XML is invalid or required elements/attributes are missing.
6354
* @throws \InvalidArgumentException if a SniffEntry is constructed with an invalid class name.
6455
*/
65-
private function parse(\SimpleXMLElement $root, string $basePath): ConfigData
56+
private function createConfigData(\SimpleXMLElement $root, string $basePath): ConfigData
6657
{
6758
// Register the namespace for xpath queries.
6859
$root->registerXPathNamespace('d', self::NAMESPACE_URI);

src/Runner/XmlSniffRunner.php

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use DocbookCS\Sniff\SniffInterface;
1111
use DocbookCS\Source\File;
1212
use DocbookCS\Violation\Violation;
13+
use DocbookCS\Xml\XmlParser;
1314

1415
final readonly class XmlSniffRunner
1516
{
@@ -18,7 +19,8 @@ public function __construct(
1819
private RunMode $mode,
1920
private array $sniffs,
2021
private EntityPreprocessor $preprocessor = new EntityPreprocessor(),
21-
private ViolationScopeFilter $violationFilter = new ViolationScopeFilter()
22+
private XmlParser $xmlParser = new XmlParser(),
23+
private ViolationScopeFilter $violationFilter = new ViolationScopeFilter(),
2224
) {
2325
}
2426

@@ -94,27 +96,6 @@ private function parseXml(File $file): \DOMDocument|\LibXMLError
9496
{
9597
$content = $this->preprocessor->processForParsing($file->content);
9698

97-
$previousUseErrors = libxml_use_internal_errors(true);
98-
libxml_clear_errors();
99-
100-
try {
101-
$document = new \DOMDocument();
102-
$document->preserveWhiteSpace = true;
103-
104-
// LIBXML_NONET prevents network access.
105-
// No LIBXML_DTDLOAD needed since we stripped the DOCTYPE.
106-
$loaded = $document->loadXML($content, LIBXML_NONET);
107-
108-
if ($loaded) {
109-
return $document;
110-
}
111-
112-
$error = libxml_get_errors()[0];
113-
} finally {
114-
libxml_clear_errors();
115-
libxml_use_internal_errors($previousUseErrors);
116-
}
117-
118-
return $error;
99+
return $this->xmlParser->parseDocument($content);
119100
}
120101
}

src/Xml/XmlParser.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DocbookCS\Xml;
6+
7+
final class XmlParser
8+
{
9+
public function parseDocument(string $content): \DOMDocument|\LibXMLError
10+
{
11+
$document = new \DOMDocument();
12+
$document->preserveWhiteSpace = true;
13+
14+
[$loaded, $error] = $this->parseWithErrors(
15+
static fn(): bool => $document->loadXML($content, LIBXML_NONET),
16+
);
17+
18+
if ($loaded) {
19+
return $document;
20+
}
21+
22+
return $this->parseError($error);
23+
}
24+
25+
public function parseElement(string $content): \SimpleXMLElement|\LibXMLError
26+
{
27+
[$element, $error] = $this->parseWithErrors(
28+
static fn(): \SimpleXMLElement|false => simplexml_load_string($content),
29+
);
30+
31+
if ($element !== false) {
32+
return $element;
33+
}
34+
35+
return $this->parseError($error);
36+
}
37+
38+
/**
39+
* @template TResult
40+
* @param callable(): TResult $parser
41+
* @return array{TResult, ?\LibXMLError}
42+
*/
43+
private function parseWithErrors(callable $parser): array
44+
{
45+
$previousUseErrors = libxml_use_internal_errors(true);
46+
libxml_clear_errors();
47+
48+
try {
49+
$result = $parser();
50+
$error = libxml_get_errors()[0] ?? null;
51+
} finally {
52+
libxml_clear_errors();
53+
libxml_use_internal_errors($previousUseErrors);
54+
}
55+
56+
return [$result, $error];
57+
}
58+
59+
private function parseError(?\LibXMLError $error): \LibXMLError
60+
{
61+
$error ??= new \LibXMLError();
62+
$error->message ??= 'Unknown parse error';
63+
$error->line ??= 0;
64+
65+
return $error;
66+
}
67+
}

tests/Support/XmlHelper.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DocbookCS\Tests\Support;
6+
7+
trait XmlHelper
8+
{
9+
private function xml(string $body): string
10+
{
11+
return <<<XML
12+
<?xml version="1.0" encoding="UTF-8"?>
13+
$body
14+
XML;
15+
}
16+
}

tests/Unit/ApplicationInputTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
use DocbookCS\Source\File;
4646
use DocbookCS\Violation\SourceRange;
4747
use DocbookCS\Violation\Violation;
48+
use DocbookCS\Xml\XmlParser;
4849
use PHPUnit\Framework\Attributes\CoversClass;
4950
use PHPUnit\Framework\Attributes\Test;
5051
use PHPUnit\Framework\Attributes\UsesClass;
@@ -92,6 +93,7 @@
9293
UsesClass(ViolationScopeFilter::class),
9394
UsesClass(XmlFileProcessor::class),
9495
UsesClass(XmlFixRunner::class),
96+
UsesClass(XmlParser::class),
9597
UsesClass(XmlSniffRunner::class),
9698
]
9799
final class ApplicationInputTest extends TestCase

tests/Unit/ApplicationTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
use DocbookCS\Runner\XmlSniffRunner;
4242
use DocbookCS\Sniff\ExceptionNameSniff;
4343
use DocbookCS\Source\File;
44+
use DocbookCS\Xml\XmlParser;
4445
use PHPUnit\Framework\Attributes\CoversClass;
4546
use PHPUnit\Framework\Attributes\Test;
4647
use PHPUnit\Framework\Attributes\UsesClass;
@@ -85,6 +86,7 @@
8586
UsesClass(UpstreamResolver::class),
8687
UsesClass(ViolationScopeFilter::class),
8788
UsesClass(XmlFileProcessor::class),
89+
UsesClass(XmlParser::class),
8890
]
8991
final class ApplicationTest extends TestCase
9092
{

tests/Unit/Config/ConfigParserTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@
88
use DocbookCS\Config\ConfigParser;
99
use DocbookCS\Config\ConfigParserException;
1010
use DocbookCS\Config\SniffEntry;
11+
use DocbookCS\Xml\XmlParser;
1112
use PHPUnit\Framework\Attributes\CoversClass;
1213
use PHPUnit\Framework\Attributes\Test;
14+
use PHPUnit\Framework\Attributes\UsesClass;
1315
use PHPUnit\Framework\TestCase;
1416

1517
#[
1618
CoversClass(ConfigData::class),
1719
CoversClass(ConfigParser::class),
1820
CoversClass(ConfigParserException::class),
1921
CoversClass(SniffEntry::class),
22+
//
23+
UsesClass(XmlParser::class),
2024
]
2125
final class ConfigParserTest extends TestCase
2226
{

tests/Unit/Runner/FixConvergenceTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use DocbookCS\Tests\Support\Fix\ToggleElementFixer;
3434
use DocbookCS\Violation\SourceRange;
3535
use DocbookCS\Violation\Violation;
36+
use DocbookCS\Xml\XmlParser;
3637
use PHPUnit\Framework\Attributes\CoversClass;
3738
use PHPUnit\Framework\Attributes\Test;
3839
use PHPUnit\Framework\Attributes\UsesClass;
@@ -65,6 +66,7 @@
6566
UsesClass(SourceRange::class),
6667
UsesClass(Violation::class),
6768
UsesClass(ViolationScopeFilter::class),
69+
UsesClass(XmlParser::class),
6870
]
6971
final class FixConvergenceTest extends TestCase
7072
{

tests/Unit/Runner/RunCoordinatorFileFailureTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
use DocbookCS\Source\File;
4444
use DocbookCS\Violation\SourceRange;
4545
use DocbookCS\Violation\Violation;
46+
use DocbookCS\Xml\XmlParser;
4647
use PHPUnit\Framework\Attributes\CoversClass;
4748
use PHPUnit\Framework\Attributes\Test;
4849
use PHPUnit\Framework\Attributes\UsesClass;
@@ -87,6 +88,7 @@
8788
UsesClass(ViolationScopeFilter::class),
8889
UsesClass(XmlFileProcessor::class),
8990
UsesClass(XmlFixRunner::class),
91+
UsesClass(XmlParser::class),
9092
UsesClass(XmlSniffRunner::class),
9193
]
9294
final class RunCoordinatorFileFailureTest extends TestCase

tests/Unit/Runner/RunScopeTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use DocbookCS\Source\Line;
4343
use DocbookCS\Violation\SourceRange;
4444
use DocbookCS\Violation\Violation;
45+
use DocbookCS\Xml\XmlParser;
4546
use PHPUnit\Framework\Attributes\CoversClass;
4647
use PHPUnit\Framework\Attributes\Test;
4748
use PHPUnit\Framework\Attributes\UsesClass;
@@ -86,6 +87,7 @@
8687
UsesClass(ViolationScopeFilter::class),
8788
UsesClass(XmlFileProcessor::class),
8889
UsesClass(XmlFixRunner::class),
90+
UsesClass(XmlParser::class),
8991
UsesClass(XmlSniffRunner::class),
9092
]
9193
final class RunScopeTest extends TestCase

0 commit comments

Comments
 (0)