From 5f3b278fd3f80c292d111afe29e9d79853ba503e Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Fri, 28 Aug 2026 14:44:14 +0200 Subject: [PATCH 1/3] ci: parse the documentation's PHP code blocks on every pull request The package this site documents lives in a different repository, so nothing in the build compiles the examples. A snippet that is not valid PHP renders perfectly and ships broken, which is how the drift reported in #63 went unnoticed for as long as it did. Adds a script that pulls every ```php fence out of _docs/ and resources/includes/ and runs php -l over it, reporting failures against the markdown file and the line the fence starts on rather than the temporary file the linter saw. The workflow runs it on pull requests and on pushes to main. Deliberately a parse check and no more. Running the examples would mean installing json-mapper/json-mapper here and pinning a version, which is the coupling this repo has so far avoided; the linter needs only a PHP binary and no Composer install, so the job takes seconds. The README gains the conventions the examples follow, since they are now partly enforced: fully-qualified names with no import block, examples defining the classes they map onto, and version annotations only for APIs added during 2.x. Refs #63 --- .github/scripts/lint-code-examples.php | 149 +++++++++++++++++++++++++ .github/workflows/lint-examples.yml | 41 +++++++ README.md | 40 +++++++ 3 files changed, 230 insertions(+) create mode 100755 .github/scripts/lint-code-examples.php create mode 100644 .github/workflows/lint-examples.yml diff --git a/.github/scripts/lint-code-examples.php b/.github/scripts/lint-code-examples.php new file mode 100755 index 0000000..3dad42c --- /dev/null +++ b/.github/scripts/lint-code-examples.php @@ -0,0 +1,149 @@ +#!/usr/bin/env php +isFile() && $file->getExtension() === 'md') { + $files[] = $file->getPathname(); + } + } + } + sort($files); + + return $files; +} + +/** + * Pull the ```php fences out of one file. + * + * @return array line is 1-indexed and + * points at the fence marker itself. + */ +function phpBlocks(string $markdown): array +{ + $blocks = []; + $lines = explode("\n", $markdown); + $open = null; + $body = []; + + foreach ($lines as $index => $line) { + if ($open === null) { + if (preg_match('/^```php\s*$/', $line)) { + $open = $index + 1; + $body = []; + } + continue; + } + + if (preg_match('/^```\s*$/', $line)) { + $blocks[] = ['line' => $open, 'code' => implode("\n", $body)]; + $open = null; + continue; + } + + $body[] = $line; + } + + return $blocks; +} + +$files = markdownFiles($root); +$checked = 0; +$failures = []; + +foreach ($files as $file) { + $relative = substr($file, strlen($root) + 1); + + foreach (phpBlocks((string) file_get_contents($file)) as $block) { + $code = ltrim($block['code'], "\n"); + + // A block may or may not open with its own tag. Normalise to exactly one, + // and remember whether that shifted the code down a line so the linter's + // line numbers can be translated back to the markdown. + if (preg_match('/^<\?php\s*$/m', strtok($code, "\n") ?: '')) { + $offset = $block['line']; + } else { + $code = "&1', escapeshellarg(PHP_BINARY), escapeshellarg($temp)), $output, $status); + unlink($temp); + $checked++; + + if ($status === 0) { + $output = []; + continue; + } + + // php -l prints two lines: the diagnostic, then "Errors parsing ". + // Keep the first, and translate its line number back into the markdown. + $message = ''; + foreach ($output as $candidate) { + if (str_contains($candidate, ' on line ')) { + $message = $candidate; + break; + } + } + $message = $message !== '' ? $message : ($output[0] ?? 'could not be parsed'); + $output = []; + + $line = $block['line']; + if (preg_match('/ on line (\d+)/', $message, $matches)) { + $line = $offset + (int) $matches[1]; + } + $message = preg_replace('/ in \S+ on line \d+/', '', $message) ?? $message; + $message = trim(preg_replace('/^(PHP )?(Parse|Fatal) error:\s*/i', '', $message) ?? $message); + + $failures[] = ['file' => $relative, 'line' => $line, 'message' => $message]; + } +} + +foreach ($failures as $failure) { + $text = sprintf('%s:%d %s', $failure['file'], $failure['line'], $failure['message']); + echo $onCi + ? sprintf("::error file=%s,line=%d::%s\n", $failure['file'], $failure['line'], $failure['message']) + : $text . "\n"; +} + +if ($failures !== []) { + printf("\n%d of %d code blocks in %d files failed to parse.\n", count($failures), $checked, count($files)); + exit(1); +} + +printf("All %d PHP code blocks in %d files parse cleanly.\n", $checked, count($files)); diff --git a/.github/workflows/lint-examples.yml b/.github/workflows/lint-examples.yml new file mode 100644 index 0000000..8c878a1 --- /dev/null +++ b/.github/workflows/lint-examples.yml @@ -0,0 +1,41 @@ +name: Lint documentation examples + +# The package this site documents lives in a different repository, so nothing in +# the build compiles the examples — a snippet that is not valid PHP renders +# perfectly and ships broken. This catches that class of mistake on every PR. +# +# It is deliberately a parse check and nothing more. Actually running the +# examples would mean installing json-mapper/json-mapper here and pinning a +# version, which is the coupling this repo has so far avoided; a linter needs +# only a PHP binary and takes seconds. + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: lint-examples-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Parse the PHP code blocks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # No Composer install: the linter uses only core PHP, so the job stays + # independent of the site's dependencies. + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + + - name: Lint the PHP code blocks in the documentation + run: php .github/scripts/lint-code-examples.php diff --git a/README.md b/README.md index b597d88..9908460 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,46 @@ from the file path. Ordering and labels live in `config/docs.php` under `sidebar Documentation output is flat, so `_docs/usage/installation.md` is served at `/docs/installation`. +## Code examples + +The package these pages document lives in +[JsonMapper/JsonMapper](https://github.com/JsonMapper/JsonMapper), so nothing in this repo compiles the +examples. A broken snippet renders perfectly and ships. Two conventions and one CI check keep that in +hand. + +**Write class names out in full; do not add an import block.** A snippet is usually a dozen lines, and +five lines of `use` above it costs more than the width does: + +```php +$mapper = (new \JsonMapper\JsonMapperFactory())->default(); +$mapper->push(new \JsonMapper\Middleware\CaseConversion( + \JsonMapper\Enums\TextNotation::STUDLY_CAPS(), + \JsonMapper\Enums\TextNotation::CAMEL_CASE() +)); +``` + +The exception is a snippet standing in for a real file in the reader's application — one that declares +its own `namespace`. Those use imports, because that is what the reader would write. + +**Let the example define the classes it maps onto.** Do not reference the package's test fixtures; a +reader copying `\JsonMapper\Tests\Implementation\SimpleObject` gets a class their project does not +have. Three lines of `class User { public string $name; }` is enough. + +**Note the introducing release only for APIs added during 2.x**, as `_Available since JsonMapper +x.y.z_` under the page intro. The site documents v2, so "available since 0.3.0" answers a question +nobody has. + +`.github/workflows/lint-examples.yml` parses every ` ```php ` block on each pull request and reports +failures against the markdown file and line. Run it locally with: + +```shell +$PHP .github/scripts/lint-code-examples.php +``` + +It is a parse check, not proof an example works — running the examples would mean installing the +package here and pinning a version. Before changing a snippet, check it against a real checkout of +JsonMapper, and make the inline `// "John Doe"` comments the output it genuinely produces. + ## Deployment Pushing to `main` triggers `.github/workflows/build.yml`, which builds the site and publishes it to From 50f21544e4df00111d6c009fbe899046c77556e7 Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Fri, 28 Aug 2026 15:11:08 +0200 Subject: [PATCH 2/3] ci: analyse the documentation examples against the real package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parse check added in the previous commit catches only invalid PHP, which is the smallest part of the problem. Every defect reported in #63 was valid PHP: JsonMapperBuilder::create() parses perfectly and does not exist, and mapToClass('{ "name": ... }', User::class) parses perfectly and is handed a string where the signature wants a \stdClass. Adds a second pass that runs PHPStan against json-mapper/json-mapper, now a dev dependency so the examples are checked against the API they document. Verified by re-introducing five of the original defects: each was reported on the right page and the exact line, while the parse pass stayed green throughout. Mechanics worth knowing: - The extractor keeps every line at its markdown line number, blanking whatever is not code, so PHPStan's line numbers need no translation. - Examples are extracted per page, since a page often defines a class in one fence and maps onto it in the next, and each page gets its own namespace so the User that several pages define does not collide. - A fence declaring its own namespace becomes its own analysed file. Classes the examples borrow from the reader's application, or from companion packages that cannot install alongside Hyde, are stubbed. Where a real package does coexist it is required instead: json-mapper/laravel-package and monolog/monolog are installed so those pages are checked for real. Four examples changed to name their placeholders in full — \App\YourExtended JsonMapper, \App\Models\License, \JsonMapper\SymfonyBundle\JsonMapperBundle — which matches the site's fully-qualified convention. The final callback example dropped a Cache::put() line that referenced an undefined $seconds and stood for nothing in particular. Still nothing executes the examples, so a well-typed snippet that throws at run time gets through; (new JsonMapperFactory())->create() with no middleware is the one #63 defect this cannot see. Refs #63 --- .github/phpstan.neon | 18 + .github/scripts/extract-code-examples.php | 160 +++ .github/stubs/placeholders.php | 85 ++ .github/workflows/lint-examples.yml | 66 +- .gitignore | 1 + README.md | 30 +- _docs/guides/symfony-usage.md | 2 +- _docs/middleware/final-callback.md | 5 +- _docs/middleware/laravel-eloquent.md | 4 +- _docs/usage/setup.md | 2 +- composer.json | 6 +- composer.lock | 1147 ++++++++++++++++++++- 12 files changed, 1498 insertions(+), 28 deletions(-) create mode 100644 .github/phpstan.neon create mode 100755 .github/scripts/extract-code-examples.php create mode 100644 .github/stubs/placeholders.php diff --git a/.github/phpstan.neon b/.github/phpstan.neon new file mode 100644 index 0000000..092e73d --- /dev/null +++ b/.github/phpstan.neon @@ -0,0 +1,18 @@ +# Static analysis of the documentation's PHP examples against the real package. +# +# The paths are supplied on the command line by check-code-examples.php, which +# extracts the code blocks first. Reported line numbers are the markdown line +# numbers: the extractor blanks out everything that is not code rather than +# collapsing it. +parameters: + level: 5 + + scanFiles: + - stubs/placeholders.php + + ignoreErrors: + # Examples routinely assign something to show the shape of the wiring + # without going on to use it — the Symfony page's constructor injection + # is the whole point of that snippet. This can never indicate a + # documentation defect. + - identifier: property.onlyWritten diff --git a/.github/scripts/extract-code-examples.php b/.github/scripts/extract-code-examples.php new file mode 100755 index 0000000..bef4c43 --- /dev/null +++ b/.github/scripts/extract-code-examples.php @@ -0,0 +1,160 @@ +#!/usr/bin/env php + + */ + +declare(strict_types=1); + +const SOURCES = ['_docs', 'resources/includes']; + +$root = dirname(__DIR__, 2); +$outputDir = $argv[1] ?? null; + +if ($outputDir === null) { + fwrite(STDERR, "usage: extract-code-examples.php \n"); + exit(1); +} + +/** A namespace segment per path component, so each page is isolated. */ +function namespaceFor(string $relative): string +{ + $parts = preg_split('#[/\\\\]#', substr($relative, 0, -3)) ?: []; + $parts = array_map( + static fn (string $part): string => str_replace(' ', '', ucwords(str_replace(['-', '_', '.'], ' ', $part))), + $parts + ); + + return 'DocExample\\' . implode('\\', $parts); +} + +$files = []; +foreach (SOURCES as $source) { + if (! is_dir("$root/$source")) { + continue; + } + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator("$root/$source")) as $file) { + if ($file->isFile() && $file->getExtension() === 'md') { + $files[] = $file->getPathname(); + } + } +} +sort($files); + +$written = 0; +foreach ($files as $file) { + $relative = substr($file, strlen($root) + 1); + $lines = explode("\n", (string) file_get_contents($file)); + + $out = []; + $ownFiles = []; + $inFence = false; + $fence = []; + $fenceStart = 0; + $seenUse = []; + + foreach ($lines as $index => $line) { + if (! $inFence) { + $out[] = ''; + if (preg_match('/^```php\s*$/', $line)) { + $inFence = true; + $fence = []; + $fenceStart = $index + 1; + } + continue; + } + + if (preg_match('/^```\s*$/', $line)) { + // A fence declaring its own namespace stands for a separate file in + // the reader's application. It gets its own analysed file, keeping + // the namespace it declares, rather than being merged into the page. + $isOwnFile = false; + foreach ($fence as $fenceLine) { + if (preg_match('/^\s*namespace\s+/', $fenceLine)) { + $isOwnFile = true; + break; + } + } + + if ($isOwnFile) { + $standalone = array_fill(0, count($lines), ''); + foreach ($fence as $offset => $fenceLine) { + $standalone[$fenceStart + $offset] = preg_match('/^<\?php\s*$/', trim($fenceLine)) + ? '' + : $fenceLine; + } + $standalone[0] = ' $fenceLine) { + $keep = ! $isOwnFile; + + if ($keep && preg_match('/^<\?php\s*$/', trim($fenceLine))) { + $keep = false; + } + + // Repeating a `use` across fences of one page is correct in the + // docs but a redeclaration once merged. + if ($keep && preg_match('/^use\s+[^;]+;$/', trim($fenceLine))) { + if (isset($seenUse[trim($fenceLine)])) { + $keep = false; + } else { + $seenUse[trim($fenceLine)] = true; + } + } + + $out[$fenceStart + $offset] = $keep ? $fenceLine : ''; + } + + $out[] = ''; + $inFence = false; + continue; + } + + $fence[] = $line; + $out[] = ''; + } + + // Mirror the source tree so a reported path maps straight back to the page. + foreach ($ownFiles as $index => $contents) { + $target = $outputDir . '/' . $relative . '.' . $index . '.php'; + if (! is_dir(dirname($target))) { + mkdir(dirname($target), 0777, true); + } + file_put_contents($target, $contents); + $written++; + } + + if (trim(implode('', $out)) === '') { + continue; + } + + // Line 1 of a markdown page is front matter or a fence marker, never code, + // so the declaration can live there without shifting anything. + $out[0] = 'cache = $cache; + } + + public function handle( + \stdClass $json, + \JsonMapper\Wrapper\ObjectWrapper $object, + \JsonMapper\ValueObjects\PropertyMap $map, + \JsonMapper\JsonMapperInterface $mapper + ): void { + } + } +} + +namespace JsonMapper\SymfonyBundle { + /** json-mapper/symfony-bundle — conflicts with Hyde's dependencies. */ + class JsonMapperBundle + { + } +} diff --git a/.github/workflows/lint-examples.yml b/.github/workflows/lint-examples.yml index 8c878a1..49bc093 100644 --- a/.github/workflows/lint-examples.yml +++ b/.github/workflows/lint-examples.yml @@ -1,13 +1,22 @@ -name: Lint documentation examples +name: Check documentation examples # The package this site documents lives in a different repository, so nothing in -# the build compiles the examples — a snippet that is not valid PHP renders -# perfectly and ships broken. This catches that class of mistake on every PR. +# the build compiles the examples — a broken snippet renders perfectly and ships +# broken. That is how the drift reported in #63 went unnoticed: the Setup page +# called JsonMapperBuilder::create(), which does not exist, and the landing page +# passed a JSON string to mapToClass(), whose first parameter is a \stdClass. # -# It is deliberately a parse check and nothing more. Actually running the -# examples would mean installing json-mapper/json-mapper here and pinning a -# version, which is the coupling this repo has so far avoided; a linter needs -# only a PHP binary and takes seconds. +# Two passes, cheapest first: +# +# 1. A parse check, needing only a PHP binary. Fails in seconds, before the +# Composer install, and keeps working if dependency resolution ever breaks. +# 2. PHPStan against the real json-mapper/json-mapper, which is what catches a +# call to a method that does not exist, a missing constructor argument or an +# argument of the wrong type. Every defect in #63 except one is caught here. +# +# Neither pass runs the examples, so a snippet that is valid and well-typed but +# throws at run time still gets through. Verify output by hand before changing an +# example. on: pull_request: @@ -19,23 +28,54 @@ permissions: contents: read concurrency: - group: lint-examples-${{ github.ref }} + group: check-examples-${{ github.ref }} cancel-in-progress: true jobs: - lint: - name: Parse the PHP code blocks + check: + name: Parse and analyse the PHP code blocks runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - # No Composer install: the linter uses only core PHP, so the job stays - # independent of the site's dependencies. - name: Set up PHP uses: shivammathur/setup-php@v2 with: php-version: '8.2' coverage: none - - name: Lint the PHP code blocks in the documentation + - name: Lint the PHP code blocks run: php .github/scripts/lint-code-examples.php + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache Composer packages + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + # Unlike the deploy workflow this needs the dev dependencies: PHPStan and + # the package whose API the examples are checked against. + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Extract the code blocks + run: php .github/scripts/extract-code-examples.php .phpstan-doc-examples + + # PHPStan reports against the extracted files; rewriting the paths puts the + # annotations back on the markdown, whose line numbers they already carry. + # pipefail keeps PHPStan's exit code through the rewrite. + - name: Analyse the code blocks against JsonMapper + run: | + set -o pipefail + vendor/bin/phpstan analyse \ + --configuration=.github/phpstan.neon \ + --error-format=github \ + --no-progress \ + --memory-limit=512M \ + .phpstan-doc-examples \ + | sed -E 's#\.phpstan-doc-examples/##; s#\.md(\.[0-9]+)?\.php#.md#' diff --git a/.gitignore b/.gitignore index f6cc552..be58729 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /.idea/ /.DS_Store .env +.phpstan-doc-examples/ diff --git a/README.md b/README.md index 9908460..bed3512 100644 --- a/README.md +++ b/README.md @@ -76,16 +76,34 @@ have. Three lines of `class User { public string $name; }` is enough. x.y.z_` under the page intro. The site documents v2, so "available since 0.3.0" answers a question nobody has. -`.github/workflows/lint-examples.yml` parses every ` ```php ` block on each pull request and reports -failures against the markdown file and line. Run it locally with: +`.github/workflows/lint-examples.yml` checks every ` ```php ` block on each pull request, in two +passes. Run them locally with: ```shell -$PHP .github/scripts/lint-code-examples.php +$PHP .github/scripts/lint-code-examples.php # 1. parse +$PHP .github/scripts/extract-code-examples.php .phpstan-doc-examples # 2. analyse +$PHP vendor/bin/phpstan analyse -c .github/phpstan.neon --memory-limit=512M .phpstan-doc-examples ``` -It is a parse check, not proof an example works — running the examples would mean installing the -package here and pinning a version. Before changing a snippet, check it against a real checkout of -JsonMapper, and make the inline `// "John Doe"` comments the output it genuinely produces. +The first is a parse check needing only a PHP binary. The second runs PHPStan against the real +`json-mapper/json-mapper`, which is a dev dependency here for exactly that reason, and is what catches +a call to a method that does not exist, a missing constructor argument, or an argument of the wrong +type. Both report against the markdown file and line: the extractor blanks out everything that is not +code rather than collapsing it, so an analysed file's line numbers are the page's line numbers. + +Examples are extracted **per page**, since a page routinely defines a class in one fence and maps onto +it in the next, and each page is given its own namespace so that the `User` several pages define does +not collide. A fence that declares its own `namespace` is analysed as its own file. + +Classes the examples borrow from the reader's imagination (`\App\…`) or from packages that cannot be +installed alongside Hyde are declared in `.github/stubs/placeholders.php`. Prefer a real dev dependency +over a stub where the two can coexist — `json-mapper/laravel-package` and `monolog/monolog` are +installed for this reason, so those pages are checked against the genuine classes. + +Neither pass runs the examples, so a snippet that is valid and well-typed but throws at run time still +gets through — `(new JsonMapperFactory())->create()` with no middleware is the case that motivated +this. Before changing a snippet, check it against a real checkout of JsonMapper and make the inline +`// "John Doe"` comments the output it genuinely produces. ## Deployment diff --git a/_docs/guides/symfony-usage.md b/_docs/guides/symfony-usage.md index a3d49e1..ca0065e 100644 --- a/_docs/guides/symfony-usage.md +++ b/_docs/guides/symfony-usage.md @@ -21,7 +21,7 @@ If your application does not use [Symfony Flex](https://symfony.com/doc/current/ return [ // ... - JsonMapper\SymfonyBundle\JsonMapperBundle::class => ['all' => true], + \JsonMapper\SymfonyBundle\JsonMapperBundle::class => ['all' => true], ]; ``` diff --git a/_docs/middleware/final-callback.md b/_docs/middleware/final-callback.md index a93702f..0c298af 100644 --- a/_docs/middleware/final-callback.md +++ b/_docs/middleware/final-callback.md @@ -27,10 +27,9 @@ $mapper->push(new \JsonMapper\Middleware\FinalCallback(function( \JsonMapper\ValueObjects\PropertyMap $map, \JsonMapper\JsonMapperInterface $mapper ) { - // Call a method on the object + // Call a method on the object now that it has been filled $object->getObject()->done(); - // Or persist it in the cache - Cache::put('key', $object->getObject(), $seconds); + // ...or hand it to a cache, a queue, an event dispatcher, and so on })); $object = new User(); diff --git a/_docs/middleware/laravel-eloquent.md b/_docs/middleware/laravel-eloquent.md index 6fcc4fc..bc21b86 100644 --- a/_docs/middleware/laravel-eloquent.md +++ b/_docs/middleware/laravel-eloquent.md @@ -17,8 +17,8 @@ $data = file_get_contents($url); $mapper = (new \JsonMapper\JsonMapperFactory())->bestFit(); $mapper->push(new \JsonMapper\EloquentMiddleware\EloquentMiddleware(new \JsonMapper\Cache\ArrayCache())); -$licenses = $mapper->mapArrayFromString($data, new License()); -\Illuminate\Support\Collection::make($licenses)->each(fn(License $l) => $l->save()); +$licenses = $mapper->mapArrayFromString($data, new \App\Models\License()); +\Illuminate\Support\Collection::make($licenses)->each(fn(\App\Models\License $l) => $l->save()); ``` _This middleware is part of separate repository and need to be installed using `composer require json-mapper/eloquent-middleware`_ diff --git a/_docs/usage/setup.md b/_docs/usage/setup.md index bd3ad39..3b8e486 100644 --- a/_docs/usage/setup.md +++ b/_docs/usage/setup.md @@ -50,7 +50,7 @@ $propertyMapper = \JsonMapper\Builders\PropertyMapperBuilder::new() ->build(); $mapper = \JsonMapper\JsonMapperBuilder::new() - ->withJsonMapperClassName(YourExtendedJsonMapper::class) + ->withJsonMapperClassName(\App\YourExtendedJsonMapper::class) ->withPropertyMapper($propertyMapper) ->withDefaultCache(new \JsonMapper\Cache\ArrayCache()) ->withDocBlockAnnotationsMiddleware() diff --git a/composer.json b/composer.json index 0410595..d56bfbb 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,11 @@ "laravel-zero/framework": "^11.0" }, "require-dev": { - "hyde/realtime-compiler": "^4.0" + "hyde/realtime-compiler": "^4.0", + "json-mapper/json-mapper": "^2.25", + "json-mapper/laravel-package": "^3.0", + "monolog/monolog": "^3.10", + "phpstan/phpstan": "^2.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 1a42db7..2646eaa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f63a645865205d08f872f5a0fdb3d2c8", + "content-hash": "bfa479ffb11a62ee224f79648a85cfc7", "packages": [ { "name": "brick/math", @@ -6618,6 +6618,54 @@ ], "time": "2024-07-14T19:41:26+00:00" }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, { "name": "hyde/realtime-compiler", "version": "v4.5.0", @@ -6687,6 +6735,1103 @@ } ], "time": "2026-07-09T23:56:00+00:00" + }, + { + "name": "json-mapper/json-mapper", + "version": "2.25.1", + "source": { + "type": "git", + "url": "https://github.com/JsonMapper/JsonMapper.git", + "reference": "4fd6cb5ccfece349ed1aeb52c818bdf84ba75b61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JsonMapper/JsonMapper/zipball/4fd6cb5ccfece349ed1aeb52c818bdf84ba75b61", + "reference": "4fd6cb5ccfece349ed1aeb52c818bdf84ba75b61", + "shasum": "" + }, + "require": { + "ext-json": "*", + "myclabs/php-enum": "^1.7", + "nikic/php-parser": "^4.13 || ^5.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-docblock": "^5.6", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "psr/simple-cache": " ^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php73": "^1.18" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.5 || ^7.0", + "php-coveralls/php-coveralls": "^2.4", + "phpstan/phpstan": "^0.12.14", + "phpstan/phpstan-phpunit": "^0.12.17", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.5", + "symfony/console": "^2.1 || ^3.0 || ^4.0 || ^5.0", + "vimeo/psalm": "^4.10 || ^5.0" + }, + "suggest": { + "json-mapper/laravel-package": "Use JsonMapper directly with Laravel", + "json-mapper/symfony-bundle": "Use JsonMapper directly with Symfony" + }, + "type": "library", + "autoload": { + "psr-4": { + "JsonMapper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Map JSON structures to PHP classes", + "homepage": "https://jsonmapper.net", + "keywords": [ + "json", + "jsonmapper", + "mapper", + "middleware" + ], + "support": { + "docs": "https://jsonmapper.net", + "issues": "https://github.com/JsonMapper/JsonMapper/issues", + "source": "https://github.com/JsonMapper/JsonMapper" + }, + "funding": [ + { + "url": "https://github.com/DannyvdSluijs", + "type": "github" + } + ], + "time": "2025-05-26T09:51:24+00:00" + }, + { + "name": "json-mapper/laravel-package", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/JsonMapper/LaravelPackage.git", + "reference": "e0494cbfa405417bb47ab06ed6f68e4f87ad36b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JsonMapper/LaravelPackage/zipball/e0494cbfa405417bb47ab06ed6f68e4f87ad36b3", + "reference": "e0494cbfa405417bb47ab06ed6f68e4f87ad36b3", + "shasum": "" + }, + "require": { + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "json-mapper/json-mapper": "^2.3", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5 || ^7.0", + "orchestra/testbench": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0", + "php-coveralls/php-coveralls": "^2.4", + "phpstan/phpstan": "^0.12.19 || ^1.0.0 || ^2.1", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.5 || ^11.5.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "JsonMapper\\LaravelPackage\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "JsonMapper\\LaravelPackage\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The JsonMapper package for Laravel", + "keywords": [ + "json", + "jsonmapper", + "laravel", + "mapper", + "middleware" + ], + "support": { + "issues": "https://github.com/JsonMapper/LaravelPackage/issues", + "source": "https://github.com/JsonMapper/LaravelPackage/tree/3.0.0" + }, + "time": "2026-04-05T19:27:41+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "myclabs/php-enum", + "version": "1.8.5", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2 || ^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "https://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.5" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2025-01-14T11:49:03+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.8.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + }, + "time": "2026-07-04T14:30:18+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.7", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "31a105931bc8ffa3a123383829772e832fd8d903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/31a105931bc8ffa3a123383829772e832fd8d903", + "reference": "31a105931bc8ffa3a123383829772e832fd8d903", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.7" + }, + "time": "2026-03-18T20:47:46+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" + }, + "time": "2025-11-21T15:09:14+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" + }, + "time": "2026-07-08T07:01:06+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.2.9", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/13d6b4f347bad222da436580c8304fa6f83e6bd0", + "reference": "13d6b4f347bad222da436580c8304fa6f83e6bd0", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-08-22T07:38:16+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "f6028442dd1dfa4f88e9c7360d753948d165e938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/f6028442dd1dfa4f88e9c7360d753948d165e938", + "reference": "f6028442dd1dfa4f88e9c7360d753948d165e938", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "ext-relay": "<0.12.1", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "^1.0.3", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-19T08:28:05+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/9789738bc19af1106dc54d6afba9a0b467516cf2", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.4.16", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "ca31404415670aa3834809005b529df1b84f0790" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/ca31404415670aa3834809005b529df1b84f0790", + "reference": "ca31404415670aa3834809005b529df1b84f0790", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.4.16" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-30T12:37:26+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, + "branch-alias": { + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.4.1" + }, + "time": "2026-06-15T15:31:57+00:00" } ], "aliases": [], From ae39590d3319f59b582ead955d012dbdf004220c Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Fri, 28 Aug 2026 15:20:23 +0200 Subject: [PATCH 3/3] feat: read the documented version from the installed package The docs index claimed "These pages describe JsonMapper 2.25.1" as prose, which is a number nobody will remember to change. Now that the examples are analysed against an installed json-mapper/json-mapper, the version the site documents and the version its examples are checked against are the same fact, so it should be read rather than written. App\Support\DocumentedVersion resolves it from Composer\InstalledVersions, and config/docs.php shows it once at the foot of the documentation sidebar, beside the existing links. Bumping the dependency now updates the site. The deploy installs with --no-dev and JsonMapper is a dev dependency, so InstalledVersions does not know it at build time. Verified that it throws OutOfBoundsException there, and the resolver falls back to composer.lock, which ships regardless. A --no-dev build was run end to end to confirm the sidebar still renders the version. Refs #63 --- README.md | 6 +++ _docs/index.md | 6 +-- app/Support/DocumentedVersion.php | 81 +++++++++++++++++++++++++++++++ config/docs.php | 8 ++- 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 app/Support/DocumentedVersion.php diff --git a/README.md b/README.md index bed3512..126a008 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,12 @@ have. Three lines of `class User { public string $name; }` is enough. x.y.z_` under the page intro. The site documents v2, so "available since 0.3.0" answers a question nobody has. +Do not write the *documented* release into the prose. It is read from the installed +`json-mapper/json-mapper` by `App\Support\DocumentedVersion` and shown once, at the foot of the +documentation sidebar, so the version the pages claim to describe is by construction the version their +examples are checked against. Bumping the dependency updates the site. The deploy installs with +`--no-dev`, where the package is absent, so the resolver falls back to `composer.lock`. + `.github/workflows/lint-examples.yml` checks every ` ```php ` block on each pull request, in two passes. Run them locally with: diff --git a/_docs/index.md b/_docs/index.md index b74f99e..a3a2ffb 100644 --- a/_docs/index.md +++ b/_docs/index.md @@ -12,9 +12,9 @@ own models. These pages cover installing it, the middleware it ships with, and h New here? Start with [Getting started](/_docs/guides/getting-started.md), which walks through mapping a first response end to end. -These pages describe JsonMapper **2.25.1**, which requires PHP 7.4 or higher. Anything added during -the 2.x line carries an "Available since" line naming the release that introduced it; everything else -has been there since 2.0. +JsonMapper requires PHP 7.4 or higher. Anything added during the 2.x line carries an "Available since" +line naming the release that introduced it; everything else has been there since 2.0. The release these +pages describe is shown at the foot of the sidebar. ## Where to look diff --git a/app/Support/DocumentedVersion.php b/app/Support/DocumentedVersion.php new file mode 100644 index 0000000..41b0ea9 --- /dev/null +++ b/app/Support/DocumentedVersion.php @@ -0,0 +1,81 @@ + true, // A string of Markdown to show in the footer. Set to `false` to disable. - 'footer' => '[GitHub](https://github.com/JsonMapper/JsonMapper) · [Twitter](https://twitter.com/JsonMapper)', + // The documented release is read from the installed JsonMapper rather + // than written out, so it cannot drift from the version the code + // examples are checked against. See App\Support\DocumentedVersion. + 'footer' => '[GitHub](https://github.com/JsonMapper/JsonMapper) · [Twitter](https://twitter.com/JsonMapper)' + .(\App\Support\DocumentedVersion::get() !== null + ? ' · Documenting JsonMapper '.\App\Support\DocumentedVersion::get() + : ''), /* |--------------------------------------------------------------------------