Skip to content

Commit c7f30db

Browse files
authored
feat: add PHPDoc check (#182)
1 parent a14f2eb commit c7f30db

File tree

10 files changed

+201
-16
lines changed

10 files changed

+201
-16
lines changed

.dagger/src/TestsGotenbergBundle.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ public function phpCsFixer(): string
105105
;
106106
}
107107

108+
#[DaggerFunction]
109+
#[Doc('Validate all URL docs.')]
110+
public function validateUrlDoc(): string
111+
{
112+
return $this->symfonyContainer
113+
->withExec(['php', './docs/ValidateUrlDoc.php'])
114+
->stdout()
115+
;
116+
}
117+
108118
#[DaggerFunction]
109119
#[Doc('Run all tests.')]
110120
#[ReturnsListOfType('string')]

.github/workflows/url-test.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: 'Doc URLS test'
2+
3+
on:
4+
push:
5+
branches: [ 'main', '1.x' ]
6+
pull_request:
7+
branches: [ 'main', '1.x' ]
8+
schedule:
9+
- cron: '0 1 1 * *'
10+
11+
jobs:
12+
13+
check:
14+
name: 'Validate URL docs'
15+
runs-on: 'ubuntu-latest'
16+
steps:
17+
- uses: 'actions/checkout@v4'
18+
19+
- name: 'Validate URLS into PhpDoc'
20+
uses: 'dagger/dagger-for-github@8.0.0'
21+
with:
22+
version: 'latest'
23+
call: 'test validate-url-doc'
24+
dagger-flags: '--silent'
25+
env:
26+
DAGGER_NO_NAG: '1'

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
"phpunit/phpunit": "^10.5.46",
4848
"shipmonk/composer-dependency-analyser": "^1.8",
4949
"symfony/asset": "^6.4 || ^7.0 || ^8.0",
50+
"symfony/css-selector": "^6.4 || ^7.0 || ^8.0",
51+
"symfony/dom-crawler": "^6.4 || ^7.0 || ^8.0",
5052
"symfony/error-handler": "^6.4 || ^7.0 || ^8.0",
5153
"symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0",
5254
"symfony/http-client": "^6.4 || ^7.0 || ^8.0",

docs/ValidateUrlDoc.php

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
use Symfony\Component\Console\Application;
5+
use Symfony\Component\Console\Command\Command;
6+
use Symfony\Component\Console\Helper\ProgressIndicator;
7+
use Symfony\Component\Console\Output\OutputInterface;
8+
use Symfony\Component\Console\Style\SymfonyStyle;
9+
use Symfony\Component\DomCrawler\Crawler;
10+
use Symfony\Component\HttpClient\HttpClient;
11+
12+
require_once \dirname(__DIR__).'/vendor/autoload.php';
13+
14+
class ValidateUrlDoc
15+
{
16+
private const AVAILABLE_EXTENSIONS = ['php', 'md'];
17+
18+
private const DIR_AND_FILES_TO_CHECK = [
19+
__DIR__.'/../src/Builder/Pdf',
20+
__DIR__.'/../src/Builder/Screenshot',
21+
__DIR__.'/../src/Builder/Behaviors',
22+
__DIR__,
23+
__DIR__.'/../README.md',
24+
__DIR__.'/../src/DependencyInjection/Configuration.php',
25+
];
26+
27+
public function validate(OutputInterface $output, SymfonyStyle $io): int
28+
{
29+
$progressBar = new ProgressIndicator($output);
30+
$progressBar->start('Processing...');
31+
32+
$urls = [];
33+
$files = $this->getFiles(self::DIR_AND_FILES_TO_CHECK);
34+
foreach ($files as $file) {
35+
array_push($urls, ...$this->extractUrls($file));
36+
}
37+
38+
$urls = array_unique($urls);
39+
40+
$client = HttpClient::create();
41+
$allResponses = [];
42+
foreach ($urls as $url) {
43+
$allResponses[] = $client->request('GET', $url);
44+
}
45+
46+
$failedUrls = [];
47+
foreach ($allResponses as $response) {
48+
try {
49+
$url = $response->getInfo('url');
50+
51+
$statusCode = $response->getStatusCode();
52+
if (200 !== $statusCode) {
53+
$failedUrls[$url] = "HTTP {$statusCode} error for: {$url}";
54+
}
55+
56+
if (!\array_key_exists($url, $failedUrls)) {
57+
$checkError = $this->checkContentResponse($response->getInfo('url'), $response->getContent());
58+
if (\is_string($checkError)) {
59+
$failedUrls[$url] = $checkError;
60+
}
61+
}
62+
} catch (Throwable $e) {
63+
$url = $response->getInfo('url');
64+
$failedUrls[$url] = "Error for {$url}: {$e->getMessage()}";
65+
} finally {
66+
$progressBar->advance();
67+
}
68+
}
69+
70+
$progressBar->finish('Finished');
71+
$failedUrls = array_unique($failedUrls);
72+
73+
if (\count($failedUrls) > 0) {
74+
$io->error('Some external links are invalid:');
75+
$io->listing($failedUrls);
76+
77+
return Command::FAILURE;
78+
}
79+
80+
$io->success('All external links are valid.');
81+
82+
return Command::SUCCESS;
83+
}
84+
85+
private function getFiles(array $paths): Generator
86+
{
87+
foreach ($paths as $path) {
88+
if (is_file($path)) {
89+
$splInfo = new SplFileInfo($path);
90+
if (!\in_array($splInfo->getExtension(), self::AVAILABLE_EXTENSIONS, true)) {
91+
throw new RuntimeException(\sprintf('File "%s" with "%s" extension is not allowed.', $splInfo->getFilename(), $splInfo->getExtension()));
92+
}
93+
94+
yield $splInfo;
95+
continue;
96+
}
97+
98+
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));
99+
foreach ($iterator as $file) {
100+
if ($file->isFile() && \in_array(pathinfo($file, \PATHINFO_EXTENSION), self::AVAILABLE_EXTENSIONS, true)) {
101+
yield $file;
102+
}
103+
}
104+
}
105+
}
106+
107+
private function extractUrls(SplFileInfo $file): array
108+
{
109+
$content = file_get_contents($file->getPathname());
110+
111+
// https://regex101.com/r/t5uiUp/1
112+
// https://regex101.com/r/p62cij/1
113+
match ($file->getExtension()) {
114+
'php' => preg_match_all('/(?:@see\s+|->info\([^)]*)(https?:\/\/[^\s\'")]+)(?:[^)]*\))?/', $content, $matches),
115+
'md' => preg_match_all('/\[[^\]]+\]\((https?:\/\/[^\s\*)]+)\)/', $content, $matches),
116+
};
117+
118+
return $matches[1] ?? [];
119+
}
120+
121+
private function checkContentResponse(string $url, string $content): string|null
122+
{
123+
$crawler = new Crawler($content);
124+
$parsedUrl = parse_url($url);
125+
126+
if (\array_key_exists('fragment', $parsedUrl)) {
127+
$fragment = $parsedUrl['fragment'];
128+
if ($crawler->filter('#'.$fragment)->count() > 0 || $crawler->filter('a[name="'.$fragment.'"]')->count() > 0) {
129+
return null;
130+
}
131+
132+
return \sprintf('Cannot find anchor "%s" for "%s", remove or update the link in the PHPdoc', $fragment, $url);
133+
}
134+
135+
return null;
136+
}
137+
}
138+
139+
$application = new Application();
140+
$application->register('check')
141+
->setCode(function (OutputInterface $output, SymfonyStyle $io) {
142+
return (new ValidateUrlDoc())->validate($output, $io);
143+
})
144+
;
145+
146+
$application->setDefaultCommand('check');
147+
$application->run();

docs/configuration.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ sensiolabs_gotenberg:
183183
# Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
184184
user_agent: null
185185

186-
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers
186+
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
187187
extra_http_headers:
188188

189189
# Prototype
@@ -386,7 +386,7 @@ sensiolabs_gotenberg:
386386
# Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
387387
user_agent: null
388388

389-
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers
389+
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
390390
extra_http_headers: []
391391

392392
# Example:
@@ -588,7 +588,7 @@ sensiolabs_gotenberg:
588588
# Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
589589
user_agent: null
590590

591-
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers
591+
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
592592
extra_http_headers: []
593593

594594
# Example:
@@ -775,16 +775,16 @@ sensiolabs_gotenberg:
775775
# Specify that a stream is inserted to the PDF file which contains the original document for archiving purposes. - default false. https://gotenberg.dev/docs/routes#page-properties-libreoffice
776776
add_original_document_as_stream: null
777777

778-
# Specify if images are exported to PDF using a lossless compression format like PNG or compressed using the JPEG format. - default false. https://gotenberg.dev/docs/routes#images-libreoffice
778+
# Specify if images are exported to PDF using a lossless compression format like PNG or compressed using the JPEG format. - default false. https://gotenberg.dev/docs/routes#compress-libreoffice
779779
lossless_image_compression: null
780780

781-
# Specify the quality of the JPG export. A higher value produces a higher-quality image and a larger file. Between 1 and 100. - default 90. https://gotenberg.dev/docs/routes#images-libreoffice
781+
# Specify the quality of the JPG export. A higher value produces a higher-quality image and a larger file. Between 1 and 100. - default 90. https://gotenberg.dev/docs/routes#compress-libreoffice
782782
quality: null
783783

784-
# Specify if the resolution of each image is reduced to the resolution specified by the form field maxImageResolution. - default false. https://gotenberg.dev/docs/routes#images-libreoffice
784+
# Specify if the resolution of each image is reduced to the resolution specified by the form field maxImageResolution. - default false. https://gotenberg.dev/docs/routes#compress-libreoffice
785785
reduce_image_resolution: null
786786

787-
# If the form field reduceImageResolution is set to true, tell if all images will be reduced to the given value in DPI. Possible values are: 75, 150, 300, 600 and 1200. - default 300. https://gotenberg.dev/docs/routes#images-libreoffice
787+
# If the form field reduceImageResolution is set to true, tell if all images will be reduced to the given value in DPI. Possible values are: 75, 150, 300, 600 and 1200. - default 300. https://gotenberg.dev/docs/routes#compress-libreoffice
788788
max_image_resolution: null # One of 75; 150; 300; 600; 1200
789789

790790
# URLs to download files from (JSON format). - default None. https://gotenberg.dev/docs/routes#download-from
@@ -972,7 +972,7 @@ sensiolabs_gotenberg:
972972
# Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
973973
user_agent: null
974974

975-
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers
975+
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
976976
extra_http_headers: []
977977

978978
# Example:
@@ -1099,7 +1099,7 @@ sensiolabs_gotenberg:
10991099
# Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
11001100
user_agent: null
11011101

1102-
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers
1102+
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
11031103
extra_http_headers: []
11041104

11051105
# Example:
@@ -1226,7 +1226,7 @@ sensiolabs_gotenberg:
12261226
# Override the default User-Agent HTTP header. - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
12271227
user_agent: null
12281228

1229-
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers
1229+
# HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium
12301230
extra_http_headers: []
12311231

12321232
# Example:
@@ -1370,7 +1370,7 @@ sensiolabs_gotenberg:
13701370
```
13711371
13721372
> [!TIP]
1373-
> For more information about [custom HTTP headers](https://gotenberg.dev/docs/routes#custom-http-headers) & [webhook custom HTTP headers](https://gotenberg.dev/docs/configuration#webhook).
1373+
> For more information about [custom HTTP headers](https://gotenberg.dev/docs/routes#custom-http-headers-chromium) & [webhook custom HTTP headers](https://gotenberg.dev/docs/configuration#webhook).
13741374
13751375
## Invalid HTTP Status Codes
13761376

docs/screenshot/HtmlScreenshotBuilder.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ return $gotenberg
371371
The compression quality from range 0 to 100 (jpeg only). (default 100).<br />
372372

373373
> [!TIP]
374-
> See: [https://gotenberg.dev/docs/routes#screenshots-rout](https://gotenberg.dev/docs/routes#screenshots-rout)
374+
> See: [https://gotenberg.dev/docs/routes#screenshots-route](https://gotenberg.dev/docs/routes#screenshots-route)
375375
376376
```php
377377
return $gotenberg

docs/screenshot/MarkdownScreenshotBuilder.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ return $gotenberg
462462
The compression quality from range 0 to 100 (jpeg only). (default 100).<br />
463463

464464
> [!TIP]
465-
> See: [https://gotenberg.dev/docs/routes#screenshots-rout](https://gotenberg.dev/docs/routes#screenshots-rout)
465+
> See: [https://gotenberg.dev/docs/routes#screenshots-route](https://gotenberg.dev/docs/routes#screenshots-route)
466466
467467
```php
468468
return $gotenberg

docs/screenshot/UrlScreenshotBuilder.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ return $gotenberg
389389
The compression quality from range 0 to 100 (jpeg only). (default 100).<br />
390390

391391
> [!TIP]
392-
> See: [https://gotenberg.dev/docs/routes#screenshots-rout](https://gotenberg.dev/docs/routes#screenshots-rout)
392+
> See: [https://gotenberg.dev/docs/routes#screenshots-route](https://gotenberg.dev/docs/routes#screenshots-route)
393393
394394
```php
395395
return $gotenberg

src/Builder/Behaviors/Chromium/ScreenshotPagePropertiesTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public function format(ScreenshotFormat $format): static
8989
*
9090
* @param int<0, 100> $quality
9191
*
92-
* @see https://gotenberg.dev/docs/routes#screenshots-rout
92+
* @see https://gotenberg.dev/docs/routes#screenshots-route
9393
*
9494
* @example quality(50)
9595
*/

src/DependencyInjection/Configuration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ private function addExtraHttpHeadersNode(): NodeDefinition
178178
$treeBuilder = new TreeBuilder('extra_http_headers');
179179

180180
return $treeBuilder->getRootNode()
181-
->info('HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers')
181+
->info('HTTP headers to send by Chromium while loading the HTML document - default None. https://gotenberg.dev/docs/routes#custom-http-headers-chromium')
182182
->defaultValue([])
183183
->normalizeKeys(false)
184184
->useAttributeAsKey('name')

0 commit comments

Comments
 (0)