Skip to content

Commit bb9a162

Browse files
committed
[BUGFIX] Validate guides.xml against XSD schema before loading
This adds proper XSD schema validation before the configuration is loaded into Symfony's container. When validation fails, users now see precise error messages with line and column numbers instead of cryptic PHP fatal errors. Example output for invalid guides.xml: Invalid guides.xml configuration Your guides.xml file failed XSD schema validation: Schema validation failed for Documentation/guides.xml Line 3, Column 0: Element 'theme': This element is not expected. The fix: - Validates all guides.xml files against the XSD schema upfront - Shows exact file path, line number, and column for each error - Links to official documentation for correct format - Keeps a fallback catch for any edge cases not covered by XSD This provides much better developer experience compared to the previous "Expected scalar, but got array" fatal error.
1 parent 449f275 commit bb9a162

File tree

1 file changed

+133
-2
lines changed

1 file changed

+133
-2
lines changed

bin/guides

100644100755
Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,138 @@
11
#!/usr/bin/env php
22
<?php
33

4+
declare(strict_types=1);
5+
6+
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
7+
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
8+
use Symfony\Component\Console\Input\ArgvInput;
9+
410
$root = dirname(__DIR__);
511

6-
require_once $root . '/vendor/autoload.php';
7-
require_once $root . '/vendor/phpdocumentor/guides-cli/bin/guides';
12+
require_once $root . '/vendor/autoload.php';
13+
14+
/**
15+
* Validates a guides.xml file against the XSD schema.
16+
*
17+
* @return array{valid: bool, errors: string[]}
18+
*/
19+
function validateGuidesXml(string $xmlPath, string $xsdPath): array
20+
{
21+
$errors = [];
22+
23+
if (!file_exists($xmlPath)) {
24+
return ['valid' => true, 'errors' => []];
25+
}
26+
27+
if (!file_exists($xsdPath)) {
28+
// XSD not found, skip validation
29+
return ['valid' => true, 'errors' => []];
30+
}
31+
32+
libxml_use_internal_errors(true);
33+
34+
$dom = new DOMDocument();
35+
if (!$dom->load($xmlPath)) {
36+
$errors[] = sprintf('Failed to load %s as XML', $xmlPath);
37+
foreach (libxml_get_errors() as $error) {
38+
$errors[] = sprintf(' Line %d, Column %d: %s', $error->line, $error->column, trim($error->message));
39+
}
40+
libxml_clear_errors();
41+
return ['valid' => false, 'errors' => $errors];
42+
}
43+
44+
if (!$dom->schemaValidate($xsdPath)) {
45+
$errors[] = sprintf('Schema validation failed for %s', $xmlPath);
46+
foreach (libxml_get_errors() as $error) {
47+
$errors[] = sprintf(' Line %d, Column %d: %s', $error->line, $error->column, trim($error->message));
48+
}
49+
libxml_clear_errors();
50+
return ['valid' => false, 'errors' => $errors];
51+
}
52+
53+
libxml_clear_errors();
54+
return ['valid' => true, 'errors' => []];
55+
}
56+
57+
/**
58+
* Outputs an error message to STDERR with formatting.
59+
*/
60+
function outputError(string $message, bool $isHeader = false): void
61+
{
62+
if ($isHeader) {
63+
fwrite(STDERR, "\033[37;41m " . $message . " \033[0m\n");
64+
} else {
65+
fwrite(STDERR, $message . "\n");
66+
}
67+
}
68+
69+
// Determine which guides.xml files will be loaded (mirrors upstream logic)
70+
$input = new ArgvInput();
71+
$vendorDir = $root . '/vendor';
72+
$xsdPath = $vendorDir . '/phpdocumentor/guides-cli/resources/schema/guides.xsd';
73+
74+
$configFiles = [];
75+
76+
// Project-level config
77+
$projectConfig = $vendorDir . '/../guides.xml';
78+
if (is_file($projectConfig)) {
79+
$realPath = realpath($projectConfig);
80+
if ($realPath !== false) {
81+
$configFiles[] = $realPath;
82+
}
83+
}
84+
85+
// Local config (from --config or working directory)
86+
$workingDir = $input->getParameterOption(['--working-dir', '-w'], getcwd(), true);
87+
$localConfigDir = $input->getParameterOption(['--config', '-c'], $workingDir, true);
88+
$localConfig = $localConfigDir . '/guides.xml';
89+
90+
if (is_file($localConfig)) {
91+
$realLocalConfig = realpath($localConfig);
92+
$realProjectConfig = isset($realPath) ? $realPath : null;
93+
if ($realLocalConfig !== false && $realLocalConfig !== $realProjectConfig) {
94+
$configFiles[] = $realLocalConfig;
95+
}
96+
}
97+
98+
// Validate all config files against XSD
99+
$hasValidationErrors = false;
100+
$allErrors = [];
101+
102+
foreach ($configFiles as $configFile) {
103+
$result = validateGuidesXml($configFile, $xsdPath);
104+
if (!$result['valid']) {
105+
$hasValidationErrors = true;
106+
$allErrors = array_merge($allErrors, $result['errors']);
107+
}
108+
}
109+
110+
if ($hasValidationErrors) {
111+
outputError('Invalid guides.xml configuration', true);
112+
fwrite(STDERR, "\n");
113+
fwrite(STDERR, "Your guides.xml file failed XSD schema validation:\n");
114+
fwrite(STDERR, "\n");
115+
foreach ($allErrors as $error) {
116+
fwrite(STDERR, "\033[33m" . $error . "\033[0m\n");
117+
}
118+
fwrite(STDERR, "\n");
119+
fwrite(STDERR, "See: https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/GeneralConventions/GuideXml.html\n");
120+
fwrite(STDERR, "\n");
121+
exit(1);
122+
}
123+
124+
// Validation passed, proceed with normal execution
125+
// Keep a fallback catch for any edge cases not covered by XSD
126+
try {
127+
require_once $root . '/vendor/phpdocumentor/guides-cli/bin/guides';
128+
} catch (InvalidTypeException|InvalidConfigurationException $e) {
129+
outputError('Invalid guides.xml configuration', true);
130+
fwrite(STDERR, "\n");
131+
fwrite(STDERR, "Your guides.xml file contains a configuration error:\n");
132+
fwrite(STDERR, " \033[33m" . $e->getMessage() . "\033[0m\n");
133+
fwrite(STDERR, "\n");
134+
fwrite(STDERR, "Run \033[33mvendor/bin/typo3-guides lint-guides-xml\033[0m for detailed validation.\n");
135+
fwrite(STDERR, "See: https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/GeneralConventions/GuideXml.html\n");
136+
fwrite(STDERR, "\n");
137+
exit(1);
138+
}

0 commit comments

Comments
 (0)