From a92efdba0c90643c2fe156eab3427177827d86fd Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 4 Nov 2026 23:12:40 +0800 Subject: [PATCH] docs: lint the PHP samples in the user guide --- .github/workflows/test-coding-standards.yml | 2 + .php-cs-fixer.dist.php | 1 + .php-cs-fixer.user-guide.php | 33 ++++ admin/lint-docs-samples.php | 205 ++++++++++++++++++++ composer.json | 6 +- contributing/documentation.md | 8 +- docs/database/query_builder.md | 1 + docs/installation/upgrade_configuration.md | 1 + docs/installation/upgrade_controllers.md | 1 + docs/installation/upgrade_database.md | 1 + docs/installation/upgrade_file_upload.md | 1 + docs/installation/upgrade_images.md | 1 + docs/installation/upgrade_migrations.md | 1 + docs/installation/upgrade_models.md | 1 + docs/installation/upgrade_pagination.md | 1 + docs/installation/upgrade_responses.md | 1 + docs/installation/upgrade_routing.md | 1 + docs/installation/upgrade_security.md | 1 + docs/installation/upgrade_validations.md | 1 + docs/installation/upgrade_view_parser.md | 1 + docs/libraries/sessions.md | 1 + 21 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 .php-cs-fixer.user-guide.php create mode 100644 admin/lint-docs-samples.php diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 274cc5311e2a..36043268a1d3 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -4,11 +4,13 @@ on: push: paths: - '**.php' + - 'docs/**.md' - 'spark' - '.github/workflows/test-coding-standards.yml' pull_request: paths: - '**.php' + - 'docs/**.md' - 'spark' - '.github/workflows/test-coding-standards.yml' diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 227d477c379d..ad6ffacb8f0c 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -30,6 +30,7 @@ __FILE__, __DIR__ . '/.php-cs-fixer.no-header.php', __DIR__ . '/.php-cs-fixer.tests.php', + __DIR__ . '/.php-cs-fixer.user-guide.php', __DIR__ . '/preload.php', __DIR__ . '/rector.php', __DIR__ . '/spark', diff --git a/.php-cs-fixer.user-guide.php b/.php-cs-fixer.user-guide.php new file mode 100644 index 000000000000..1312c8b1344c --- /dev/null +++ b/.php-cs-fixer.user-guide.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use PhpCsFixer\ConfigInterface; + +/** @var ConfigInterface $config */ +$config = require __DIR__ . '/.php-cs-fixer.dist.php'; + +$overrides = [ + 'echo_tag_syntax' => false, + 'header_comment' => false, + 'php_unit_internal_class' => false, + 'no_unused_imports' => false, + 'class_attributes_separation' => false, + 'fully_qualified_strict_types' => [ + 'import_symbols' => false, + 'leading_backslash_in_global_namespace' => true, + ], +]; + +return $config + ->setCacheFile('build/.php-cs-fixer.user-guide.cache') + ->setRules(array_merge($config->getRules(), $overrides)); diff --git a/admin/lint-docs-samples.php b/admin/lint-docs-samples.php new file mode 100644 index 000000000000..0b9d6a695403 --- /dev/null +++ b/admin/lint-docs-samples.php @@ -0,0 +1,205 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +/** + * Checks the code style of the PHP samples in the user guide. + * + * Every ```php fence that starts with an opening tag and parses is run through + * PHP CS Fixer with .php-cs-fixer.user-guide.php. A fence directly below a + * `` line is skipped. With --fix, the fences are + * rewritten in place. Otherwise, nothing is changed, and the script exits + * with 1 when a sample needs fixing. + * + * Usage: php admin/lint-docs-samples.php [--fix] [--docs=] + */ + +/** + * @return list + */ +function docs_pages(string $docs): array +{ + $pages = []; + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($docs, FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS), + ); + + /** @var SplFileInfo $file */ + foreach ($iterator as $file) { + $path = $file->getPathname(); + + if ($file->getExtension() === 'md' && ! str_contains($path, '/overrides/')) { + $pages[] = $path; + } + } + + sort($pages); + + return $pages; +} + +function parses(string $code): bool +{ + try { + token_get_all($code, TOKEN_PARSE); + } catch (ParseError) { + return false; + } + + return true; +} + +/** + * @param list $lines + * + * @return list + */ +function find_samples(array $lines): array +{ + $samples = []; + $count = count($lines); + + for ($i = 0; $i < $count; $i++) { + if (preg_match('/^(\s*)```php\s*$/', $lines[$i], $opening) !== 1) { + continue; + } + + $indent = $opening[1]; + $skip = $i > 0 && trim($lines[$i - 1]) === ''; + + for ($end = $i + 1; $end < $count; $end++) { + if (rtrim($lines[$end]) === $indent . '```') { + break; + } + } + + $body = array_map( + static fn (string $line): string => str_starts_with($line, $indent) + ? substr($line, strlen($indent)) + : ltrim($line), + array_slice($lines, $i + 1, $end - $i - 1), + ); + $code = implode("\n", $body) . "\n"; + + if (! $skip && str_starts_with(ltrim($code), ' $i + 1, 'end' => $end, 'indent' => $indent, 'code' => $code]; + } + + $i = $end; + } + + return $samples; +} + +function remove_directory(string $directory): void +{ + foreach (glob($directory . '/*') ?: [] as $file) { + unlink($file); + } + + rmdir($directory); +} + +$options = getopt('', ['fix', 'docs::']); +$fix = isset($options['fix']); +$root = dirname(__DIR__); +$docs = rtrim(is_string($options['docs'] ?? null) ? $options['docs'] : $root . '/docs', '/'); + +$found = []; +$files = []; + +foreach (docs_pages($docs) as $page) { + $lines = explode("\n", (string) file_get_contents($page)); + + foreach (find_samples($lines) as $sample) { + $found[] = ['page' => $page, 'lines' => $lines] + $sample; + } +} + +$work = sys_get_temp_dir() . '/docs-samples-' . getmypid(); +mkdir($work); + +foreach ($found as $id => $sample) { + $files[$id] = sprintf('%s/%05d.php', $work, $id); + file_put_contents($files[$id], $sample['code']); +} + +$command = sprintf( + '%s %s %s --verbose --ansi --using-cache=no --config=%s %s 2>&1', + escapeshellarg(PHP_BINARY), + escapeshellarg($root . '/utils/vendor/bin/php-cs-fixer'), + $fix ? 'fix' : 'check --diff', + escapeshellarg($root . '/.php-cs-fixer.user-guide.php'), + escapeshellarg($work), +); + +exec($command, $output, $status); + +if ($status !== 0 && ($fix || $status !== 8)) { + fwrite(STDERR, implode("\n", $output) . "\n"); + remove_directory($work); + + exit(2); +} + +if (! $fix && $status === 8) { + $labels = []; + + foreach ($found as $id => $sample) { + $labels[$files[$id]] = sprintf('%s:%d', substr($sample['page'], strlen($root) + 1), $sample['start'] + 1); + } + + echo strtr(implode("\n", $output), $labels), "\n"; + remove_directory($work); + + exit(1); +} + +$changed = []; + +foreach ($found as $id => $sample) { + $fixed = (string) file_get_contents($files[$id]); + + if ($fixed === $sample['code']) { + continue; + } + + $changed[$sample['page']][] = $sample + ['fixed' => $fixed, 'file' => $files[$id]]; +} + +if ($changed === []) { + printf("\033[32m%s PHP samples checked, none need fixing.\033[0m\n", number_format(count($found))); + remove_directory($work); + + exit(0); +} + +$total = 0; + +foreach ($changed as $page => $samples) { + $lines = $samples[0]['lines']; + + foreach (array_reverse($samples) as $sample) { + $total++; + $body = array_map( + static fn (string $line): string => $line === '' ? '' : $sample['indent'] . $line, + explode("\n", rtrim($sample['fixed'], "\n")), + ); + array_splice($lines, $sample['start'], $sample['end'] - $sample['start'], $body); + } + + file_put_contents($page, implode("\n", $lines)); +} + +remove_directory($work); + +printf("\033[32m%s of %s PHP samples fixed.\033[0m\n", number_format($total), number_format(count($found))); diff --git a/composer.json b/composer.json index 745c817e5e75..47752ea4039a 100644 --- a/composer.json +++ b/composer.json @@ -104,13 +104,15 @@ "Composer\\Config::disableProcessTimeout", "utils/vendor/bin/php-cs-fixer check --ansi --verbose --diff --config=.php-cs-fixer.no-header.php", "utils/vendor/bin/php-cs-fixer check --ansi --verbose --diff --config=.php-cs-fixer.tests.php", - "utils/vendor/bin/php-cs-fixer check --ansi --verbose --diff" + "utils/vendor/bin/php-cs-fixer check --ansi --verbose --diff", + "@php admin/lint-docs-samples.php" ], "cs-fix": [ "Composer\\Config::disableProcessTimeout", "utils/vendor/bin/php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.no-header.php", "utils/vendor/bin/php-cs-fixer fix --ansi --verbose --diff --config=.php-cs-fixer.tests.php", - "utils/vendor/bin/php-cs-fixer fix --ansi --verbose --diff" + "utils/vendor/bin/php-cs-fixer fix --ansi --verbose --diff", + "@php admin/lint-docs-samples.php --fix" ], "phpstan:baseline": [ "phpstan analyse --ansi --generate-baseline=utils/phpstan-baseline/loader.neon", diff --git a/contributing/documentation.md b/contributing/documentation.md index ae0231411a7b..728daeb0f393 100644 --- a/contributing/documentation.md +++ b/contributing/documentation.md @@ -153,15 +153,19 @@ Open the **app/Config/Filters.php** file and update the `$methods` property like ## Code Blocks -Fence a code block with three backticks and the name of its language. -PHP samples have no opening tag, and the highlighter expects that: +Fence a code block with three backticks and the name of its language: ````markdown ```php +` on the line above its fence. + ### CLI Command Use the `console` language for a command that is run in a terminal: diff --git a/docs/database/query_builder.md b/docs/database/query_builder.md index 855377f78261..b6ef52c772c0 100644 --- a/docs/database/query_builder.md +++ b/docs/database/query_builder.md @@ -1147,6 +1147,7 @@ The behavior is the same as the `union()` method. However, all results will be r Query grouping allows you to create groups of **WHERE** clauses by enclosing them in parentheses. This will allow you to create queries with complex **WHERE** clauses. Nested groups are supported. Example: + ```php ```php ```php ```php ```php ```php ```php ```php ```php ```php ```php ```php ```php ```php push('hobbies', [ Just as with any other variable, unsetting a value in `$_SESSION` can be done through `unset()`: + ```php