Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 133 additions & 2 deletions bin/guides
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,7 +1,138 @@
#!/usr/bin/env php
<?php

declare(strict_types=1);

use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
use Symfony\Component\Console\Input\ArgvInput;

$root = dirname(__DIR__);

require_once $root . '/vendor/autoload.php';
require_once $root . '/vendor/phpdocumentor/guides-cli/bin/guides';
require_once $root . '/vendor/autoload.php';

/**
* Validates a guides.xml file against the XSD schema.
*
* @return array{valid: bool, errors: string[]}
*/
function validateGuidesXml(string $xmlPath, string $xsdPath): array
{
$errors = [];

if (!file_exists($xmlPath)) {
return ['valid' => true, 'errors' => []];
}

if (!file_exists($xsdPath)) {
// XSD not found, skip validation
return ['valid' => true, 'errors' => []];
}

libxml_use_internal_errors(true);

$dom = new DOMDocument();
if (!$dom->load($xmlPath)) {
$errors[] = sprintf('Failed to load %s as XML', $xmlPath);
foreach (libxml_get_errors() as $error) {
$errors[] = sprintf(' Line %d, Column %d: %s', $error->line, $error->column, trim($error->message));
}
libxml_clear_errors();
return ['valid' => false, 'errors' => $errors];
}

if (!$dom->schemaValidate($xsdPath)) {
$errors[] = sprintf('Schema validation failed for %s', $xmlPath);
foreach (libxml_get_errors() as $error) {
$errors[] = sprintf(' Line %d, Column %d: %s', $error->line, $error->column, trim($error->message));
}
libxml_clear_errors();
return ['valid' => false, 'errors' => $errors];
}

libxml_clear_errors();
return ['valid' => true, 'errors' => []];
}

/**
* Outputs an error message to STDERR with formatting.
*/
function outputError(string $message, bool $isHeader = false): void
{
if ($isHeader) {
fwrite(STDERR, "\033[37;41m " . $message . " \033[0m\n");
} else {
fwrite(STDERR, $message . "\n");
}
}

// Determine which guides.xml files will be loaded (mirrors upstream logic)
$input = new ArgvInput();
$vendorDir = $root . '/vendor';
$xsdPath = $vendorDir . '/phpdocumentor/guides-cli/resources/schema/guides.xsd';

$configFiles = [];

// Project-level config
$projectConfig = $vendorDir . '/../guides.xml';
if (is_file($projectConfig)) {
$realPath = realpath($projectConfig);
if ($realPath !== false) {
$configFiles[] = $realPath;
}
}

// Local config (from --config or working directory)
$workingDir = $input->getParameterOption(['--working-dir', '-w'], getcwd(), true);
$localConfigDir = $input->getParameterOption(['--config', '-c'], $workingDir, true);
$localConfig = $localConfigDir . '/guides.xml';

if (is_file($localConfig)) {
$realLocalConfig = realpath($localConfig);
$realProjectConfig = isset($realPath) ? $realPath : null;
if ($realLocalConfig !== false && $realLocalConfig !== $realProjectConfig) {
$configFiles[] = $realLocalConfig;
}
}

// Validate all config files against XSD
$hasValidationErrors = false;
$allErrors = [];

foreach ($configFiles as $configFile) {
$result = validateGuidesXml($configFile, $xsdPath);
if (!$result['valid']) {
$hasValidationErrors = true;
$allErrors = array_merge($allErrors, $result['errors']);
}
}

if ($hasValidationErrors) {
outputError('Invalid guides.xml configuration', true);
fwrite(STDERR, "\n");
fwrite(STDERR, "Your guides.xml file failed XSD schema validation:\n");
fwrite(STDERR, "\n");
foreach ($allErrors as $error) {
fwrite(STDERR, "\033[33m" . $error . "\033[0m\n");
}
fwrite(STDERR, "\n");
fwrite(STDERR, "See: https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/GeneralConventions/GuideXml.html\n");
fwrite(STDERR, "\n");
exit(1);
}

// Validation passed, proceed with normal execution
// Keep a fallback catch for any edge cases not covered by XSD
try {
require_once $root . '/vendor/phpdocumentor/guides-cli/bin/guides';
} catch (InvalidTypeException|InvalidConfigurationException $e) {
outputError('Invalid guides.xml configuration', true);
fwrite(STDERR, "\n");
fwrite(STDERR, "Your guides.xml file contains a configuration error:\n");
fwrite(STDERR, " \033[33m" . $e->getMessage() . "\033[0m\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "Run \033[33mvendor/bin/typo3-guides lint-guides-xml\033[0m for detailed validation.\n");
fwrite(STDERR, "See: https://docs.typo3.org/m/typo3/docs-how-to-document/main/en-us/GeneralConventions/GuideXml.html\n");
fwrite(STDERR, "\n");
exit(1);
}
119 changes: 119 additions & 0 deletions tests/Integration/InvalidGuidesXmlTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

declare(strict_types=1);

namespace T3Docs\Typo3DocsTheme\Integration;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Process;

/**
* Tests error handling for invalid guides.xml configurations.
*
* The fixture is stored as guides.xml.fixture to avoid being picked up by lint-guides-xml.
* Tests copy it to a temp directory with the correct name before execution.
*/
final class InvalidGuidesXmlTest extends TestCase
{
private const FIXTURE_SOURCE = __DIR__ . '/../fixtures/invalid-guides-xml';

private string $tempDir = '';

protected function setUp(): void
{
$this->tempDir = sys_get_temp_dir() . '/render-guides-invalid-test-' . uniqid();
mkdir($this->tempDir, 0755, true);

// Copy fixture files to temp directory
copy(self::FIXTURE_SOURCE . '/guides.xml.fixture', $this->tempDir . '/guides.xml');
copy(self::FIXTURE_SOURCE . '/Index.rst', $this->tempDir . '/Index.rst');
}

protected function tearDown(): void
{
if (is_dir($this->tempDir)) {
system('rm -rf ' . escapeshellarg($this->tempDir));
}
}

public function testInvalidGuidesXmlShowsHelpfulErrorMessage(): void
{
$binPath = dirname(__DIR__, 2) . '/bin/guides';

$process = new Process([
'php',
$binPath,
'run',
'--config=' . $this->tempDir,
$this->tempDir,
]);

$process->run();

// Should fail with exit code 1, not crash with fatal error
self::assertSame(1, $process->getExitCode(), 'Expected exit code 1 for invalid guides.xml');

$stderr = $process->getErrorOutput();

// Should contain helpful error message, not PHP fatal error
self::assertStringContainsString('Invalid guides.xml configuration', $stderr);
self::assertStringNotContainsString('PHP Fatal error', $stderr);
self::assertStringNotContainsString('Stack trace', $stderr);

// Should reference documentation
self::assertStringContainsString('https://docs.typo3.org', $stderr);
}

public function testInvalidGuidesXmlShowsLineNumber(): void
{
$binPath = dirname(__DIR__, 2) . '/bin/guides';

$process = new Process([
'php',
$binPath,
'run',
'--config=' . $this->tempDir,
$this->tempDir,
]);

$process->run();

$stderr = $process->getErrorOutput();

// Should show XSD validation error with line number
self::assertStringContainsString('Line 3', $stderr);
self::assertStringContainsString('theme', $stderr);
}

public function testValidGuidesXmlRendersSuccessfully(): void
{
$binPath = dirname(__DIR__, 2) . '/bin/guides';
$validFixturePath = __DIR__ . '/tests/getting-started/input';

// Skip if fixture doesn't exist
if (!is_dir($validFixturePath)) {
self::markTestSkipped('Valid fixture not available');
}

$outputPath = sys_get_temp_dir() . '/render-guides-test-' . uniqid();

$process = new Process([
'php',
$binPath,
'run',
'--config=' . $validFixturePath,
'--output=' . $outputPath,
$validFixturePath,
]);

$process->run();

// Clean up
if (is_dir($outputPath)) {
system('rm -rf ' . escapeshellarg($outputPath));
}

// Should succeed
self::assertSame(0, $process->getExitCode(), 'Expected exit code 0 for valid guides.xml. Error: ' . $process->getErrorOutput());
}
}
5 changes: 5 additions & 0 deletions tests/fixtures/invalid-guides-xml/Index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
=====
Test
=====

This should not render due to invalid guides.xml.
5 changes: 5 additions & 0 deletions tests/fixtures/invalid-guides-xml/guides.xml.fixture
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<guides xmlns="https://www.phpdoc.org/guides">
<theme name="typo3docs" />
<project title="Test" />
</guides>