diff --git a/.changes/0.1.0-alpha.0.md b/.changes/0.1.0-alpha.0.md new file mode 100644 index 0000000..9bf295a --- /dev/null +++ b/.changes/0.1.0-alpha.0.md @@ -0,0 +1,5 @@ +## 0.1.0-alpha.0 - 2026-10-02 + +### Added + +- Add the offline core of spec 0.1.0: parse, normalize, isLegacy, stateName, precisionForAccuracy, truncate, parent, contains and redact. diff --git a/.changes/header.tpl.md b/.changes/header.tpl.md new file mode 100644 index 0000000..ef6a82a --- /dev/null +++ b/.changes/header.tpl.md @@ -0,0 +1,3 @@ +# Changelog + +This file lists each notable change to `gatepost/postcode`. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Changie writes this file from the change files in `.changes/`. diff --git a/.changes/unreleased/.gitkeep b/.changes/unreleased/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.changie.yaml b/.changie.yaml new file mode 100644 index 0000000..3f816a7 --- /dev/null +++ b/.changie.yaml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +changesDir: .changes +unreleasedDir: unreleased +headerPath: header.tpl.md +changelogPath: CHANGELOG.md +versionExt: md +versionFormat: '## {{.Version}} - {{.Time.Format "2006-01-02"}}' +kindFormat: '### {{.Kind}}' +changeFormat: '- {{.Body}}' +kinds: + - label: Added + auto: minor + - label: Changed + auto: major + - label: Deprecated + auto: minor + - label: Removed + auto: major + - label: Fixed + auto: patch + - label: Security + auto: patch +newlines: + afterChangelogHeader: 1 + beforeKind: 1 + afterKind: 1 + endOfVersion: 1 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..948eb75 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 100 + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..947d607 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +# Packagist builds each release from a git archive. These paths stay out of it. +/.changes export-ignore +/.changie.yaml export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.gitmodules export-ignore +/.php-cs-fixer.dist.php export-ignore +/AGENTS.md export-ignore +/CLAUDE.md export-ignore +/CODING_STANDARDS.md export-ignore +/CONTEXT.md export-ignore +/REUSE.toml export-ignore +/composer.lock export-ignore +/docs export-ignore +/infection.json5 export-ignore +/phpmd.xml export-ignore +/phpstan.dist.neon export-ignore +/phpunit.xml.dist export-ignore +/renovate.json export-ignore +/scripts export-ignore +/spec export-ignore +/tests export-ignore +/tools export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ed24c10 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +name: ci + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + env: + # Each download has a SHA-256 sum. Renovate moves only the version, so a new version fails + # the sum check until a person writes the new sum. + # renovate: depName=miniscruff/changie + CHANGIE_VERSION: 1.26.0 + CHANGIE_SHA256: eab168c8287a6e91912e1c02e5260911232d945bfd3c89d8a0e1ace6bb7b6161 + # renovate: depName=gitleaks/gitleaks + GITLEAKS_VERSION: 8.30.1 + GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # The API check reads the release tags, and gitleaks reads the whole history. + fetch-depth: 0 + submodules: true + persist-credentials: false + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.4' + coverage: xdebug + # Composer requires ext-intl to install the BC tool. The package does not need it, as + # the tests job shows. + extensions: intl + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.11' + - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + with: + # composer check starts reuse and zizmor with uvx. The tools are small, and no lock file + # keys a cache. + enable-cache: false + - name: Install changie and gitleaks + working-directory: ${{ runner.temp }} + run: | + changie_archive="changie_${CHANGIE_VERSION}_linux_amd64.tar.gz" + gitleaks_archive="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + curl -sSfLO "https://github.com/miniscruff/changie/releases/download/v${CHANGIE_VERSION}/${changie_archive}" + curl -sSfLO "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${gitleaks_archive}" + echo "$CHANGIE_SHA256 $changie_archive" | sha256sum -c - + echo "$GITLEAKS_SHA256 $gitleaks_archive" | sha256sum -c - + mkdir bin + tar -xzf "$changie_archive" -C bin changie + tar -xzf "$gitleaks_archive" -C bin gitleaks + echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" + - run: composer install --no-interaction --no-progress + - run: composer --working-dir=tools/bc-check install --no-interaction --no-progress + - run: composer check + + tests: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + # The oldest PHP that the package supports, and the newest release. + php: ['8.1', '8.5'] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: true + persist-credentials: false + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: ${{ matrix.php }} + coverage: none + # The package must run without ext-intl (PHP-9). + extensions: :intl + - name: Check that ext-intl is off + run: | + php -r 'exit(extension_loaded("intl") ? 1 : 0);' + - run: composer install --no-interaction --no-progress + - name: Use the newest PHPUnit on the newest PHP + if: matrix.php != '8.1' + run: | + composer config --unset platform.php + composer update --no-interaction --no-progress --with-all-dependencies phpunit/phpunit + # GenerateDataTest reads the UnicodeData.txt that this command downloads and checks. It also + # checks that the generated classes match the spec data on this PHP version. + - run: composer generate:check + - run: composer test + # The dev packages include polyfills of ext-intl, which could hide a call to an intl + # function. This step leaves them out of the autoloader and runs the quickstart. The job + # keeps ext-mbstring, because PHPUnit needs it, so PortabilityTest checks those names. + - name: Run the quickstart without the dev packages + run: | + composer dump-autoload --no-dev + php -r ' + require "vendor/autoload.php"; + echo Gatepost\Postcode\Postcode::parse("ek 01 a03 fk 01")->value->canonical; + ' | grep -x 'EK-01-A03-FK-01' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..8521027 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +name: codeql + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: '30 3 * * 1' + +permissions: + contents: read + +jobs: + analyse: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + # The analysis uploads its results to the code scanning page. + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + with: + # CodeQL cannot read PHP. It checks the workflows in this repo. + languages: actions + - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml new file mode 100644 index 0000000..8bbe261 --- /dev/null +++ b/.github/workflows/mutation.yml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +name: mutation + +on: + schedule: + - cron: '0 5 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + core: + runs-on: ubuntu-24.04 + # A run takes a few minutes. The limit stops a run that hangs. + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: true + persist-credentials: false + - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.4' + coverage: xdebug + - run: composer install --no-interaction --no-progress + - run: composer --working-dir=tools/infection install --no-interaction --no-progress + # GenerateDataTest reads the UnicodeData.txt that this command downloads and checks. + - run: composer generate:check + - run: composer mutation diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..dc973ee --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +name: pull request + +# A squash merge puts the title and the body of the pull request on main as the commit message. +# An edit of the title or the body does not push a commit, so it must start these jobs again. +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: pull-request-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + sign-off: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # The check reads each commit from the base to the head. + fetch-depth: 0 + submodules: true + persist-credentials: false + # Only the message that reaches main gets the other message rules, so a commit needs a + # sign-off and nothing more. + - name: Check that each commit has a sign-off + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: python3 spec/scripts/check-tells --signed-off "$BASE..$HEAD" + + squash-message: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: true + persist-credentials: false + # The title and the body come through env, so that no text of the pull request ends up + # inside the script. + - name: Check the message that the squash merge puts on main + env: + TITLE: ${{ github.event.pull_request.title }} + NUMBER: ${{ github.event.pull_request.number }} + BODY: ${{ github.event.pull_request.body }} + run: | + printf '%s (#%s)\n\n%s\n' "$TITLE" "$NUMBER" "$BODY" > "$RUNNER_TEMP/message" + python3 spec/scripts/check-tells --squash-msg "$RUNNER_TEMP/message" + scripts/check-commit-msg "$RUNNER_TEMP/message" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..ba5d229 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +name: scorecard + +on: + push: + branches: [main] + schedule: + - cron: '30 4 * * 1' + +permissions: read-all + +jobs: + analysis: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + # The upload step sends the results to the code scanning page. + security-events: write + # Scorecard signs the results that it publishes, with the OIDC token of this job. + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97df7fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +/build/ +/vendor/ +/tools/*/vendor/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5cf127e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "spec"] + path = spec + url = https://github.com/gatepost-dev/spec diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..250b710 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,36 @@ +in([__DIR__ . '/scripts', __DIR__ . '/src', __DIR__ . '/tests']) + ->append([__FILE__]) + // The generator writes these two files. Its --check mode keeps them in step. + ->notPath(['Internal/NfkcTable.php', 'Internal/SpecData.php']); + +return (new Config()) + ->setRiskyAllowed(true) + ->setCacheFile(__DIR__ . '/build/php-cs-fixer.cache') + ->setFinder($finder) + ->setRules([ + '@PER-CS' => true, + '@PER-CS:risky' => true, + 'declare_strict_types' => true, + 'final_class' => true, + // On PHP 8.4, an unqualified call to a native function in a namespace compiles to two + // branches, and Xdebug counts the one that tests cannot reach. T-7 measures branches. + 'native_function_invocation' => ['include' => ['@internal'], 'scope' => 'namespaced'], + 'no_unused_imports' => true, + 'ordered_imports' => [ + 'imports_order' => ['class', 'function', 'const'], + 'sort_algorithm' => 'alpha', + ], + 'strict_comparison' => true, + 'strict_param' => true, + ]); diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d125f32 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# AGENTS.md + +This repo is part of Gatepost, unofficial open-source developer tools for Nigeria's National Digital Postcode. This repo holds the PHP SDK, `gatepost/postcode`. Today it has the core. + +## Read first + +- `CODING_STANDARDS.md` and the two files it names hold the rules. Cite rule IDs, such as `API-3`, in reviews and commit bodies. +- `CONTEXT.md` is the glossary. Name things with its terms in code, tests, docs and commits. +- `docs/adr/` holds the decisions. When a change contradicts an ADR, say so in the pull request. +- `spec/` holds the grammar, with its Interface section, and the vectors. The spec wins over code and over these docs. + +## Rules that no config file shows + +- Call only the documented NIPOST gateway endpoints. +- Commit only NIPOST's published test codes and synthetic values, such as `FC-01-Z99-ZZ-01`. +- Sign off each commit with `git commit -s`. The person who opens the pull request is the only author. Leave out co-author lines for AI tools. +- Change `main` only through pull requests. Keep the history of `main` and of other people's branches as it is: never force-push to them. +- A commit with no sign-off fails the `sign-off` job in CI. If a commit lacks its sign-off, run `git rebase --signoff` with the base branch. Then push to your own branch with `--force-with-lease`. +- Send every call to the gateway through the client. The client limits how many calls run at once. +- Write prose in plain British English, with short sentences in the active voice. +- Some agent tools change a typed backslash-u escape, such as the one for an en dash, into the character itself. Build such characters with code, for example `chr(0x2013)` in Python or `String.fromCodePoint(0x2013)` in TypeScript. In data and prose, write `U+2013`. + +## Done means all of these + +1. A failing test came first, and it passes now. +2. `composer check` passes. It runs the formatter, linters, tests, vectors, size limits and `check-tells`. +3. Each user-visible change has a change file. +4. A change to the public interface also updates the API dump and the docs. `composer api:dump` writes the dump to `docs/api-dump.md`. +5. Each new domain term is in `CONTEXT.md`. +6. The diff touches only the lines that the task needs. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..519c14d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +This file lists each notable change to `gatepost/postcode`. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Changie writes this file from the change files in `.changes/`. + +## 0.1.0-alpha.0 - 2026-10-02 + +### Added + +- Add the offline core of spec 0.1.0: parse, normalize, isLegacy, stateName, precisionForAccuracy, truncate, parent, contains and redact. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CODING_STANDARDS.md b/CODING_STANDARDS.md new file mode 100644 index 0000000..985b82e --- /dev/null +++ b/CODING_STANDARDS.md @@ -0,0 +1,23 @@ +# Coding standards + +This repo follows two files. Both are part of its standard: + +1. `spec/standards/CODING_STANDARDS.md` holds the rules for every Gatepost repo. +2. `spec/standards/languages/php.md` holds the rules for PHP. Part A applies to this repo. + +Rules for this repo only: call each native function with a leading backslash, such as `\strlen()`. PHP-CS-Fixer adds it. On PHP 8.4, an unqualified call inside a namespace compiles to two branches, and Xdebug counts the one that no test can reach, so the branch coverage floor of T-7 needs the backslash. + +Another rule for this repo only: use no feature of PHP 8.2 or later. PHPStan checks PHP 8.1 to 8.5 together, so it does not stop every such feature. PHP 8.1 cannot run these features, so use none of them, even when PHPStan accepts the code: + +- DNF types, such as `(A&B)|null` +- the standalone `null` type +- constants in traits +- a dynamic class constant fetch, such as `self::{$name}` +- `new` without parentheses, such as `new Foo()->bar()` +- property hooks +- `array_find()`, `array_any()` and `array_all()` +- `str_increment()` +- `ini_parse_quantity()` +- an anonymous readonly class + +The CI job that runs the tests on PHP 8.1 is the real guard against these features. Do not rely on PHPStan for this rule. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..64a0f59 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,162 @@ +# Gatepost + +Unofficial, open-source developer tools for Nigeria's National Digital Postcode. This glossary fixes one name for each concept, in every language and every repo. + +## Language + +### The postcode + +**Postcode**: +The 11-character code that NIPOST gives to one addressable building or location. +_Avoid_: digital address, address code, zip code, postal code + +**Legacy postcode**: +An old 6-digit NIPOST postcode. It names an area, not a building. +_Avoid_: old code, zip + +**Segment**: +One of the five parts of a postcode: state, LGA, district, area and unit. +_Avoid_: part, field, component, chunk + +**State**: +The first segment. Two letters that name one of the 36 states or the Federal Capital Territory. +_Avoid_: region, province + +**LGA**: +The second segment. Two digits that name a local government area within a state. +_Avoid_: council, local government, municipality + +**District**: +The third segment. Three characters that name a district within an LGA. +_Avoid_: sorting code, sector + +**Area**: +The fourth segment. Two letters that name the smallest polygon in the system. An area holds at most 99 units. +_Avoid_: zone, block, neighbourhood + +**Unit**: +The fifth segment. Two digits that name one building or delivery point within an area. +_Avoid_: building number, house number, delivery unit + +**Partial postcode**: +A postcode that stops after the state, LGA, district or area segment. +_Avoid_: prefix, incomplete code +Exception: `prefix` is the name of the first parameter of `contains`. + +**Precision**: +The most specific segment that a postcode or a result contains. +_Avoid_: level, granularity, resolution + +### Forms of a postcode + +**Compact form**: +The postcode with no separators, for example `EK01A03FK01`. +_Avoid_: raw, plain + +**Canonical form**: +The postcode with hyphens between segments, for example `EK-01-A03-FK-01`. +_Avoid_: formatted, standard form + +**Display form**: +The postcode with spaces between segments, for example `EK 01 A03 FK 01`. +_Avoid_: pretty, human form + +**Normalise**: +To change input in three steps, in this order: apply NFKC, remove the separators, and change the ASCII letters to upper case. `grammar.md` defines the steps. +_Avoid_: sanitise, canonicalise + +**Code point**: +One Unicode code point. It is not a UTF-16 unit, a byte or a grapheme cluster. For example, U+1D404 is one code point and two UTF-16 units. +_Avoid_: character (when you count input), char + +**Input limit**: +The most code points that `parse` and `isLegacy` read. It is `maxInputCodePoints` in `data/format.json`. Over the limit, `parse` gives `bad_length` and `isLegacy` gives false. Both functions apply the limit before they normalise. +_Avoid_: max length, length limit, size limit + +**Suggestion**: +The canonical form that `parse` offers with `unknown_state` or `bad_segment`, when a fix of look-alike characters makes the code parse. It is a hint, never a success. The word means this hint from `parse`. It does not mean the suggestions that NIPOST's autocomplete API returns. +_Avoid_: correction, autocorrect + +**Parse result**: +What `parse` returns: a postcode, or a parse error. The Interface section of `grammar.md` defines its fields. +_Avoid_: outcome, response + +**Parse error**: +The reason that `parse` rejected its input. It has a code, the failing segment and a suggestion. It is a value that `parse` returns, not an exception. +_Avoid_: validation error, exception + +### NIPOST's API + +**Gateway**: +NIPOST's public API at `api.postcode.gov.ng`. +_Avoid_: server, backend, endpoint (for the whole API) + +**Lookup**: +A request to the gateway for the facts about one postcode. +_Avoid_: query, fetch, resolve + +**Lookup level**: +How much data a lookup returns, from 1 for validity only to 5 for point geometry. +_Avoid_: tier, access level, depth + +**Reverse geocode**: +A request for the nearest unit to a coordinate. +_Avoid_: reverse lookup, locate + +**Accuracy**: +The radius in metres within which a GPS fix is likely correct. +_Avoid_: precision, error margin + +**Secret key**: +An API key for servers. It starts with `nipost_test_` or `nipost_live_`. +_Avoid_: private key, API secret + +**Publishable key**: +An API key that can ship inside an app or a web page. It starts with `nipost_pk_`. +_Avoid_: public key, client key + +**Widget**: +NIPOST's own postcode picker. Gatepost does not build widgets. +_Avoid_: using this word for a Gatepost component + +### Gatepost + +**Spec**: +The `spec` repo. It holds the grammar, the data, the vectors and the standards. The API fixtures and the completed OpenAPI file come later. +_Avoid_: schema, contract (for the repo) + +**Vector**: +One shared test case in `spec/vectors/` that every SDK must pass. +_Avoid_: fixture, example, golden file + +**Fixture**: +Synthetic data that the mock server returns. +_Avoid_: vector, stub, sample + +**Mock server**: +The local server that imitates the gateway with fixtures. +_Avoid_: fake API, stub server, sandbox + +**SDK**: +The core and the client for one language. +_Avoid_: library (for the pair), wrapper, binding + +**Core**: +The part of an SDK that parses and formats postcodes without network access. +_Avoid_: utils, base, common + +**Client**: +The part of an SDK that calls the gateway. +_Avoid_: API wrapper, service, fetcher + +**Field**: +A Gatepost UI component in which a user enters or picks a postcode. +_Avoid_: input, widget, picker + +**Confirm**: +To show the user what a postcode points to before the app stores it. +_Avoid_: verify, validate (for this step) + +**Tell**: +A habit that makes code look machine-written, such as very long lines. +_Avoid_: smell (smells are design problems), anti-pattern diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSES/Unicode-3.0.txt b/LICENSES/Unicode-3.0.txt new file mode 100644 index 0000000..11f2842 --- /dev/null +++ b/LICENSES/Unicode-3.0.txt @@ -0,0 +1,39 @@ +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2023 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..799ea06 --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +Gatepost +Copyright 2026 The Gatepost authors + +This product is unofficial. The Nigerian Postal Service (NIPOST) did not make it and does not endorse it. +Gatepost uses the name NIPOST only to describe compatibility with NIPOST's National Digital Postcode. + +The file src/Internal/NfkcTable.php holds data from UnicodeData.txt, a file of the Unicode Character Database 17.0.0. Copyright 1991-2026 Unicode, Inc. +The Unicode License v3 (SPDX identifier Unicode-3.0) covers this data. LICENSES/Unicode-3.0.txt holds the licence text. diff --git a/README.md b/README.md new file mode 100644 index 0000000..396e2ee --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ + + + gatepost + + +# gatepost/postcode + +Parse, check and format Nigeria's digital postcodes in PHP, with no network access. + +> Unofficial. Not made or endorsed by NIPOST. + +[![CI](https://github.com/gatepost-dev/php/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/gatepost-dev/php/actions/workflows/ci.yml) +[![Licence](https://img.shields.io/badge/licence-Apache--2.0-blue)](https://github.com/gatepost-dev/php/blob/main/LICENSE) + +## Install + +```sh +composer require gatepost/postcode:^0.1@alpha +``` + +Composer installs only stable releases unless the constraint allows an alpha. + +## Quickstart + +```php +isOk()) { + echo $result->value->canonical; // EK-01-A03-FK-01 +} else { + echo $result->error->code->value; // for example bad_length +} +``` + +`parse` accepts any spacing, dashes and letter case. For any string, it returns a result and never throws. + +## What it does + +- Reads codes that users type or paste, including full-width letters and digits. +- Names the problem in a bad code, and suggests a fix for common typos, such as O in place of 0. +- Recognises old 6-digit postcodes. +- Writes a code in its compact, canonical and display forms. +- Shortens a code to its area, district, LGA or state, and checks whether one code contains another. +- Hides the unit of a code for logs. +- Runs without the intl and mbstring extensions. + +### Limit the input + +`parse` reads at most 64 code points of input. This is the input limit. Longer input fails with `ParseErrorCode::BadLength`, even when it holds a valid code. The check runs before `parse` normalises the input, so long text cannot stall a server. `isLegacy` applies the same limit. + +```php +isOk() ? 'ok' : 'failed', "\n"; // ok +echo Postcode::parse(str_pad($code, 65))->error?->code->value, "\n"; // bad_length +``` + +`normalize` has no input limit, so call `parse` for text from an untrusted source. + +## Requirements + +| Requirement | Version | +|---|---| +| PHP | 8.1 or later | +| Gatepost spec | 0.1.0 | + +## Docs + +The Gatepost docs site will hold the guide and the API reference. Until it is live, each public method of `Postcode` has a doc comment with an example. + +## Support + +Ask questions and report bugs in [GitHub Issues](https://github.com/gatepost-dev/php/issues). Report security problems privately, as [`SECURITY.md`](https://github.com/gatepost-dev/.github/blob/main/SECURITY.md) describes. + +## Contributing + +Read [`CONTRIBUTING.md`](https://github.com/gatepost-dev/.github/blob/main/CONTRIBUTING.md) before you open a pull request. The title and the body of the pull request become the squash commit on `main`. The title and the suffix ` (#N)` that GitHub adds have at most 72 characters together. + +To run every check, you need PHP 8.4 with Xdebug, Composer, Python 3.11 or later, uv, gitleaks and changie. Clone with `git clone --recurse-submodules`, then run: + +```sh +composer install +composer --working-dir=tools/infection install +composer --working-dir=tools/bc-check install --ignore-platform-req=ext-intl +composer check +``` + +The BC check tool lists ext-intl as a requirement. Its check runs without that extension, so the install line ignores the requirement. The line also works on a machine that has ext-intl. + +## Licence + +Apache-2.0. See [`LICENSE`](https://github.com/gatepost-dev/php/blob/main/LICENSE) and [`NOTICE`](https://github.com/gatepost-dev/php/blob/main/NOTICE). diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..06964b5 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,9 @@ +version = 1 + +[[annotations]] +path = [ + "**/*.md", "**/*.json", "**/composer.lock", ".gitmodules", ".changes/**", "NOTICE", +] +precedence = "aggregate" +SPDX-FileCopyrightText = "2026 The Gatepost authors" +SPDX-License-Identifier = "Apache-2.0" diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..465caf1 --- /dev/null +++ b/composer.json @@ -0,0 +1,108 @@ +{ + "name": "gatepost/postcode", + "description": "Unofficial. Not made or endorsed by NIPOST. Parse Nigerian postcodes offline.", + "license": "Apache-2.0", + "type": "library", + "require": { + "php": "^8.1" + }, + "require-dev": { + "composer/semver": "^3.5.0", + "ergebnis/composer-normalize": "^2.54.0", + "friendsofphp/php-cs-fixer": "^3.95.27", + "phpmd/phpmd": "^2.15.0", + "phpstan/phpstan": "^2.2.16", + "phpstan/phpstan-deprecation-rules": "^2.0.5", + "phpstan/phpstan-phpunit": "^2.0.21", + "phpstan/phpstan-strict-rules": "^2.0.12", + "phpunit/phpunit": "^10.5.65 || ^13.4.0" + }, + "autoload": { + "psr-4": { + "Gatepost\\Postcode\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Gatepost\\Postcode\\Tests\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + }, + "platform": { + "php": "8.1.0" + }, + "sort-packages": true + }, + "scripts": { + "api:check": "scripts/check-api.sh", + "api:dump": "@php scripts/dump-api.php", + "api:dump:check": "@php scripts/dump-api.php --check", + "changes:check": "changie merge --dry-run | cmp - CHANGELOG.md", + "check": [ + "@generate:check", + "@context:check", + "@composer validate --strict", + "@composer normalize --dry-run", + "@composer dump-autoload --optimize --strict-psr --no-scripts", + "@cs:check", + "@phpstan", + "@phpmd", + "@test:coverage", + "@api:dump:check", + "@api:check", + "@changes:check", + "@licences", + "@deps:licences", + "@secrets", + "@workflows:check", + "@tells" + ], + "context:check": "cmp CONTEXT.md spec/CONTEXT.md", + "cs": "php-cs-fixer fix", + "cs:check": "php-cs-fixer fix --dry-run --diff", + "deps:licences": "@php scripts/check-licences.php", + "generate": "@php scripts/generate-data.php", + "generate:check": "@php scripts/generate-data.php --check", + "licences": "uvx --with charset-normalizer==3.5.2 reuse@6.2.0 lint", + "mutation": [ + "@putenv XDEBUG_MODE=coverage", + "tools/infection/vendor/bin/infection --threads=max" + ], + "phpmd": "@php -d error_reporting=24575 vendor/bin/phpmd scripts,src,tests text phpmd.xml", + "phpstan": "phpstan analyse --no-progress", + "secrets": "gitleaks git --no-banner --redact .", + "tells": "python3 spec/scripts/check-tells", + "test": "phpunit", + "test:coverage": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --path-coverage --coverage-cobertura build/coverage/cobertura.xml", + "@php scripts/check-coverage.php build/coverage/cobertura.xml" + ], + "workflows:check": "uvx zizmor@1.30.1 --offline .github/workflows" + }, + "scripts-descriptions": { + "api:check": "Compare the public API with the newest release tag.", + "api:dump": "Write the list of the public symbols to docs/api-dump.md.", + "api:dump:check": "Check that docs/api-dump.md matches the public symbols.", + "changes:check": "Check that CHANGELOG.md matches the change files.", + "check": "Run every check that CI runs.", + "context:check": "Check that CONTEXT.md matches the copy in the spec.", + "cs": "Fix the code style.", + "cs:check": "Check the code style.", + "deps:licences": "Check that each locked package has a permissive licence (DEP-4).", + "generate": "Write the generated classes in src/Internal.", + "generate:check": "Check that the generated classes are up to date.", + "licences": "Check the licence information of each file.", + "mutation": "Run the mutation tests. They need Xdebug.", + "phpmd": "Check size and complexity. 24575 hides the PHP 8.4 notices of PHPMD itself.", + "phpstan": "Run static analysis.", + "secrets": "Scan the git history for secrets.", + "tells": "Check for agent tells.", + "test": "Run the tests.", + "test:coverage": "Run the tests with branch coverage. They need Xdebug.", + "workflows:check": "Check the workflows with zizmor." + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..f04e432 --- /dev/null +++ b/composer.lock @@ -0,0 +1,5623 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "8437dea3b19e96c2b2e727b8cb79e4bd", + "packages": [], + "packages-dev": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, + { + "name": "composer/semver", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5", + "reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Version comparison library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.5.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-09-24T14:38:51+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "ergebnis/agent-detector", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/agent-detector.git", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.51.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "infection/infection": "^0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a detector for detecting the presence of an agent.", + "homepage": "https://github.com/ergebnis/agent-detector", + "support": { + "issues": "https://github.com/ergebnis/agent-detector/issues", + "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/agent-detector" + }, + "time": "2026-05-07T08:19:07+00:00" + }, + { + "name": "ergebnis/composer-normalize", + "version": "2.54.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/composer-normalize.git", + "reference": "6a247926bc4e85b06bb12fa23d9593bc973be8d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/6a247926bc4e85b06bb12fa23d9593bc973be8d9", + "reference": "6a247926bc4e85b06bb12fa23d9593bc973be8d9", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.0.0", + "ergebnis/json": "^1.4.0", + "ergebnis/json-normalizer": "^4.9.0", + "ergebnis/json-printer": "^3.7.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", + "localheinz/diff": "^1.3.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "composer/composer": "^2.10.3", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.63.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.25.0", + "ergebnis/rector-rules": "^1.18.3", + "fakerphp/faker": "^1.24.1", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.55", + "phpstan/phpstan-deprecation-rules": "^2.0.5", + "phpstan/phpstan-phpunit": "^2.0.18", + "phpstan/phpstan-strict-rules": "^2.0.12", + "phpunit/phpunit": "^9.6.33", + "rector/rector": "^2.6.7", + "symfony/filesystem": "^5.4.41" + }, + "type": "composer-plugin", + "extra": { + "class": "Ergebnis\\Composer\\Normalize\\NormalizePlugin", + "branch-alias": { + "dev-main": "2.54-dev" + }, + "plugin-optional": true, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Composer\\Normalize\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a composer plugin for normalizing composer.json.", + "homepage": "https://github.com/ergebnis/composer-normalize", + "keywords": [ + "composer", + "normalize", + "normalizer", + "plugin" + ], + "support": { + "issues": "https://github.com/ergebnis/composer-normalize/issues", + "security": "https://github.com/ergebnis/composer-normalize/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/composer-normalize" + }, + "time": "2026-09-24T19:41:19+00:00" + }, + { + "name": "ergebnis/json", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json.git", + "reference": "829ce8b25c5e452249492c824f1d227777709403" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json/zipball/829ce8b25c5e452249492c824f1d227777709403", + "reference": "829ce8b25c5e452249492c824f1d227777709403", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/data-provider": "^3.6.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.63.3", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.25.0", + "ergebnis/rector-rules": "^1.18.4", + "fakerphp/faker": "^1.24.1", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.2.14", + "phpstan/phpstan-deprecation-rules": "^2.0.5", + "phpstan/phpstan-phpunit": "^2.0.18", + "phpstan/phpstan-strict-rules": "^2.0.12", + "phpunit/phpunit": "^9.6.24", + "rector/rector": "^2.6.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.7-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a Json value object for representing a valid JSON string.", + "homepage": "https://github.com/ergebnis/json", + "keywords": [ + "json" + ], + "support": { + "issues": "https://github.com/ergebnis/json/issues", + "security": "https://github.com/ergebnis/json/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json" + }, + "time": "2026-09-24T19:42:16+00:00" + }, + { + "name": "ergebnis/json-normalizer", + "version": "4.11.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-normalizer.git", + "reference": "db539052df534709ca899620844bf046d0fa34f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-normalizer/zipball/db539052df534709ca899620844bf046d0fa34f2", + "reference": "db539052df534709ca899620844bf046d0fa34f2", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.2.0", + "ergebnis/json-pointer": "^3.4.0", + "ergebnis/json-printer": "^3.5.0", + "ergebnis/json-schema-validator": "^4.2.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "composer/semver": "^3.4.4", + "ergebnis/data-provider": "^3.6.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.63.3", + "ergebnis/phpunit-slow-test-detector": "^2.25.0", + "ergebnis/rector-rules": "^1.18.4", + "fakerphp/faker": "^1.24.1", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.2.15", + "phpstan/phpstan-deprecation-rules": "^2.0.5", + "phpstan/phpstan-phpunit": "^2.0.18", + "phpstan/phpstan-strict-rules": "^2.0.12", + "phpunit/phpunit": "^9.6.19", + "rector/rector": "^2.6.7" + }, + "suggest": { + "composer/semver": "If you want to use ComposerJsonNormalizer or VersionConstraintNormalizer" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.11-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Normalizer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides generic and vendor-specific normalizers for normalizing JSON documents.", + "homepage": "https://github.com/ergebnis/json-normalizer", + "keywords": [ + "json", + "normalizer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-normalizer/issues", + "security": "https://github.com/ergebnis/json-normalizer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-normalizer" + }, + "time": "2026-09-24T19:42:25+00:00" + }, + { + "name": "ergebnis/json-pointer", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-pointer.git", + "reference": "414a90039cdf458eeb71297315a2be6de195697a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-pointer/zipball/414a90039cdf458eeb71297315a2be6de195697a", + "reference": "414a90039cdf458eeb71297315a2be6de195697a", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/data-provider": "^3.6.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.63.3", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.25.0", + "ergebnis/rector-rules": "^1.18.3", + "fakerphp/faker": "^1.24.1", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.2.14", + "phpstan/phpstan-deprecation-rules": "^2.0.5", + "phpstan/phpstan-phpunit": "^2.0.18", + "phpstan/phpstan-strict-rules": "^2.0.12", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.6.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.9-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Pointer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides an abstraction of a JSON pointer.", + "homepage": "https://github.com/ergebnis/json-pointer", + "keywords": [ + "RFC6901", + "json", + "pointer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-pointer/issues", + "security": "https://github.com/ergebnis/json-pointer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-pointer" + }, + "time": "2026-09-24T19:42:34+00:00" + }, + { + "name": "ergebnis/json-printer", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-printer.git", + "reference": "7c8f49d86229947b4034e4637da95cb4390073e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-printer/zipball/7c8f49d86229947b4034e4637da95cb4390073e6", + "reference": "7c8f49d86229947b4034e4637da95cb4390073e6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/data-provider": "^3.6.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.63.3", + "ergebnis/phpunit-slow-test-detector": "^2.25.0", + "ergebnis/rector-rules": "^1.18.4", + "fakerphp/faker": "^1.24.0", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.2.15", + "phpstan/phpstan-deprecation-rules": "^2.0.5", + "phpstan/phpstan-phpunit": "^2.0.18", + "phpstan/phpstan-strict-rules": "^2.0.12", + "phpunit/phpunit": "^9.6.21", + "rector/rector": "^2.6.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.9-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Printer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON printer, allowing for flexible indentation.", + "homepage": "https://github.com/ergebnis/json-printer", + "keywords": [ + "formatter", + "json", + "printer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-printer/issues", + "security": "https://github.com/ergebnis/json-printer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-printer" + }, + "time": "2026-09-24T19:42:43+00:00" + }, + { + "name": "ergebnis/json-schema-validator", + "version": "4.6.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-schema-validator.git", + "reference": "228bf86d963c06c863a3143c9d8531bb3ef72fbe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-schema-validator/zipball/228bf86d963c06c863a3143c9d8531bb3ef72fbe", + "reference": "228bf86d963c06c863a3143c9d8531bb3ef72fbe", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.2.0", + "ergebnis/json-pointer": "^3.4.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/data-provider": "^3.6.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.63.3", + "ergebnis/phpunit-slow-test-detector": "^2.25.0", + "ergebnis/rector-rules": "^1.18.4", + "fakerphp/faker": "^1.24.0", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.2.15", + "phpstan/phpstan-deprecation-rules": "^2.0.5", + "phpstan/phpstan-phpunit": "^2.0.18", + "phpstan/phpstan-strict-rules": "^2.0.12", + "phpunit/phpunit": "^9.6.20", + "rector/rector": "^2.6.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.6-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\SchemaValidator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON schema validator, building on top of justinrainbow/json-schema.", + "homepage": "https://github.com/ergebnis/json-schema-validator", + "keywords": [ + "json", + "schema", + "validator" + ], + "support": { + "issues": "https://github.com/ergebnis/json-schema-validator/issues", + "security": "https://github.com/ergebnis/json-schema-validator/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-schema-validator" + }, + "time": "2026-09-24T19:42:52+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "d384a7d0d6e67aa497327260ca95023e92dc2032" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/d384a7d0d6e67aa497327260ca95023e92dc2032", + "reference": "d384a7d0d6e67aa497327260ca95023e92dc2032", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0 || ^1.1.2", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "symfony/polyfill-mbstring": "^1.33", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.4.1" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2026-09-29T22:11:39+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.95.27", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "1d0c68ee5f37446d8ad5970b82384cd1bb0a3317" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/1d0c68ee5f37446d8ad5970b82384cd1bb0a3317", + "reference": "1d0c68ee5f37446d8ad5970b82384cd1bb0a3317", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.3", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.5", + "ergebnis/agent-detector": "^1.2", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.3", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0 || ^9.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.37", + "symfony/polyfill-php80": "^1.37", + "symfony/polyfill-php81": "^1.37", + "symfony/polyfill-php84": "^1.37", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.11.0", + "infection/infection": "^0.32.7", + "justinrainbow/json-schema": "^6.12.0", + "keradus/cli-executor": "^2.3", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", + "phpunit/phpunit": "^9.6.36 || ^10.5.64 || ^11.5.56 || ^12.5.35 || ^13.3.3", + "symfony/polyfill-php85": "^1.41", + "symfony/var-dumper": "^5.4.48 || ^6.4.45 || ^7.4.18 || ^8.1.7", + "symfony/yaml": "^5.4.53 || ^6.4.45 || ^7.4.18 || ^8.1.6" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/**/Internal/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.27" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2026-09-22T09:54:55+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "6.13.1", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "e54aacc4c0657b3a4af4f628428d2702146828c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/e54aacc4c0657b3a4af4f628428d2702146828c6", + "reference": "e54aacc4c0657b3a4af4f628428d2702146828c6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-json": "*", + "marc-mabe/php-enum": "^4.4", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "dev-main", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Danny van der Sluijs", + "email": "danny.vandersluijs@icloud.com", + "role": "Maintainer" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.13.1" + }, + "time": "2026-09-30T07:42:28+00:00" + }, + { + "name": "localheinz/diff", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/localheinz/diff.git", + "reference": "33bd840935970cda6691c23fc7d94ae764c0734c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/localheinz/diff/zipball/33bd840935970cda6691c23fc7d94ae764c0734c", + "reference": "33bd840935970cda6691c23fc7d94ae764c0734c", + "shasum": "" + }, + "require": { + "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.0 || ^8.5.23", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Fork of sebastian/diff for use with ergebnis/composer-normalize", + "homepage": "https://github.com/localheinz/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/localheinz/diff/issues", + "source": "https://github.com/localheinz/diff/tree/1.3.0" + }, + "time": "2025-08-30T09:44:18+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "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.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, + { + "name": "pdepend/pdepend", + "version": "2.16.2", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "shasum": "" + }, + "require": { + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/polyfill-mbstring": "^1.19" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0|^1.2.3", + "gregwar/rst": "^1.0", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "keywords": [ + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" + ], + "support": { + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2023-12-17T18:09:59+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpmd/phpmd", + "version": "2.15.0", + "source": { + "type": "git", + "url": "https://github.com/phpmd/phpmd.git", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0", + "ext-xml": "*", + "pdepend/pdepend": "^2.16.1", + "php": ">=5.3.9" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", + "gregwar/rst": "^1.0", + "mikey179/vfsstream": "^1.6.8", + "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHPMD\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, + { + "name": "Marc Würth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" + }, + { + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" + } + ], + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "https://phpmd.org/", + "keywords": [ + "dev", + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" + ], + "support": { + "irc": "irc://irc.freenode.org/phpmd", + "issues": "https://github.com/phpmd/phpmd/issues", + "source": "https://github.com/phpmd/phpmd/tree/2.15.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], + "time": "2023-12-11T08:22:20+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.2.16", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92", + "reference": "46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92", + "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-09-25T09:31:51+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "67bedd65c24bc72840afc45aed48b1059dd44bec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/67bedd65c24bc72840afc45aed48b1059dd44bec", + "reference": "67bedd65c24bc72840afc45aed48b1059dd44bec", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.39" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "shipmonk/name-collision-detector": "^2.1" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.5" + }, + "time": "2026-07-22T06:50:43+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "2.0.21", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "8dbdc3a3e2d7c5dfea655d20ecca5bc9b1b1e441" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/8dbdc3a3e2d7c5dfea655d20ecca5bc9b1b1e441", + "reference": "8dbdc3a3e2d7c5dfea655d20ecca5bc9b1b1e441", + "shasum": "" + }, + "require": { + "phar-io/version": "^3.2", + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.2.6" + }, + "conflict": { + "phpunit/phpunit": "<7.0" + }, + "require-dev": { + "nikic/php-parser": "^5", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "shipmonk/name-collision-detector": "^2.1" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPUnit extensions and rules for PHPStan", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.21" + }, + "time": "2026-10-01T11:59:29+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.12", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2bc5ae19ae965663b62ac907ee6342c3903ec93b", + "reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.52" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "shipmonk/name-collision-detector": "^2.1" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.12" + }, + "time": "2026-07-19T07:24:06+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.65", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b4f678d4ebb686d6d3bd6a7c193e8a067d1a7f01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b4f678d4ebb686d6d3bd6a7c193e8a067d1a7f01", + "reference": "b4f678d4ebb686d6d3bd6a7c193e8a067d1a7f01", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.2", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.65" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-09-23T10:01:12+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.7", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-12-23T15:25:20+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5d32fe257a9b39cb63146924d6b4e32a22d4502a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5d32fe257a9b39cb63146924d6b4e32a22d4502a", + "reference": "5d32fe257a9b39cb63146924d6b4e32a22d4502a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2026-08-11T05:27:39+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "symfony/config", + "version": "v6.4.44", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "6a5abf67fd5b138df96984fe6f8fd736fb7d6b29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/6a5abf67fd5b138df96984fe6f8fd736fb7d6b29", + "reference": "6a5abf67fd5b138df96984fe6f8fd736fb7d6b29", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v6.4.44" + }, + "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-20T09:38:57+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0f758dfab4608cac6a342b7e7f80cb84c9cc98ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0f758dfab4608cac6a342b7e7f80cb84c9cc98ca", + "reference": "0f758dfab4608cac6a342b7e7f80cb84c9cc98ca", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.47" + }, + "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-09-18T10:23:48+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v6.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "c7ffb6da26739b461ae18dca3438bc68c35cd2a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c7ffb6da26739b461ae18dca3438bc68c35cd2a0", + "reference": "c7ffb6da26739b461ae18dca3438bc68c35cd2a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4.20|^7.2.5" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.1", + "symfony/finder": "<5.4", + "symfony/proxy-manager-bridge": "<6.3", + "symfony/yaml": "<5.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.1|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.47" + }, + "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-09-29T10:29:26+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "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": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-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/event-dispatcher", + "version": "v6.4.44", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "d7110a878e7e7cbd8aaebe9f55da8af885b8c0af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d7110a878e7e7cbd8aaebe9f55da8af885b8c0af", + "reference": "d7110a878e7e7cbd8aaebe9f55da8af885b8c0af", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.44" + }, + "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-21T14:01:40+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "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\\EventDispatcher\\": "" + } + }, + "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 dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-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/filesystem", + "version": "v6.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "6dabf41c21957a2060f101bd7bf7e322989866bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/6dabf41c21957a2060f101bd7bf7e322989866bd", + "reference": "6dabf41c21957a2060f101bd7bf7e322989866bd", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.4.45" + }, + "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-23T07:59:31+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "c8d1b2a09bb820175d1f2f9a3cf30303200b10a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/c8d1b2a09bb820175d1f2f9a3cf30303200b10a7", + "reference": "c8d1b2a09bb820175d1f2f9a3cf30303200b10a7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.47" + }, + "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-09-25T09:03:59+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.4.30", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "eeaa8cabe54c7b3516938c72a4a161c0cc80a34f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/eeaa8cabe54c7b3516938c72a4a161c0cc80a34f", + "reference": "eeaa8cabe54c7b3516938c72a4a161c0cc80a34f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.4.30" + }, + "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": "2025-11-12T13:06:53+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/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": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "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 for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.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": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "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 for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.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": "2026-09-23T22:02:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "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 for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.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": "2026-09-24T15:19:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "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\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.43.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": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "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\\Php81\\": "" + }, + "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 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.38.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-05-26T12:45:58+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/22c6548499cbe5c5bb072fc0c42312dff062a015", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015", + "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\\Php84\\": "" + }, + "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 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.43.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": "2026-09-23T22:01:09+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.46", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b9f3d9ddf67543b92dbfa6d210c407d7a7982781" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b9f3d9ddf67543b92dbfa6d210c407d7a7982781", + "reference": "b9f3d9ddf67543b92dbfa6d210c407d7a7982781", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.46" + }, + "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-09-02T12:35:55+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "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\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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 writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "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-27T15:39:01+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b67e94e06a05d9572c2fa354483b3e13e3cb1898", + "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v6.4.24" + }, + "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": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.46", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "4513ab2b0ad2d7845a2f920e2f30b61633c9e606" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/4513ab2b0ad2d7845a2f920e2f30b61633c9e606", + "reference": "4513ab2b0ad2d7845a2f920e2f30b61633c9e606", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "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 an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.46" + }, + "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-09-11T09:03:57+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v6.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "a4e458999ee554a667cd19227f0b97a751bd61e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/a4e458999ee554a667cd19227f0b97a751bd61e6", + "reference": "a4e458999ee554a667cd19227f0b97a751bd61e6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.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/v6.4.45" + }, + "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-23T09:39:49+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "8.1.0" + }, + "plugin-api-version": "2.6.0" +} diff --git a/docs/adr/0001-support-php-8-1.md b/docs/adr/0001-support-php-8-1.md new file mode 100644 index 0000000..66a1b7d --- /dev/null +++ b/docs/adr/0001-support-php-8-1.md @@ -0,0 +1,9 @@ +# Support PHP 8.1 after its end of life + +PHP 8.1 reached its end of life on 31 Dec 2025. VER-1 asks for an ADR when a runtime floor is past its end of life. + +On 2 Oct 2026, the WordPress.org statistics (https://api.wordpress.org/stats/php/1.0/) showed 11.1 % of WordPress sites on PHP 8.1. They showed 73.6 % of sites on PHP 8.1 or later. Gatepost's planned WooCommerce plugin will bundle this package. The plugin must run on the stores that still use PHP 8.1, so the package keeps PHP 8.1 as its floor. + +CI tests PHP 8.1 and the newest PHP release. The cost is a PHP 8.1 test job, and a few newer language features that the code cannot use, such as readonly classes. PHPUnit 11 and later need PHP 8.2, so the PHP 8.1 job runs PHPUnit 10. + +Check these numbers again before each minor release. Raise the floor in a minor release (VER-2) when the numbers no longer justify the cost. diff --git a/docs/adr/0002-nfkc-from-a-generated-table.md b/docs/adr/0002-nfkc-from-a-generated-table.md new file mode 100644 index 0000000..e5f09f1 --- /dev/null +++ b/docs/adr/0002-nfkc-from-a-generated-table.md @@ -0,0 +1,5 @@ +# Apply NFKC from a generated table, not from ext-intl + +The grammar applies Unicode NFKC before it checks a code. PHP has NFKC only in ext-intl, which many WordPress hosts lack (PHP-9), and the Symfony polyfill would add a runtime dependency to the core (DEP-1). So `scripts/generate-data.php` reads UnicodeData.txt 17.0.0, the NFKC baseline that grammar.md names, and writes `src/Internal/NfkcTable.php`. The table maps each character whose NFKC form holds only ASCII characters and separators to that form, and `Postcode::normalize()` applies it with `strtr()`. + +For these characters the table equals NFKC. None of the characters in their decompositions is a combining mark, so nothing in a decomposition composes. A combining mark next to a table character can compose with it in full NFKC, as U+0300 does with the letter A. The composed character is not ASCII. The table leaves the mark in place, and the mark is not ASCII either. `parse` rejects both results as `bad_character`, so `parse` and `isLegacy` give the same result as an SDK with full NFKC for every input. `normalize` keeps every other character as it is, so its result can differ from NFKC for text that holds no postcode, such as the micro sign U+00B5. diff --git a/docs/api-dump.md b/docs/api-dump.md new file mode 100644 index 0000000..c00ff7b --- /dev/null +++ b/docs/api-dump.md @@ -0,0 +1,68 @@ +# API dump of gatepost/postcode + +Do not edit this file. `composer api:dump` writes it from the public symbols of the package. It leaves out each `Internal` namespace and each symbol with `@internal` in its doc comment. It shows native types only, so a type that only a doc comment gives is not here. `composer api:dump:check` fails when this file is out of date. + +```php +namespace Gatepost\Postcode; + +final class ParseError +{ + public readonly ParseErrorCode $code; + public readonly ?Precision $segment; + public readonly ?string $suggestion; +} + +enum ParseErrorCode: string +{ + case Empty = 'empty'; + case LegacyCode = 'legacy_code'; + case BadCharacter = 'bad_character'; + case BadLength = 'bad_length'; + case UnknownState = 'unknown_state'; + case BadSegment = 'bad_segment'; +} + +final class ParseResult +{ + public readonly ?Postcode $value; + public readonly ?ParseError $error; + public function isOk(): bool; +} + +final class Postcode +{ + public const SPEC_VERSION = '0.1.0'; + public readonly string $compact; + public readonly string $canonical; + public readonly string $display; + public readonly Segments $segments; + public readonly Precision $precision; + public static function parse(string $input, bool $allowPartial = false): ParseResult; + public static function normalize(string $input): string; + public static function isLegacy(string $input): bool; + public static function stateName(string $stateCode): ?string; + public static function precisionForAccuracy(?float $accuracyM): Precision; + public function truncate(Precision $to): self; + public function parent(): ?self; + public function contains(self $code): bool; + public function redact(): string; +} + +enum Precision: string +{ + case State = 'state'; + case Lga = 'lga'; + case District = 'district'; + case Area = 'area'; + case Unit = 'unit'; +} + +final class Segments +{ + public readonly string $state; + public readonly ?string $lga; + public readonly ?string $district; + public readonly ?string $area; + public readonly ?string $unit; +} +``` diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 0000000..9530fc8 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 The Gatepost authors +// SPDX-License-Identifier: Apache-2.0 +{ + "$schema": "tools/infection/vendor/infection/infection/resources/schema.json", + "source": { + "directories": ["src"], + // The generator writes these files, and its --check mode keeps them in step. + "excludes": ["Internal/NfkcTable.php", "Internal/SpecData.php"], + }, + "phpUnit": { + "customPath": "vendor/bin/phpunit", + }, + "mutators": { + "@default": true, + }, + "minMsi": 85, + "tmpDir": "build/infection", + "logs": { + "text": "build/infection/infection.log", + }, +} diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 0000000..dfa151b --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,30 @@ + + + + + Size and complexity limits from php.md, for TELL-2 and TELL-3. + + src/Internal/NfkcTable.php + src/Internal/SpecData.php + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 0000000..e0636e1 --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +includes: + - vendor/phpstan/phpstan-deprecation-rules/rules.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon + +parameters: + level: max + # The package supports PHP 8.1 and every later release. + phpVersion: + min: 80100 + max: 80599 + paths: + - scripts + - src + - tests + tmpDir: build/phpstan diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..c8eeccb --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + + + + + + tests + + + + + src + + + diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..cfa86f7 --- /dev/null +++ b/renovate.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "description": [ + "Each pull request body ends with the sign-off line that the pull request job needs.", + "prHeader holds the line because a JSON string cannot wrap, and the template ends with it." + ], + "extends": ["schedule:weekly", "helpers:pinGitHubActionDigests", ":gitSignOff"], + "semanticCommits": "enabled", + "semanticCommitType": "chore", + "semanticCommitScope": "deps", + "commitMessageTopic": "{{depName}}", + "customManagers": [ + { + "description": "Pins of the uvx tools in the scripts of composer.json", + "customType": "regex", + "managerFilePatterns": ["/^composer\\.json$/"], + "matchStrings": [ + "uvx (?:--with \\S+ )*(?[A-Za-z0-9][A-Za-z0-9._-]*)@(?[^\\s\"]+)", + "--with (?[A-Za-z0-9][A-Za-z0-9._-]*)==(?[^\\s\"]+)" + ], + "datasourceTemplate": "pypi" + }, + { + "description": [ + "Versions of the tools that the workflows download from GitHub releases.", + "A comment above each version names the repo of the tool." + ], + "customType": "regex", + "managerFilePatterns": ["/^\\.github/workflows/[^/]+\\.ya?ml$/"], + "matchStrings": [ + "# renovate: depName=(?\\S+)\\s+[A-Z_]+_VERSION: (?\\S+)" + ], + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.+)$" + } + ], + "prHeader": "Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>", + "prBodyTemplate": "Weekly dependency update.\n\n{{{header}}}" +} diff --git a/scripts/check-api.sh b/scripts/check-api.sh new file mode 100755 index 0000000..7f0e4af --- /dev/null +++ b/scripts/check-api.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +# Compares the public API with the newest release tag (GIT-7, API-13). Before the first tag, +# no released API exists, so the script says so and stops. +set -euo pipefail + +# Git sorts v0.1.0-alpha.0 above v0.1.0 unless the config names the suffix. Without the option, +# the check would compare a release with its own alpha. +tag="$(git -c versionsort.suffix=- tag --list 'v*' --sort=-version:refname | head -n 1)" +if [ -z "$tag" ]; then + echo "No release tag yet, so there is no released API to compare." + exit 0 +fi +exec tools/bc-check/vendor/bin/roave-backward-compatibility-check --from="$tag" diff --git a/scripts/check-commit-msg b/scripts/check-commit-msg new file mode 100755 index 0000000..32f6bba --- /dev/null +++ b/scripts/check-commit-msg @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 The Gatepost authors +# SPDX-License-Identifier: Apache-2.0 +# Checks one commit message: a Conventional Commits subject with a scope (GIT-1), a sign-off +# (GIT-2), no co-author line for an AI tool (GIT-3), then the agent tells that check-tells +# finds (TELL-14, TELL-18). The commit-msg hook and CI run it. +set -euo pipefail + +types='build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test' +scopes='core|client|repo|deps|release' +subject="$(sed -n '/^[^#[:space:]]/{p;q;}' "$1")" +if ! printf '%s\n' "$subject" | grep -Eq "^(${types})\((${scopes})\)!?: [^ ]"; then + echo "Write the subject as type(scope): summary, for example fix(core): reject a unit of 00." >&2 + echo "Types: ${types}. Scopes: ${scopes}." >&2 + exit 1 +fi +if ! grep -Eiq '^signed-off-by: .+ <.+>' "$1"; then + echo "Add a Signed-off-by line with a name and an email address. Use git commit -s (GIT-2)." >&2 + exit 1 +fi +# A person can be named Claude or Copilot, so the check reads the address of the trailer. It +# refuses each address at the domain of an AI company, and each address that an AI tool writes +# elsewhere. To refuse a new tool, add its address here and a row to CheckCommitMsgTest. +ai_addresses='[^ <>@]+@(anthropic|openai)\.com|noreply@aider\.chat|cursoragent@cursor\.com' +ai_addresses="${ai_addresses}|copilot@github\.com|claude-code@users\.noreply\.github\.com" +ai_addresses="${ai_addresses}|gemini-code-assist@google\.com" +ai_addresses="${ai_addresses}|[0-9]+\+copilot@users\.noreply\.github\.com" +if grep -Eiq "^co-authored-by:.*[ <](${ai_addresses})([> ]|\$)" "$1"; then + echo "Remove the Co-Authored-By line for an AI tool (GIT-3)." >&2 + exit 1 +fi +python3 spec/scripts/check-tells --commit-msg "$1" diff --git a/scripts/check-coverage.php b/scripts/check-coverage.php new file mode 100644 index 0000000..38f57c8 --- /dev/null +++ b/scripts/check-coverage.php @@ -0,0 +1,65 @@ + + */ +function unrunClasses(\DOMDocument $report): array +{ + $names = []; + foreach ($report->getElementsByTagName('class') as $class) { + $hasCoveredLine = (float) $class->getAttribute('line-rate') > 0.0; + if (!$hasCoveredLine && (int) $class->getAttribute('complexity') > 0) { + $names[] = $class->getAttribute('name'); + } + } + + return $names; +} + +$report = $argv[1] ?? ''; +$document = new \DOMDocument(); +$isReadable = \is_file($report) && \is_readable($report); +if (!$isReadable || !$document->load($report) || $document->documentElement === null) { + \fwrite(\STDERR, "Name a Cobertura report, such as build/coverage/cobertura.xml.\n"); + exit(2); +} +$lines = (int) $document->documentElement->getAttribute('lines-valid'); +$branches = (int) $document->documentElement->getAttribute('branches-valid'); +$covered = (int) $document->documentElement->getAttribute('branches-covered'); +if ($branches === 0 && $lines === 0) { + \fwrite(\STDOUT, "The report has no branches, so there is nothing to cover.\n"); + exit(0); +} +if ($branches === 0) { + \fwrite(\STDERR, "The report has lines and no branch data. Add --path-coverage to PHPUnit.\n"); + exit(1); +} +$unrun = unrunClasses($document); +if ($unrun !== []) { + $classes = \implode(', ', $unrun); + \fwrite(\STDERR, "No test runs these classes: {$classes}. Add a test for each class.\n"); + exit(1); +} +$percent = 100 * $covered / $branches; +$summary = \sprintf('Branch coverage is %.2f %% (%d of %d).', $percent, $covered, $branches); +if ($percent < BRANCH_FLOOR_PERCENT) { + \fwrite(\STDERR, $summary . ' The floor for core code is ' . BRANCH_FLOOR_PERCENT . " %.\n"); + exit(1); +} +\fwrite(\STDOUT, $summary . "\n"); diff --git a/scripts/check-licences.php b/scripts/check-licences.php new file mode 100644 index 0000000..d92e4c6 --- /dev/null +++ b/scripts/check-licences.php @@ -0,0 +1,168 @@ + $licences The items of the license field of a package. + */ +function hasAllowedLicence(array $licences): bool +{ + foreach ($licences as $item) { + if (\preg_match(LICENCE_ITEM, $item, $match) !== 1) { + continue; + } + $alternatives = \array_filter( + \explode(' ', $match[1]), + static fn(string $word): bool => \strcasecmp($word, 'or') !== 0, + ); + if (\array_intersect($alternatives, ALLOWED_LICENCES) !== []) { + return true; + } + } + + return false; +} + +/** + * @return array{name: string, version: string, licences: list} + */ +function lockedPackage(mixed $package, string $label): array +{ + $isNamed = \is_array($package) + && \is_string($package['name'] ?? null) + && \is_string($package['version'] ?? null); + if (!$isNamed) { + throw new RuntimeException("{$label} holds a package that has no name or no version."); + } + $licences = \is_array($package['license'] ?? null) ? $package['license'] : []; + + return [ + 'name' => $package['name'], + 'version' => $package['version'], + 'licences' => \array_values(\array_filter($licences, \is_string(...))), + ]; +} + +// A lock file is a JSON object, but the decoded value is not checked here. +function readLock(string $label, string $path): mixed +{ + $json = \is_file($path) && \is_readable($path) ? \file_get_contents($path) : false; + if ($json === false) { + throw new RuntimeException("Cannot read {$label}."); + } + $lock = \json_decode($json, true); + if ($lock === null && \json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException("{$label} is not valid JSON: " . \json_last_error_msg() . '.'); + } + + return $lock; +} + +/** + * @return list}> + */ +function lockedPackages(string $label, string $path): array +{ + $lock = readLock($label, $path); + $packages = []; + foreach (['packages', 'packages-dev'] as $key) { + $group = \is_array($lock) ? ($lock[$key] ?? null) : null; + foreach (\is_array($group) ? $group : [] as $package) { + $packages[] = lockedPackage($package, $label); + } + } + if ($packages === []) { + throw new RuntimeException("{$label} lists no package, so it proves nothing."); + } + + return $packages; +} + +/** + * @return array Each lock file of the repo, by its path from the root. + */ +function repoLocks(string $root): array +{ + $found = \glob("{$root}/tools/*/composer.lock"); + $toolLocks = $found === false ? [] : $found; + $locks = ['composer.lock' => "{$root}/composer.lock"]; + foreach ($toolLocks as $path) { + $locks[\substr($path, \strlen($root) + 1)] = $path; + } + + return $locks; +} + +/** + * @param list $arguments + */ +function checkLicences(string $root, array $arguments): int +{ + $locks = $arguments === [] ? repoLocks($root) : \array_combine($arguments, $arguments); + $violations = []; + $summaries = []; + try { + foreach ($locks as $label => $path) { + $packages = lockedPackages($label, $path); + foreach ($packages as $package) { + if (!hasAllowedLicence($package['licences'])) { + $licence = $package['licences'] === [] + ? 'no licence' + : 'the licence ' . \implode(' or ', $package['licences']); + $violations[] = "{$label}: {$package['name']} {$package['version']} has " + . "{$licence}."; + } + } + $count = \count($packages); + $noun = $count === 1 ? 'package' : 'packages'; + $summaries[] = "{$label}: {$count} {$noun}, each with an allowed licence."; + } + } catch (RuntimeException $problem) { + \fwrite(\STDERR, $problem->getMessage() . "\n"); + + return 2; + } + if ($violations === []) { + \fwrite(\STDOUT, \implode("\n", $summaries) . "\n"); + + return 0; + } + \fwrite(\STDERR, \implode("\n", $violations) . "\n"); + \fwrite(\STDERR, 'DEP-4 allows only these licences: ' . \implode(', ', ALLOWED_LICENCES) . '. ' + . "Replace the package, or change ALLOWED_LICENCES in this script after a review.\n"); + + return 1; +} + +exit(checkLicences(\dirname(__DIR__), \array_slice($argv, 1))); diff --git a/scripts/dump-api-symbols.php b/scripts/dump-api-symbols.php new file mode 100644 index 0000000..6a93a03 --- /dev/null +++ b/scripts/dump-api-symbols.php @@ -0,0 +1,335 @@ + $aliases The class names that PHP 8.5 gives for self and parent, + * each with the word that PHP 8.1 to 8.4 give. + */ +function typeText(?ReflectionType $type, string $namespace, array $aliases): string +{ + if ($type instanceof ReflectionNamedType) { + // PHP 8.5 reports the class for a type written self, and older versions report self. The + // dump writes self for both, so that it does not depend on the version. + $name = $aliases[$type->getName()] ?? $type->getName(); + $isClass = !$type->isBuiltin() && !\in_array($name, SELF_NAMES, true); + $text = $isClass ? relativeName($name, $namespace) : $name; + $isNullable = $type->allowsNull() && !\in_array($name, ['mixed', 'null'], true); + + return $isNullable ? "?{$text}" : $text; + } + if ($type instanceof ReflectionUnionType || $type instanceof ReflectionIntersectionType) { + $separator = $type instanceof ReflectionUnionType ? '|' : '&'; + + return \implode($separator, \array_map( + static fn(ReflectionType $part): string => typeText($part, $namespace, $aliases), + $type->getTypes(), + )); + } + + return ''; +} + +function floatLiteral(float $value): ?string +{ + $json = \json_encode($value, JSON_PRESERVE_ZERO_FRACTION); + + return $json === false ? null : $json; +} + +function stringLiteral(string $value): ?string +{ + if (\preg_match('/\A[ -~]*\z/', $value) !== 1) { + return null; + } + + return "'" . \strtr($value, ['\\' => '\\\\', "'" => "\\'"]) . "'"; +} + +// The script writes only the values that its tests cover. A value of another kind stops the run, +// so that the dump never holds a wrong or a missing value. +function literal(mixed $value, string $namespace): ?string +{ + return match (true) { + $value === null => 'null', + \is_bool($value) => $value ? 'true' : 'false', + \is_int($value) => (string) $value, + \is_float($value) => floatLiteral($value), + \is_string($value) => stringLiteral($value), + $value === [] => '[]', + $value instanceof UnitEnum => relativeName($value::class, $namespace) . '::' . $value->name, + default => null, + }; +} + +function unwritable(string $what, mixed $value): RuntimeException +{ + $type = \is_string($value) + ? 'string with a character outside printable ASCII' + : \get_debug_type($value); + + return new RuntimeException( + "Cannot write {$what}. The script writes null, booleans, numbers, printable ASCII text, " + . "an empty array and enum cases, but the value has the type {$type}.", + ); +} + +// The doc comments of the package say @internal for a public symbol that only the package calls. +function isInternal(string|false $docComment): bool +{ + return $docComment !== false && \str_contains($docComment, '@internal'); +} + +/** + * The class names that stand for self and parent in a type, as PHP 8.5 reports them. + * + * @param ReflectionClass $class + * + * @return array + */ +function typeAliases(ReflectionClass $class): array +{ + $parent = $class->getParentClass(); + + return [$class->name => 'self', ...($parent === false ? [] : [$parent->name => 'parent'])]; +} + +/** + * @param array $aliases + */ +function parameterText(ReflectionParameter $parameter, string $namespace, array $aliases): string +{ + $type = typeText($parameter->getType(), $namespace, $aliases); + $text = ($type === '' ? '' : "{$type} ") . ($parameter->isPassedByReference() ? '&' : '') + . ($parameter->isVariadic() ? '...' : '') . '$' . $parameter->name; + if (!$parameter->isDefaultValueAvailable()) { + return $text; + } + $default = $parameter->getDefaultValue(); + $owner = ($parameter->getDeclaringClass()->name ?? '') . '::' + . $parameter->getDeclaringFunction()->name . '()'; + + return $text . ' = ' . (literal($default, $namespace) + ?? throw unwritable("the default value of \${$parameter->name} in {$owner}", $default)); +} + +/** + * @param ReflectionClass $class + * + * @return list + */ +function caseLines(ReflectionClass $class, string $namespace): array +{ + if (!$class instanceof ReflectionEnum) { + return []; + } + $lines = []; + foreach ($class->getCases() as $case) { + if (!$case instanceof ReflectionEnumBackedCase) { + $lines[] = "case {$case->name};"; + continue; + } + $value = $case->getBackingValue(); + $what = "the value of the case {$class->name}::{$case->name}"; + $lines[] = "case {$case->name} = " . (literal($value, $namespace) + ?? throw unwritable($what, $value)) . ';'; + } + + return $lines; +} + +/** + * @param ReflectionClass $class + * + * @return list + */ +function publicConstantLines(ReflectionClass $class, string $namespace): array +{ + $lines = []; + foreach ($class->getReflectionConstants(ReflectionClassConstant::IS_PUBLIC) as $constant) { + $isOwn = $constant->getDeclaringClass()->name === $class->name; + if (!$isOwn || $constant->isEnumCase() || isInternal($constant->getDocComment())) { + continue; + } + $value = $constant->getValue(); + $lines[] = ($constant->isFinal() ? 'final ' : '') . "public const {$constant->name} = " + . (literal($value, $namespace) ?? throw unwritable( + "the value of the constant {$class->name}::{$constant->name}", + $value, + )) . ';'; + } + + return $lines; +} + +/** + * @param ReflectionClass $class + * + * @return list + */ +function propertyLines(ReflectionClass $class, string $namespace): array +{ + // PHP gives each enum the properties name and value, and an enum cannot declare more. + $properties = $class->isEnum() ? [] : $class->getProperties(ReflectionProperty::IS_PUBLIC); + $lines = []; + foreach ($properties as $property) { + $isOwn = $property->getDeclaringClass()->name === $class->name; + if (!$isOwn || isInternal($property->getDocComment())) { + continue; + } + $type = typeText($property->getType(), $namespace, typeAliases($class)); + $lines[] = 'public ' . ($property->isStatic() ? 'static ' : '') + . ($property->isReadOnly() ? 'readonly ' : '') + . ($type === '' ? '' : "{$type} ") . '$' . $property->name . ';'; + } + + return $lines; +} + +/** + * @param ReflectionClass $class + */ +function methodLine(ReflectionClass $class, ReflectionMethod $method, string $namespace): string +{ + $aliases = typeAliases($class); + $parameters = \array_map( + static fn(ReflectionParameter $parameter): string => parameterText( + $parameter, + $namespace, + $aliases, + ), + $method->getParameters(), + ); + $returns = typeText($method->getReturnType(), $namespace, $aliases); + // An interface method is abstract without saying so. + $isAbstract = $method->isAbstract() && !$class->isInterface(); + $modifiers = ($isAbstract ? 'abstract ' : '') . ($method->isFinal() ? 'final ' : '') + . 'public ' . ($method->isStatic() ? 'static ' : ''); + + return "{$modifiers}function {$method->name}(" . \implode(', ', $parameters) . ')' + . ($returns === '' ? '' : ": {$returns}") . ';'; +} + +/** + * @param ReflectionClass $class + * + * @return list + */ +function methodLines(ReflectionClass $class, string $namespace): array +{ + $lines = []; + foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + $isOwn = $method->getDeclaringClass()->name === $class->name; + $isImplicit = $class->isEnum() && \in_array($method->name, ENUM_METHODS, true); + if ($isOwn && !$isImplicit && !isInternal($method->getDocComment())) { + $lines[] = methodLine($class, $method, $namespace); + } + } + + return $lines; +} + +function clause(string $keyword, string $names): string +{ + return $names === '' ? '' : " {$keyword} {$names}"; +} + +/** + * The interfaces that the symbol names itself. A parent class gives the others, and PHP adds two + * to each enum. + * + * @param ReflectionClass $class + */ +function interfaceNames(ReflectionClass $class, string $namespace): string +{ + $parent = $class->getParentClass(); + $inherited = $parent === false ? [] : $parent->getInterfaceNames(); + $names = \array_diff($class->getInterfaceNames(), $inherited, ENUM_INTERFACES); + \sort($names); + + return \implode(', ', \array_map( + static fn(string $name): string => relativeName($name, $namespace), + $names, + )); +} + +/** + * @param ReflectionClass $class + */ +function classHeader(ReflectionClass $class, string $namespace): string +{ + $name = $class->getShortName(); + $interfaces = interfaceNames($class, $namespace); + if ($class->isInterface()) { + return "interface {$name}" . clause('extends', $interfaces); + } + $parent = $class->getParentClass(); + $tail = clause('extends', $parent === false ? '' : relativeName($parent->name, $namespace)) + . clause('implements', $interfaces); + if ($class instanceof ReflectionEnum) { + $backing = $class->getBackingType(); + + return "enum {$name}" . ($backing === null ? '' : ": {$backing}") . $tail; + } + + return ($class->isAbstract() ? 'abstract ' : '') . ($class->isFinal() ? 'final ' : '') + . "class {$name}{$tail}"; +} + +/** + * @param ReflectionClass $class + * + * @return list + */ +function classBlock(ReflectionClass $class, string $namespace): array +{ + $members = [ + ...caseLines($class, $namespace), + ...publicConstantLines($class, $namespace), + ...propertyLines($class, $namespace), + ...methodLines($class, $namespace), + ]; + + return [ + classHeader($class, $namespace), + '{', + ...\array_map(static fn(string $member): string => " {$member}", $members), + '}', + ]; +} diff --git a/scripts/dump-api.php b/scripts/dump-api.php new file mode 100644 index 0000000..69c7e58 --- /dev/null +++ b/scripts/dump-api.php @@ -0,0 +1,197 @@ + + */ +function readComposerJson(string $root): array +{ + $path = "{$root}/composer.json"; + $json = \is_file($path) && \is_readable($path) ? \file_get_contents($path) : false; + $composer = $json === false ? null : \json_decode($json, true); + if (!\is_array($composer)) { + throw new RuntimeException('Cannot read composer.json.'); + } + + return $composer; +} + +/** + * @param array $composer + * + * @return array Each PSR-4 prefix with its folder. + */ +function psr4Folders(array $composer, string $root): array +{ + $autoload = $composer['autoload'] ?? null; + $map = \is_array($autoload) ? ($autoload['psr-4'] ?? null) : null; + if (!\is_array($map) || $map === []) { + throw new RuntimeException('composer.json needs an autoload.psr-4 map.'); + } + $folders = []; + foreach ($map as $prefix => $folder) { + if (!\is_string($prefix) || !\is_string($folder)) { + throw new RuntimeException('Each PSR-4 entry of composer.json must name one folder.'); + } + $folders[$prefix] = $root . '/' . \rtrim($folder, '/'); + } + + return $folders; +} + +/** + * @param array $folders + */ +function registerAutoloader(array $folders): void +{ + \spl_autoload_register(static function (string $name) use ($folders): void { + foreach ($folders as $prefix => $folder) { + $file = $folder . '/' . \str_replace('\\', '/', \substr($name, \strlen($prefix))); + if (\str_starts_with($name, $prefix) && \is_file("{$file}.php")) { + require_once "{$file}.php"; + } + } + }); +} + +/** + * @param array $folders + * + * @return list Each class name that the PSR-4 map gives to a file outside Internal. + */ +function classNames(array $folders): array +{ + $names = []; + foreach ($folders as $prefix => $folder) { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS), + ); + foreach ($files as $file) { + if (!$file instanceof SplFileInfo || $file->getExtension() !== 'php') { + continue; + } + $path = \substr($file->getPathname(), \strlen($folder) + 1, -\strlen('.php')); + $segments = \explode(\DIRECTORY_SEPARATOR, \dirname($path)); + if (!\in_array(INTERNAL_NAMESPACE, $segments, true)) { + $names[] = $prefix . \str_replace(\DIRECTORY_SEPARATOR, '\\', $path); + } + } + } + \sort($names); + + return $names; +} + +/** + * @return ReflectionClass + */ +function reflectSymbol(string $name): ReflectionClass +{ + if (\enum_exists($name)) { + return new ReflectionEnum($name); + } + if (!\class_exists($name) && !\interface_exists($name)) { + throw new RuntimeException( + "{$name} is not a class, an interface or an enum. " + . 'Check that the file name follows the class name.', + ); + } + + return new ReflectionClass($name); +} + +/** + * @return list The lines of the dump. + */ +function dumpLines(string $root): array +{ + $composer = readComposerJson($root); + $package = $composer['name'] ?? null; + if (!\is_string($package)) { + throw new RuntimeException('composer.json needs a name.'); + } + $folders = psr4Folders($composer, $root); + registerAutoloader($folders); + $lines = ["# API dump of {$package}", '', DUMP_NOTE, '', '```php']; + $previous = null; + foreach (classNames($folders) as $name) { + $class = reflectSymbol($name); + $namespace = $class->getNamespaceName(); + if (isInternal($class->getDocComment())) { + continue; + } + if ($namespace !== $previous) { + $gap = $previous === null ? [] : ['']; + $lines = [...$lines, ...$gap, "namespace {$namespace};", '']; + $previous = $namespace; + } else { + $lines[] = ''; + } + $lines = [...$lines, ...classBlock($class, $namespace)]; + } + + return [...$lines, '```']; +} + +/** + * @param list $arguments + */ +function dumpApi(string $root, array $arguments): int +{ + $unknown = \array_values(\array_diff($arguments, ['--check'])); + if ($unknown !== []) { + \fwrite(\STDERR, "Unknown argument {$unknown[0]}. Use --check or no argument.\n"); + + return 2; + } + $path = "{$root}/" . DUMP_FILE; + try { + $text = \implode("\n", dumpLines($root)) . "\n"; + if ($arguments === [] && \file_put_contents($path, $text) === false) { + throw new RuntimeException('Cannot write ' . DUMP_FILE . '.'); + } + } catch (RuntimeException $problem) { + \fwrite(\STDERR, $problem->getMessage() . "\n"); + + return 2; + } + if ($arguments !== [] && (!\is_readable($path) || \file_get_contents($path) !== $text)) { + \fwrite(\STDERR, DUMP_FILE . " is out of date. Run composer api:dump.\n"); + + return 1; + } + + return 0; +} + +exit(dumpApi(\dirname(__DIR__), \array_slice($argv, 1))); diff --git a/scripts/generate-data.php b/scripts/generate-data.php new file mode 100644 index 0000000..7db069e --- /dev/null +++ b/scripts/generate-data.php @@ -0,0 +1,158 @@ + $arguments + */ +function main(string $root, array $arguments): int +{ + $unknown = \array_values(\array_diff($arguments, ['--check'])); + if ($unknown !== []) { + \fwrite(STDERR, "Unknown argument {$unknown[0]}. Use --check or no argument.\n"); + + return 2; + } + $format = readJson("{$root}/" . FORMAT_FILE); + $separators = separatorCodePoints($format['separators'] ?? null); + $files = [ + 'src/Internal/SpecData.php' => specData($root, $format, $separators), + 'src/Internal/NfkcTable.php' => nfkcTable(unicodeData($root), $separators), + ]; + if ($arguments === []) { + foreach ($files as $path => $contents) { + writeFile("{$root}/{$path}", $contents); + } + + return 0; + } + $stale = \array_filter( + \array_keys($files), + static fn(string $path): bool => !\is_readable("{$root}/{$path}") + || \file_get_contents("{$root}/{$path}") !== $files[$path], + ); + foreach ($stale as $path) { + \fwrite(STDERR, "{$path} is out of date. Run composer generate.\n"); + } + + return $stale === [] ? 0 : 1; +} + +function writeFile(string $path, string $contents): void +{ + if (!\is_dir(\dirname($path))) { + \mkdir(\dirname($path), 0o777, true); + } + if (\file_put_contents($path, $contents) === false) { + throw new RuntimeException("Cannot write {$path}."); + } +} + +function quoted(string $ascii): string +{ + return "'" . \strtr($ascii, ['\\' => '\\\\', "'" => "\\'"]) . "'"; +} + +/** + * @param list $codePoints + */ +function escaped(array $codePoints): string +{ + $literal = ''; + foreach ($codePoints as $codePoint) { + $printable = $codePoint >= 0x20 && $codePoint <= 0x7E; + $special = \in_array($codePoint, [0x22, 0x24, 0x5C], true); + $literal .= $printable && !$special ? \chr($codePoint) : \sprintf('\u{%04X}', $codePoint); + } + + return '"' . $literal . '"'; +} + +/** + * @param string|list $value A PHP literal, or the entry lines of an array. + * + * @return list + */ +function constantLines(string $summary, string $name, string|array $value): array +{ + $doc = ['', ' /**', " * {$summary}", ' */']; + if (\is_string($value)) { + return [...$doc, " public const {$name} = {$value};"]; + } + + return [...$doc, " public const {$name} = [", ...$value, ' ];']; +} + +/** + * @param list $tags The SPDX tags of the file. + * + * @return list + */ +function fileHeader(string $source, array $tags): array +{ + return [ + ' $header The lines before the strict_types declaration. + * @param list $constants + */ +function classFile(array $header, string $summary, string $class, array $constants): string +{ + return \implode("\n", [ + ...$header, + 'declare(strict_types=1);', + '', + 'namespace Gatepost\Postcode\Internal;', + '', + '/**', + " * {$summary}", + ' *', + ' * @internal', + ' */', + "final class {$class}", + '{', + // constantLines() starts each constant with a blank line. Drop the first one, which + // would follow the opening brace. + ...\array_slice($constants, 1), + '}', + '', + ]); +} + +exit(main(\dirname(__DIR__), \array_slice($argv, 1))); diff --git a/scripts/nfkc-table.php b/scripts/nfkc-table.php new file mode 100644 index 0000000..293cdd5 --- /dev/null +++ b/scripts/nfkc-table.php @@ -0,0 +1,147 @@ + $separators + * + * @return array> + */ +function nfkcForms(string $unicodeData, array $separators): array +{ + $mappings = []; + foreach (\explode("\n", \trim($unicodeData)) as $line) { + $fields = \explode(';', $line); + $mapping = \preg_replace('/\A<[a-zA-Z]+> /', '', $fields[5] ?? ''); + if ($mapping !== null && $mapping !== '') { + $mappings[\intval($fields[0], 16)] = \array_map( + static fn(string $hex): int => \intval($hex, 16), + \explode(' ', $mapping), + ); + } + } + $forms = []; + foreach (\array_keys($mappings) as $codePoint) { + $form = decomposition($codePoint, $mappings); + $others = \array_filter( + $form, + static fn(int $part): bool => $part > 0x7F && !\in_array($part, $separators, true), + ); + if ($others === []) { + $forms[$codePoint] = $form; + } + } + + return $forms; +} + +/** + * @param array> $mappings + * + * @return list + */ +function decomposition(int $codePoint, array $mappings): array +{ + if (!isset($mappings[$codePoint])) { + return [$codePoint]; + } + + return \array_merge(...\array_map( + static fn(int $part): array => decomposition($part, $mappings), + $mappings[$codePoint], + )); +} + +/** + * @param list $separators + */ +function nfkcTable(string $unicodeData, array $separators): string +{ + $entries = []; + foreach (nfkcForms($unicodeData, $separators) as $codePoint => $form) { + $entries[] = ENTRY . escaped([$codePoint]) . ' => ' . escaped($form) . ','; + } + $constants = [ + ...constantLines( + 'The version of the Unicode Character Database that the table comes from.', + 'UNICODE_VERSION', + quoted(UNICODE_VERSION), + ), + ...constantLines( + 'Each character whose NFKC form holds only ASCII characters and separators.', + 'FORMS', + $entries, + ), + ]; + $summary = 'NFKC forms for strtr(). The table leaves out each character that no ' + . 'postcode can hold.'; + $header = fileHeader('UnicodeData.txt ' . UNICODE_VERSION, UNICODE_TAGS); + + return classFile($header, $summary, 'NfkcTable', $constants); +} diff --git a/scripts/spec-data.php b/scripts/spec-data.php new file mode 100644 index 0000000..f2ca754 --- /dev/null +++ b/scripts/spec-data.php @@ -0,0 +1,343 @@ + ['[A-Z]', '[0-9]'], + 'letters' => ['[0-9]', '[A-Z]'], +]; + +/** + * @return array + */ +function readJson(string $path): array +{ + // A file that is missing or that no user can read gives false here, so the clear message + // below replaces a PHP warning. + $text = \is_file($path) && \is_readable($path) ? \file_get_contents($path) : false; + if ($text === false) { + throw new RuntimeException( + "Cannot read {$path}. Run git submodule update --init, or check the permissions.", + ); + } + $decoded = \json_decode($text, true); + if ($decoded === null && \json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException("{$path} is not valid JSON: " . \json_last_error_msg() . '.'); + } + if (!\is_array($decoded) || ($decoded['version'] ?? null) !== DATA_VERSION) { + throw new RuntimeException("{$path} must hold an object with version " . DATA_VERSION); + } + + return $decoded; +} + +/** + * @return list> + */ +function objects(mixed $value, string $where): array +{ + if (!\is_array($value) || !\array_is_list($value) || $value === []) { + throw new RuntimeException("{$where} must be a list with at least one object."); + } + + return \array_map(static function (mixed $item) use ($where): array { + if (!\is_array($item)) { + throw new RuntimeException("Each item in {$where} must be an object."); + } + + return $item; + }, $value); +} + +// The generated files must be ASCII for TELL-14. The spec's own tests keep its names in ASCII, +// so other text is an error here, not something to escape. +function text(mixed $value, string $where): string +{ + if (!\is_string($value) || \preg_match('/\A[ -~]+\z/', $value) !== 1) { + throw new RuntimeException("{$where} must be printable ASCII text."); + } + + return $value; +} + +// The generated class is an array, and a repeated key would keep only the last value. So a value +// that appears twice stops the build. +/** + * @param list $values + */ +function rejectRepeats(array $values, string $file, string $what): void +{ + foreach (\array_count_values($values) as $value => $count) { + if ($count > 1) { + throw new RuntimeException("{$file}: the {$what} {$value} appears more than once."); + } + } +} + +function wholeNumber(mixed $value, string $where): int +{ + if (!\is_int($value) || $value < 0) { + throw new RuntimeException("{$where} must be a whole number of 0 or more."); + } + + return $value; +} + +/** + * @param list $choices + */ +function choice(mixed $value, array $choices, string $where): string +{ + $chosen = text($value, $where); + if (!\in_array($chosen, $choices, true)) { + throw new RuntimeException("{$where} must be one of " . \implode(', ', $choices) . '.'); + } + + return $chosen; +} + +// A label such as "U+2013" names one code point. A label of another shape would give a wrong +// table and no error, so it throws. A label above U+10FFFF writes a class that does not parse, +// and a surrogate writes a key that no well-formed text can match. +/** + * @return list + */ +function separatorCodePoints(mixed $labels): array +{ + if (!\is_array($labels) || !\array_is_list($labels) || $labels === []) { + throw new RuntimeException('separators must be a list with at least one label.'); + } + + $codePoints = \array_map(static function (mixed $label): int { + if (!\is_string($label) || \preg_match('/\AU\+([0-9A-F]{4,6})\z/', $label, $hex) !== 1) { + throw new RuntimeException( + FORMAT_FILE . ': each separator must be written as U+ and 4 to 6 hex digits, ' + . 'but it is ' . \json_encode($label, JSON_THROW_ON_ERROR) . '.', + ); + } + $codePoint = \intval($hex[1], 16); + $isSurrogate = $codePoint >= 0xD800 && $codePoint <= 0xDFFF; + if ($codePoint > 0x10FFFF || $isSurrogate) { + throw new RuntimeException( + FORMAT_FILE . ": the separator {$label} must be a code point from U+0000 to " + . 'U+10FFFF that is not a surrogate.', + ); + } + + return $codePoint; + }, $labels); + // Two labels can name one code point, such as U+002D and U+00002D. + $labelsOfCodePoints = \array_map( + static fn(int $codePoint): string => \sprintf('U+%04X', $codePoint), + $codePoints, + ); + rejectRepeats($labelsOfCodePoints, FORMAT_FILE, 'separator'); + + return $codePoints; +} + +/** + * @param array $format + * @param list $separators + */ +function specData(string $root, array $format, array $separators): string +{ + $precision = readJson("{$root}/" . PRECISION_FILE); + $limit = wholeNumber($format['maxInputCodePoints'] ?? null, 'maxInputCodePoints'); + $fallback = choice($precision['fallback'] ?? null, PRECISIONS, 'fallback'); + $digitFixes = fixes($format, 'digits'); + $letterFixes = fixes($format, 'letters'); + $removals = \array_map( + static fn(int $codePoint): string => ENTRY . escaped([$codePoint]) . " => '',", + $separators, + ); + $constants = [ + ...constantLines( + 'The most code points that parse reads. Longer input fails before normalize runs.', + 'MAX_INPUT_CODE_POINTS', + (string) $limit, + ), + ...constantLines('State codes and names.', 'STATES', stateEntries($root)), + ...constantLines( + 'Each segment from the least to the most precise, with its place in a compact code.', + 'SEGMENTS', + segmentEntries($format['segments'] ?? null), + ), + ...constantLines( + 'GPS accuracy limits in metres. Each limit is inclusive.', + 'PRECISION_THRESHOLDS', + thresholdEntries($precision['thresholds'] ?? null), + ), + ...constantLines( + 'The precision for an accuracy that no limit covers.', + 'PRECISION_FALLBACK', + quoted($fallback), + ), + ...constantLines( + 'Each separator that normalize removes, mapped to the empty string for strtr().', + 'SEPARATORS', + $removals, + ), + ...constantLines( + 'Old 6-digit NIPOST postcodes. With D, $ does not match before a final line feed.', + 'LEGACY_PATTERN', + quoted(legacyPattern($format['legacyPattern'] ?? null)), + ), + ...constantLines('Typo fixes in digit segments.', 'DIGIT_FIXES', $digitFixes), + ...constantLines('Typo fixes in letter segments.', 'LETTER_FIXES', $letterFixes), + ]; + $summary = 'The values in the JSON files in spec/data.'; + $header = fileHeader('spec/data', GATEPOST_TAGS); + + return classFile($header, $summary, 'SpecData', $constants); +} + +function legacyPattern(mixed $pattern): string +{ + $legacy = text($pattern, 'legacyPattern'); + if (\str_contains($legacy, '/') || \preg_match("/{$legacy}/D", '') === false) { + throw new RuntimeException('legacyPattern must be a regular expression without a slash.'); + } + + return "/{$legacy}/D"; +} + +/** + * @return list + */ +function stateEntries(string $root): array +{ + $states = readJson("{$root}/" . STATES_FILE)['states'] ?? null; + $codes = []; + $entries = []; + foreach (objects($states, 'states') as $state) { + $code = text($state['code'] ?? null, 'state code'); + $codes[] = $code; + $name = quoted(text($state['name'] ?? null, 'state name')); + $entries[] = ENTRY . quoted($code) . " => {$name},"; + } + rejectRepeats($codes, STATES_FILE, 'state code'); + + return $entries; +} + +/** + * @return list + */ +function segmentEntries(mixed $segments): array +{ + $entries = []; + $names = []; + $start = 0; + foreach (objects($segments, 'segments') as $segment) { + $end = $start + wholeNumber($segment['length'] ?? null, 'segment length'); + $minimum = $segment['minimum'] ?? null; + $name = choice($segment['name'] ?? null, PRECISIONS, 'segment name'); + $names[] = $name; + $entries[] = ENTRY . \sprintf( + "%s => ['start' => %d, 'length' => %d, 'end' => %d, " + . "'characters' => %s, 'minimum' => %s],", + quoted($name), + $start, + $end - $start, + $end, + quoted(choice($segment['characters'] ?? null, CHARACTER_RULES, 'segment characters')), + $minimum === null ? 'null' : wholeNumber($minimum, 'segment minimum'), + ); + $start = $end; + } + rejectRepeats($names, FORMAT_FILE, 'segment name'); + + return $entries; +} + +/** + * @return list + */ +function thresholdEntries(mixed $thresholds): array +{ + $entries = []; + $previous = null; + foreach (objects($thresholds, 'thresholds') as $threshold) { + $limit = $threshold['maxAccuracyM'] ?? null; + if (!\is_int($limit) && !\is_float($limit)) { + throw new RuntimeException('maxAccuracyM must be a number.'); + } + // precisionForAccuracy returns the first limit that covers an accuracy, so a limit that + // does not rise above the one before it would never apply. + if ($previous !== null && $limit <= $previous) { + throw new RuntimeException( + PRECISION_FILE . ": the limit {$limit} must be above the limit {$previous}. " + . 'List the limits from the lowest to the highest.', + ); + } + $previous = $limit; + $precision = choice($threshold['precision'] ?? null, PRECISIONS, 'threshold precision'); + $entries[] = ENTRY . \sprintf( + "['maxAccuracyM' => %s, 'precision' => %s],", + \json_encode($limit, JSON_THROW_ON_ERROR), + quoted($precision), + ); + } + + return $entries; +} + +/** + * @param array $format + * @param 'digits'|'letters' $kind + * + * @return list + */ +function fixes(array $format, string $kind): array +{ + $suggestions = $format['suggestions'] ?? null; + $fixes = \is_array($suggestions) ? ($suggestions[$kind] ?? null) : null; + if (!\is_array($fixes) || $fixes === []) { + throw new RuntimeException( + FORMAT_FILE . ": suggestions.{$kind} must be an object with at least one fix, " + . 'but it is ' . \json_encode($fixes, JSON_THROW_ON_ERROR) . '.', + ); + } + [$changed, $written] = FIX_CHARACTERS[$kind]; + $entries = []; + foreach ($fixes as $from => $to) { + // PHP turns the key "0" into the integer 0, so the cast restores the text. At run time + // the key is the integer 0 again, so code that reads the table must use strtr() or cast + // the keys. + $from = text((string) $from, 'fix'); + $to = text($to, 'fix'); + $isFix = \preg_match("/\A{$changed}\z/", $from) === 1 + && \preg_match("/\A{$written}\z/", $to) === 1; + if (!$isFix) { + throw new RuntimeException( + FORMAT_FILE . ": suggestions.{$kind} has the fix {$from} => {$to}. " + . "A key must match {$changed} and a value must match {$written}.", + ); + } + $entries[] = ENTRY . quoted($from) . ' => ' . quoted($to) . ','; + } + + return $entries; +} diff --git a/spec b/spec new file mode 160000 index 0000000..ca4afdd --- /dev/null +++ b/spec @@ -0,0 +1 @@ +Subproject commit ca4afddf65e2897c71bb49ef7bed71b1a9741408 diff --git a/src/Internal/NfkcTable.php b/src/Internal/NfkcTable.php new file mode 100644 index 0000000..5070306 --- /dev/null +++ b/src/Internal/NfkcTable.php @@ -0,0 +1,1475 @@ + " ", + "\u{00AA}" => "a", + "\u{00B2}" => "2", + "\u{00B3}" => "3", + "\u{00B9}" => "1", + "\u{00BA}" => "o", + "\u{0132}" => "IJ", + "\u{0133}" => "ij", + "\u{017F}" => "s", + "\u{01C7}" => "LJ", + "\u{01C8}" => "Lj", + "\u{01C9}" => "lj", + "\u{01CA}" => "NJ", + "\u{01CB}" => "Nj", + "\u{01CC}" => "nj", + "\u{01F1}" => "DZ", + "\u{01F2}" => "Dz", + "\u{01F3}" => "dz", + "\u{02B0}" => "h", + "\u{02B2}" => "j", + "\u{02B3}" => "r", + "\u{02B7}" => "w", + "\u{02B8}" => "y", + "\u{02E1}" => "l", + "\u{02E2}" => "s", + "\u{02E3}" => "x", + "\u{037E}" => ";", + "\u{1D2C}" => "A", + "\u{1D2E}" => "B", + "\u{1D30}" => "D", + "\u{1D31}" => "E", + "\u{1D33}" => "G", + "\u{1D34}" => "H", + "\u{1D35}" => "I", + "\u{1D36}" => "J", + "\u{1D37}" => "K", + "\u{1D38}" => "L", + "\u{1D39}" => "M", + "\u{1D3A}" => "N", + "\u{1D3C}" => "O", + "\u{1D3E}" => "P", + "\u{1D3F}" => "R", + "\u{1D40}" => "T", + "\u{1D41}" => "U", + "\u{1D42}" => "W", + "\u{1D43}" => "a", + "\u{1D47}" => "b", + "\u{1D48}" => "d", + "\u{1D49}" => "e", + "\u{1D4D}" => "g", + "\u{1D4F}" => "k", + "\u{1D50}" => "m", + "\u{1D52}" => "o", + "\u{1D56}" => "p", + "\u{1D57}" => "t", + "\u{1D58}" => "u", + "\u{1D5B}" => "v", + "\u{1D62}" => "i", + "\u{1D63}" => "r", + "\u{1D64}" => "u", + "\u{1D65}" => "v", + "\u{1D9C}" => "c", + "\u{1DA0}" => "f", + "\u{1DBB}" => "z", + "\u{1FEF}" => "`", + "\u{2000}" => " ", + "\u{2001}" => " ", + "\u{2002}" => " ", + "\u{2003}" => " ", + "\u{2004}" => " ", + "\u{2005}" => " ", + "\u{2006}" => " ", + "\u{2007}" => " ", + "\u{2008}" => " ", + "\u{2009}" => " ", + "\u{200A}" => " ", + "\u{2011}" => "\u{2010}", + "\u{2024}" => ".", + "\u{2025}" => "..", + "\u{2026}" => "...", + "\u{202F}" => " ", + "\u{203C}" => "!!", + "\u{2047}" => "??", + "\u{2048}" => "?!", + "\u{2049}" => "!?", + "\u{205F}" => " ", + "\u{2070}" => "0", + "\u{2071}" => "i", + "\u{2074}" => "4", + "\u{2075}" => "5", + "\u{2076}" => "6", + "\u{2077}" => "7", + "\u{2078}" => "8", + "\u{2079}" => "9", + "\u{207A}" => "+", + "\u{207B}" => "\u{2212}", + "\u{207C}" => "=", + "\u{207D}" => "(", + "\u{207E}" => ")", + "\u{207F}" => "n", + "\u{2080}" => "0", + "\u{2081}" => "1", + "\u{2082}" => "2", + "\u{2083}" => "3", + "\u{2084}" => "4", + "\u{2085}" => "5", + "\u{2086}" => "6", + "\u{2087}" => "7", + "\u{2088}" => "8", + "\u{2089}" => "9", + "\u{208A}" => "+", + "\u{208B}" => "\u{2212}", + "\u{208C}" => "=", + "\u{208D}" => "(", + "\u{208E}" => ")", + "\u{2090}" => "a", + "\u{2091}" => "e", + "\u{2092}" => "o", + "\u{2093}" => "x", + "\u{2095}" => "h", + "\u{2096}" => "k", + "\u{2097}" => "l", + "\u{2098}" => "m", + "\u{2099}" => "n", + "\u{209A}" => "p", + "\u{209B}" => "s", + "\u{209C}" => "t", + "\u{20A8}" => "Rs", + "\u{2100}" => "a/c", + "\u{2101}" => "a/s", + "\u{2102}" => "C", + "\u{2105}" => "c/o", + "\u{2106}" => "c/u", + "\u{210A}" => "g", + "\u{210B}" => "H", + "\u{210C}" => "H", + "\u{210D}" => "H", + "\u{210E}" => "h", + "\u{2110}" => "I", + "\u{2111}" => "I", + "\u{2112}" => "L", + "\u{2113}" => "l", + "\u{2115}" => "N", + "\u{2116}" => "No", + "\u{2119}" => "P", + "\u{211A}" => "Q", + "\u{211B}" => "R", + "\u{211C}" => "R", + "\u{211D}" => "R", + "\u{2120}" => "SM", + "\u{2121}" => "TEL", + "\u{2122}" => "TM", + "\u{2124}" => "Z", + "\u{2128}" => "Z", + "\u{212A}" => "K", + "\u{212C}" => "B", + "\u{212D}" => "C", + "\u{212F}" => "e", + "\u{2130}" => "E", + "\u{2131}" => "F", + "\u{2133}" => "M", + "\u{2134}" => "o", + "\u{2139}" => "i", + "\u{213B}" => "FAX", + "\u{2145}" => "D", + "\u{2146}" => "d", + "\u{2147}" => "e", + "\u{2148}" => "i", + "\u{2149}" => "j", + "\u{2160}" => "I", + "\u{2161}" => "II", + "\u{2162}" => "III", + "\u{2163}" => "IV", + "\u{2164}" => "V", + "\u{2165}" => "VI", + "\u{2166}" => "VII", + "\u{2167}" => "VIII", + "\u{2168}" => "IX", + "\u{2169}" => "X", + "\u{216A}" => "XI", + "\u{216B}" => "XII", + "\u{216C}" => "L", + "\u{216D}" => "C", + "\u{216E}" => "D", + "\u{216F}" => "M", + "\u{2170}" => "i", + "\u{2171}" => "ii", + "\u{2172}" => "iii", + "\u{2173}" => "iv", + "\u{2174}" => "v", + "\u{2175}" => "vi", + "\u{2176}" => "vii", + "\u{2177}" => "viii", + "\u{2178}" => "ix", + "\u{2179}" => "x", + "\u{217A}" => "xi", + "\u{217B}" => "xii", + "\u{217C}" => "l", + "\u{217D}" => "c", + "\u{217E}" => "d", + "\u{217F}" => "m", + "\u{2460}" => "1", + "\u{2461}" => "2", + "\u{2462}" => "3", + "\u{2463}" => "4", + "\u{2464}" => "5", + "\u{2465}" => "6", + "\u{2466}" => "7", + "\u{2467}" => "8", + "\u{2468}" => "9", + "\u{2469}" => "10", + "\u{246A}" => "11", + "\u{246B}" => "12", + "\u{246C}" => "13", + "\u{246D}" => "14", + "\u{246E}" => "15", + "\u{246F}" => "16", + "\u{2470}" => "17", + "\u{2471}" => "18", + "\u{2472}" => "19", + "\u{2473}" => "20", + "\u{2474}" => "(1)", + "\u{2475}" => "(2)", + "\u{2476}" => "(3)", + "\u{2477}" => "(4)", + "\u{2478}" => "(5)", + "\u{2479}" => "(6)", + "\u{247A}" => "(7)", + "\u{247B}" => "(8)", + "\u{247C}" => "(9)", + "\u{247D}" => "(10)", + "\u{247E}" => "(11)", + "\u{247F}" => "(12)", + "\u{2480}" => "(13)", + "\u{2481}" => "(14)", + "\u{2482}" => "(15)", + "\u{2483}" => "(16)", + "\u{2484}" => "(17)", + "\u{2485}" => "(18)", + "\u{2486}" => "(19)", + "\u{2487}" => "(20)", + "\u{2488}" => "1.", + "\u{2489}" => "2.", + "\u{248A}" => "3.", + "\u{248B}" => "4.", + "\u{248C}" => "5.", + "\u{248D}" => "6.", + "\u{248E}" => "7.", + "\u{248F}" => "8.", + "\u{2490}" => "9.", + "\u{2491}" => "10.", + "\u{2492}" => "11.", + "\u{2493}" => "12.", + "\u{2494}" => "13.", + "\u{2495}" => "14.", + "\u{2496}" => "15.", + "\u{2497}" => "16.", + "\u{2498}" => "17.", + "\u{2499}" => "18.", + "\u{249A}" => "19.", + "\u{249B}" => "20.", + "\u{249C}" => "(a)", + "\u{249D}" => "(b)", + "\u{249E}" => "(c)", + "\u{249F}" => "(d)", + "\u{24A0}" => "(e)", + "\u{24A1}" => "(f)", + "\u{24A2}" => "(g)", + "\u{24A3}" => "(h)", + "\u{24A4}" => "(i)", + "\u{24A5}" => "(j)", + "\u{24A6}" => "(k)", + "\u{24A7}" => "(l)", + "\u{24A8}" => "(m)", + "\u{24A9}" => "(n)", + "\u{24AA}" => "(o)", + "\u{24AB}" => "(p)", + "\u{24AC}" => "(q)", + "\u{24AD}" => "(r)", + "\u{24AE}" => "(s)", + "\u{24AF}" => "(t)", + "\u{24B0}" => "(u)", + "\u{24B1}" => "(v)", + "\u{24B2}" => "(w)", + "\u{24B3}" => "(x)", + "\u{24B4}" => "(y)", + "\u{24B5}" => "(z)", + "\u{24B6}" => "A", + "\u{24B7}" => "B", + "\u{24B8}" => "C", + "\u{24B9}" => "D", + "\u{24BA}" => "E", + "\u{24BB}" => "F", + "\u{24BC}" => "G", + "\u{24BD}" => "H", + "\u{24BE}" => "I", + "\u{24BF}" => "J", + "\u{24C0}" => "K", + "\u{24C1}" => "L", + "\u{24C2}" => "M", + "\u{24C3}" => "N", + "\u{24C4}" => "O", + "\u{24C5}" => "P", + "\u{24C6}" => "Q", + "\u{24C7}" => "R", + "\u{24C8}" => "S", + "\u{24C9}" => "T", + "\u{24CA}" => "U", + "\u{24CB}" => "V", + "\u{24CC}" => "W", + "\u{24CD}" => "X", + "\u{24CE}" => "Y", + "\u{24CF}" => "Z", + "\u{24D0}" => "a", + "\u{24D1}" => "b", + "\u{24D2}" => "c", + "\u{24D3}" => "d", + "\u{24D4}" => "e", + "\u{24D5}" => "f", + "\u{24D6}" => "g", + "\u{24D7}" => "h", + "\u{24D8}" => "i", + "\u{24D9}" => "j", + "\u{24DA}" => "k", + "\u{24DB}" => "l", + "\u{24DC}" => "m", + "\u{24DD}" => "n", + "\u{24DE}" => "o", + "\u{24DF}" => "p", + "\u{24E0}" => "q", + "\u{24E1}" => "r", + "\u{24E2}" => "s", + "\u{24E3}" => "t", + "\u{24E4}" => "u", + "\u{24E5}" => "v", + "\u{24E6}" => "w", + "\u{24E7}" => "x", + "\u{24E8}" => "y", + "\u{24E9}" => "z", + "\u{24EA}" => "0", + "\u{2A74}" => "::=", + "\u{2A75}" => "==", + "\u{2A76}" => "===", + "\u{2C7C}" => "j", + "\u{2C7D}" => "V", + "\u{3000}" => " ", + "\u{3250}" => "PTE", + "\u{3251}" => "21", + "\u{3252}" => "22", + "\u{3253}" => "23", + "\u{3254}" => "24", + "\u{3255}" => "25", + "\u{3256}" => "26", + "\u{3257}" => "27", + "\u{3258}" => "28", + "\u{3259}" => "29", + "\u{325A}" => "30", + "\u{325B}" => "31", + "\u{325C}" => "32", + "\u{325D}" => "33", + "\u{325E}" => "34", + "\u{325F}" => "35", + "\u{32B1}" => "36", + "\u{32B2}" => "37", + "\u{32B3}" => "38", + "\u{32B4}" => "39", + "\u{32B5}" => "40", + "\u{32B6}" => "41", + "\u{32B7}" => "42", + "\u{32B8}" => "43", + "\u{32B9}" => "44", + "\u{32BA}" => "45", + "\u{32BB}" => "46", + "\u{32BC}" => "47", + "\u{32BD}" => "48", + "\u{32BE}" => "49", + "\u{32BF}" => "50", + "\u{32CC}" => "Hg", + "\u{32CD}" => "erg", + "\u{32CE}" => "eV", + "\u{32CF}" => "LTD", + "\u{3371}" => "hPa", + "\u{3372}" => "da", + "\u{3373}" => "AU", + "\u{3374}" => "bar", + "\u{3375}" => "oV", + "\u{3376}" => "pc", + "\u{3377}" => "dm", + "\u{3378}" => "dm2", + "\u{3379}" => "dm3", + "\u{337A}" => "IU", + "\u{3380}" => "pA", + "\u{3381}" => "nA", + "\u{3383}" => "mA", + "\u{3384}" => "kA", + "\u{3385}" => "KB", + "\u{3386}" => "MB", + "\u{3387}" => "GB", + "\u{3388}" => "cal", + "\u{3389}" => "kcal", + "\u{338A}" => "pF", + "\u{338B}" => "nF", + "\u{338E}" => "mg", + "\u{338F}" => "kg", + "\u{3390}" => "Hz", + "\u{3391}" => "kHz", + "\u{3392}" => "MHz", + "\u{3393}" => "GHz", + "\u{3394}" => "THz", + "\u{3396}" => "ml", + "\u{3397}" => "dl", + "\u{3398}" => "kl", + "\u{3399}" => "fm", + "\u{339A}" => "nm", + "\u{339C}" => "mm", + "\u{339D}" => "cm", + "\u{339E}" => "km", + "\u{339F}" => "mm2", + "\u{33A0}" => "cm2", + "\u{33A1}" => "m2", + "\u{33A2}" => "km2", + "\u{33A3}" => "mm3", + "\u{33A4}" => "cm3", + "\u{33A5}" => "m3", + "\u{33A6}" => "km3", + "\u{33A9}" => "Pa", + "\u{33AA}" => "kPa", + "\u{33AB}" => "MPa", + "\u{33AC}" => "GPa", + "\u{33AD}" => "rad", + "\u{33B0}" => "ps", + "\u{33B1}" => "ns", + "\u{33B3}" => "ms", + "\u{33B4}" => "pV", + "\u{33B5}" => "nV", + "\u{33B7}" => "mV", + "\u{33B8}" => "kV", + "\u{33B9}" => "MV", + "\u{33BA}" => "pW", + "\u{33BB}" => "nW", + "\u{33BD}" => "mW", + "\u{33BE}" => "kW", + "\u{33BF}" => "MW", + "\u{33C2}" => "a.m.", + "\u{33C3}" => "Bq", + "\u{33C4}" => "cc", + "\u{33C5}" => "cd", + "\u{33C7}" => "Co.", + "\u{33C8}" => "dB", + "\u{33C9}" => "Gy", + "\u{33CA}" => "ha", + "\u{33CB}" => "HP", + "\u{33CC}" => "in", + "\u{33CD}" => "KK", + "\u{33CE}" => "KM", + "\u{33CF}" => "kt", + "\u{33D0}" => "lm", + "\u{33D1}" => "ln", + "\u{33D2}" => "log", + "\u{33D3}" => "lx", + "\u{33D4}" => "mb", + "\u{33D5}" => "mil", + "\u{33D6}" => "mol", + "\u{33D7}" => "PH", + "\u{33D8}" => "p.m.", + "\u{33D9}" => "PPM", + "\u{33DA}" => "PR", + "\u{33DB}" => "sr", + "\u{33DC}" => "Sv", + "\u{33DD}" => "Wb", + "\u{33FF}" => "gal", + "\u{A7F1}" => "S", + "\u{A7F2}" => "C", + "\u{A7F3}" => "F", + "\u{A7F4}" => "Q", + "\u{FB00}" => "ff", + "\u{FB01}" => "fi", + "\u{FB02}" => "fl", + "\u{FB03}" => "ffi", + "\u{FB04}" => "ffl", + "\u{FB05}" => "st", + "\u{FB06}" => "st", + "\u{FB29}" => "+", + "\u{FE10}" => ",", + "\u{FE13}" => ":", + "\u{FE14}" => ";", + "\u{FE15}" => "!", + "\u{FE16}" => "?", + "\u{FE19}" => "...", + "\u{FE30}" => "..", + "\u{FE31}" => "\u{2014}", + "\u{FE32}" => "\u{2013}", + "\u{FE33}" => "_", + "\u{FE34}" => "_", + "\u{FE35}" => "(", + "\u{FE36}" => ")", + "\u{FE37}" => "{", + "\u{FE38}" => "}", + "\u{FE47}" => "[", + "\u{FE48}" => "]", + "\u{FE4D}" => "_", + "\u{FE4E}" => "_", + "\u{FE4F}" => "_", + "\u{FE50}" => ",", + "\u{FE52}" => ".", + "\u{FE54}" => ";", + "\u{FE55}" => ":", + "\u{FE56}" => "?", + "\u{FE57}" => "!", + "\u{FE58}" => "\u{2014}", + "\u{FE59}" => "(", + "\u{FE5A}" => ")", + "\u{FE5B}" => "{", + "\u{FE5C}" => "}", + "\u{FE5F}" => "#", + "\u{FE60}" => "&", + "\u{FE61}" => "*", + "\u{FE62}" => "+", + "\u{FE63}" => "-", + "\u{FE64}" => "<", + "\u{FE65}" => ">", + "\u{FE66}" => "=", + "\u{FE68}" => "\u{005C}", + "\u{FE69}" => "\u{0024}", + "\u{FE6A}" => "%", + "\u{FE6B}" => "@", + "\u{FF01}" => "!", + "\u{FF02}" => "\u{0022}", + "\u{FF03}" => "#", + "\u{FF04}" => "\u{0024}", + "\u{FF05}" => "%", + "\u{FF06}" => "&", + "\u{FF07}" => "'", + "\u{FF08}" => "(", + "\u{FF09}" => ")", + "\u{FF0A}" => "*", + "\u{FF0B}" => "+", + "\u{FF0C}" => ",", + "\u{FF0D}" => "-", + "\u{FF0E}" => ".", + "\u{FF0F}" => "/", + "\u{FF10}" => "0", + "\u{FF11}" => "1", + "\u{FF12}" => "2", + "\u{FF13}" => "3", + "\u{FF14}" => "4", + "\u{FF15}" => "5", + "\u{FF16}" => "6", + "\u{FF17}" => "7", + "\u{FF18}" => "8", + "\u{FF19}" => "9", + "\u{FF1A}" => ":", + "\u{FF1B}" => ";", + "\u{FF1C}" => "<", + "\u{FF1D}" => "=", + "\u{FF1E}" => ">", + "\u{FF1F}" => "?", + "\u{FF20}" => "@", + "\u{FF21}" => "A", + "\u{FF22}" => "B", + "\u{FF23}" => "C", + "\u{FF24}" => "D", + "\u{FF25}" => "E", + "\u{FF26}" => "F", + "\u{FF27}" => "G", + "\u{FF28}" => "H", + "\u{FF29}" => "I", + "\u{FF2A}" => "J", + "\u{FF2B}" => "K", + "\u{FF2C}" => "L", + "\u{FF2D}" => "M", + "\u{FF2E}" => "N", + "\u{FF2F}" => "O", + "\u{FF30}" => "P", + "\u{FF31}" => "Q", + "\u{FF32}" => "R", + "\u{FF33}" => "S", + "\u{FF34}" => "T", + "\u{FF35}" => "U", + "\u{FF36}" => "V", + "\u{FF37}" => "W", + "\u{FF38}" => "X", + "\u{FF39}" => "Y", + "\u{FF3A}" => "Z", + "\u{FF3B}" => "[", + "\u{FF3C}" => "\u{005C}", + "\u{FF3D}" => "]", + "\u{FF3E}" => "^", + "\u{FF3F}" => "_", + "\u{FF40}" => "`", + "\u{FF41}" => "a", + "\u{FF42}" => "b", + "\u{FF43}" => "c", + "\u{FF44}" => "d", + "\u{FF45}" => "e", + "\u{FF46}" => "f", + "\u{FF47}" => "g", + "\u{FF48}" => "h", + "\u{FF49}" => "i", + "\u{FF4A}" => "j", + "\u{FF4B}" => "k", + "\u{FF4C}" => "l", + "\u{FF4D}" => "m", + "\u{FF4E}" => "n", + "\u{FF4F}" => "o", + "\u{FF50}" => "p", + "\u{FF51}" => "q", + "\u{FF52}" => "r", + "\u{FF53}" => "s", + "\u{FF54}" => "t", + "\u{FF55}" => "u", + "\u{FF56}" => "v", + "\u{FF57}" => "w", + "\u{FF58}" => "x", + "\u{FF59}" => "y", + "\u{FF5A}" => "z", + "\u{FF5B}" => "{", + "\u{FF5C}" => "|", + "\u{FF5D}" => "}", + "\u{FF5E}" => "~", + "\u{107A5}" => "q", + "\u{1CCD6}" => "A", + "\u{1CCD7}" => "B", + "\u{1CCD8}" => "C", + "\u{1CCD9}" => "D", + "\u{1CCDA}" => "E", + "\u{1CCDB}" => "F", + "\u{1CCDC}" => "G", + "\u{1CCDD}" => "H", + "\u{1CCDE}" => "I", + "\u{1CCDF}" => "J", + "\u{1CCE0}" => "K", + "\u{1CCE1}" => "L", + "\u{1CCE2}" => "M", + "\u{1CCE3}" => "N", + "\u{1CCE4}" => "O", + "\u{1CCE5}" => "P", + "\u{1CCE6}" => "Q", + "\u{1CCE7}" => "R", + "\u{1CCE8}" => "S", + "\u{1CCE9}" => "T", + "\u{1CCEA}" => "U", + "\u{1CCEB}" => "V", + "\u{1CCEC}" => "W", + "\u{1CCED}" => "X", + "\u{1CCEE}" => "Y", + "\u{1CCEF}" => "Z", + "\u{1CCF0}" => "0", + "\u{1CCF1}" => "1", + "\u{1CCF2}" => "2", + "\u{1CCF3}" => "3", + "\u{1CCF4}" => "4", + "\u{1CCF5}" => "5", + "\u{1CCF6}" => "6", + "\u{1CCF7}" => "7", + "\u{1CCF8}" => "8", + "\u{1CCF9}" => "9", + "\u{1D400}" => "A", + "\u{1D401}" => "B", + "\u{1D402}" => "C", + "\u{1D403}" => "D", + "\u{1D404}" => "E", + "\u{1D405}" => "F", + "\u{1D406}" => "G", + "\u{1D407}" => "H", + "\u{1D408}" => "I", + "\u{1D409}" => "J", + "\u{1D40A}" => "K", + "\u{1D40B}" => "L", + "\u{1D40C}" => "M", + "\u{1D40D}" => "N", + "\u{1D40E}" => "O", + "\u{1D40F}" => "P", + "\u{1D410}" => "Q", + "\u{1D411}" => "R", + "\u{1D412}" => "S", + "\u{1D413}" => "T", + "\u{1D414}" => "U", + "\u{1D415}" => "V", + "\u{1D416}" => "W", + "\u{1D417}" => "X", + "\u{1D418}" => "Y", + "\u{1D419}" => "Z", + "\u{1D41A}" => "a", + "\u{1D41B}" => "b", + "\u{1D41C}" => "c", + "\u{1D41D}" => "d", + "\u{1D41E}" => "e", + "\u{1D41F}" => "f", + "\u{1D420}" => "g", + "\u{1D421}" => "h", + "\u{1D422}" => "i", + "\u{1D423}" => "j", + "\u{1D424}" => "k", + "\u{1D425}" => "l", + "\u{1D426}" => "m", + "\u{1D427}" => "n", + "\u{1D428}" => "o", + "\u{1D429}" => "p", + "\u{1D42A}" => "q", + "\u{1D42B}" => "r", + "\u{1D42C}" => "s", + "\u{1D42D}" => "t", + "\u{1D42E}" => "u", + "\u{1D42F}" => "v", + "\u{1D430}" => "w", + "\u{1D431}" => "x", + "\u{1D432}" => "y", + "\u{1D433}" => "z", + "\u{1D434}" => "A", + "\u{1D435}" => "B", + "\u{1D436}" => "C", + "\u{1D437}" => "D", + "\u{1D438}" => "E", + "\u{1D439}" => "F", + "\u{1D43A}" => "G", + "\u{1D43B}" => "H", + "\u{1D43C}" => "I", + "\u{1D43D}" => "J", + "\u{1D43E}" => "K", + "\u{1D43F}" => "L", + "\u{1D440}" => "M", + "\u{1D441}" => "N", + "\u{1D442}" => "O", + "\u{1D443}" => "P", + "\u{1D444}" => "Q", + "\u{1D445}" => "R", + "\u{1D446}" => "S", + "\u{1D447}" => "T", + "\u{1D448}" => "U", + "\u{1D449}" => "V", + "\u{1D44A}" => "W", + "\u{1D44B}" => "X", + "\u{1D44C}" => "Y", + "\u{1D44D}" => "Z", + "\u{1D44E}" => "a", + "\u{1D44F}" => "b", + "\u{1D450}" => "c", + "\u{1D451}" => "d", + "\u{1D452}" => "e", + "\u{1D453}" => "f", + "\u{1D454}" => "g", + "\u{1D456}" => "i", + "\u{1D457}" => "j", + "\u{1D458}" => "k", + "\u{1D459}" => "l", + "\u{1D45A}" => "m", + "\u{1D45B}" => "n", + "\u{1D45C}" => "o", + "\u{1D45D}" => "p", + "\u{1D45E}" => "q", + "\u{1D45F}" => "r", + "\u{1D460}" => "s", + "\u{1D461}" => "t", + "\u{1D462}" => "u", + "\u{1D463}" => "v", + "\u{1D464}" => "w", + "\u{1D465}" => "x", + "\u{1D466}" => "y", + "\u{1D467}" => "z", + "\u{1D468}" => "A", + "\u{1D469}" => "B", + "\u{1D46A}" => "C", + "\u{1D46B}" => "D", + "\u{1D46C}" => "E", + "\u{1D46D}" => "F", + "\u{1D46E}" => "G", + "\u{1D46F}" => "H", + "\u{1D470}" => "I", + "\u{1D471}" => "J", + "\u{1D472}" => "K", + "\u{1D473}" => "L", + "\u{1D474}" => "M", + "\u{1D475}" => "N", + "\u{1D476}" => "O", + "\u{1D477}" => "P", + "\u{1D478}" => "Q", + "\u{1D479}" => "R", + "\u{1D47A}" => "S", + "\u{1D47B}" => "T", + "\u{1D47C}" => "U", + "\u{1D47D}" => "V", + "\u{1D47E}" => "W", + "\u{1D47F}" => "X", + "\u{1D480}" => "Y", + "\u{1D481}" => "Z", + "\u{1D482}" => "a", + "\u{1D483}" => "b", + "\u{1D484}" => "c", + "\u{1D485}" => "d", + "\u{1D486}" => "e", + "\u{1D487}" => "f", + "\u{1D488}" => "g", + "\u{1D489}" => "h", + "\u{1D48A}" => "i", + "\u{1D48B}" => "j", + "\u{1D48C}" => "k", + "\u{1D48D}" => "l", + "\u{1D48E}" => "m", + "\u{1D48F}" => "n", + "\u{1D490}" => "o", + "\u{1D491}" => "p", + "\u{1D492}" => "q", + "\u{1D493}" => "r", + "\u{1D494}" => "s", + "\u{1D495}" => "t", + "\u{1D496}" => "u", + "\u{1D497}" => "v", + "\u{1D498}" => "w", + "\u{1D499}" => "x", + "\u{1D49A}" => "y", + "\u{1D49B}" => "z", + "\u{1D49C}" => "A", + "\u{1D49E}" => "C", + "\u{1D49F}" => "D", + "\u{1D4A2}" => "G", + "\u{1D4A5}" => "J", + "\u{1D4A6}" => "K", + "\u{1D4A9}" => "N", + "\u{1D4AA}" => "O", + "\u{1D4AB}" => "P", + "\u{1D4AC}" => "Q", + "\u{1D4AE}" => "S", + "\u{1D4AF}" => "T", + "\u{1D4B0}" => "U", + "\u{1D4B1}" => "V", + "\u{1D4B2}" => "W", + "\u{1D4B3}" => "X", + "\u{1D4B4}" => "Y", + "\u{1D4B5}" => "Z", + "\u{1D4B6}" => "a", + "\u{1D4B7}" => "b", + "\u{1D4B8}" => "c", + "\u{1D4B9}" => "d", + "\u{1D4BB}" => "f", + "\u{1D4BD}" => "h", + "\u{1D4BE}" => "i", + "\u{1D4BF}" => "j", + "\u{1D4C0}" => "k", + "\u{1D4C1}" => "l", + "\u{1D4C2}" => "m", + "\u{1D4C3}" => "n", + "\u{1D4C5}" => "p", + "\u{1D4C6}" => "q", + "\u{1D4C7}" => "r", + "\u{1D4C8}" => "s", + "\u{1D4C9}" => "t", + "\u{1D4CA}" => "u", + "\u{1D4CB}" => "v", + "\u{1D4CC}" => "w", + "\u{1D4CD}" => "x", + "\u{1D4CE}" => "y", + "\u{1D4CF}" => "z", + "\u{1D4D0}" => "A", + "\u{1D4D1}" => "B", + "\u{1D4D2}" => "C", + "\u{1D4D3}" => "D", + "\u{1D4D4}" => "E", + "\u{1D4D5}" => "F", + "\u{1D4D6}" => "G", + "\u{1D4D7}" => "H", + "\u{1D4D8}" => "I", + "\u{1D4D9}" => "J", + "\u{1D4DA}" => "K", + "\u{1D4DB}" => "L", + "\u{1D4DC}" => "M", + "\u{1D4DD}" => "N", + "\u{1D4DE}" => "O", + "\u{1D4DF}" => "P", + "\u{1D4E0}" => "Q", + "\u{1D4E1}" => "R", + "\u{1D4E2}" => "S", + "\u{1D4E3}" => "T", + "\u{1D4E4}" => "U", + "\u{1D4E5}" => "V", + "\u{1D4E6}" => "W", + "\u{1D4E7}" => "X", + "\u{1D4E8}" => "Y", + "\u{1D4E9}" => "Z", + "\u{1D4EA}" => "a", + "\u{1D4EB}" => "b", + "\u{1D4EC}" => "c", + "\u{1D4ED}" => "d", + "\u{1D4EE}" => "e", + "\u{1D4EF}" => "f", + "\u{1D4F0}" => "g", + "\u{1D4F1}" => "h", + "\u{1D4F2}" => "i", + "\u{1D4F3}" => "j", + "\u{1D4F4}" => "k", + "\u{1D4F5}" => "l", + "\u{1D4F6}" => "m", + "\u{1D4F7}" => "n", + "\u{1D4F8}" => "o", + "\u{1D4F9}" => "p", + "\u{1D4FA}" => "q", + "\u{1D4FB}" => "r", + "\u{1D4FC}" => "s", + "\u{1D4FD}" => "t", + "\u{1D4FE}" => "u", + "\u{1D4FF}" => "v", + "\u{1D500}" => "w", + "\u{1D501}" => "x", + "\u{1D502}" => "y", + "\u{1D503}" => "z", + "\u{1D504}" => "A", + "\u{1D505}" => "B", + "\u{1D507}" => "D", + "\u{1D508}" => "E", + "\u{1D509}" => "F", + "\u{1D50A}" => "G", + "\u{1D50D}" => "J", + "\u{1D50E}" => "K", + "\u{1D50F}" => "L", + "\u{1D510}" => "M", + "\u{1D511}" => "N", + "\u{1D512}" => "O", + "\u{1D513}" => "P", + "\u{1D514}" => "Q", + "\u{1D516}" => "S", + "\u{1D517}" => "T", + "\u{1D518}" => "U", + "\u{1D519}" => "V", + "\u{1D51A}" => "W", + "\u{1D51B}" => "X", + "\u{1D51C}" => "Y", + "\u{1D51E}" => "a", + "\u{1D51F}" => "b", + "\u{1D520}" => "c", + "\u{1D521}" => "d", + "\u{1D522}" => "e", + "\u{1D523}" => "f", + "\u{1D524}" => "g", + "\u{1D525}" => "h", + "\u{1D526}" => "i", + "\u{1D527}" => "j", + "\u{1D528}" => "k", + "\u{1D529}" => "l", + "\u{1D52A}" => "m", + "\u{1D52B}" => "n", + "\u{1D52C}" => "o", + "\u{1D52D}" => "p", + "\u{1D52E}" => "q", + "\u{1D52F}" => "r", + "\u{1D530}" => "s", + "\u{1D531}" => "t", + "\u{1D532}" => "u", + "\u{1D533}" => "v", + "\u{1D534}" => "w", + "\u{1D535}" => "x", + "\u{1D536}" => "y", + "\u{1D537}" => "z", + "\u{1D538}" => "A", + "\u{1D539}" => "B", + "\u{1D53B}" => "D", + "\u{1D53C}" => "E", + "\u{1D53D}" => "F", + "\u{1D53E}" => "G", + "\u{1D540}" => "I", + "\u{1D541}" => "J", + "\u{1D542}" => "K", + "\u{1D543}" => "L", + "\u{1D544}" => "M", + "\u{1D546}" => "O", + "\u{1D54A}" => "S", + "\u{1D54B}" => "T", + "\u{1D54C}" => "U", + "\u{1D54D}" => "V", + "\u{1D54E}" => "W", + "\u{1D54F}" => "X", + "\u{1D550}" => "Y", + "\u{1D552}" => "a", + "\u{1D553}" => "b", + "\u{1D554}" => "c", + "\u{1D555}" => "d", + "\u{1D556}" => "e", + "\u{1D557}" => "f", + "\u{1D558}" => "g", + "\u{1D559}" => "h", + "\u{1D55A}" => "i", + "\u{1D55B}" => "j", + "\u{1D55C}" => "k", + "\u{1D55D}" => "l", + "\u{1D55E}" => "m", + "\u{1D55F}" => "n", + "\u{1D560}" => "o", + "\u{1D561}" => "p", + "\u{1D562}" => "q", + "\u{1D563}" => "r", + "\u{1D564}" => "s", + "\u{1D565}" => "t", + "\u{1D566}" => "u", + "\u{1D567}" => "v", + "\u{1D568}" => "w", + "\u{1D569}" => "x", + "\u{1D56A}" => "y", + "\u{1D56B}" => "z", + "\u{1D56C}" => "A", + "\u{1D56D}" => "B", + "\u{1D56E}" => "C", + "\u{1D56F}" => "D", + "\u{1D570}" => "E", + "\u{1D571}" => "F", + "\u{1D572}" => "G", + "\u{1D573}" => "H", + "\u{1D574}" => "I", + "\u{1D575}" => "J", + "\u{1D576}" => "K", + "\u{1D577}" => "L", + "\u{1D578}" => "M", + "\u{1D579}" => "N", + "\u{1D57A}" => "O", + "\u{1D57B}" => "P", + "\u{1D57C}" => "Q", + "\u{1D57D}" => "R", + "\u{1D57E}" => "S", + "\u{1D57F}" => "T", + "\u{1D580}" => "U", + "\u{1D581}" => "V", + "\u{1D582}" => "W", + "\u{1D583}" => "X", + "\u{1D584}" => "Y", + "\u{1D585}" => "Z", + "\u{1D586}" => "a", + "\u{1D587}" => "b", + "\u{1D588}" => "c", + "\u{1D589}" => "d", + "\u{1D58A}" => "e", + "\u{1D58B}" => "f", + "\u{1D58C}" => "g", + "\u{1D58D}" => "h", + "\u{1D58E}" => "i", + "\u{1D58F}" => "j", + "\u{1D590}" => "k", + "\u{1D591}" => "l", + "\u{1D592}" => "m", + "\u{1D593}" => "n", + "\u{1D594}" => "o", + "\u{1D595}" => "p", + "\u{1D596}" => "q", + "\u{1D597}" => "r", + "\u{1D598}" => "s", + "\u{1D599}" => "t", + "\u{1D59A}" => "u", + "\u{1D59B}" => "v", + "\u{1D59C}" => "w", + "\u{1D59D}" => "x", + "\u{1D59E}" => "y", + "\u{1D59F}" => "z", + "\u{1D5A0}" => "A", + "\u{1D5A1}" => "B", + "\u{1D5A2}" => "C", + "\u{1D5A3}" => "D", + "\u{1D5A4}" => "E", + "\u{1D5A5}" => "F", + "\u{1D5A6}" => "G", + "\u{1D5A7}" => "H", + "\u{1D5A8}" => "I", + "\u{1D5A9}" => "J", + "\u{1D5AA}" => "K", + "\u{1D5AB}" => "L", + "\u{1D5AC}" => "M", + "\u{1D5AD}" => "N", + "\u{1D5AE}" => "O", + "\u{1D5AF}" => "P", + "\u{1D5B0}" => "Q", + "\u{1D5B1}" => "R", + "\u{1D5B2}" => "S", + "\u{1D5B3}" => "T", + "\u{1D5B4}" => "U", + "\u{1D5B5}" => "V", + "\u{1D5B6}" => "W", + "\u{1D5B7}" => "X", + "\u{1D5B8}" => "Y", + "\u{1D5B9}" => "Z", + "\u{1D5BA}" => "a", + "\u{1D5BB}" => "b", + "\u{1D5BC}" => "c", + "\u{1D5BD}" => "d", + "\u{1D5BE}" => "e", + "\u{1D5BF}" => "f", + "\u{1D5C0}" => "g", + "\u{1D5C1}" => "h", + "\u{1D5C2}" => "i", + "\u{1D5C3}" => "j", + "\u{1D5C4}" => "k", + "\u{1D5C5}" => "l", + "\u{1D5C6}" => "m", + "\u{1D5C7}" => "n", + "\u{1D5C8}" => "o", + "\u{1D5C9}" => "p", + "\u{1D5CA}" => "q", + "\u{1D5CB}" => "r", + "\u{1D5CC}" => "s", + "\u{1D5CD}" => "t", + "\u{1D5CE}" => "u", + "\u{1D5CF}" => "v", + "\u{1D5D0}" => "w", + "\u{1D5D1}" => "x", + "\u{1D5D2}" => "y", + "\u{1D5D3}" => "z", + "\u{1D5D4}" => "A", + "\u{1D5D5}" => "B", + "\u{1D5D6}" => "C", + "\u{1D5D7}" => "D", + "\u{1D5D8}" => "E", + "\u{1D5D9}" => "F", + "\u{1D5DA}" => "G", + "\u{1D5DB}" => "H", + "\u{1D5DC}" => "I", + "\u{1D5DD}" => "J", + "\u{1D5DE}" => "K", + "\u{1D5DF}" => "L", + "\u{1D5E0}" => "M", + "\u{1D5E1}" => "N", + "\u{1D5E2}" => "O", + "\u{1D5E3}" => "P", + "\u{1D5E4}" => "Q", + "\u{1D5E5}" => "R", + "\u{1D5E6}" => "S", + "\u{1D5E7}" => "T", + "\u{1D5E8}" => "U", + "\u{1D5E9}" => "V", + "\u{1D5EA}" => "W", + "\u{1D5EB}" => "X", + "\u{1D5EC}" => "Y", + "\u{1D5ED}" => "Z", + "\u{1D5EE}" => "a", + "\u{1D5EF}" => "b", + "\u{1D5F0}" => "c", + "\u{1D5F1}" => "d", + "\u{1D5F2}" => "e", + "\u{1D5F3}" => "f", + "\u{1D5F4}" => "g", + "\u{1D5F5}" => "h", + "\u{1D5F6}" => "i", + "\u{1D5F7}" => "j", + "\u{1D5F8}" => "k", + "\u{1D5F9}" => "l", + "\u{1D5FA}" => "m", + "\u{1D5FB}" => "n", + "\u{1D5FC}" => "o", + "\u{1D5FD}" => "p", + "\u{1D5FE}" => "q", + "\u{1D5FF}" => "r", + "\u{1D600}" => "s", + "\u{1D601}" => "t", + "\u{1D602}" => "u", + "\u{1D603}" => "v", + "\u{1D604}" => "w", + "\u{1D605}" => "x", + "\u{1D606}" => "y", + "\u{1D607}" => "z", + "\u{1D608}" => "A", + "\u{1D609}" => "B", + "\u{1D60A}" => "C", + "\u{1D60B}" => "D", + "\u{1D60C}" => "E", + "\u{1D60D}" => "F", + "\u{1D60E}" => "G", + "\u{1D60F}" => "H", + "\u{1D610}" => "I", + "\u{1D611}" => "J", + "\u{1D612}" => "K", + "\u{1D613}" => "L", + "\u{1D614}" => "M", + "\u{1D615}" => "N", + "\u{1D616}" => "O", + "\u{1D617}" => "P", + "\u{1D618}" => "Q", + "\u{1D619}" => "R", + "\u{1D61A}" => "S", + "\u{1D61B}" => "T", + "\u{1D61C}" => "U", + "\u{1D61D}" => "V", + "\u{1D61E}" => "W", + "\u{1D61F}" => "X", + "\u{1D620}" => "Y", + "\u{1D621}" => "Z", + "\u{1D622}" => "a", + "\u{1D623}" => "b", + "\u{1D624}" => "c", + "\u{1D625}" => "d", + "\u{1D626}" => "e", + "\u{1D627}" => "f", + "\u{1D628}" => "g", + "\u{1D629}" => "h", + "\u{1D62A}" => "i", + "\u{1D62B}" => "j", + "\u{1D62C}" => "k", + "\u{1D62D}" => "l", + "\u{1D62E}" => "m", + "\u{1D62F}" => "n", + "\u{1D630}" => "o", + "\u{1D631}" => "p", + "\u{1D632}" => "q", + "\u{1D633}" => "r", + "\u{1D634}" => "s", + "\u{1D635}" => "t", + "\u{1D636}" => "u", + "\u{1D637}" => "v", + "\u{1D638}" => "w", + "\u{1D639}" => "x", + "\u{1D63A}" => "y", + "\u{1D63B}" => "z", + "\u{1D63C}" => "A", + "\u{1D63D}" => "B", + "\u{1D63E}" => "C", + "\u{1D63F}" => "D", + "\u{1D640}" => "E", + "\u{1D641}" => "F", + "\u{1D642}" => "G", + "\u{1D643}" => "H", + "\u{1D644}" => "I", + "\u{1D645}" => "J", + "\u{1D646}" => "K", + "\u{1D647}" => "L", + "\u{1D648}" => "M", + "\u{1D649}" => "N", + "\u{1D64A}" => "O", + "\u{1D64B}" => "P", + "\u{1D64C}" => "Q", + "\u{1D64D}" => "R", + "\u{1D64E}" => "S", + "\u{1D64F}" => "T", + "\u{1D650}" => "U", + "\u{1D651}" => "V", + "\u{1D652}" => "W", + "\u{1D653}" => "X", + "\u{1D654}" => "Y", + "\u{1D655}" => "Z", + "\u{1D656}" => "a", + "\u{1D657}" => "b", + "\u{1D658}" => "c", + "\u{1D659}" => "d", + "\u{1D65A}" => "e", + "\u{1D65B}" => "f", + "\u{1D65C}" => "g", + "\u{1D65D}" => "h", + "\u{1D65E}" => "i", + "\u{1D65F}" => "j", + "\u{1D660}" => "k", + "\u{1D661}" => "l", + "\u{1D662}" => "m", + "\u{1D663}" => "n", + "\u{1D664}" => "o", + "\u{1D665}" => "p", + "\u{1D666}" => "q", + "\u{1D667}" => "r", + "\u{1D668}" => "s", + "\u{1D669}" => "t", + "\u{1D66A}" => "u", + "\u{1D66B}" => "v", + "\u{1D66C}" => "w", + "\u{1D66D}" => "x", + "\u{1D66E}" => "y", + "\u{1D66F}" => "z", + "\u{1D670}" => "A", + "\u{1D671}" => "B", + "\u{1D672}" => "C", + "\u{1D673}" => "D", + "\u{1D674}" => "E", + "\u{1D675}" => "F", + "\u{1D676}" => "G", + "\u{1D677}" => "H", + "\u{1D678}" => "I", + "\u{1D679}" => "J", + "\u{1D67A}" => "K", + "\u{1D67B}" => "L", + "\u{1D67C}" => "M", + "\u{1D67D}" => "N", + "\u{1D67E}" => "O", + "\u{1D67F}" => "P", + "\u{1D680}" => "Q", + "\u{1D681}" => "R", + "\u{1D682}" => "S", + "\u{1D683}" => "T", + "\u{1D684}" => "U", + "\u{1D685}" => "V", + "\u{1D686}" => "W", + "\u{1D687}" => "X", + "\u{1D688}" => "Y", + "\u{1D689}" => "Z", + "\u{1D68A}" => "a", + "\u{1D68B}" => "b", + "\u{1D68C}" => "c", + "\u{1D68D}" => "d", + "\u{1D68E}" => "e", + "\u{1D68F}" => "f", + "\u{1D690}" => "g", + "\u{1D691}" => "h", + "\u{1D692}" => "i", + "\u{1D693}" => "j", + "\u{1D694}" => "k", + "\u{1D695}" => "l", + "\u{1D696}" => "m", + "\u{1D697}" => "n", + "\u{1D698}" => "o", + "\u{1D699}" => "p", + "\u{1D69A}" => "q", + "\u{1D69B}" => "r", + "\u{1D69C}" => "s", + "\u{1D69D}" => "t", + "\u{1D69E}" => "u", + "\u{1D69F}" => "v", + "\u{1D6A0}" => "w", + "\u{1D6A1}" => "x", + "\u{1D6A2}" => "y", + "\u{1D6A3}" => "z", + "\u{1D7CE}" => "0", + "\u{1D7CF}" => "1", + "\u{1D7D0}" => "2", + "\u{1D7D1}" => "3", + "\u{1D7D2}" => "4", + "\u{1D7D3}" => "5", + "\u{1D7D4}" => "6", + "\u{1D7D5}" => "7", + "\u{1D7D6}" => "8", + "\u{1D7D7}" => "9", + "\u{1D7D8}" => "0", + "\u{1D7D9}" => "1", + "\u{1D7DA}" => "2", + "\u{1D7DB}" => "3", + "\u{1D7DC}" => "4", + "\u{1D7DD}" => "5", + "\u{1D7DE}" => "6", + "\u{1D7DF}" => "7", + "\u{1D7E0}" => "8", + "\u{1D7E1}" => "9", + "\u{1D7E2}" => "0", + "\u{1D7E3}" => "1", + "\u{1D7E4}" => "2", + "\u{1D7E5}" => "3", + "\u{1D7E6}" => "4", + "\u{1D7E7}" => "5", + "\u{1D7E8}" => "6", + "\u{1D7E9}" => "7", + "\u{1D7EA}" => "8", + "\u{1D7EB}" => "9", + "\u{1D7EC}" => "0", + "\u{1D7ED}" => "1", + "\u{1D7EE}" => "2", + "\u{1D7EF}" => "3", + "\u{1D7F0}" => "4", + "\u{1D7F1}" => "5", + "\u{1D7F2}" => "6", + "\u{1D7F3}" => "7", + "\u{1D7F4}" => "8", + "\u{1D7F5}" => "9", + "\u{1D7F6}" => "0", + "\u{1D7F7}" => "1", + "\u{1D7F8}" => "2", + "\u{1D7F9}" => "3", + "\u{1D7FA}" => "4", + "\u{1D7FB}" => "5", + "\u{1D7FC}" => "6", + "\u{1D7FD}" => "7", + "\u{1D7FE}" => "8", + "\u{1D7FF}" => "9", + "\u{1F100}" => "0.", + "\u{1F101}" => "0,", + "\u{1F102}" => "1,", + "\u{1F103}" => "2,", + "\u{1F104}" => "3,", + "\u{1F105}" => "4,", + "\u{1F106}" => "5,", + "\u{1F107}" => "6,", + "\u{1F108}" => "7,", + "\u{1F109}" => "8,", + "\u{1F10A}" => "9,", + "\u{1F110}" => "(A)", + "\u{1F111}" => "(B)", + "\u{1F112}" => "(C)", + "\u{1F113}" => "(D)", + "\u{1F114}" => "(E)", + "\u{1F115}" => "(F)", + "\u{1F116}" => "(G)", + "\u{1F117}" => "(H)", + "\u{1F118}" => "(I)", + "\u{1F119}" => "(J)", + "\u{1F11A}" => "(K)", + "\u{1F11B}" => "(L)", + "\u{1F11C}" => "(M)", + "\u{1F11D}" => "(N)", + "\u{1F11E}" => "(O)", + "\u{1F11F}" => "(P)", + "\u{1F120}" => "(Q)", + "\u{1F121}" => "(R)", + "\u{1F122}" => "(S)", + "\u{1F123}" => "(T)", + "\u{1F124}" => "(U)", + "\u{1F125}" => "(V)", + "\u{1F126}" => "(W)", + "\u{1F127}" => "(X)", + "\u{1F128}" => "(Y)", + "\u{1F129}" => "(Z)", + "\u{1F12B}" => "C", + "\u{1F12C}" => "R", + "\u{1F12D}" => "CD", + "\u{1F12E}" => "WZ", + "\u{1F130}" => "A", + "\u{1F131}" => "B", + "\u{1F132}" => "C", + "\u{1F133}" => "D", + "\u{1F134}" => "E", + "\u{1F135}" => "F", + "\u{1F136}" => "G", + "\u{1F137}" => "H", + "\u{1F138}" => "I", + "\u{1F139}" => "J", + "\u{1F13A}" => "K", + "\u{1F13B}" => "L", + "\u{1F13C}" => "M", + "\u{1F13D}" => "N", + "\u{1F13E}" => "O", + "\u{1F13F}" => "P", + "\u{1F140}" => "Q", + "\u{1F141}" => "R", + "\u{1F142}" => "S", + "\u{1F143}" => "T", + "\u{1F144}" => "U", + "\u{1F145}" => "V", + "\u{1F146}" => "W", + "\u{1F147}" => "X", + "\u{1F148}" => "Y", + "\u{1F149}" => "Z", + "\u{1F14A}" => "HV", + "\u{1F14B}" => "MV", + "\u{1F14C}" => "SD", + "\u{1F14D}" => "SS", + "\u{1F14E}" => "PPV", + "\u{1F14F}" => "WC", + "\u{1F16A}" => "MC", + "\u{1F16B}" => "MD", + "\u{1F16C}" => "MR", + "\u{1F190}" => "DJ", + "\u{1FBF0}" => "0", + "\u{1FBF1}" => "1", + "\u{1FBF2}" => "2", + "\u{1FBF3}" => "3", + "\u{1FBF4}" => "4", + "\u{1FBF5}" => "5", + "\u{1FBF6}" => "6", + "\u{1FBF7}" => "7", + "\u{1FBF8}" => "8", + "\u{1FBF9}" => "9", + ]; +} diff --git a/src/Internal/SegmentRules.php b/src/Internal/SegmentRules.php new file mode 100644 index 0000000..5460f26 --- /dev/null +++ b/src/Internal/SegmentRules.php @@ -0,0 +1,95 @@ + '/\A[A-Z]+\z/', + 'digits' => '/\A[0-9]+\z/', + 'letters-or-digits' => '/\A[A-Z0-9]+\z/', + ]; + + // NIPOST has not confirmed which characters a district uses, so no fix changes one. + private const FIXES = [ + 'letters' => SpecData::LETTER_FIXES, + 'digits' => SpecData::DIGIT_FIXES, + 'letters-or-digits' => [], + ]; + + /** + * Returns the first rule that a code of a valid length breaks, or null when it breaks none. + * The state comes first, then each segment in order. + */ + public static function check(string $compact, bool $allowPartial): ?ParseError + { + $segments = Segments::fromCompact($compact); + if (!isset(SpecData::STATES[$segments->state])) { + $suggestion = self::suggest($segments, $compact, $allowPartial); + + return ParseError::of(ParseErrorCode::UnknownState, Precision::State, $suggestion); + } + foreach ($segments->toArray() as $name => $text) { + if ($text !== null && !self::follows($text, SpecData::SEGMENTS[$name])) { + $segment = Precision::from($name); + $suggestion = self::suggest($segments, $compact, $allowPartial); + + return ParseError::of(ParseErrorCode::BadSegment, $segment, $suggestion); + } + } + + return null; + } + + /** + * @param array{characters: key-of, minimum: ?int} $rule + */ + private static function follows(string $text, array $rule): bool + { + if (\preg_match(self::PATTERNS[$rule['characters']], $text) !== 1) { + return false; + } + + return $rule['minimum'] === null || (int) $text >= $rule['minimum']; + } + + // A suggestion is a hint for the user. parse never returns the fixed code as a success. + // The nested parse stops after one level. A fix table never writes a character that it + // changes, so a fixed code has nothing left to fix, and the second suggest() returns null. + private static function suggest( + Segments $segments, + string $compact, + bool $allowPartial, + ): ?string { + $fixed = ''; + foreach ($segments->toArray() as $name => $text) { + if ($text !== null) { + $fixed .= \strtr($text, self::FIXES[SpecData::SEGMENTS[$name]['characters']]); + } + } + if ($fixed === $compact) { + return null; + } + + return Postcode::parse($fixed, allowPartial: $allowPartial)->value?->canonical; + } +} diff --git a/src/Internal/SpecData.php b/src/Internal/SpecData.php new file mode 100644 index 0000000..db02a79 --- /dev/null +++ b/src/Internal/SpecData.php @@ -0,0 +1,160 @@ + 'Abia', + 'AD' => 'Adamawa', + 'AK' => 'Akwa Ibom', + 'AN' => 'Anambra', + 'BA' => 'Bauchi', + 'BE' => 'Benue', + 'BR' => 'Borno', + 'BY' => 'Bayelsa', + 'CR' => 'Cross River', + 'DE' => 'Delta', + 'EB' => 'Ebonyi', + 'ED' => 'Edo', + 'EK' => 'Ekiti', + 'EN' => 'Enugu', + 'FC' => 'Federal Capital Territory', + 'GM' => 'Gombe', + 'IM' => 'Imo', + 'JI' => 'Jigawa', + 'KD' => 'Kaduna', + 'KE' => 'Kebbi', + 'KG' => 'Kogi', + 'KN' => 'Kano', + 'KT' => 'Katsina', + 'KW' => 'Kwara', + 'LA' => 'Lagos', + 'NA' => 'Nasarawa', + 'NI' => 'Niger', + 'OG' => 'Ogun', + 'ON' => 'Ondo', + 'OS' => 'Osun', + 'OY' => 'Oyo', + 'PL' => 'Plateau', + 'RI' => 'Rivers', + 'SK' => 'Sokoto', + 'TA' => 'Taraba', + 'YB' => 'Yobe', + 'ZA' => 'Zamfara', + ]; + + /** + * Each segment from the least to the most precise, with its place in a compact code. + */ + public const SEGMENTS = [ + 'state' => ['start' => 0, 'length' => 2, 'end' => 2, 'characters' => 'letters', 'minimum' => null], + 'lga' => ['start' => 2, 'length' => 2, 'end' => 4, 'characters' => 'digits', 'minimum' => 1], + 'district' => ['start' => 4, 'length' => 3, 'end' => 7, 'characters' => 'letters-or-digits', 'minimum' => null], + 'area' => ['start' => 7, 'length' => 2, 'end' => 9, 'characters' => 'letters', 'minimum' => null], + 'unit' => ['start' => 9, 'length' => 2, 'end' => 11, 'characters' => 'digits', 'minimum' => 1], + ]; + + /** + * GPS accuracy limits in metres. Each limit is inclusive. + */ + public const PRECISION_THRESHOLDS = [ + ['maxAccuracyM' => 8, 'precision' => 'unit'], + ['maxAccuracyM' => 20, 'precision' => 'area'], + ['maxAccuracyM' => 50, 'precision' => 'district'], + ]; + + /** + * The precision for an accuracy that no limit covers. + */ + public const PRECISION_FALLBACK = 'lga'; + + /** + * Each separator that normalize removes, mapped to the empty string for strtr(). + */ + public const SEPARATORS = [ + "\u{0009}" => '', + "\u{000A}" => '', + "\u{000B}" => '', + "\u{000C}" => '', + "\u{000D}" => '', + " " => '', + "-" => '', + "." => '', + "\u{0085}" => '', + "\u{00A0}" => '', + "\u{00AD}" => '', + "\u{1680}" => '', + "\u{2000}" => '', + "\u{2001}" => '', + "\u{2002}" => '', + "\u{2003}" => '', + "\u{2004}" => '', + "\u{2005}" => '', + "\u{2006}" => '', + "\u{2007}" => '', + "\u{2008}" => '', + "\u{2009}" => '', + "\u{200A}" => '', + "\u{200B}" => '', + "\u{200C}" => '', + "\u{200D}" => '', + "\u{200E}" => '', + "\u{200F}" => '', + "\u{2010}" => '', + "\u{2011}" => '', + "\u{2012}" => '', + "\u{2013}" => '', + "\u{2014}" => '', + "\u{2015}" => '', + "\u{2028}" => '', + "\u{2029}" => '', + "\u{202F}" => '', + "\u{205F}" => '', + "\u{2060}" => '', + "\u{2212}" => '', + "\u{3000}" => '', + "\u{FEFF}" => '', + ]; + + /** + * Old 6-digit NIPOST postcodes. With D, $ does not match before a final line feed. + */ + public const LEGACY_PATTERN = '/^[0-9]{6}$/D'; + + /** + * Typo fixes in digit segments. + */ + public const DIGIT_FIXES = [ + 'O' => '0', + 'I' => '1', + 'L' => '1', + ]; + + /** + * Typo fixes in letter segments. + */ + public const LETTER_FIXES = [ + '0' => 'O', + '1' => 'I', + ]; +} diff --git a/src/ParseError.php b/src/ParseError.php new file mode 100644 index 0000000..f573e96 --- /dev/null +++ b/src/ParseError.php @@ -0,0 +1,35 @@ +isOk()) { + * echo $result->value->canonical; + * } else { + * echo $result->error->code->value; + * } + * ``` + */ +final class ParseResult +{ + private function __construct( + /** The postcode, or null when the parse failed. */ + public readonly ?Postcode $value, + /** Why the parse failed, or null when it succeeded. */ + public readonly ?ParseError $error, + ) {} + + /** + * @internal Postcode::parse() creates each result. + */ + public static function success(Postcode $value): self + { + return new self($value, null); + } + + /** + * @internal Postcode::parse() creates each result. + */ + public static function failure(ParseError $error): self + { + return new self(null, $error); + } + + /** + * Tells whether the parse found a postcode. Static analysers such as PHPStan then know + * which of value and error is set. + * + * @phpstan-assert-if-true !null $this->value + * @phpstan-assert-if-true null $this->error + * @phpstan-assert-if-false null $this->value + * @phpstan-assert-if-false !null $this->error + */ + public function isOk(): bool + { + return $this->value !== null; + } +} diff --git a/src/Postcode.php b/src/Postcode.php new file mode 100644 index 0000000..3d7443b --- /dev/null +++ b/src/Postcode.php @@ -0,0 +1,320 @@ +compact = $compact; + $this->segments = Segments::fromCompact($compact); + $present = \array_filter( + $this->segments->toArray(), + static fn(?string $text): bool => $text !== null, + ); + $this->canonical = \implode('-', $present); + $this->display = \implode(' ', $present); + $this->precision = $precision; + } + + /** + * Reads a postcode that a user typed or pasted. It accepts spaces, hyphens, dashes and any + * letter case. It never throws. Input over the spec's input limit fails with BadLength + * before parse normalises it, so long text cannot stall a server. Input that is not valid + * UTF-8 fails with BadCharacter, or with BadLength when it has more than 4 bytes for each + * code point of the input limit. + * + * After normalisation, parse applies these checks in order. The first check that fails + * gives the error code: Empty, LegacyCode, BadCharacter, BadLength, UnknownState and + * BadSegment. For UnknownState and BadSegment, the error names the segment, and it can + * suggest the canonical form of one fixed code. The fix replaces a look-alike character + * in the wrong place, such as the letter O in an LGA. It never changes the district. + * + * ```php + * $result = Postcode::parse('ek 01 a03 fk 01'); + * if ($result->isOk()) { + * echo $result->value->canonical; // EK-01-A03-FK-01 + * } + * ``` + * + * @param string $input Text from a user. + * @param bool $allowPartial Also accept a code that stops after the state, LGA, district + * or area. + */ + public static function parse(string $input, bool $allowPartial = false): ParseResult + { + $inputProblem = self::checkInput($input); + if ($inputProblem !== null) { + return self::failure($inputProblem); + } + $text = self::normalize($input); + if ($text === '') { + return self::failure(ParseErrorCode::Empty); + } + if (\preg_match(SpecData::LEGACY_PATTERN, $text) === 1) { + return self::failure(ParseErrorCode::LegacyCode); + } + if (\preg_match(SegmentRules::PATTERNS['letters-or-digits'], $text) !== 1) { + return self::failure(ParseErrorCode::BadCharacter); + } + $precision = self::precisionOfLength(\strlen($text)); + if ($precision === null || (!$allowPartial && $precision !== Precision::Unit)) { + return self::failure(ParseErrorCode::BadLength); + } + $segmentError = SegmentRules::check($text, $allowPartial); + if ($segmentError !== null) { + return ParseResult::failure($segmentError); + } + + return ParseResult::success(new self($text, $precision)); + } + + /** + * Normalises text that a user typed or pasted. It applies Unicode NFKC to each character whose + * NFKC form holds only ASCII characters and separators, such as full-width letters. It + * removes the separators that the spec lists: white space, hyphens and dashes, the full + * stop and some zero-width characters. Then it makes the ASCII letters a to z upper case. + * It keeps every other character and every byte that is not valid UTF-8. It does not + * check the result, and it does not limit the length of its input. + * + * ```php + * echo Postcode::normalize(' ek-01 a03.fk-01 '); // EK01A03FK01 + * ``` + * + * @param string $input Text from a user. + */ + public static function normalize(string $input): string + { + $compatible = \strtr($input, NfkcTable::FORMS); + + return self::upperAscii(\strtr($compatible, SpecData::SEPARATORS)); + } + + /** + * Tells whether text is an old 6-digit NIPOST postcode, which names an area, not a + * building. It is a thin wrapper over parse(), so it is false for input over the input + * limit and for input that is not valid UTF-8. + * + * ```php + * Postcode::isLegacy('900 108'); // true + * ``` + * + * @param string $input Text from a user. + */ + public static function isLegacy(string $input): bool + { + return self::parse($input)->error?->code === ParseErrorCode::LegacyCode; + } + + /** + * Returns the English name of a state from its two-letter code. The lookup ignores the + * case of the ASCII letters only. It does not trim spaces or change other characters. + * + * ```php + * echo Postcode::stateName('fc'); // Federal Capital Territory + * ``` + * + * @param string $stateCode A state code, such as EK. + * + * @return ?string The name, or null for an unknown code. + */ + public static function stateName(string $stateCode): ?string + { + return SpecData::STATES[self::upperAscii($stateCode)] ?? null; + } + + /** + * Returns the most precise segment that a GPS fix of this accuracy supports. Each limit + * in the spec is inclusive, so a fix equal to a limit gets the precision of that limit. + * + * ```php + * Postcode::precisionForAccuracy(35.0); // Precision::District + * ``` + * + * @param ?float $accuracyM The accuracy radius in metres. Null, NaN, infinite and negative + * values mean that the accuracy is unknown, which gives the LGA. + */ + public static function precisionForAccuracy(?float $accuracyM): Precision + { + if ($accuracyM === null || $accuracyM < 0) { + return Precision::from(SpecData::PRECISION_FALLBACK); + } + // NaN and positive infinity fail every comparison below, so they reach the fallback. + foreach (SpecData::PRECISION_THRESHOLDS as $threshold) { + if ($accuracyM <= $threshold['maxAccuracyM']) { + return Precision::from($threshold['precision']); + } + } + + return Precision::from(SpecData::PRECISION_FALLBACK); + } + + /** + * Shortens the postcode to a less precise segment, for example from a building to its area. + * + * ```php + * echo $postcode->truncate(Precision::District)->canonical; // EK-01-A03 + * ``` + * + * @param Precision $to The precision to keep. It must not be more precise than the code. + * + * @throws InvalidArgumentException When $to is more precise than the code. + */ + public function truncate(Precision $to): self + { + $end = SpecData::SEGMENTS[$to->value]['end']; + if ($end > \strlen($this->compact)) { + throw new InvalidArgumentException(\sprintf( + 'Cannot truncate a %1$s postcode to %2$s. Choose %1$s or a less precise segment.', + $this->precision->value, + $to->value, + )); + } + + return new self(\substr($this->compact, 0, $end), $to); + } + + /** + * Returns the postcode one segment less precise, such as the area of a building. + * + * ```php + * echo $postcode->parent()?->canonical; // EK-01-A03-FK + * ``` + * + * @return ?self The parent, or null for a state code. + */ + public function parent(): ?self + { + $larger = null; + // SEGMENTS run from the least to the most precise, so the last match is the parent. + foreach (SpecData::SEGMENTS as $name => $segment) { + if ($segment['end'] < \strlen($this->compact)) { + $larger = Precision::from($name); + } + } + + return $larger === null ? null : $this->truncate($larger); + } + + /** + * Tells whether another postcode lies inside this one, such as a building inside its + * district. A postcode contains itself. + * + * ```php + * $district->contains($building); // true + * ``` + * + * @param self $code The postcode to test. + */ + public function contains(self $code): bool + { + // A precision has one fixed compact length, so a prefix always ends at a segment boundary. + return \str_starts_with($code->compact, $this->compact); + } + + /** + * Hides the unit, so that a log shows the area but not the building. A code without a + * unit stays as it is. + * + * ```php + * echo $postcode->redact(); // EK-01-A03-FK-** + * ``` + */ + public function redact(): string + { + if ($this->segments->unit === null) { + return $this->canonical; + } + + return \substr($this->canonical, 0, -\strlen($this->segments->unit)) . '**'; + } + + private static function checkInput(string $input): ?ParseErrorCode + { + if (\strlen($input) > self::MAX_BYTES_PER_CODE_POINT * SpecData::MAX_INPUT_CODE_POINTS) { + return ParseErrorCode::BadLength; + } + + // With the u flag, preg_match() returns false for input that is not valid UTF-8. + return match (\preg_match(self::WITHIN_INPUT_LIMIT, $input)) { + 1 => null, + 0 => ParseErrorCode::BadLength, + false => ParseErrorCode::BadCharacter, + }; + } + + private static function precisionOfLength(int $length): ?Precision + { + foreach (SpecData::SEGMENTS as $name => $segment) { + if ($segment['end'] === $length) { + return Precision::from($name); + } + } + + return null; + } + + private static function failure(ParseErrorCode $code): ParseResult + { + return ParseResult::failure(ParseError::of($code)); + } + + private static function upperAscii(string $text): string + { + return \strtr($text, self::LOWER_ASCII, self::UPPER_ASCII); + } +} diff --git a/src/Precision.php b/src/Precision.php new file mode 100644 index 0000000..b6fe70c --- /dev/null +++ b/src/Precision.php @@ -0,0 +1,30 @@ +state = \substr($compact, $state['start'], $state['length']); + $this->lga = self::slice($compact, 'lga'); + $this->district = self::slice($compact, 'district'); + $this->area = self::slice($compact, 'area'); + $this->unit = self::slice($compact, 'unit'); + } + + /** + * @internal Postcode builds its segments from a checked compact code. + */ + public static function fromCompact(string $compact): self + { + return new self($compact); + } + + /** + * @internal The segments in order, keyed by the name of their precision. + * + * @return array{state: string, lga: ?string, district: ?string, area: ?string, unit: ?string} + */ + public function toArray(): array + { + return [ + 'state' => $this->state, + 'lga' => $this->lga, + 'district' => $this->district, + 'area' => $this->area, + 'unit' => $this->unit, + ]; + } + + /** + * @param key-of $name + */ + private static function slice(string $compact, string $name): ?string + { + $segment = SpecData::SEGMENTS[$name]; + if (\strlen($compact) < $segment['end']) { + return null; + } + + return \substr($compact, $segment['start'], $segment['length']); + } +} diff --git a/tests/Fixtures/ApiDump/Base.php b/tests/Fixtures/ApiDump/Base.php new file mode 100644 index 0000000..acd93b1 --- /dev/null +++ b/tests/Fixtures/ApiDump/Base.php @@ -0,0 +1,18 @@ +name(); + } +} diff --git a/tests/Fixtures/ApiDump/Colour.php b/tests/Fixtures/ApiDump/Colour.php new file mode 100644 index 0000000..d79cb4f --- /dev/null +++ b/tests/Fixtures/ApiDump/Colour.php @@ -0,0 +1,26 @@ +mark() . $this->value; + } + + private function mark(): string + { + return '#'; + } +} diff --git a/tests/Fixtures/ApiDump/Hidden.php b/tests/Fixtures/ApiDump/Hidden.php new file mode 100644 index 0000000..7aa5541 --- /dev/null +++ b/tests/Fixtures/ApiDump/Hidden.php @@ -0,0 +1,16 @@ +side * $this->side; + } + + public function name(): string + { + return $this->reset() . self::SECRET . $this->note; + } + + public static function of(float $side = 2.5): self + { + return new self($side); + } + + // PHP 8.5 reports self as the class name, so a type that names the class must read as self. + public function twin(Square $other): Square + { + return $other; + } + + public function join(string ...$parts): string + { + return \implode(',', $parts); + } + + public function swap(int &$left, int &$right): void + { + [$left, $right] = [$right, $left]; + } + + public function scale(int|float $factor): static + { + return new static($this->side * $factor); + } + + /** + * @param array $options + */ + public function configure( + array $options = [], + ?string $label = null, + Colour $colour = Colour::Red, + bool $strict = false, + ): void {} + + /** + * @internal Only the package calls this method. + */ + public function wire(): void {} + + protected function guard(): void {} + + private function reset(): string + { + return ''; + } +} diff --git a/tests/Fixtures/ApiDump/Sub/Part.php b/tests/Fixtures/ApiDump/Sub/Part.php new file mode 100644 index 0000000..c3895dd --- /dev/null +++ b/tests/Fixtures/ApiDump/Sub/Part.php @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/tests/Fixtures/coverage/below-floor.xml b/tests/Fixtures/coverage/below-floor.xml new file mode 100644 index 0000000..beae0bf --- /dev/null +++ b/tests/Fixtures/coverage/below-floor.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/tests/Fixtures/coverage/constants-class.xml b/tests/Fixtures/coverage/constants-class.xml new file mode 100644 index 0000000..76aa1b1 --- /dev/null +++ b/tests/Fixtures/coverage/constants-class.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/Fixtures/coverage/no-branch-attributes.xml b/tests/Fixtures/coverage/no-branch-attributes.xml new file mode 100644 index 0000000..beedbc5 --- /dev/null +++ b/tests/Fixtures/coverage/no-branch-attributes.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/tests/Fixtures/coverage/no-branch-data.xml b/tests/Fixtures/coverage/no-branch-data.xml new file mode 100644 index 0000000..0a3249f --- /dev/null +++ b/tests/Fixtures/coverage/no-branch-data.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/tests/Fixtures/coverage/no-code.xml b/tests/Fixtures/coverage/no-code.xml new file mode 100644 index 0000000..a223581 --- /dev/null +++ b/tests/Fixtures/coverage/no-code.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/tests/Fixtures/coverage/unrun-class.xml b/tests/Fixtures/coverage/unrun-class.xml new file mode 100644 index 0000000..5a9c1c9 --- /dev/null +++ b/tests/Fixtures/coverage/unrun-class.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/Fixtures/coverage/unrun-straight-class.xml b/tests/Fixtures/coverage/unrun-straight-class.xml new file mode 100644 index 0000000..0d2a093 --- /dev/null +++ b/tests/Fixtures/coverage/unrun-straight-class.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/Fixtures/generator/serve-download.php b/tests/Fixtures/generator/serve-download.php new file mode 100644 index 0000000..657b90c --- /dev/null +++ b/tests/Fixtures/generator/serve-download.php @@ -0,0 +1,56 @@ +payload = $payload; + + return true; + } + + public function stream_read(int $count): string + { + $chunk = \substr($this->payload, $this->offset, $count); + $this->offset += \strlen($chunk); + + return $chunk; + } + + public function stream_eof(): bool + { + return $this->offset >= \strlen($this->payload); + } + + /** + * @return array + */ + public function stream_stat(): array + { + return ['size' => \strlen($this->payload)]; + } +}; + +\stream_wrapper_unregister('https'); +\stream_wrapper_register('https', $standIn::class); diff --git a/tests/Fixtures/generator/spec/data/format.json b/tests/Fixtures/generator/spec/data/format.json new file mode 100644 index 0000000..f5d2ac0 --- /dev/null +++ b/tests/Fixtures/generator/spec/data/format.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "maxInputCodePoints": 64, + "segments": [ + { "name": "state", "length": 2, "characters": "letters" }, + { "name": "lga", "length": 2, "characters": "digits", "minimum": 1 } + ], + "legacyPattern": "^[0-9]{6}$", + "separators": ["U+0020", "U+002D"], + "suggestions": { + "digits": { "O": "0", "I": "1", "L": "1" }, + "letters": { "0": "O", "1": "I" } + } +} diff --git a/tests/Fixtures/generator/spec/data/precision.json b/tests/Fixtures/generator/spec/data/precision.json new file mode 100644 index 0000000..bae9dd9 --- /dev/null +++ b/tests/Fixtures/generator/spec/data/precision.json @@ -0,0 +1,9 @@ +{ + "version": 1, + "source": "A small data set for the generator tests.", + "thresholds": [ + { "maxAccuracyM": 8, "precision": "unit" }, + { "maxAccuracyM": 20, "precision": "area" } + ], + "fallback": "lga" +} diff --git a/tests/Fixtures/generator/spec/data/states.json b/tests/Fixtures/generator/spec/data/states.json new file mode 100644 index 0000000..16bbb42 --- /dev/null +++ b/tests/Fixtures/generator/spec/data/states.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "source": "A small data set for the generator tests.", + "states": [ + { "code": "EK", "name": "Ekiti", "iso": "NG-EK" }, + { "code": "LA", "name": "Lagos", "iso": "NG-LA" } + ] +} diff --git a/tests/Fixtures/licences/allowed.json b/tests/Fixtures/licences/allowed.json new file mode 100644 index 0000000..7f3f36a --- /dev/null +++ b/tests/Fixtures/licences/allowed.json @@ -0,0 +1,17 @@ +{ + "packages": [ + { "name": "example/mit", "version": "1.0.0", "license": ["MIT"] }, + { "name": "example/isc", "version": "1.0.0", "license": ["ISC"] }, + { "name": "example/apache", "version": "1.0.0", "license": ["Apache-2.0"] }, + { "name": "example/bsd-2", "version": "1.0.0", "license": ["BSD-2-Clause"] }, + { "name": "example/bsd-3", "version": "1.0.0", "license": ["BSD-3-Clause"] }, + { "name": "example/zero-bsd", "version": "1.0.0", "license": ["0BSD"] }, + { "name": "example/blue-oak", "version": "1.0.0", "license": ["BlueOak-1.0.0"] }, + { "name": "example/cc0", "version": "1.0.0", "license": ["CC0-1.0"] }, + { "name": "example/python", "version": "1.0.0", "license": ["Python-2.0"] } + ], + "packages-dev": [ + { "name": "example/dual", "version": "2.0.0", "license": ["GPL-3.0-only", "MIT"] }, + { "name": "example/or-text", "version": "v3.0.0", "license": ["(GPL-2.0-only or Apache-2.0)"] } + ] +} diff --git a/tests/Fixtures/licences/nameless.json b/tests/Fixtures/licences/nameless.json new file mode 100644 index 0000000..5e3cc0b --- /dev/null +++ b/tests/Fixtures/licences/nameless.json @@ -0,0 +1 @@ +{ "packages": [{ "version": "1.0.0", "license": ["MIT"] }], "packages-dev": [] } diff --git a/tests/Fixtures/licences/no-packages.json b/tests/Fixtures/licences/no-packages.json new file mode 100644 index 0000000..78e804d --- /dev/null +++ b/tests/Fixtures/licences/no-packages.json @@ -0,0 +1 @@ +{ "packages": [], "packages-dev": [] } diff --git a/tests/Fixtures/licences/not-json.json b/tests/Fixtures/licences/not-json.json new file mode 100644 index 0000000..32595a6 --- /dev/null +++ b/tests/Fixtures/licences/not-json.json @@ -0,0 +1 @@ +{ "packages": [ diff --git a/tests/Fixtures/licences/refused.json b/tests/Fixtures/licences/refused.json new file mode 100644 index 0000000..89a6a85 --- /dev/null +++ b/tests/Fixtures/licences/refused.json @@ -0,0 +1,20 @@ +{ + "packages": [ + { "name": "example/copyleft", "version": "1.0.0", "license": ["GPL-3.0-only"] }, + { "name": "example/network", "version": "1.1.0", "license": ["AGPL-3.0-only"] }, + { "name": "example/weak", "version": "1.2.0", "license": ["MPL-2.0"] }, + { "name": "example/closed", "version": "1.3.0", "license": ["proprietary"] }, + { "name": "example/fine", "version": "1.4.0", "license": ["MIT"] } + ], + "packages-dev": [ + { + "name": "example/no-choice", + "version": "2.0.0", + "license": ["GPL-3.0-only", "LGPL-3.0-only"] + }, + { "name": "example/both", "version": "2.1.0", "license": ["(MIT and BSD-3-Clause)"] }, + { "name": "example/similar", "version": "2.2.0", "license": ["BSD-4-Clause"] }, + { "name": "example/silent", "version": "2.3.0" }, + { "name": "example/empty", "version": "2.4.0", "license": [] } + ] +} diff --git a/tests/Fixtures/vectors/no-cases.json b/tests/Fixtures/vectors/no-cases.json new file mode 100644 index 0000000..781cd72 --- /dev/null +++ b/tests/Fixtures/vectors/no-cases.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "function": "stateName", + "description": "A small vector file for the loader tests.", + "cases": [] +} diff --git a/tests/Fixtures/vectors/repeated-id.json b/tests/Fixtures/vectors/repeated-id.json new file mode 100644 index 0000000..17b660f --- /dev/null +++ b/tests/Fixtures/vectors/repeated-id.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "function": "stateName", + "description": "A small vector file for the loader tests.", + "cases": [ + { + "id": "state-name-001", + "description": "names EK", + "input": "EK", + "options": {}, + "expect": { + "value": "Ekiti" + } + }, + { + "id": "state-name-001", + "description": "returns nothing for an unknown code", + "input": "XX", + "options": {}, + "expect": { + "value": null + } + } + ] +} diff --git a/tests/Fixtures/vectors/same-name.json b/tests/Fixtures/vectors/same-name.json new file mode 100644 index 0000000..090f022 --- /dev/null +++ b/tests/Fixtures/vectors/same-name.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "function": "stateName", + "description": "A small vector file for the loader tests.", + "cases": [ + { + "id": "a b", + "description": "c", + "input": "EK", + "options": {}, + "expect": { + "value": "Ekiti" + } + }, + { + "id": "a", + "description": "b c", + "input": "XX", + "options": {}, + "expect": { + "value": null + } + } + ] +} diff --git a/tests/Fixtures/vectors/valid.json b/tests/Fixtures/vectors/valid.json new file mode 100644 index 0000000..1fee99e --- /dev/null +++ b/tests/Fixtures/vectors/valid.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "function": "stateName", + "description": "A small vector file for the loader tests.", + "cases": [ + { + "id": "state-name-001", + "description": "names EK", + "input": "EK", + "options": {}, + "expect": { "value": "Ekiti" } + }, + { + "id": "state-name-002", + "description": "returns nothing for an unknown code", + "input": "XX", + "options": {}, + "expect": { "value": null } + } + ] +} diff --git a/tests/Fixtures/vectors/wrong-version.json b/tests/Fixtures/vectors/wrong-version.json new file mode 100644 index 0000000..369a3d1 --- /dev/null +++ b/tests/Fixtures/vectors/wrong-version.json @@ -0,0 +1,21 @@ +{ + "version": 2, + "function": "stateName", + "description": "A small vector file for the loader tests.", + "cases": [ + { + "id": "state-name-001", + "description": "names EK", + "input": "EK", + "options": {}, + "expect": { "value": "Ekiti" } + }, + { + "id": "state-name-002", + "description": "returns nothing for an unknown code", + "input": "XX", + "options": {}, + "expect": { "value": null } + } + ] +} diff --git a/tests/ScriptRun.php b/tests/ScriptRun.php new file mode 100644 index 0000000..036629d --- /dev/null +++ b/tests/ScriptRun.php @@ -0,0 +1,32 @@ +&1'; + \exec($commandLine, $lines, $exitCode); + + return new self($exitCode, \implode("\n", $lines)); + } +} diff --git a/tests/TemporaryFolder.php b/tests/TemporaryFolder.php new file mode 100644 index 0000000..6c021af --- /dev/null +++ b/tests/TemporaryFolder.php @@ -0,0 +1,60 @@ +isDir()) { + \rmdir($item->getPathname()); + } else { + \unlink($item->getPathname()); + } + } + \rmdir($path); + } +} diff --git a/tests/Unit/CheckApiTest.php b/tests/Unit/CheckApiTest.php new file mode 100644 index 0000000..e9fc5c6 --- /dev/null +++ b/tests/Unit/CheckApiTest.php @@ -0,0 +1,122 @@ +repo = $repo; + $stub = "{$repo}/tools/bc-check/vendor/bin/roave-backward-compatibility-check"; + \mkdir(\dirname($stub), 0o777, true); + \file_put_contents($stub, self::STUB); + \chmod($stub, 0o755); + } + + protected function tearDown(): void + { + TemporaryFolder::remove($this->repo); + } + + /** + * @return array, string}> + */ + public static function tagsAndTheNewestOne(): array + { + return [ + 'a release and its alpha' => [['v0.1.0-alpha.0', 'v0.1.0'], 'v0.1.0'], + 'the alpha of the next version' => [ + ['v0.1.0-alpha.0', 'v0.1.0', 'v0.1.1-alpha.0'], + 'v0.1.1-alpha.0', + ], + 'versions with different numbers of digits' => [['v0.9.0', 'v0.10.0'], 'v0.10.0'], + ]; + } + + /** + * @param list $tags + */ + #[Test] + #[DataProvider('tagsAndTheNewestOne')] + public function startsTheBcCheckFromTheNewestTag(array $tags, string $newest): void + { + $this->startRepo(...$tags); + + $run = $this->checkApi(); + + self::assertSame(0, $run->exitCode, $run->output); + self::assertSame("BC check from {$newest}", $run->output); + } + + #[Test] + public function stopsBeforeTheFirstTag(): void + { + $this->startRepo(); + + $run = $this->checkApi(); + + self::assertSame(0, $run->exitCode, $run->output); + self::assertSame( + 'No release tag yet, so there is no released API to compare.', + $run->output, + ); + } + + // The repo starts in each test, so that tearDown() removes it even when a git command fails. + private function startRepo(string ...$tags): void + { + $this->git('init', '--quiet'); + $this->git('commit', '--quiet', '--allow-empty', '--message', 'Start'); + foreach ($tags as $tag) { + $this->git('tag', $tag); + } + } + + private function git(string ...$arguments): void + { + $run = ScriptRun::of( + 'git', + '-C', + $this->repo, + '-c', + 'user.name=Test', + '-c', + 'user.email=test@example.com', + '-c', + 'commit.gpgsign=false', + '-c', + 'tag.gpgsign=false', + ...$arguments, + ); + + self::assertSame(0, $run->exitCode, $run->output); + } + + private function checkApi(): ScriptRun + { + $script = \dirname(__DIR__, 2) . '/scripts/check-api.sh'; + + return ScriptRun::of('sh', '-c', 'cd "$1" && exec "$2"', 'sh', $this->repo, $script); + } +} diff --git a/tests/Unit/CheckCommitMsgTest.php b/tests/Unit/CheckCommitMsgTest.php new file mode 100644 index 0000000..680ec9e --- /dev/null +++ b/tests/Unit/CheckCommitMsgTest.php @@ -0,0 +1,164 @@ +'; + + /** + * @return array + */ + public static function acceptedMessages(): array + { + return [ + 'a subject, a body and a sign-off' => [self::message(self::SUBJECT, self::SIGN_OFF)], + 'a subject of 72 characters' => [ + self::message('chore(repo): ' . \str_repeat('a', 59), self::SIGN_OFF), + ], + 'a co-author line for a person' => [ + self::message( + self::SUBJECT, + 'Co-authored-by: Grace Hopper ', + self::SIGN_OFF, + ), + ], + // The check reads the address, so the first name of a person is no reason to refuse. + 'a co-author line for a person named Claude' => [ + self::message( + self::SUBJECT, + 'Co-authored-by: Claude Monet ', + self::SIGN_OFF, + ), + ], + 'a co-author line for a person named Copilot' => [ + self::message( + self::SUBJECT, + 'Co-authored-by: Copilot Jones ', + self::SIGN_OFF, + ), + ], + ]; + } + + #[Test] + #[DataProvider('acceptedMessages')] + public function acceptsAMessage(string $message): void + { + $run = self::check($message); + + self::assertSame(0, $run->exitCode, $run->output); + self::assertSame('', $run->output); + } + + /** + * @return array + */ + public static function rejectedMessages(): array + { + return [ + 'a subject with no type and scope' => [ + self::message('Set up the tools', self::SIGN_OFF), + 'Write the subject as type(scope)', + ], + 'a subject of 73 characters' => [ + self::message('chore(repo): ' . \str_repeat('a', 60), self::SIGN_OFF), + 'The subject has 73 characters', + ], + 'a message with no sign-off' => [self::message(self::SUBJECT), 'Signed-off-by'], + 'a sign-off with no email address' => [ + self::message(self::SUBJECT, 'Signed-off-by: Ada Lovelace'), + 'Signed-off-by', + ], + 'a co-author line for Claude' => self::coAuthorRefused( + 'Co-Authored-By: Claude ', + ), + 'a co-author line with the address of Claude and another name' => self::coAuthorRefused( + 'Co-authored-by: Assistant ', + ), + 'a co-author line for Copilot' => self::coAuthorRefused( + 'Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>', + ), + 'a co-author line for Codex' => self::coAuthorRefused( + 'Co-authored-by: Codex ', + ), + 'a co-author line at the domain of Anthropic' => self::coAuthorRefused( + 'Co-authored-by: Claude ', + ), + 'a co-author line at the domain of OpenAI' => self::coAuthorRefused( + 'Co-authored-by: Assistant ', + ), + 'a co-author line for Copilot at github.com' => self::coAuthorRefused( + 'Co-authored-by: Copilot ', + ), + 'a co-author line for Claude Code at GitHub' => self::coAuthorRefused( + 'Co-authored-by: Claude Code ', + ), + 'a co-author line for Gemini Code Assist' => self::coAuthorRefused( + 'Co-authored-by: Gemini ', + ), + 'a co-author line for Cursor' => self::coAuthorRefused( + 'Co-authored-by: Cursor Agent ', + ), + 'a co-author line for Aider' => self::coAuthorRefused( + 'Co-authored-by: aider (gpt-4o) ', + ), + 'a co-author line in lower case' => self::coAuthorRefused( + 'co-authored-by: claude ', + ), + ]; + } + + #[Test] + #[DataProvider('rejectedMessages')] + public function rejectsAMessage(string $message, string $reason): void + { + $run = self::check($message); + + self::assertSame(1, $run->exitCode); + self::assertStringContainsString($reason, $run->output); + } + + /** + * @return array{string, string} + */ + private static function coAuthorRefused(string $trailer): array + { + return [self::message(self::SUBJECT, $trailer, self::SIGN_OFF), 'Co-Authored-By']; + } + + private static function message(string $subject, string ...$trailers): string + { + $lines = [$subject, '', 'The body gives the reason.', '', ...$trailers]; + + return \implode("\n", $lines) . "\n"; + } + + private static function check(string $message): ScriptRun + { + $file = \tempnam(\sys_get_temp_dir(), 'gatepost-message-'); + self::assertIsString($file); + \file_put_contents($file, $message); + + try { + return ScriptRun::of('scripts/check-commit-msg', $file); + } finally { + \unlink($file); + } + } +} diff --git a/tests/Unit/CheckCoverageTest.php b/tests/Unit/CheckCoverageTest.php new file mode 100644 index 0000000..1ef615a --- /dev/null +++ b/tests/Unit/CheckCoverageTest.php @@ -0,0 +1,90 @@ + + */ + public static function acceptedReports(): array + { + return [ + 'branch coverage at the floor' => [ + 'at-floor.xml', + 'Branch coverage is 95.00 % (19 of 20).', + ], + 'a report with no code' => [ + 'no-code.xml', + 'The report has no branches, so there is nothing to cover.', + ], + 'a class of constants next to covered code' => [ + 'constants-class.xml', + 'Branch coverage is 95.00 % (19 of 20).', + ], + ]; + } + + #[Test] + #[DataProvider('acceptedReports')] + public function acceptsAReport(string $report, string $summary): void + { + $run = self::check($report); + + self::assertSame(0, $run->exitCode, $run->output); + self::assertSame($summary, $run->output); + } + + /** + * @return array + */ + public static function rejectedReports(): array + { + return [ + 'branch coverage below the floor' => [ + 'below-floor.xml', + 'Branch coverage is 94.00 % (47 of 50). The floor for core code is 95 %.', + ], + 'lines and a branch count of zero' => ['no-branch-data.xml', 'no branch data'], + 'lines and no branch attributes' => ['no-branch-attributes.xml', 'no branch data'], + 'a class that no test runs' => ['unrun-class.xml', 'Gatepost\Postcode\Neglected'], + 'a class with one path that no test runs' => [ + 'unrun-straight-class.xml', + 'Gatepost\Postcode\Straight', + ], + ]; + } + + #[Test] + #[DataProvider('rejectedReports')] + public function rejectsAReport(string $report, string $reason): void + { + $run = self::check($report); + + self::assertSame(1, $run->exitCode); + self::assertStringContainsString($reason, $run->output); + } + + private static function check(string $report): ScriptRun + { + return ScriptRun::of( + \PHP_BINARY, + 'scripts/check-coverage.php', + \dirname(__DIR__) . '/Fixtures/coverage/' . $report, + ); + } +} diff --git a/tests/Unit/CheckLicencesTest.php b/tests/Unit/CheckLicencesTest.php new file mode 100644 index 0000000..370fec2 --- /dev/null +++ b/tests/Unit/CheckLicencesTest.php @@ -0,0 +1,216 @@ +repo !== '') { + TemporaryFolder::remove($this->repo); + } + } + + #[Test] + public function acceptsPackagesWithAnAllowedLicenceOrAnAllowedAlternative(): void + { + $run = self::check('allowed.json'); + + self::assertSame(0, $run->exitCode, $run->output); + self::assertSame( + self::FIXTURES . 'allowed.json: 11 packages, each with an allowed licence.', + $run->output, + ); + } + + #[Test] + public function namesEachPackageWhoseLicenceIsNotAllowed(): void + { + $file = self::FIXTURES . 'refused.json'; + + $run = self::check('refused.json'); + + self::assertSame(1, $run->exitCode); + $lines = \explode("\n", $run->output); + self::assertStringContainsString('DEP-4', \array_pop($lines)); + self::assertSame( + [ + "{$file}: example/copyleft 1.0.0 has the licence GPL-3.0-only.", + "{$file}: example/network 1.1.0 has the licence AGPL-3.0-only.", + "{$file}: example/weak 1.2.0 has the licence MPL-2.0.", + "{$file}: example/closed 1.3.0 has the licence proprietary.", + "{$file}: example/no-choice 2.0.0 has the licence GPL-3.0-only or LGPL-3.0-only.", + "{$file}: example/both 2.1.0 has the licence (MIT and BSD-3-Clause).", + "{$file}: example/similar 2.2.0 has the licence BSD-4-Clause.", + "{$file}: example/silent 2.3.0 has no licence.", + "{$file}: example/empty 2.4.0 has no licence.", + ], + $lines, + ); + } + + #[Test] + public function checksEachFileThatItGetsAndReportsOnlyTheFailures(): void + { + $run = self::check('allowed.json', 'refused.json'); + + self::assertSame(1, $run->exitCode); + self::assertStringContainsString('refused.json: example/copyleft 1.0.0', $run->output); + self::assertStringNotContainsString('allowed.json', $run->output); + } + + /** + * @return array + */ + public static function unusableLockFiles(): array + { + $dir = self::FIXTURES; + + return [ + 'a file that does not exist' => ['absent.json', "Cannot read {$dir}absent.json."], + 'a file that is not JSON' => [ + 'not-json.json', + "{$dir}not-json.json is not valid JSON: Syntax error.", + ], + 'a lock with no package' => [ + 'no-packages.json', + "{$dir}no-packages.json lists no package, so it proves nothing.", + ], + 'a package with no name' => [ + 'nameless.json', + "{$dir}nameless.json holds a package that has no name or no version.", + ], + ]; + } + + #[Test] + #[DataProvider('unusableLockFiles')] + public function failsOnALockFileThatItCannotUse(string $fixture, string $message): void + { + $run = self::check($fixture); + + self::assertSame(2, $run->exitCode); + self::assertSame($message, $run->output); + } + + #[Test] + public function failsOnALockFileThatNoUserCanRead(): void + { + $lock = \tempnam(\sys_get_temp_dir(), 'gatepost-lock-'); + self::assertIsString($lock); + + try { + \copy(\dirname(__DIR__) . '/Fixtures/licences/allowed.json', $lock); + \chmod($lock, 0o000); + if (\is_readable($lock)) { + self::markTestSkipped('This user can read each file, as root can.'); + } + + $run = ScriptRun::of(\PHP_BINARY, 'scripts/check-licences.php', $lock); + } finally { + \chmod($lock, 0o600); + \unlink($lock); + } + + self::assertSame(2, $run->exitCode); + self::assertSame("Cannot read {$lock}.", $run->output); + } + + #[Test] + public function readsComposerLockAndEachToolLockWhenItGetsNoFile(): void + { + $this->startRepo('composer.lock', 'tools/alpha/composer.lock', 'tools/beta/composer.lock'); + + $run = $this->checkRepo(); + + self::assertSame(0, $run->exitCode, $run->output); + self::assertSame( + [ + 'composer.lock: 11 packages, each with an allowed licence.', + 'tools/alpha/composer.lock: 11 packages, each with an allowed licence.', + 'tools/beta/composer.lock: 11 packages, each with an allowed licence.', + ], + \explode("\n", $run->output), + ); + } + + #[Test] + public function failsWhenTheLockOfAToolProjectBreaksThePolicy(): void + { + $this->startRepo('composer.lock', 'tools/alpha/composer.lock'); + $refused = \dirname(__DIR__) . '/Fixtures/licences/refused.json'; + \copy($refused, "{$this->repo}/tools/alpha/composer.lock"); + + $run = $this->checkRepo(); + + self::assertSame(1, $run->exitCode); + self::assertStringContainsString( + 'tools/alpha/composer.lock: example/copyleft 1.0.0', + $run->output, + ); + self::assertStringNotContainsString("\ncomposer.lock", "\n" . $run->output); + } + + #[Test] + public function failsWhenTheRepoHasNoComposerLock(): void + { + $this->startRepo(); + + $run = $this->checkRepo(); + + self::assertSame(2, $run->exitCode); + self::assertSame('Cannot read composer.lock.', $run->output); + } + + private static function check(string ...$fixtures): ScriptRun + { + $paths = \array_map(static fn(string $name): string => self::FIXTURES . $name, $fixtures); + + return ScriptRun::of(\PHP_BINARY, 'scripts/check-licences.php', ...$paths); + } + + /** + * Makes a repo with a copy of the script and the given lock files, each of which holds the + * packages of allowed.json. The script finds its lock files from its own place, so the copy + * checks the repo that the test made. + */ + private function startRepo(string ...$locks): void + { + $this->repo = TemporaryFolder::create('gatepost-licences-'); + \mkdir("{$this->repo}/scripts"); + $script = \dirname(__DIR__, 2) . '/scripts/check-licences.php'; + \copy($script, "{$this->repo}/scripts/check-licences.php"); + foreach ($locks as $lock) { + $folder = \dirname("{$this->repo}/{$lock}"); + if (!\is_dir($folder)) { + \mkdir($folder, 0o777, true); + } + \copy(\dirname(__DIR__) . '/Fixtures/licences/allowed.json', "{$this->repo}/{$lock}"); + } + } + + private function checkRepo(): ScriptRun + { + return ScriptRun::of(\PHP_BINARY, "{$this->repo}/scripts/check-licences.php"); + } +} diff --git a/tests/Unit/DocExamplesTest.php b/tests/Unit/DocExamplesTest.php new file mode 100644 index 0000000..bcc128b --- /dev/null +++ b/tests/Unit/DocExamplesTest.php @@ -0,0 +1,191 @@ +redact(); // EK-01-A03-FK-**` + * shows the text that the line prints, and `$district->contains($building); // true` shows a value. + * The doc comment of a method may use the object of that method with no line that makes it, so each + * example runs after the lines of SETUP. + */ +final class DocExamplesTest extends TestCase +{ + private const SETUP = [ + '$input = \'ek 01 a03 fk 01\';', + '$postcode = Postcode::parse(\'EK-01-A03-FK-01\')->value;', + '$building = $postcode;', + '$district = Postcode::parse(\'EK-01-A03\', allowPartial: true)->value;', + ]; + + private const COMMENT_MARK = '/^\s*(?:\/\*\*|\*\/|\*)\s?/'; + private const ECHO_LINE = '/^(\s*)echo (.+); \/\/ (.+)$/'; + private const VALUE = '(?:true|false|null|-?\d+(?:\.\d+)?|\'[^\']*\'|[A-Z]\w*::\w+)'; + private const VALUE_LINE = '/^(\s*)(.+); \/\/ (' . self::VALUE . ')$/'; + // A comment that starts like a value, but that VALUE_LINE cannot read, would leave its result + // unchecked. The test fails on such a line, so that the author writes a form that it reads. + private const LOOKS_LIKE_VALUE = '/; \/\/ (?:true|false|null|-?\d|[\'"\[{]|[A-Z]\w*::)/'; + + /** + * @return array Each example, by its file and its place in the file. + */ + public static function examples(): array + { + $examples = []; + foreach (self::docComments() as $file => $comments) { + $blocks = []; + foreach ($comments as $comment) { + \preg_match_all('/^```php\n(.*?)^```$/ms', $comment, $found); + $blocks = [...$blocks, ...$found[1]]; + } + foreach ($blocks as $number => $block) { + $examples[$file . ' example ' . ($number + 1)] = [$block]; + } + } + + return $examples; + } + + #[Test] + #[DataProvider('examples')] + public function runsEachExampleAndComparesEachResultItShows(string $example): void + { + self::assertSame( + [], + self::unreadableResults($example), + 'The example shows a result in a form that this test cannot read.', + ); + $checked = 0; + $compare = function (mixed $actual, mixed $expected) use (&$checked): void { + self::assertSame($expected, $actual); + ++$checked; + }; + $file = \tempnam(\sys_get_temp_dir(), 'doc-example'); + self::assertIsString($file); + + // An example may print, and the lines that show a result print nothing, so the output + // of the example is dropped. + \ob_start(); + try { + \file_put_contents($file, self::module($example)); + $run = require $file; + self::assertInstanceOf(Closure::class, $run); + $run($compare); + } finally { + \ob_end_clean(); + \unlink($file); + } + + self::assertSame( + self::countResults($example), + $checked, + 'The example skipped a result that it shows. A guard, such as an if, can skip a line.', + ); + } + + /** + * The doc comments of each PHP file in src, without their comment marks. + * + * @return array> + */ + private static function docComments(): array + { + $source = \dirname(__DIR__, 2) . '/src'; + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS), + ); + $comments = []; + foreach ($files as $file) { + if (!$file instanceof SplFileInfo || $file->getExtension() !== 'php') { + continue; + } + $code = \file_get_contents($file->getPathname()); + self::assertIsString($code); + $name = \substr($file->getPathname(), \strlen($source) + 1); + foreach (\token_get_all($code) as $token) { + if (\is_array($token) && $token[0] === \T_DOC_COMMENT) { + $comments[$name][] = self::withoutMarks($token[1]); + } + } + } + \ksort($comments); + + return $comments; + } + + // A code fence starts a line only after the asterisks are gone. + private static function withoutMarks(string $comment): string + { + $lines = []; + foreach (\explode("\n", $comment) as $line) { + $lines[] = \preg_replace(self::COMMENT_MARK, '', $line) ?? $line; + } + + return \implode("\n", $lines); + } + + /** + * Turns an example into a file that returns a closure. Each line that shows a result calls + * $shown with the actual result and the shown one. + */ + private static function module(string $example): string + { + $lines = \array_map(static function (string $line): string { + if (\preg_match(self::ECHO_LINE, $line, $echo) === 1) { + $printed = \var_export($echo[3], true); + + return $echo[1] . '$shown((string) (' . $echo[2] . '), ' . $printed . ');'; + } + if (\preg_match(self::VALUE_LINE, $line, $value) === 1) { + return $value[1] . '$shown(' . $value[2] . ', ' . $value[3] . ');'; + } + + return $line; + }, \explode("\n", $example)); + + return " \preg_match(self::ECHO_LINE, $line) === 1 + || \preg_match(self::VALUE_LINE, $line) === 1, + ); + + return \count($results); + } + + /** + * @return list + */ + private static function unreadableResults(string $example): array + { + return \array_values(\array_filter( + \explode("\n", $example), + static fn(string $line): bool => \preg_match(self::LOOKS_LIKE_VALUE, $line) === 1 + && \preg_match(self::ECHO_LINE, $line) !== 1 + && \preg_match(self::VALUE_LINE, $line) !== 1, + )); + } +} diff --git a/tests/Unit/DumpApiFailureTest.php b/tests/Unit/DumpApiFailureTest.php new file mode 100644 index 0000000..0ecafe0 --- /dev/null +++ b/tests/Unit/DumpApiFailureTest.php @@ -0,0 +1,133 @@ +dump('--bogus'); + + self::assertSame(2, $run->exitCode); + self::assertSame('Unknown argument --bogus. Use --check or no argument.', $run->output); + } + + #[Test] + public function saysWhenTheRepoHasNoComposerJson(): void + { + \unlink("{$this->repo}/composer.json"); + + $run = $this->dump(); + + self::assertSame(2, $run->exitCode); + self::assertSame('Cannot read composer.json.', $run->output); + } + + /** + * @return array, string}> + */ + public static function unusableComposerFiles(): array + { + $prefix = self::FIXTURE_NAMESPACE . '\\'; + + return [ + 'no name' => [ + ['autoload' => ['psr-4' => [$prefix => 'src/']]], + 'composer.json needs a name.', + ], + 'no PSR-4 map' => [ + ['name' => 'fixture/api'], + 'composer.json needs an autoload.psr-4 map.', + ], + 'a prefix with two folders' => [ + ['name' => 'fixture/api', 'autoload' => ['psr-4' => [$prefix => ['src/', 'lib/']]]], + 'Each PSR-4 entry of composer.json must name one folder.', + ], + ]; + } + + /** + * @param array $composer + */ + #[Test] + #[DataProvider('unusableComposerFiles')] + public function saysWhatAComposerJsonLacks(array $composer, string $message): void + { + \file_put_contents("{$this->repo}/composer.json", \json_encode($composer)); + + $run = $this->dump(); + + self::assertSame(2, $run->exitCode, $run->output); + self::assertSame($message, $run->output); + } + + #[Test] + public function namesTheFileThatDeclaresNoSymbolOfItsName(): void + { + \file_put_contents( + "{$this->repo}/src/Stray.php", + "dump(); + + self::assertSame(2, $run->exitCode, $run->output); + self::assertSame( + self::FIXTURE_NAMESPACE . '\\Stray is not a class, an interface or an enum. ' + . 'Check that the file name follows the class name.', + $run->output, + ); + } + + #[Test] + public function saysWhichDefaultValueItCannotWrite(): void + { + $code = "repo}/src/Odd.php", $code); + + $run = $this->dump(); + + self::assertSame(2, $run->exitCode, $run->output); + $owner = self::FIXTURE_NAMESPACE . '\\Odd::make()'; + self::assertSame( + "Cannot write the default value of \$tool in {$owner}. The script writes null, " + . 'booleans, numbers, printable ASCII text, an empty array and enum cases, but the ' + . 'value has the type stdClass.', + $run->output, + ); + } + + #[Test] + public function saysWhichConstantHoldsTextThatItCannotWrite(): void + { + $code = "repo}/src/Cafe.php", $code); + + $run = $this->dump(); + + self::assertSame(2, $run->exitCode, $run->output); + self::assertSame( + 'Cannot write the value of the constant ' . self::FIXTURE_NAMESPACE . '\\Cafe::NAME. ' + . 'The script writes null, booleans, numbers, printable ASCII text, an empty array and ' + . 'enum cases, but the value has the type string with a character outside printable ' + . 'ASCII.', + $run->output, + ); + } +} diff --git a/tests/Unit/DumpApiTest.php b/tests/Unit/DumpApiTest.php new file mode 100644 index 0000000..e8dab1d --- /dev/null +++ b/tests/Unit/DumpApiTest.php @@ -0,0 +1,89 @@ +dump(); + + self::assertSame(0, $run->exitCode, $run->output); + self::assertSame('', $run->output); + $expected = \file_get_contents(\dirname(__DIR__) . '/Fixtures/ApiDump/expected.md'); + self::assertSame($expected, $this->dumpFile()); + } + + // The tests job of CI runs this on each PHP version, and the dump must not depend on it. + #[Test] + public function writesTheCommittedDumpOfThisPackageOnEachPhpVersion(): void + { + $run = ScriptRun::of(\PHP_BINARY, 'scripts/dump-api.php', '--check'); + + self::assertSame(0, $run->exitCode, $run->output); + } + + #[Test] + public function passesTheCheckWhenTheFileIsCurrent(): void + { + $this->dump(); + + $run = $this->dump('--check'); + + self::assertSame(0, $run->exitCode, $run->output); + self::assertSame('', $run->output); + } + + #[Test] + public function failsTheCheckWhenAPublicMethodChangedAfterTheLastWrite(): void + { + $this->dump(); + $square = "{$this->repo}/src/Square.php"; + $code = \file_get_contents($square); + self::assertIsString($code); + \file_put_contents($square, \str_replace('function join(', 'function glue(', $code)); + $before = $this->dumpFile(); + + $run = $this->dump('--check'); + + self::assertSame(1, $run->exitCode); + self::assertSame(self::DUMP . ' is out of date. Run composer api:dump.', $run->output); + self::assertSame($before, $this->dumpFile()); + } + + #[Test] + public function failsTheCheckWhenSomeoneEditedTheFile(): void + { + $this->dump(); + \file_put_contents("{$this->repo}/" . self::DUMP, "\nA line from a person.\n", FILE_APPEND); + + $run = $this->dump('--check'); + + self::assertSame(1, $run->exitCode); + self::assertSame(self::DUMP . ' is out of date. Run composer api:dump.', $run->output); + } + + #[Test] + public function failsTheCheckWhenTheFileDoesNotExist(): void + { + $run = $this->dump('--check'); + + self::assertSame(1, $run->exitCode); + self::assertSame(self::DUMP . ' is out of date. Run composer api:dump.', $run->output); + self::assertFileDoesNotExist("{$this->repo}/" . self::DUMP); + } +} diff --git a/tests/Unit/DumpApiTestCase.php b/tests/Unit/DumpApiTestCase.php new file mode 100644 index 0000000..f1b3f4c --- /dev/null +++ b/tests/Unit/DumpApiTestCase.php @@ -0,0 +1,83 @@ +repo = TemporaryFolder::create('gatepost-dump-api-'); + \mkdir("{$this->repo}/scripts"); + \mkdir("{$this->repo}/docs"); + foreach (['dump-api.php', 'dump-api-symbols.php'] as $script) { + \copy(\dirname(__DIR__, 2) . "/scripts/{$script}", "{$this->repo}/scripts/{$script}"); + } + \file_put_contents("{$this->repo}/composer.json", \json_encode([ + 'name' => 'fixture/api', + 'autoload' => ['psr-4' => [self::FIXTURE_NAMESPACE . '\\' => 'src/']], + ], JSON_THROW_ON_ERROR)); + self::copyFixtureClasses(\dirname(__DIR__) . '/Fixtures/ApiDump', "{$this->repo}/src"); + } + + protected function tearDown(): void + { + TemporaryFolder::remove($this->repo); + } + + protected function dump(string ...$arguments): ScriptRun + { + return ScriptRun::of(\PHP_BINARY, "{$this->repo}/scripts/dump-api.php", ...$arguments); + } + + protected function dumpFile(): string + { + $text = \file_get_contents("{$this->repo}/" . self::DUMP); + self::assertIsString($text); + + return $text; + } + + /** + * Copies each PHP file of a folder, with its place in the folder. The expected dump stays + * behind, because it is not a class. + */ + private static function copyFixtureClasses(string $from, string $to): void + { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($from, FilesystemIterator::SKIP_DOTS), + ); + foreach ($files as $file) { + if (!$file instanceof SplFileInfo || $file->getExtension() !== 'php') { + continue; + } + $target = $to . \substr($file->getPathname(), \strlen($from)); + if (!\is_dir(\dirname($target))) { + \mkdir(\dirname($target), 0o777, true); + } + \copy($file->getPathname(), $target); + } + } +} diff --git a/tests/Unit/GenerateDataFileTest.php b/tests/Unit/GenerateDataFileTest.php new file mode 100644 index 0000000..0954ead --- /dev/null +++ b/tests/Unit/GenerateDataFileTest.php @@ -0,0 +1,75 @@ + Each row breaks one file and + * gives the reason that must show. + */ + public static function brokenFiles(): array + { + $format = 'spec/data/format.json'; + $states = 'spec/data/states.json'; + + return [ + 'a data file that is not JSON' => [ + self::edit('spec/data/precision.json', '"version": 1', '"version": '), + 'spec/data/precision.json is not valid JSON: Syntax error.', + ], + 'a state code that appears twice' => [ + self::edit($states, '"code": "LA"', '"code": "EK"'), + 'spec/data/states.json: the state code EK appears more than once.', + ], + 'a segment name that appears twice' => [ + self::edit($format, '"name": "lga"', '"name": "state"'), + 'spec/data/format.json: the segment name state appears more than once.', + ], + 'a separator that appears twice' => [ + self::edit($format, '"U+0020"', '"U+002D"'), + 'spec/data/format.json: the separator U+002D appears more than once.', + ], + // The pattern of a label allows 4 to 6 digits, so U+002D has two more spellings. + 'two labels for one separator' => [ + self::edit($format, '"U+0020"', '"U+00002D"'), + 'spec/data/format.json: the separator U+002D appears more than once.', + ], + 'a data file that no user can read' => [ + static function (string $root) use ($states): void { + \chmod("{$root}/{$states}", 0o000); + if (\is_readable("{$root}/{$states}")) { + self::markTestSkipped('This user can read each file, as root can.'); + } + }, + 'Cannot read', + ], + ]; + } + + /** + * @param Closure(string): void $breakFile + */ + #[Test] + #[DataProvider('brokenFiles')] + public function stopsOnABrokenFileWithItsNameAndNoPhpWarning( + Closure $breakFile, + string $reason, + ): void { + $this->assertStopsOnBrokenData($breakFile, $reason); + } +} diff --git a/tests/Unit/GenerateDataTest.php b/tests/Unit/GenerateDataTest.php new file mode 100644 index 0000000..7fa4897 --- /dev/null +++ b/tests/Unit/GenerateDataTest.php @@ -0,0 +1,259 @@ +generate(); + $checked = $this->generate('--check'); + + self::assertSame(0, $written->exitCode, $written->output); + self::assertSame('', $written->output); + $classes = $this->generatedClasses(); + self::assertSame(self::GENERATED, \array_keys($classes)); + $specData = $classes[self::SPEC_DATA]; + self::assertStringContainsString("'EK' => 'Ekiti',", $specData); + self::assertStringContainsString('LEGACY_PATTERN = \'/^[0-9]{6}$/D\';', $specData); + self::assertStringNotContainsString('NG-EK', $specData); + $table = $classes[self::NFKC_TABLE]; + $version = self::UNICODE_VERSION; + self::assertStringContainsString("UNICODE_VERSION = '{$version}';", $table); + self::assertStringContainsString('"\u{FF21}" => "A",', $table); + self::assertStringNotContainsString('"\u{00B5}" =>', $table); + self::assertSame(0, $checked->exitCode, $checked->output); + self::assertSame('', $checked->output); + } + + /** + * Each row makes a written class stale or removes it, and gives the path that the check must + * name. + * + * @return array + */ + public static function staleClasses(): array + { + return [ + 'a value that someone edited in the class' => [ + self::edit( + self::SPEC_DATA, + 'MAX_INPUT_CODE_POINTS = 64;', + 'MAX_INPUT_CODE_POINTS = 65;', + ), + self::SPEC_DATA, + ], + 'a limit that the spec changed after the last run' => [ + self::edit('spec/data/precision.json', '"maxAccuracyM": 8', '"maxAccuracyM": 9'), + self::SPEC_DATA, + ], + 'a class that someone deleted' => [self::delete(self::SPEC_DATA), self::SPEC_DATA], + 'a form that someone edited in the table' => [ + self::edit(self::NFKC_TABLE, '"\u{00AA}" => "a",', '"\u{00AA}" => "b",'), + self::NFKC_TABLE, + ], + ]; + } + + /** + * @param Closure(string): void $makeStale + */ + #[Test] + #[DataProvider('staleClasses')] + public function failsTheCheckAndLeavesTheFilesAsTheyAre( + Closure $makeStale, + string $stalePath, + ): void { + $this->generate(); + $makeStale($this->root); + $before = $this->generatedClasses(); + + $run = $this->generate('--check'); + + self::assertSame(1, $run->exitCode, $run->output); + self::assertSame("{$stalePath} is out of date. Run composer generate.", $run->output); + self::assertSame($before, $this->generatedClasses()); + } + + /** + * @return array}> + */ + public static function unknownArguments(): array + { + return [ + 'an unknown argument' => [['--bogus']], + 'an unknown argument next to --check' => [['--check', '--bogus']], + ]; + } + + /** + * @param list $arguments + */ + #[Test] + #[DataProvider('unknownArguments')] + public function rejectsAnUnknownArgument(array $arguments): void + { + $run = $this->generate(...$arguments); + + self::assertSame(2, $run->exitCode, $run->output); + self::assertStringContainsString('Unknown argument --bogus', $run->output); + self::assertSame([], $this->generatedClasses()); + } + + /** + * @return array Each row breaks the data and + * gives the reason that must + * show. + */ + public static function brokenData(): array + { + $format = 'spec/data/format.json'; + $precision = 'spec/data/precision.json'; + $digits = '"digits": { "O": "0", "I": "1", "L": "1" }'; + $letters = '"letters": { "0": "O", "1": "I" }'; + $notCodePoint = 'must be a code point from U+0000'; + + return [ + 'format.json with version 2' => [ + self::edit($format, '"version": 1', '"version": 2'), + 'spec/data/format.json must hold an object with version 1', + ], + 'precision.json with version 2' => [ + self::edit($precision, '"version": 1', '"version": 2'), + 'spec/data/precision.json must hold an object with version 1', + ], + 'states.json with version 2' => [ + self::edit('spec/data/states.json', '"version": 1', '"version": 2'), + 'spec/data/states.json must hold an object with version 1', + ], + 'a missing data file' => [ + self::delete('spec/data/states.json'), + 'Cannot read', + ], + 'a fix table that is a list' => [ + self::edit($format, $digits, '"digits": ["O", "I", "L"]'), + 'spec/data/format.json: suggestions.digits has the fix 0 => O', + ], + 'a fix table that writes a letter in a digit segment' => [ + self::edit($format, '"O": "0", "I": "1"', '"O": "A", "I": "1"'), + 'spec/data/format.json: suggestions.digits has the fix O => A', + ], + 'a fix table that writes a digit in a letter segment' => [ + self::edit($format, $letters, '"letters": { "O": "0" }'), + 'spec/data/format.json: suggestions.letters has the fix O => 0', + ], + // The row above breaks both halves of a letter fix, so it cannot show that each half + // is checked. Each of the next three rows breaks one half. + 'a letter segment fix that writes another digit' => [ + self::edit($format, $letters, '"letters": { "0": "1" }'), + 'spec/data/format.json: suggestions.letters has the fix 0 => 1', + ], + 'a letter segment fix that changes a letter' => [ + self::edit($format, $letters, '"letters": { "O": "A" }'), + 'spec/data/format.json: suggestions.letters has the fix O => A', + ], + 'a digit segment fix that changes a digit' => [ + self::edit($format, $digits, '"digits": { "0": "1" }'), + 'spec/data/format.json: suggestions.digits has the fix 0 => 1', + ], + 'a fix table that is not an object' => [ + self::edit($format, $digits, '"digits": "none"'), + 'spec/data/format.json: suggestions.digits must be an object with at least one ' + . 'fix, but it is "none"', + ], + 'a fix table with no fix' => [ + self::edit($format, $digits, '"digits": {}'), + 'spec/data/format.json: suggestions.digits must be an object with at least one ' + . 'fix, but it is []', + ], + 'limits that fall' => [ + self::edit($precision, '"maxAccuracyM": 20', '"maxAccuracyM": 5'), + 'spec/data/precision.json: the limit 5 must be above the limit 8', + ], + 'limits that repeat' => [ + self::edit($precision, '"maxAccuracyM": 20', '"maxAccuracyM": 8'), + 'spec/data/precision.json: the limit 8 must be above the limit 8', + ], + 'a separator above U+10FFFF' => [ + self::edit($format, '"U+002D"', '"U+110000"'), + "spec/data/format.json: the separator U+110000 {$notCodePoint}", + ], + 'the lowest surrogate as a separator' => [ + self::edit($format, '"U+002D"', '"U+D800"'), + "spec/data/format.json: the separator U+D800 {$notCodePoint}", + ], + 'the highest surrogate as a separator' => [ + self::edit($format, '"U+002D"', '"U+DFFF"'), + "spec/data/format.json: the separator U+DFFF {$notCodePoint}", + ], + 'a separator that is not written as a code point' => [ + self::edit($format, '"U+002D"', '"0x0009"'), + 'spec/data/format.json: each separator must be written as U+ and 4 to 6 hex ' + . 'digits, but it is "0x0009"', + ], + 'a cached UnicodeData.txt that someone changed' => [ + self::edit( + self::UNICODE_DATA, + 'FF21;FULLWIDTH LATIN CAPITAL LETTER A;Lu;0;L; 0041;', + 'FF21;FULLWIDTH LATIN CAPITAL LETTER A;Lu;0;L; 0042;', + ), + 'does not match its SHA-256 hash', + ], + ]; + } + + /** + * @param Closure(string): void $breakData + */ + #[Test] + #[DataProvider('brokenData')] + public function stopsOnBrokenDataWithAClearReasonAndKeepsTheOldClasses( + Closure $breakData, + string $reason, + ): void { + $this->assertStopsOnBrokenData($breakData, $reason); + } + + /** + * @return array + */ + public static function edgeSeparators(): array + { + return [ + 'the lowest code point' => ['U+0000'], + 'the code point before the surrogates' => ['U+D7FF'], + 'the code point after the surrogates' => ['U+E000'], + 'the highest code point' => ['U+10FFFF'], + ]; + } + + #[Test] + #[DataProvider('edgeSeparators')] + public function writesAClassThatParsesForAnEdgeCodePoint(string $label): void + { + self::edit('spec/data/format.json', '"U+002D"', "\"{$label}\"")($this->root); + + $run = $this->generate(); + + self::assertSame(0, $run->exitCode, $run->output); + $class = $this->generatedClasses()[self::SPEC_DATA] ?? null; + self::assertNotNull($class); + // PHP throws a ParseError here when the class has a syntax error. + self::assertNotSame([], \token_get_all($class, TOKEN_PARSE)); + } +} diff --git a/tests/Unit/GenerateNfkcTableTest.php b/tests/Unit/GenerateNfkcTableTest.php new file mode 100644 index 0000000..12b8238 --- /dev/null +++ b/tests/Unit/GenerateNfkcTableTest.php @@ -0,0 +1,131 @@ +generate(); + $classes = $this->generatedClasses(); + + self::assertSame($tableTags, self::spdxLines($classes[self::NFKC_TABLE] ?? '')); + self::assertSame($specDataTags, self::spdxLines($classes[self::SPEC_DATA] ?? '')); + } + + #[Test] + public function marksEachClassAsGeneratedWithinItsFirstFiveLines(): void + { + $this->generate(); + + $classes = $this->generatedClasses(); + + self::assertSame(self::GENERATED, \array_keys($classes)); + foreach ($classes as $path => $class) { + // check-tells reads only these lines to exempt a generated file from TELL-1. The + // three tags of the table would push the marker past them if the marker came last. + $head = \implode("\n", \array_slice(\explode("\n", $class), 0, 5)); + self::assertStringContainsString('@generated', $head, "{$path} has no marker."); + } + } + + #[Test] + public function cachesADownloadThatMatchesItsHash(): void + { + $unicodeData = self::cachedUnicodeData(); + + $run = $this->generateAfterDownloading($unicodeData); + + self::assertSame(0, $run->exitCode, $run->output); + self::assertSame('', $run->output); + self::assertSame( + \hash('sha256', $unicodeData), + \hash_file('sha256', "{$this->root}/" . self::UNICODE_DATA), + ); + self::assertSame(self::GENERATED, \array_keys($this->generatedClasses())); + } + + #[Test] + public function cachesNothingWhenADownloadFailsItsHash(): void + { + $partial = \substr(self::cachedUnicodeData(), 0, 1000); + + $run = $this->generateAfterDownloading($partial); + + self::assertNotSame(0, $run->exitCode, $run->output); + self::assertStringContainsString('does not match its SHA-256 hash', $run->output); + // A cached file that fails its hash would stop each later run until someone deleted it. + self::assertFileDoesNotExist("{$this->root}/" . self::UNICODE_DATA); + self::assertSame([], $this->generatedClasses()); + } + + /** + * Removes the cached UnicodeData.txt, so that the generator has to download it. The stand-in + * serves the given bytes for that download. + */ + private function generateAfterDownloading(string $served): ScriptRun + { + \unlink("{$this->root}/" . self::UNICODE_DATA); + self::assertNotFalse(\file_put_contents("{$this->root}/served-download", $served)); + $standIn = "{$this->root}/serve-download.php"; + \copy(\dirname(__DIR__) . '/Fixtures/generator/serve-download.php', $standIn); + + return ScriptRun::of( + \PHP_BINARY, + '-d', + 'display_errors=1', + '-d', + "auto_prepend_file={$standIn}", + "{$this->root}/scripts/generate-data.php", + ); + } + + private static function cachedUnicodeData(): string + { + $unicodeData = \file_get_contents(\dirname(__DIR__, 2) . '/' . self::UNICODE_DATA); + self::assertIsString($unicodeData); + + return $unicodeData; + } + + /** + * @return list The SPDX tags in the header of a written class. + */ + private static function spdxLines(string $class): array + { + $lines = \explode("\n", $class); + + return \array_values( + \array_filter($lines, static fn(string $line): bool => \str_contains($line, 'SPDX-')), + ); + } +} diff --git a/tests/Unit/GeneratorTestCase.php b/tests/Unit/GeneratorTestCase.php new file mode 100644 index 0000000..866ce4b --- /dev/null +++ b/tests/Unit/GeneratorTestCase.php @@ -0,0 +1,141 @@ +root = $root; + self::copyFiles(\dirname(__DIR__, 2) . '/scripts/*.php', "{$root}/scripts"); + self::copyFiles( + \dirname(__DIR__) . '/Fixtures/generator/spec/data/*.json', + "{$root}/spec/data", + ); + $cached = \dirname(__DIR__, 2) . '/' . self::UNICODE_DATA; + self::assertFileExists( + $cached, + 'The cached UnicodeData.txt is missing. Run composer generate first. It downloads ' + . 'the file into build/unicode.', + ); + self::copyFiles($cached, "{$root}/build/unicode"); + } + + protected function tearDown(): void + { + TemporaryFolder::remove($this->root); + } + + protected function generate(string ...$arguments): ScriptRun + { + return ScriptRun::of( + \PHP_BINARY, + '-d', + 'display_errors=1', + "{$this->root}/scripts/generate-data.php", + ...$arguments, + ); + } + + /** + * Breaks the data, runs the generator, and checks that it stops with the reason, prints no PHP + * warning and leaves the old classes as they were. + * + * @param Closure(string): void $breakData + */ + protected function assertStopsOnBrokenData(Closure $breakData, string $reason): void + { + $this->generate(); + $old = $this->generatedClasses(); + $breakData($this->root); + + $run = $this->generate(); + + self::assertNotSame(0, $run->exitCode, $run->output); + self::assertStringContainsString($reason, $run->output); + self::assertStringNotContainsString('Warning', $run->output); + self::assertSame($old, $this->generatedClasses()); + } + + /** + * @return array The written classes by path. A class that the root lacks has + * no entry. + */ + protected function generatedClasses(): array + { + $classes = []; + foreach (self::GENERATED as $path) { + $file = "{$this->root}/{$path}"; + if (!\is_file($file)) { + continue; + } + $class = \file_get_contents($file); + self::assertIsString($class); + $classes[$path] = $class; + } + + return $classes; + } + + /** + * @return Closure(string): void Changes text in a file under the temporary root. + */ + protected static function edit(string $file, string $search, string $replacement): Closure + { + return static function (string $root) use ($file, $search, $replacement): void { + $text = \file_get_contents("{$root}/{$file}"); + self::assertIsString($text); + self::assertStringContainsString($search, $text, "{$file} lacks the text."); + self::assertNotFalse( + \file_put_contents("{$root}/{$file}", \str_replace($search, $replacement, $text)), + ); + }; + } + + /** + * @return Closure(string): void Removes a file under the temporary root. + */ + protected static function delete(string $file): Closure + { + return static function (string $root) use ($file): void { + \unlink("{$root}/{$file}"); + }; + } + + private static function copyFiles(string $pattern, string $folder): void + { + $files = \glob($pattern); + self::assertIsArray($files); + self::assertNotSame([], $files, "{$pattern} matches no file."); + \mkdir($folder, 0o777, true); + foreach ($files as $file) { + \copy($file, "{$folder}/" . \basename($file)); + } + } +} diff --git a/tests/Unit/InputLimitTest.php b/tests/Unit/InputLimitTest.php new file mode 100644 index 0000000..711007d --- /dev/null +++ b/tests/Unit/InputLimitTest.php @@ -0,0 +1,59 @@ +error?->code); + self::assertSame(ParseErrorCode::BadLength, Postcode::parse($sixtyFive)->error?->code); + } + + #[Test] + public function countsEachCombiningMarkAsOneCodePoint(): void + { + $sixtyFour = 'E' . \str_repeat("\u{0301}", 63); + $sixtyFive = $sixtyFour . "\u{0301}"; + + self::assertSame(ParseErrorCode::BadCharacter, Postcode::parse($sixtyFour)->error?->code); + self::assertSame(ParseErrorCode::BadLength, Postcode::parse($sixtyFive)->error?->code); + } + + #[Test] + public function rejectsTenMegabytesOfTextAsOverTheInputLimit(): void + { + $result = Postcode::parse(\str_repeat('EK01A03FK01 ', 900_000)); + + self::assertSame(ParseErrorCode::BadLength, $result->error?->code); + } + + #[Test] + public function parsesACodeThatSpansLines(): void + { + // The limit check counts a line break as one code point. If its pattern lacked the s + // flag, the dot would stop at a line feed, and this short input would count as too long. + $result = Postcode::parse("EK-01\nA03\r\nFK-01\r"); + + self::assertSame('EK01A03FK01', $result->value?->compact); + } +} diff --git a/tests/Unit/InvalidUtf8Test.php b/tests/Unit/InvalidUtf8Test.php new file mode 100644 index 0000000..c3bea2c --- /dev/null +++ b/tests/Unit/InvalidUtf8Test.php @@ -0,0 +1,84 @@ + + */ + public static function invalidUtf8(): array + { + return [ + 'a byte that UTF-8 never uses' => ["EK01A03FK0\xFF"], + 'a sequence cut short' => ["EK01A03FK0\xC3"], + 'an encoded surrogate' => ["\xED\xA0\x80EK01A03FK01"], + 'an overlong slash' => ["EK01\xC0\xAFA03FK01"], + 'a lone continuation byte' => ["\x80EK01A03FK01"], + ]; + } + + #[Test] + #[DataProvider('invalidUtf8')] + public function rejectsTextThatIsNotValidUtf8AsABadCharacter(string $input): void + { + self::assertSame(ParseErrorCode::BadCharacter, Postcode::parse($input)->error?->code); + } + + #[Test] + public function rejectsMoreThan256BytesOfInvalidUtf8AsOverTheInputLimit(): void + { + $result = Postcode::parse(\str_repeat("\xFF", 257)); + + self::assertSame(ParseErrorCode::BadLength, $result->error?->code); + } + + #[Test] + public function rejectsInvalidUtf8UpTo256BytesAsABadCharacter(): void + { + // Both strings have more bytes than the input limit has code points. The limit for + // invalid text is 4 bytes for each of those code points, so 256 bytes. + $aboveTheCodePointLimit = Postcode::parse(\str_repeat('A', 70) . "\xFF"); + $atTheByteLimit = Postcode::parse(\str_repeat('A', 255) . "\xFF"); + + self::assertSame(ParseErrorCode::BadCharacter, $aboveTheCodePointLimit->error?->code); + self::assertSame(ParseErrorCode::BadCharacter, $atTheByteLimit->error?->code); + } + + #[Test] + public function rejectsInvalidUtf8WithOneByteOver256AsOverTheInputLimit(): void + { + $result = Postcode::parse(\str_repeat('A', 256) . "\xFF"); + + self::assertSame(ParseErrorCode::BadLength, $result->error?->code); + } + + #[Test] + public function findsNoLegacyPostcodeInTextThatIsNotValidUtf8(): void + { + self::assertFalse(Postcode::isLegacy("900108\xFF")); + } + + #[Test] + public function keepsTheBytesThatAreNotValidUtf8WhenItNormalises(): void + { + self::assertSame("EK\xFF01", Postcode::normalize("e-k\xFF 01")); + } +} diff --git a/tests/Unit/NfkcTableTest.php b/tests/Unit/NfkcTableTest.php new file mode 100644 index 0000000..e28ea9c --- /dev/null +++ b/tests/Unit/NfkcTableTest.php @@ -0,0 +1,102 @@ + + */ + public static function compatibilityForms(): array + { + return [ + 'a full-width capital A' => ["\u{FF21}", 'A'], + 'a full-width digit 7' => ["\u{FF17}", '7'], + 'a mathematical bold capital E' => ["\u{1D404}", 'E'], + 'a mathematical bold digit 1' => ["\u{1D7CF}", '1'], + 'a circled digit 1' => ["\u{2460}", '1'], + 'a superscript 2' => ["\u{00B2}", '2'], + 'the ligature fi' => ["\u{FB01}", 'fi'], + 'a Roman numeral 12' => ["\u{216B}", 'XII'], + 'a horizontal ellipsis' => ["\u{2026}", '...'], + 'the Kelvin sign' => ["\u{212A}", 'K'], + 'a subscript minus' => ["\u{208B}", "\u{2212}"], + 'a vertical em dash' => ["\u{FE31}", "\u{2014}"], + ]; + } + + #[Test] + #[DataProvider('compatibilityForms')] + public function mapsACharacterToItsNfkcForm(string $character, string $form): void + { + self::assertSame($form, NfkcTable::FORMS[$character] ?? null); + } + + #[Test] + public function mapsEachCharacterToAsciiTextOrSeparators(): void + { + $outside = []; + foreach (NfkcTable::FORMS as $character => $form) { + $oneCodePoint = \preg_match('/\A.\z/su', $character) === 1; + $ascii = \preg_match('/\A[\x00-\x7F]*\z/', \strtr($form, SpecData::SEPARATORS)) === 1; + if (!$oneCodePoint || !$ascii) { + $outside[] = \bin2hex($character); + } + } + + self::assertSame([], $outside); + } + + #[Test] + public function changesMathematicalBoldLettersAndDigitsToAscii(): void + { + // Both characters lie above U+FFFF, so their keys take 4 bytes in UTF-8. + self::assertSame('E1', Postcode::normalize("\u{1D404}\u{1D7CF}")); + } + + #[Test] + public function namesTheUnicodeDataInTheNotice(): void + { + $notice = \file_get_contents(\dirname(__DIR__, 2) . '/NOTICE'); + self::assertIsString($notice); + + self::assertStringContainsString('src/Internal/NfkcTable.php', $notice); + self::assertStringContainsString( + 'Unicode Character Database ' . NfkcTable::UNICODE_VERSION, + $notice, + ); + self::assertStringContainsString('Unicode-3.0', $notice); + } + + #[Test] + public function keepsACharacterWhoseNfkcFormHasOtherLetters(): void + { + // NFKC changes the micro sign U+00B5 to the Greek letter mu U+03BC. Neither can be part + // of a postcode, so the table leaves the micro sign out. + self::assertSame("EK\u{00B5}", Postcode::normalize("ek\u{00B5}")); + } + + #[Test] + public function rejectsACharacterThatTheTableLeavesOutAsFullNfkcWould(): void + { + $result = Postcode::parse("EK01A03FK0\u{00B5}"); + + self::assertSame(ParseErrorCode::BadCharacter, $result->error?->code); + } +} diff --git a/tests/Unit/PortabilityTest.php b/tests/Unit/PortabilityTest.php new file mode 100644 index 0000000..7ff39cf --- /dev/null +++ b/tests/Unit/PortabilityTest.php @@ -0,0 +1,337 @@ +, list}> + */ + public static function bannedNames(): array + { + return [ + 'functions from ext-mbstring (PHP-9)' => [[], ['mb_']], + 'classes and functions from ext-intl (PHP-9)' => [ + [ + 'collator', 'intlbreakiterator', 'intlchar', 'intldateformatter', 'locale', + 'messageformatter', 'normalizer', 'numberformatter', 'resourcebundle', + 'spoofchecker', 'transliterator', 'uconverter', + ], + [ + 'collator_', 'datefmt_', 'grapheme_', 'idn_', 'intl', 'locale_', 'msgfmt_', + 'normalizer_', 'numfmt_', 'resourcebundle_', 'transliterator_', + ], + ], + 'case functions that follow the locale on PHP 8.1' => [ + ['lcfirst', 'strtolower', 'strtoupper', 'ucfirst', 'ucwords'], + [], + ], + 'file access, which a Packagist install cannot serve (CS-2)' => [ + [ + 'chdir', 'chmod', 'copy', 'dir', 'directoryiterator', 'fclose', 'feof', + 'fflush', 'fgetc', 'fgetcsv', 'fgets', 'file', 'file_exists', + 'file_get_contents', 'file_put_contents', 'filemtime', 'filesize', + 'filesystemiterator', 'flock', 'fopen', 'fpassthru', 'fputcsv', 'fputs', + 'fread', 'fscanf', 'fseek', 'ftell', 'ftruncate', 'fwrite', 'getcwd', 'glob', + 'globiterator', 'hash_file', 'include', 'include_once', 'is_dir', 'is_file', + 'is_link', 'is_readable', 'is_writable', 'md5_file', 'mkdir', 'opendir', + 'parse_ini_file', 'readdir', 'readfile', 'realpath', + 'recursivedirectoryiterator', 'rename', 'require', 'require_once', 'rmdir', + 'scandir', 'sha1_file', 'splfileinfo', 'splfileobject', 'spltempfileobject', + 'stat', 'symlink', 'tempnam', 'tmpfile', 'touch', 'unlink', + ], + ['stream_'], + ], + 'the environment, the network and other processes (CS-2)' => [ + [ + 'exec', 'fsockopen', 'getenv', 'mail', 'passthru', 'pfsockopen', 'popen', + 'putenv', 'shell_exec', 'system', + ], + ['curl', 'gethostby', 'proc_', 'socket_'], + ], + 'the clock and randomness (CS-2)' => [ + [ + 'date', 'date_create', 'date_create_immutable', 'date_default_timezone_get', + 'date_default_timezone_set', 'datetime', 'datetimeimmutable', 'getdate', + 'gettimeofday', 'gmdate', 'gmmktime', 'gmstrftime', 'hrtime', 'idate', + 'localtime', 'microtime', 'mktime', 'sleep', 'strftime', 'strtotime', + 'time', 'time_nanosleep', 'time_sleep_until', 'usleep', + 'array_rand', 'lcg_value', 'mt_rand', 'mt_srand', 'openssl_random_pseudo_bytes', + 'password_hash', 'rand', 'random_bytes', 'random_int', 'shuffle', 'srand', + 'str_shuffle', 'uniqid', + ], + ['random\\'], + ], + ]; + } + + /** + * Code that uses a banned name, and the names that the check must find in it. The code is + * what a contributor could write in src. A name that only looks like a banned one must pass. + * + * @return array}> + */ + public static function plantedCode(): array + { + return [ + 'a call of fgets' => ['fgets($handle);', ['fgets']], + 'a call of unlink' => ['\unlink($path);', ['unlink']], + 'a call of mkdir' => ['\mkdir($path);', ['mkdir']], + 'a call of getenv' => ['\getenv("HOME");', ['getenv']], + 'a call of curl_init' => ['\curl_init();', ['curl_init']], + 'a new DateTimeImmutable' => ['new \DateTimeImmutable();', ['datetimeimmutable']], + 'a call of exec' => ['\exec($command);', ['exec']], + 'a class of the random namespace' => [ + 'new \Random\Randomizer();', + ['random\randomizer'], + ], + 'a require_once' => ["require_once 'data.php';", ['require_once']], + 'a function name as a string' => ['\array_map("strtoupper", $parts);', ['strtoupper']], + 'a static method of a banned class as a string' => [ + "\\call_user_func('Normalizer::normalize', \$text);", + ['normalizer'], + ], + 'the same with a backslash before the class' => [ + "\\call_user_func('\\Normalizer::normalize', \$text);", + ['normalizer'], + ], + 'methods and properties that share the name of a function' => [ + '$box->file(); $box->time; $box?->date(); Box::rand(); Box::DATE;', + [], + ], + 'a common word as a string' => [ + "\$kind = 'copy'; \$tool = \"system\"; \$id = 'rand';", + [], + ], + 'a common word as the class of a callable string' => [ + "\\call_user_func('Locale::getDefault');", + ['locale'], + ], + 'text that mentions a banned name' => [ + "throw new \\LogicException('Cannot find the file at this time.');", + [], + ], + 'a comment that mentions a banned name' => [ + "// fgets() and unlink()\n/* mkdir() */", + [], + ], + ]; + } + + // One pass over the source for every list. Under Xdebug path coverage, a pass for each list + // made composer check much slower. + #[Test] + public function keepsTheSourceFreeOfEveryBannedName(): void + { + $lookup = self::lookup(); + $found = []; + foreach (self::sourceFiles('src') as $file => $code) { + foreach (self::bannedIn($code, $lookup) as [$name, $line]) { + $found[] = "{$name} in {$file}:{$line}"; + } + } + + self::assertSame([], $found); + } + + /** + * @param list $expected + */ + #[Test] + #[DataProvider('plantedCode')] + public function findsABannedNameInPlantedCode(string $statements, array $expected): void + { + $found = self::bannedIn(" $code) { + foreach (\token_get_all($code) as $token) { + if ($token === '@' || (\is_array($token) && $token[0] === \T_EVAL)) { + $found[] = $file; + } + } + } + + self::assertSame([], $found, 'PHP-11 bans the @ operator and eval.'); + } + + /** + * The lists as lookups: the names as keys, and the prefixes as one pattern. + * + * @return array{array, string} + */ + private static function lookup(): array + { + $names = []; + $prefixes = []; + foreach (self::bannedNames() as [$groupNames, $groupPrefixes]) { + $names = [...$names, ...$groupNames]; + $prefixes = [...$prefixes, ...$groupPrefixes]; + } + $quoted = \array_map( + static fn(string $prefix): string => \preg_quote($prefix, '/'), + $prefixes, + ); + + return [\array_flip($names), '/\A(?:' . \implode('|', $quoted) . ')/']; + } + + /** + * The banned names that the code holds, each with its line. + * + * @param array{array, string} $lookup + * + * @return list + */ + private static function bannedIn(string $code, array $lookup): array + { + $found = []; + $previous = null; + foreach (\token_get_all($code) as $token) { + if (!\is_array($token)) { + $previous = $token; + continue; + } + [$id, $text, $line] = $token; + if ($id === \T_WHITESPACE || $id === \T_COMMENT || $id === \T_DOC_COMMENT) { + continue; + } + $isName = \in_array($id, self::NAME_TOKENS, true) + && !\in_array($previous, self::MEMBER_OPERATORS, true); + $previous = $id; + $isString = $id === \T_CONSTANT_ENCAPSED_STRING; + $name = $isName ? self::bannedName($text, $lookup, $isString) : null; + if ($name !== null) { + $found[] = [$name, $line]; + } + } + + return $found; + } + + /** + * @param array{array, string} $lookup + */ + private static function bannedName(string $text, array $lookup, bool $isString): ?string + { + [$banned, $prefixPattern] = $lookup; + foreach (self::spellings($text) as $name) { + if ($isString && \in_array($name, self::COMMON_WORDS, true)) { + continue; + } + if (isset($banned[$name]) || \preg_match($prefixPattern, $name) === 1) { + return $name; + } + } + + return null; + } + + /** + * The names that a token can mean, in lower case. A string literal can name a method as a + * callable, such as 'Normalizer::normalize'. Then the name of the class counts too. + * + * @return list + */ + private static function spellings(string $text): array + { + // The quotes of a string literal are not part of the name. + $name = \strtolower(\ltrim(\trim($text, '\'"'), '\\')); + + return [$name, \explode('::', $name)[0]]; + } + + /** + * The code of each PHP file in one folder of the repo, by its path from the repo root. + * + * @return array + */ + private static function sourceFiles(string $folder): array + { + $root = \dirname(__DIR__, 2); + $files = []; + $source = new RecursiveDirectoryIterator("{$root}/{$folder}"); + foreach (new RecursiveIteratorIterator($source) as $file) { + if (!$file instanceof SplFileInfo || $file->getExtension() !== 'php') { + continue; + } + $code = \file_get_contents($file->getPathname()); + self::assertIsString($code); + $files[\substr($file->getPathname(), \strlen($root) + 1)] = $code; + } + + return $files; + } +} diff --git a/tests/Unit/PostcodeTest.php b/tests/Unit/PostcodeTest.php new file mode 100644 index 0000000..a6cdcd9 --- /dev/null +++ b/tests/Unit/PostcodeTest.php @@ -0,0 +1,144 @@ + + */ + public static function valueTypes(): array + { + return [ + 'Postcode' => [Postcode::class], + 'Segments' => [Segments::class], + 'ParseResult' => [ParseResult::class], + 'ParseError' => [ParseError::class], + ]; + } + + /** + * Digit segments with a letter after a digit. The (int) cast of the minimum check reads + * only the leading digits of such text, so only the end of the digit pattern rejects it. + * + * @return array + */ + public static function lettersAfterADigit(): array + { + return [ + 'in the LGA' => ['EK1OA03FK01', Precision::Lga, 'EK-10-A03-FK-01'], + 'in the unit' => ['EK01A03FK1O', Precision::Unit, 'EK-01-A03-FK-10'], + ]; + } + + #[Test] + public function claimsTheVersionOfTheSpecSubmodule(): void + { + $specVersion = \file_get_contents(\dirname(__DIR__, 2) . '/spec/VERSION'); + self::assertIsString($specVersion); + + self::assertSame(\trim($specVersion), Postcode::SPEC_VERSION); + } + + /** + * @param class-string $class + */ + #[Test] + #[DataProvider('valueTypes')] + public function keepsEachValueTypeImmutable(string $class): void + { + $reflection = new ReflectionClass($class); + $constructor = $reflection->getConstructor(); + $writable = \array_filter( + $reflection->getProperties(), + static fn(ReflectionProperty $property): bool => !$property->isReadOnly(), + ); + + self::assertTrue($reflection->isFinal()); + self::assertTrue($constructor !== null && $constructor->isPrivate()); + self::assertSame([], $writable); + } + + #[Test] + public function rejectsAPartialCodeUnlessTheCallerAllowsOne(): void + { + self::assertSame(ParseErrorCode::BadLength, Postcode::parse('EK01A03')->error?->code); + } + + #[Test] + #[DataProvider('lettersAfterADigit')] + public function rejectsALetterAfterADigitInADigitSegment( + string $input, + Precision $segment, + string $suggestion, + ): void { + $error = Postcode::parse($input)->error; + + self::assertNotNull($error); + self::assertSame(ParseErrorCode::BadSegment, $error->code); + self::assertSame($segment, $error->segment); + self::assertSame($suggestion, $error->suggestion); + } + + /** + * The vectors compare only the canonical form. A wrong compact form can give the same one. + */ + #[Test] + public function givesATruncatedCodeTheCompactFormAndPrecisionOfItsSegment(): void + { + $unit = Postcode::parse('EK-01-A03-FK-01')->value; + self::assertNotNull($unit); + + $area = $unit->truncate(Precision::Area); + + self::assertSame('EK01A03FK', $area->compact); + self::assertSame(Precision::Area, $area->precision); + } + + /** + * No vector has a code with a later segment that spells the state. A search for the state + * anywhere in the code would pass every vector. + */ + #[Test] + public function rejectsACodeWhoseDistrictSpellsTheState(): void + { + $state = Postcode::parse('FC', allowPartial: true)->value; + $code = Postcode::parse('EK-01-FC0-FK-01')->value; + self::assertNotNull($state); + self::assertNotNull($code); + + self::assertFalse($state->contains($code)); + } + + #[Test] + public function saysHowToFixATruncateToAMorePreciseSegment(): void + { + $district = Postcode::parse('EK-01-A03', allowPartial: true)->value; + self::assertNotNull($district); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Cannot truncate a district postcode to unit. ' + . 'Choose district or a less precise segment.', + ); + $district->truncate(Precision::Unit); + } +} diff --git a/tests/Unit/PropertiesTest.php b/tests/Unit/PropertiesTest.php new file mode 100644 index 0000000..efd572f --- /dev/null +++ b/tests/Unit/PropertiesTest.php @@ -0,0 +1,310 @@ + self::LETTERS, + 'digits' => self::DIGITS, + 'letters-or-digits' => self::LETTERS . self::DIGITS, + ]; + + protected function setUp(): void + { + \mt_srand(self::SEED); + } + + #[Test] + public function parsesEveryValidCodeAndKeepsItsFormsInStep(): void + { + $violations = []; + for ($run = 0; $run < self::RUNS; ++$run) { + $compact = self::validCode(); + $postcode = Postcode::parse($compact)->value; + + if (!self::formsAreInStep($postcode, $compact)) { + $violations[] = $postcode === null + ? "{$compact} did not parse" + : "{$compact} gave {$postcode->canonical} and {$postcode->display}"; + } + } + + self::assertNoViolations($violations, 'A valid code did not parse, or has wrong forms.'); + } + + #[Test] + public function readsTheSameCodeWhenSeparatorsSitBetweenCharacters(): void + { + $separators = \array_keys(SpecData::SEPARATORS); + for ($run = 0; $run < self::RUNS; ++$run) { + $compact = self::validCode(); + $spaced = ''; + foreach (\str_split($compact) as $character) { + $spaced .= $character . self::pick($separators); + } + + $parsed = Postcode::parse($spaced)->value?->compact; + + self::assertSame($compact, $parsed, \bin2hex($spaced)); + } + } + + #[Test] + public function returnsAPostcodeOrAnErrorForAnyText(): void + { + $separators = \array_keys(SpecData::SEPARATORS); + $noise = [ + ...\str_split(self::LETTERS . self::DIGITS), + ...$separators, + ...\array_keys(NfkcTable::FORMS), + "\u{0301}", "\u{00B5}", "\u{202E}", "\u{1F600}", "\xFF", "\xC3", "\xED\xA0\x80", + ]; + $nearCodes = [...self::codePieces(), ...$separators]; + $parsed = 0; + for ($run = 0; $run < self::RUNS; ++$run) { + // A long text of noise fails an early check. A short text of codes, typos and + // separators passes the early checks, so it reaches the later checks, and some of + // these texts parse. + $text = \mt_rand(0, 1) === 0 + ? self::randomText($noise, \mt_rand(0, 80)) + : self::randomText($nearCodes, \mt_rand(1, 3)); + + $result = self::parseOrFail($text, \mt_rand(0, 1) === 1); + if ($result->isOk()) { + ++$parsed; + } + + self::assertNotSame($result->value === null, $result->error === null, \bin2hex($text)); + } + + self::assertGreaterThan(0, $parsed, 'No text parsed, so the property reached no success.'); + } + + #[Test] + public function suggestsOnlyCodesThatParse(): void + { + $suggestions = []; + for ($run = 0; $run < self::RUNS; ++$run) { + $typo = self::withTypo(self::validCode()); + $suggestion = Postcode::parse($typo)->error?->suggestion; + if ($suggestion !== null) { + $suggestions[$typo] = $suggestion; + } + } + + self::assertNotEmpty( + $suggestions, + 'No typo gave a suggestion, so the property checked nothing.', + ); + foreach ($suggestions as $typo => $suggestion) { + self::assertTrue(Postcode::parse($suggestion)->isOk(), "{$typo} gave {$suggestion}"); + } + } + + #[Test] + public function normalisesAsciiTextCompletelyInOnePass(): void + { + // A one-byte separator is an ASCII character, and the text holds only ASCII characters. + $asciiSeparators = \implode('', \array_filter( + \array_keys(SpecData::SEPARATORS), + static fn(string $separator): bool => \strlen($separator) === 1, + )); + $violations = []; + for ($run = 0; $run < self::RUNS; ++$run) { + $text = ''; + for ($count = \mt_rand(0, 40); $count > 0; --$count) { + $text .= \chr(\mt_rand(0, 0x7F)); + } + $once = Postcode::normalize($text); + + if ($once !== Postcode::normalize($once) + || \preg_match('/[a-z]/', $once) === 1 + || \strpbrk($once, $asciiSeparators) !== false) { + $violations[] = \bin2hex($text) . ' gave ' . \bin2hex($once); + } + } + + self::assertNoViolations($violations, 'A text, in hex, was not normalised in one pass.'); + } + + // The state comes from the state list, because most pairs of letters are not a state. Each + // other segment follows its length, characters and minimum in the spec data. + private static function validCode(): string + { + $code = ''; + foreach (SpecData::SEGMENTS as $name => $rule) { + $code .= $name === Precision::State->value + ? self::pick(\array_keys(SpecData::STATES)) + : self::segment($rule); + } + + return $code; + } + + /** + * @param array{length: int, characters: key-of, minimum: ?int} $rule + */ + private static function segment(array $rule): string + { + $alphabet = \str_split(self::ALPHABETS[$rule['characters']]); + // Digits can fall below the minimum, such as 00 for an LGA, so the loop draws again. + do { + $text = ''; + for ($place = 0; $place < $rule['length']; ++$place) { + $text .= self::pick($alphabet); + } + } while ((int) $text < ($rule['minimum'] ?? 0)); + + return $text; + } + + /** + * Whole codes cut after a random segment, a typo of each, and legacy postcodes. A code that + * stops after a segment parses only with allowPartial. + * + * @return list + */ + private static function codePieces(): array + { + $ends = \array_column(SpecData::SEGMENTS, 'end'); + $pieces = []; + for ($index = 0; $index < self::CODE_PIECES; ++$index) { + $code = \substr(self::validCode(), 0, self::pick($ends)); + $pieces[] = $code; + $pieces[] = self::withTypo($code); + $pieces[] = \sprintf('%06d', \mt_rand(0, 999999)); + } + + return $pieces; + } + + private static function withTypo(string $code): string + { + $place = \mt_rand(0, \strlen($code) - 1); + + return \substr_replace($code, self::pick(self::lookAlikes()), $place, 1); + } + + /** + * The characters that the fix tables of the spec change. A fix that the spec adds later then + * gets tested too. + * + * @return list + */ + private static function lookAlikes(): array + { + $keys = [...\array_keys(SpecData::DIGIT_FIXES), ...\array_keys(SpecData::LETTER_FIXES)]; + + // PHP turns the keys 0 and 1 of the letter table into integers. + return \array_map(\strval(...), $keys); + } + + /** + * PHPUnit prints no input when parse throws. The property says that parse never throws, so + * each throw fails the test, and the failure names the input. + */ + private static function parseOrFail(string $text, bool $allowPartial): ParseResult + { + try { + return Postcode::parse($text, allowPartial: $allowPartial); + } catch (Throwable $thrown) { + throw new AssertionFailedError( + \sprintf('parse threw %s. The input in hex: %s', $thrown::class, \bin2hex($text)), + 0, + $thrown, + ); + } + } + + /** + * @param list $pieces + */ + private static function randomText(array $pieces, int $count): string + { + $text = ''; + for ($piece = 0; $piece < $count; ++$piece) { + $text .= self::pick($pieces); + } + + return $text; + } + + /** + * A failure shows only the first violations, because a mistake in the code can break each run. + * + * @param list $violations + */ + private static function assertNoViolations(array $violations, string $rule): void + { + $count = \count($violations); + $message = \sprintf('%d of %d runs broke this rule. %s', $count, self::RUNS, $rule); + + self::assertSame([], \array_slice($violations, 0, self::SHOWN_VIOLATIONS), $message); + } + + // The same characters can sit in the wrong places, so the segment lengths count too. + private static function formsAreInStep(?Postcode $postcode, string $compact): bool + { + $shape = \implode(',', \array_column(SpecData::SEGMENTS, 'length')); + + return $postcode !== null + && $postcode->compact === $compact + && \str_replace('-', '', $postcode->canonical) === $compact + && \str_replace(' ', '', $postcode->display) === $compact + && self::segmentShape('-', $postcode->canonical) === $shape + && self::segmentShape(' ', $postcode->display) === $shape; + } + + /** + * The length of each segment of a form, such as 2,2,3,2,2. + * + * @param non-empty-string $separator + */ + private static function segmentShape(string $separator, string $form): string + { + return \implode(',', \array_map(\strlen(...), \explode($separator, $form))); + } + + /** + * @template T + * + * @param list $items + * + * @return T + */ + private static function pick(array $items): mixed + { + return $items[\mt_rand(0, \count($items) - 1)]; + } +} diff --git a/tests/Unit/ReadmeTest.php b/tests/Unit/ReadmeTest.php new file mode 100644 index 0000000..7141b17 --- /dev/null +++ b/tests/Unit/ReadmeTest.php @@ -0,0 +1,160 @@ + + */ + public static function examples(): array + { + return [ + 'the quickstart' => ['Quickstart', 'EK-01-A03-FK-01'], + 'the input limit' => ['Limit the input', "ok\nbad_length\n"], + ]; + } + + #[Test] + #[DataProvider('examples')] + public function runsEachPhpExampleAndPrintsItsResult(string $heading, string $printed): void + { + $code = self::phpExample(self::section($heading)); + $file = \tempnam(\sys_get_temp_dir(), 'readme-example'); + self::assertIsString($file); + + // Each statement after tempnam() sits in the try block, so a failure removes the file. + try { + \file_put_contents($file, $code); + $this->expectOutputString($printed); + // A closure keeps the variables of the example away from the variables of the test. + (static function () use ($file): void { + require $file; + })(); + } finally { + \unlink($file); + } + } + + /** + * @return array + */ + public static function constraintsAndVersions(): array + { + return [ + 'an alpha in the range, with the flag' => ['^0.1@alpha', '0.1.0-alpha.0', true], + 'a stable release in the range' => ['^0.1@alpha', '0.1.3', true], + 'an alpha of the next minor version' => ['^0.1@alpha', '0.2.0-alpha.0', false], + 'an alpha, with no flag' => ['^0.1', '0.1.0-alpha.0', false], + 'a stable release, with no flag' => ['^0.1', '0.1.1', true], + 'a release below the range' => ['^1.0', '0.9.0', false], + ]; + } + + #[Test] + #[DataProvider('constraintsAndVersions')] + public function readsWhichVersionsAConstraintAllows( + string $constraint, + string $version, + bool $allowed, + ): void { + self::assertSame($allowed, self::constraintAllows($constraint, $version)); + } + + #[Test] + public function listsEachPhpExampleOfTheReadmeInTheTest(): void + { + $examples = \preg_match_all('/^```php$/m', self::repoFile('README.md')); + + self::assertSame( + \count(self::examples()), + $examples, + 'Add each PHP example of the README to examples(), so that the test runs it.', + ); + } + + #[Test] + public function statesTheInputLimitOfTheSpec(): void + { + $section = self::section('Limit the input'); + + self::assertSame(1, \preg_match('/at most (\d+) code points/', $section, $limit)); + self::assertSame(SpecData::MAX_INPUT_CODE_POINTS, (int) $limit[1]); + } + + #[Test] + public function installsWithAConstraintThatAllowsTheNewestRelease(): void + { + $found = \preg_match('/^## (\S+) - /m', self::repoFile('CHANGELOG.md'), $newest); + self::assertSame(1, $found, 'The changelog has no release.'); + $pattern = '/^composer require gatepost\/postcode(?::(\S+))?$/m'; + $found = \preg_match($pattern, self::section('Install'), $command); + self::assertSame(1, $found, 'The Install section has no composer require line.'); + $constraint = $command[1] ?? '*'; + + self::assertTrue( + self::constraintAllows($constraint, $newest[1]), + "The install constraint {$constraint} does not allow the newest release {$newest[1]}.", + ); + } + + private static function repoFile(string $name): string + { + $contents = \file_get_contents(\dirname(__DIR__, 2) . '/' . $name); + self::assertIsString($contents); + + return $contents; + } + + // Composer installs a pre-release only when the constraint names its stability, such as @alpha. + // The version must also lie in the range of the constraint. + private static function constraintAllows(string $constraint, string $version): bool + { + return \str_contains($constraint, self::stabilityFlag($version)) + && Semver::satisfies($version, $constraint); + } + + private static function stabilityFlag(string $version): string + { + return \preg_match('/-(alpha|beta|rc)\b/i', $version, $label) === 1 ? '@' . $label[1] : ''; + } + + /** + * The text of a section, from its heading to the next heading. + */ + private static function section(string $heading): string + { + $pattern = '/^#{2,3} ' . \preg_quote($heading, '/') . '\n(.*?)(?=^#|\z)/ms'; + $found = \preg_match($pattern, self::repoFile('README.md'), $section); + + self::assertSame(1, $found, "The README has no section {$heading}."); + + return $section[1]; + } + + private static function phpExample(string $section): string + { + $found = \preg_match('/^```php\n(.*?)^```$/ms', $section, $example); + + self::assertSame(1, $found, 'The section has no PHP example.'); + + return $example[1]; + } +} diff --git a/tests/Unit/SourceCoverageTest.php b/tests/Unit/SourceCoverageTest.php new file mode 100644 index 0000000..685d651 --- /dev/null +++ b/tests/Unit/SourceCoverageTest.php @@ -0,0 +1,65 @@ + + */ + private static function classNames(): array + { + $source = \dirname(__DIR__, 2) . '/src'; + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS), + ); + $names = []; + foreach ($files as $file) { + if (!$file instanceof SplFileInfo || $file->getExtension() !== 'php') { + continue; + } + $path = \substr($file->getPathname(), \strlen($source) + 1, -\strlen('.php')); + $names[] = self::SOURCE_NAMESPACE . \str_replace(\DIRECTORY_SEPARATOR, '\\', $path); + } + + return $names; + } +} diff --git a/tests/Unit/VectorFileListTest.php b/tests/Unit/VectorFileListTest.php new file mode 100644 index 0000000..1135169 --- /dev/null +++ b/tests/Unit/VectorFileListTest.php @@ -0,0 +1,110 @@ + The name of each .json file in spec/vectors, without the extension. + */ + private static function filesInSpec(): array + { + $paths = \glob(\dirname(__DIR__, 2) . '/spec/vectors/*.json'); + self::assertIsArray($paths); + self::assertNotSame([], $paths, 'spec/vectors holds no file. Check out the submodule.'); + $names = \array_map(static fn(string $path): string => \basename($path, '.json'), $paths); + \sort($names); + + return $names; + } + + /** + * The code of each runner, without its comments. A call that someone commented out loads + * no file, so it must not count. + * + * @return list + */ + private static function runnerCode(): array + { + $paths = \glob(\dirname(__DIR__) . '/Vectors/*Test.php'); + self::assertIsArray($paths); + $runners = []; + foreach ($paths as $path) { + $source = \file_get_contents($path); + self::assertIsString($source); + $code = ''; + foreach (\token_get_all($source) as $token) { + $isComment = \is_array($token) + && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT], true); + $code .= match (true) { + $isComment => '', + \is_array($token) => $token[1], + default => $token, + }; + } + $runners[] = $code; + } + + return $runners; + } +} diff --git a/tests/Unit/VectorFileTest.php b/tests/Unit/VectorFileTest.php new file mode 100644 index 0000000..de13616 --- /dev/null +++ b/tests/Unit/VectorFileTest.php @@ -0,0 +1,120 @@ +.json, so two steps up reach the repo root. + */ + private const FIXTURES = '../../tests/Fixtures/vectors/'; + + #[Test] + public function namesEachCaseByItsIdAndItsDescription(): void + { + $cases = VectorFile::cases(self::FIXTURES . 'valid', 'stateName'); + + self::assertSame( + ['state-name-001 names EK', 'state-name-002 returns nothing for an unknown code'], + \array_keys($cases), + ); + self::assertSame('EK', $cases['state-name-001 names EK'][0]->text()); + } + + /** + * @return array + */ + public static function brokenFiles(): array + { + return [ + 'a wrong format version' => ['wrong-version', 'stateName', 'format version 1'], + 'a function that the file does not test' => ['valid', 'parse', 'does not test parse'], + 'an empty case list' => ['no-cases', 'stateName', 'has no cases'], + 'one id on two cases' => ['repeated-id', 'stateName', 'the id state-name-001 twice'], + 'two cases with one name' => ['same-name', 'stateName', 'the name a b c'], + ]; + } + + #[Test] + #[DataProvider('brokenFiles')] + public function rejectsABrokenFile(string $file, string $function, string $reason): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage($reason); + + VectorFile::cases(self::FIXTURES . $file, $function); + } + + #[Test] + public function explainsAFileThatNoUserCanReadWithoutAPhpWarning(): void + { + $name = 'unreadable'; + $path = \dirname(__DIR__) . "/Fixtures/vectors/{$name}.json"; + \copy(\dirname(__DIR__) . '/Fixtures/vectors/valid.json', $path); + \chmod($path, 0o000); + + try { + if (\is_readable($path)) { + self::markTestSkipped('This user can read each file, as root can.'); + } + $warnings = []; + $collect = static function (int $level, string $message) use (&$warnings): bool { + $warnings[] = $message; + + return true; + }; + \set_error_handler($collect); + try { + VectorFile::cases(self::FIXTURES . $name, 'stateName'); + self::fail('The loader accepted a file that it cannot read.'); + } catch (UnexpectedValueException $error) { + self::assertStringContainsString('Cannot read', $error->getMessage()); + } finally { + \restore_error_handler(); + } + self::assertSame([], $warnings); + } finally { + \chmod($path, 0o600); + \unlink($path); + } + } + + #[Test] + public function explainsAMissingFileWithoutAPhpWarning(): void + { + $warnings = []; + \set_error_handler(static function (int $level, string $message) use (&$warnings): bool { + $warnings[] = $message; + + return true; + }); + + $error = null; + try { + VectorFile::cases(self::FIXTURES . 'absent', 'stateName'); + } catch (UnexpectedValueException $caught) { + $error = $caught; + } finally { + \restore_error_handler(); + } + + self::assertInstanceOf(UnexpectedValueException::class, $error); + self::assertStringContainsString('Cannot read', $error->getMessage()); + self::assertSame([], $warnings); + } +} diff --git a/tests/Vectors/HierarchyTest.php b/tests/Vectors/HierarchyTest.php new file mode 100644 index 0000000..6aa66a9 --- /dev/null +++ b/tests/Vectors/HierarchyTest.php @@ -0,0 +1,76 @@ + + */ + public static function truncateVectors(): array + { + return VectorFile::cases('truncate', 'truncate'); + } + + /** + * @return array + */ + public static function parentVectors(): array + { + return VectorFile::cases('parent', 'parent'); + } + + /** + * @return array + */ + public static function containsVectors(): array + { + return VectorFile::cases('contains', 'contains'); + } + + #[Test] + #[DataProvider('truncateVectors')] + public function truncatesAsTheVectorSays(VectorCase $vector): void + { + $code = $vector->postcode('code'); + $to = Precision::from($vector->field('to')); + if (\array_key_exists('rejects', $vector->expected())) { + $this->expectException(InvalidArgumentException::class); + $code->truncate($to); + + return; + } + $canonical = $code->truncate($to)->canonical; + + self::assertSame($vector->expectedField('canonical'), $canonical); + } + + #[Test] + #[DataProvider('parentVectors')] + public function findsTheParentAsTheVectorSays(VectorCase $vector): void + { + $parent = $vector->postcode('code')->parent(); + + self::assertSame($vector->expectedField('canonical'), $parent?->canonical); + } + + #[Test] + #[DataProvider('containsVectors')] + public function findsContainedCodesAsTheVectorSays(VectorCase $vector): void + { + $contains = $vector->postcode('prefix')->contains($vector->postcode('code')); + + self::assertSame($vector->expectedField('value'), $contains); + } +} diff --git a/tests/Vectors/IsLegacyTest.php b/tests/Vectors/IsLegacyTest.php new file mode 100644 index 0000000..767516e --- /dev/null +++ b/tests/Vectors/IsLegacyTest.php @@ -0,0 +1,31 @@ + + */ + public static function vectors(): array + { + return VectorFile::cases('is-legacy', 'isLegacy'); + } + + #[Test] + #[DataProvider('vectors')] + public function findsLegacyCodesAsTheVectorSays(VectorCase $vector): void + { + self::assertSame($vector->expectedField('value'), Postcode::isLegacy($vector->text())); + } +} diff --git a/tests/Vectors/NormalizeTest.php b/tests/Vectors/NormalizeTest.php new file mode 100644 index 0000000..2c1c7b1 --- /dev/null +++ b/tests/Vectors/NormalizeTest.php @@ -0,0 +1,31 @@ + + */ + public static function vectors(): array + { + return VectorFile::cases('normalize', 'normalize'); + } + + #[Test] + #[DataProvider('vectors')] + public function normalizesAsTheVectorSays(VectorCase $vector): void + { + self::assertSame($vector->expectedField('value'), Postcode::normalize($vector->text())); + } +} diff --git a/tests/Vectors/ParseTest.php b/tests/Vectors/ParseTest.php new file mode 100644 index 0000000..9187dec --- /dev/null +++ b/tests/Vectors/ParseTest.php @@ -0,0 +1,70 @@ + + */ + public static function vectors(): array + { + return [ + ...VectorFile::cases('parse', 'parse'), + ...VectorFile::cases('parse-segments', 'parse'), + ...VectorFile::cases('parse-states', 'parse'), + ]; + } + + #[Test] + #[DataProvider('vectors')] + public function parsesAsTheVectorSays(VectorCase $vector): void + { + $result = Postcode::parse($vector->text(), allowPartial: $vector->allowPartial()); + + self::assertSame($vector->expected(), self::asVector($result)); + } + + /** + * Writes a result in the shape of a vector's expect field. The keys follow the order in + * spec/vectors/README.md, because assertSame() compares the order of keys too. + * + * @return array + */ + private static function asVector(ParseResult $result): array + { + if (!$result->isOk()) { + $error = $result->error; + + return [ + 'ok' => false, + 'error' => [ + 'code' => $error->code->value, + 'segment' => $error->segment?->value, + 'suggestion' => $error->suggestion, + ], + ]; + } + $postcode = $result->value; + + return [ + 'ok' => true, + 'compact' => $postcode->compact, + 'canonical' => $postcode->canonical, + 'display' => $postcode->display, + 'precision' => $postcode->precision->value, + 'segments' => $postcode->segments->toArray(), + ]; + } +} diff --git a/tests/Vectors/PrecisionForAccuracyTest.php b/tests/Vectors/PrecisionForAccuracyTest.php new file mode 100644 index 0000000..ec16d6f --- /dev/null +++ b/tests/Vectors/PrecisionForAccuracyTest.php @@ -0,0 +1,33 @@ + + */ + public static function vectors(): array + { + return VectorFile::cases('precision-for-accuracy', 'precisionForAccuracy'); + } + + #[Test] + #[DataProvider('vectors')] + public function mapsTheAccuracyAsTheVectorSays(VectorCase $vector): void + { + $precision = Postcode::precisionForAccuracy($vector->accuracy()); + + self::assertSame($vector->expectedField('value'), $precision->value); + } +} diff --git a/tests/Vectors/RedactTest.php b/tests/Vectors/RedactTest.php new file mode 100644 index 0000000..157ad7f --- /dev/null +++ b/tests/Vectors/RedactTest.php @@ -0,0 +1,30 @@ + + */ + public static function vectors(): array + { + return VectorFile::cases('redact', 'redact'); + } + + #[Test] + #[DataProvider('vectors')] + public function hidesTheUnitAsTheVectorSays(VectorCase $vector): void + { + self::assertSame($vector->expectedField('value'), $vector->postcode('code')->redact()); + } +} diff --git a/tests/Vectors/StateNameTest.php b/tests/Vectors/StateNameTest.php new file mode 100644 index 0000000..76bd589 --- /dev/null +++ b/tests/Vectors/StateNameTest.php @@ -0,0 +1,31 @@ + + */ + public static function vectors(): array + { + return VectorFile::cases('state-name', 'stateName'); + } + + #[Test] + #[DataProvider('vectors')] + public function namesTheStateAsTheVectorSays(VectorCase $vector): void + { + self::assertSame($vector->expectedField('value'), Postcode::stateName($vector->text())); + } +} diff --git a/tests/Vectors/VectorCase.php b/tests/Vectors/VectorCase.php new file mode 100644 index 0000000..20b37a6 --- /dev/null +++ b/tests/Vectors/VectorCase.php @@ -0,0 +1,131 @@ +, expect: array} $case + */ + private function __construct( + public readonly string $id, + public readonly string $description, + private readonly array $case, + ) {} + + public static function fromJson(mixed $case, string $file): self + { + if ( + !\is_array($case) + || !\is_string($case['id'] ?? null) + || !\is_string($case['description'] ?? null) + || !\array_key_exists('input', $case) + || !\is_array($case['options'] ?? null) + || !\is_array($case['expect'] ?? null) + ) { + throw new UnexpectedValueException("{$file}.json holds a case that lacks a field."); + } + + return new self($case['id'], $case['description'], [ + 'input' => $case['input'], + 'options' => $case['options'], + 'expect' => $case['expect'], + ]); + } + + /** + * The input of a function that takes one string. + */ + public function text(): string + { + if (!\is_string($this->case['input'])) { + throw new UnexpectedValueException("{$this->id} has no text input."); + } + + return $this->case['input']; + } + + /** + * The input of precisionForAccuracy. JSON has no NaN or infinity, so the vectors name them. + */ + public function accuracy(): ?float + { + $input = $this->case['input']; + + return match (true) { + $input === null, \is_int($input), \is_float($input) => $input, + $input === 'NaN' => NAN, + $input === 'Infinity' => INF, + $input === '-Infinity' => -INF, + default => throw new UnexpectedValueException("{$this->id} has no accuracy input."), + }; + } + + /** + * One named field of the input, such as the precision in a truncate case. + */ + public function field(string $name): string + { + $input = $this->case['input']; + $value = \is_array($input) ? ($input[$name] ?? null) : null; + if (!\is_string($value)) { + throw new UnexpectedValueException("{$this->id} has no input field {$name}."); + } + + return $value; + } + + /** + * Parses the canonical code in one field of the input. The runner rules in + * spec/vectors/README.md allow partial codes here. + */ + public function postcode(string $name): Postcode + { + $code = $this->field($name); + $result = Postcode::parse($code, allowPartial: true); + if (!$result->isOk()) { + throw new UnexpectedValueException("{$this->id} holds {$code}, which does not parse."); + } + + return $result->value; + } + + public function allowPartial(): bool + { + $allowPartial = $this->case['options']['allowPartial'] ?? false; + if (!\is_bool($allowPartial)) { + throw new UnexpectedValueException("{$this->id} has a non-boolean allowPartial."); + } + + return $allowPartial; + } + + /** + * @return array + */ + public function expected(): array + { + return $this->case['expect']; + } + + public function expectedField(string $name): mixed + { + if (!\array_key_exists($name, $this->case['expect'])) { + throw new UnexpectedValueException("{$this->id} expects no field {$name}."); + } + + return $this->case['expect'][$name]; + } +} diff --git a/tests/Vectors/VectorFile.php b/tests/Vectors/VectorFile.php new file mode 100644 index 0000000..0fa93a7 --- /dev/null +++ b/tests/Vectors/VectorFile.php @@ -0,0 +1,96 @@ + Each case, keyed by its id and description, + * for a PHPUnit data provider. + */ + public static function cases(string $file, string $function): array + { + $text = self::read(\dirname(__DIR__, 2) . "/spec/vectors/{$file}.json"); + $vectors = \json_decode($text, true, 512, JSON_THROW_ON_ERROR); + if (!\is_array($vectors) || ($vectors['version'] ?? null) !== self::FORMAT_VERSION) { + throw new UnexpectedValueException( + "{$file}.json does not have format version " . self::FORMAT_VERSION . '.', + ); + } + if (($vectors['function'] ?? null) !== $function) { + throw new UnexpectedValueException("{$file}.json does not test {$function}."); + } + $cases = $vectors['cases'] ?? null; + if (!\is_array($cases) || $cases === []) { + throw new UnexpectedValueException("{$file}.json has no cases."); + } + $ids = []; + $provided = []; + foreach ($cases as $case) { + $vector = VectorCase::fromJson($case, $file); + if (isset($ids[$vector->id])) { + throw new UnexpectedValueException("{$file}.json uses the id {$vector->id} twice."); + } + $ids[$vector->id] = true; + $name = "{$vector->id} {$vector->description}"; + // Two ids can give one name, such as "a b" with "c" and "a" with "b c". An array + // key holds one value, so the second case would silently replace the first. + if (isset($provided[$name])) { + throw new UnexpectedValueException( + "{$file}.json gives two cases the name {$name}.", + ); + } + $provided[$name] = [$vector]; + } + + return $provided; + } + + private static function read(string $path): string + { + // A missing file gives false here, so the clear message below replaces a PHP warning. + $text = \is_readable($path) && \is_file($path) ? \file_get_contents($path) : false; + if ($text === false) { + throw new UnexpectedValueException("Cannot read {$path}. Check out the submodule."); + } + + return $text; + } +} diff --git a/tools/bc-check/composer.json b/tools/bc-check/composer.json new file mode 100644 index 0000000..076b7d0 --- /dev/null +++ b/tools/bc-check/composer.json @@ -0,0 +1,11 @@ +{ + "require-dev": { + "roave/backward-compatibility-check": "^8.23.0" + }, + "config": { + "platform": { + "php": "8.4.1" + }, + "sort-packages": true + } +} diff --git a/tools/bc-check/composer.lock b/tools/bc-check/composer.lock new file mode 100644 index 0000000..b83d0a4 --- /dev/null +++ b/tools/bc-check/composer.lock @@ -0,0 +1,4669 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d1028d006f2c4ee56c2cb1af07fece85", + "packages": [], + "packages-dev": [ + { + "name": "beberlei/assert", + "version": "v3.3.4", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "f193f4613c7d7fbcee2c05e4daff4061d49c040e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/f193f4613c7d7fbcee2c05e4daff4061d49c040e", + "reference": "f193f4613c7d7fbcee2c05e4daff4061d49c040e", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=6.0.0", + "yoast/phpunit-polyfills": "^0.1.0" + }, + "suggest": { + "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Assert/functions.php" + ], + "psr-4": { + "Assert\\": "lib/Assert" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "support": { + "issues": "https://github.com/beberlei/assert/issues", + "source": "https://github.com/beberlei/assert/tree/v3.3.4" + }, + "time": "2026-06-10T19:47:05+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.5.14", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "0c8abba0634f637bd78c4e451981da368d403463" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c8abba0634f637bd78c4e451981da368d403463", + "reference": "0c8abba0634f637bd78c4e451981da368d403463", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.14" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-08-21T14:57:29+00:00" + }, + { + "name": "composer/class-map-generator", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/composer/class-map-generator.git", + "reference": "c0efbeb2f3fd7e23c7f180a59765c0b48a31fbc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/c0efbeb2f3fd7e23c7f180a59765c0b48a31fbc4", + "reference": "c0efbeb2f3fd7e23c7f180a59765c0b48a31fbc4", + "shasum": "" + }, + "require": { + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7 || ^8" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpunit/phpunit": "^8", + "symfony/filesystem": "^5.4 || ^6 || ^7 || ^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\ClassMapGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Utilities to scan PHP code and generate class maps.", + "keywords": [ + "classmap" + ], + "support": { + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.8.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-09-25T15:24:19+00:00" + }, + { + "name": "composer/composer", + "version": "2.10.3", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "f0de0bf90226853b841672f086d8b58b02332504" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/f0de0bf90226853b841672f086d8b58b02332504", + "reference": "f0de0bf90226853b841672f086d8b58b02332504", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.5", + "composer/class-map-generator": "^1.4.0", + "composer/metadata-minifier": "^1.0", + "composer/pcre": "^2.3 || ^3.3", + "composer/semver": "^3.3", + "composer/spdx-licenses": "^1.5.7", + "composer/xdebug-handler": "^2.0.2 || ^3.0.3", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "justinrainbow/json-schema": "^6.5.1", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "react/promise": "^3.3", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.2", + "seld/signal-handler": "^2.0", + "symfony/console": "^5.4.47 || ^6.4.25 || ^7.1.10 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.1.10 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.1.10 || ^8.0", + "symfony/polyfill-php73": "^1.24", + "symfony/polyfill-php80": "^1.24", + "symfony/polyfill-php81": "^1.24", + "symfony/polyfill-php84": "^1.30", + "symfony/process": "^5.4.47 || ^6.4.25 || ^7.1.10 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "phpstan/phpstan-symfony": "^1.4.0", + "symfony/phpunit-bridge": "^6.4.25 || ^7.3.3 || ^8.0" + }, + "suggest": { + "ext-curl": "Provides HTTP support (will fallback to PHP streams if missing)", + "ext-openssl": "Enables access to repositories and packages over HTTPS", + "ext-zip": "Allows direct extraction of ZIP archives (unzip/7z binaries will be used instead if available)", + "ext-zlib": "Enables gzip for HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "phpstan/rules.neon" + ] + }, + "branch-alias": { + "dev-main": "2.10-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "https://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/composer/issues", + "security": "https://github.com/composer/composer/security/policy", + "source": "https://github.com/composer/composer/tree/2.10.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-08-27T11:34:23+00:00" + }, + { + "name": "composer/metadata-minifier", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/composer/metadata-minifier.git", + "reference": "8e86142e3ade750b837d55e55f0cdc786d8da006" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/8e86142e3ade750b837d55e55f0cdc786d8da006", + "reference": "8e86142e3ade750b837d55e55f0cdc786d8da006", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2", + "phpstan/phpstan": "^1", + "symfony/phpunit-bridge": "^4.2 || ^5 || ^6 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\MetadataMinifier\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Small utility library that handles metadata minification and expansion.", + "keywords": [ + "composer", + "compression" + ], + "support": { + "issues": "https://github.com/composer/metadata-minifier/issues", + "source": "https://github.com/composer/metadata-minifier/tree/1.0.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-30T11:34:59+00:00" + }, + { + "name": "composer/pcre", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, + { + "name": "composer/semver", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5", + "reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Version comparison library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.5.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-09-24T14:38:51+00:00" + }, + { + "name": "composer/spdx-licenses", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "5ecd0cb4177696f9fd48f1605dda81db3dee7889" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/5ecd0cb4177696f9fd48f1605dda81db3dee7889", + "reference": "5ecd0cb4177696f9fd48f1605dda81db3dee7889", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^6.4.25 || ^7.3.3 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Spdx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "SPDX licenses list and validation library.", + "keywords": [ + "license", + "spdx", + "validator" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.6.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-04-08T20:18:39+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "jetbrains/phpstorm-stubs", + "version": "v2026.2", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs", + "reference": "709e512210784a7c0a677b3a89d35def844a59b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/709e512210784a7c0a677b3a89d35def844a59b9", + "reference": "709e512210784a7c0a677b3a89d35def844a59b9", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.86", + "nikic/php-parser": "^v5.6", + "phpdocumentor/reflection-docblock": "^5.6", + "phpunit/phpunit": "^12.3" + }, + "type": "library", + "autoload": { + "files": [ + "PhpStormStubsMap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "PHP runtime & extensions header files for PhpStorm", + "homepage": "https://www.jetbrains.com/phpstorm", + "keywords": [ + "autocomplete", + "code", + "inference", + "inspection", + "jetbrains", + "phpstorm", + "stubs", + "type" + ], + "time": "2026-06-12T13:19:10+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "6.13.1", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "e54aacc4c0657b3a4af4f628428d2702146828c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/e54aacc4c0657b3a4af4f628428d2702146828c6", + "reference": "e54aacc4c0657b3a4af4f628428d2702146828c6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-json": "*", + "marc-mabe/php-enum": "^4.4", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "dev-main", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Danny van der Sluijs", + "email": "danny.vandersluijs@icloud.com", + "role": "Maintainer" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.13.1" + }, + "time": "2026-09-30T07:42:28+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "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.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, + { + "name": "nikolaposa/version", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/nikolaposa/version.git", + "reference": "2b9ee2f0b09333b6ce00bd6b63132cdf1d7a1428" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikolaposa/version/zipball/2b9ee2f0b09333b6ce00bd6b63132cdf1d7a1428", + "reference": "2b9ee2f0b09333b6ce00bd6b63132cdf1d7a1428", + "shasum": "" + }, + "require": { + "beberlei/assert": "^3.2", + "php": "^8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.44", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-beberlei-assert": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Version\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nikola Poša", + "email": "posa.nikola@gmail.com", + "homepage": "https://www.nikolaposa.in.rs" + } + ], + "description": "Value Object that represents a SemVer-compliant version number.", + "homepage": "https://github.com/nikolaposa/version", + "keywords": [ + "semantic", + "semver", + "version", + "versioning" + ], + "support": { + "issues": "https://github.com/nikolaposa/version/issues", + "source": "https://github.com/nikolaposa/version/tree/4.2.1" + }, + "time": "2025-03-24T19:12:02+00:00" + }, + { + "name": "ocramius/package-versions", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/PackageVersions.git", + "reference": "18b02a63e837246e812cae72e211db32d7980019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/18b02a63e837246e812cae72e211db32d7980019", + "reference": "18b02a63e837246e812cae72e211db32d7980019", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2.0", + "php": "~8.4.0 || ~8.5.0" + }, + "replace": { + "composer/package-versions-deprecated": "*" + }, + "require-dev": { + "composer/composer": "^2.9.8", + "doctrine/coding-standard": "^14.0.0", + "ext-zip": "^1.15.0", + "phpunit/phpunit": "^13.1.11", + "psalm/plugin-phpunit": "^0.19.7", + "roave/infection-static-analysis-plugin": "^1.44.0", + "vimeo/psalm": "^6.16.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/Ocramius/PackageVersions/issues", + "source": "https://github.com/Ocramius/PackageVersions/tree/2.12.0" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ocramius/package-versions", + "type": "tidelift" + } + ], + "time": "2026-05-21T19:52:53+00:00" + }, + { + "name": "php-standard-library/async", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/async.git", + "reference": "3d2fbaf391cd256f5543f96889e9e738840d321a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/async/zipball/3d2fbaf391cd256f5543f96889e9e738840d321a", + "reference": "3d2fbaf391cd256f5543f96889e9e738840d321a", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/date-time": "^6.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/promise": "^6.0", + "revolt/event-loop": "^1.0.8" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/dict": "^6.0", + "php-standard-library/result": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Async\\": "src/Psl/Async/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Fiber-based structured concurrency using cooperative multitasking", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "Fibers", + "async", + "concurrency", + "cooperative-multitasking" + ], + "support": { + "source": "https://github.com/php-standard-library/async/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/channel", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/channel.git", + "reference": "f2f5f0ee4d04dccc57d33e6620671c6296e881e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/channel/zipball/f2f5f0ee4d04dccc57d33e6620671c6296e881e2", + "reference": "f2f5f0ee4d04dccc57d33e6620671c6296e881e2", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.0", + "php-standard-library/foundation": "^6.0", + "revolt/event-loop": "^1.0.8" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/date-time": "^6.0", + "php-standard-library/file": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Channel\\": "src/Psl/Channel/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Message-passing channels for async communication, inspired by Go and Rust", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "async", + "channel", + "concurrency", + "message-passing" + ], + "support": { + "source": "https://github.com/php-standard-library/channel/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/collection", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/collection.git", + "reference": "d31f6a015958ae4e13e2c1d47621d9786ee1ec1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/collection/zipball/d31f6a015958ae4e13e2c1d47621d9786ee1ec1a", + "reference": "d31f6a015958ae4e13e2c1d47621d9786ee1ec1a", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/str": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Collection\\": "src/Psl/Collection/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Generic, object-oriented Vector, Map, and Set collections with immutable and mutable variants", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "collection", + "generics", + "map", + "set", + "vector" + ], + "support": { + "source": "https://github.com/php-standard-library/collection/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/comparison", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/comparison.git", + "reference": "e77454e109feaa13732a3d556e1f8855b216b4fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/comparison/zipball/e77454e109feaa13732a3d556e1f8855b216b4fd", + "reference": "e77454e109feaa13732a3d556e1f8855b216b4fd", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Comparison\\": "src/Psl/Comparison/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Interfaces and functions for type-safe, consistent value comparison", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "comparable", + "comparison", + "ordering" + ], + "support": { + "source": "https://github.com/php-standard-library/comparison/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/date-time", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/date-time.git", + "reference": "4d280c6192e1b17ee8a59e07ed0fc29129bcd373" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/date-time/zipball/4d280c6192e1b17ee8a59e07ed0fc29129bcd373", + "reference": "4d280c6192e1b17ee8a59e07ed0fc29129bcd373", + "shasum": "" + }, + "require": { + "ext-intl": "*", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/comparison": "^6.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/interoperability": "^6.0", + "php-standard-library/locale": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/json": "^6.0", + "php-standard-library/math": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\DateTime\\": "src/Psl/DateTime/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Immutable, timezone-aware date and time types with Duration, Period, and Interval", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "datetime", + "duration", + "immutable", + "timezone" + ], + "support": { + "source": "https://github.com/php-standard-library/date-time/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/default", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/default.git", + "reference": "6f7daf0864b4f319dee645cb0ffd7c4be0c9907c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/default/zipball/6f7daf0864b4f319dee645cb0ffd7c4be0c9907c", + "reference": "6f7daf0864b4f319dee645cb0ffd7c4be0c9907c", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Default\\": "src/Psl/Default/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "DefaultInterface for classes to provide standardized default instances", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "default", + "interface" + ], + "support": { + "source": "https://github.com/php-standard-library/default/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/dict", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/dict.git", + "reference": "0c3f7546c4dd5887e085e21663eedaa3df78a00d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/dict/zipball/0c3f7546c4dd5887e085e21663eedaa3df78a00d", + "reference": "0c3f7546c4dd5887e085e21663eedaa3df78a00d", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/collection": "^6.0", + "php-standard-library/iter": "^6.0", + "php-standard-library/str": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Dict\\": "src/Psl/Dict/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Functions for creating and transforming associative arrays with preserved keys", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "Associative", + "Dict", + "array", + "map" + ], + "support": { + "source": "https://github.com/php-standard-library/dict/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/either-or-both", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/either-or-both.git", + "reference": "cd443b661dae1dc87f309e17953e9a7426690195" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/either-or-both/zipball/cd443b661dae1dc87f309e17953e9a7426690195", + "reference": "cd443b661dae1dc87f309e17953e9a7426690195", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/comparison": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/option": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\EitherOrBoth\\": "src/Psl/EitherOrBoth/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Three-variant disjoint union type (Left/Right/Both) for values that may be present on either or both of two sides", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "both", + "either", + "either-or-both", + "these", + "union-type" + ], + "support": { + "source": "https://github.com/php-standard-library/either-or-both/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/env", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/env.git", + "reference": "093b68fbe9f1104142735c2e2a5ca42931c9a8f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/env/zipball/093b68fbe9f1104142735c2e2a5ca42931c9a8f1", + "reference": "093b68fbe9f1104142735c2e2a5ca42931c9a8f1", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/filesystem": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Env\\": "src/Psl/Env/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Functions for inspecting and modifying environment variables, working directory, and paths", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "env", + "environment" + ], + "support": { + "source": "https://github.com/php-standard-library/env/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/file", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/file.git", + "reference": "8e6c84b64060278c833e64fb8fb3298a34d2d3af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/file/zipball/8e6c84b64060278c833e64fb8fb3298a34d2d3af", + "reference": "8e6c84b64060278c833e64fb8fb3298a34d2d3af", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.0", + "php-standard-library/date-time": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/io": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/env": "^6.0", + "php-standard-library/filesystem": "^6.0", + "php-standard-library/os": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\File\\": "src/Psl/File/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Typed file handles for reading and writing with write modes and advisory locking", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "file", + "handle", + "io" + ], + "support": { + "source": "https://github.com/php-standard-library/file/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/filesystem", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/filesystem.git", + "reference": "65a13875f9f918a10b072a3f6975dfd9e4e412e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/filesystem/zipball/65a13875f9f918a10b072a3f6975dfd9e4e412e6", + "reference": "65a13875f9f918a10b072a3f6975dfd9e4e412e6", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/env": "^6.0", + "php-standard-library/file": "^6.0", + "php-standard-library/os": "^6.0", + "php-standard-library/str": "^6.0", + "php-standard-library/type": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Filesystem\\": "src/Psl/Filesystem/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Type-safe functions for file system operations with proper exception handling", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "file-system", + "filesystem" + ], + "support": { + "source": "https://github.com/php-standard-library/filesystem/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/foundation", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/foundation.git", + "reference": "9b464388f93b5c496bd6a17912e5cdc846b9df1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/foundation/zipball/9b464388f93b5c496bd6a17912e5cdc846b9df1e", + "reference": "9b464388f93b5c496bd6a17912e5cdc846b9df1e", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\": "src/Psl/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Exceptions, Ref, and invariant functions", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "exceptions", + "foundation", + "invariant" + ], + "support": { + "source": "https://github.com/php-standard-library/foundation/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/interoperability", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/interoperability.git", + "reference": "d95ae8d2502d05b7aa14e456b2c7f5e9c1510228" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/interoperability/zipball/d95ae8d2502d05b7aa14e456b2c7f5e9c1510228", + "reference": "d95ae8d2502d05b7aa14e456b2c7f5e9c1510228", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Interoperability\\": "src/Psl/Interoperability/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Interfaces for converting between PSL types and PHP stdlib/intl equivalents", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "conversion", + "interoperability" + ], + "support": { + "source": "https://github.com/php-standard-library/interoperability/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/io", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/io.git", + "reference": "cd55c50f5158871c9a77b3fc6a1fca14f3d4d082" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/io/zipball/cd55c50f5158871c9a77b3fc6a1fca14f3d4d082", + "reference": "cd55c50f5158871c9a77b3fc6a1fca14f3d4d082", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.0", + "php-standard-library/channel": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/result": "^6.0", + "revolt/event-loop": "^1.0.8" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/date-time": "^6.0", + "php-standard-library/os": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\IO\\": "src/Psl/IO/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Handle-based I/O abstractions - composable, testable, and async-ready", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "async", + "handle", + "io", + "stream" + ], + "support": { + "source": "https://github.com/php-standard-library/io/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/iter", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/iter.git", + "reference": "9a53e9e4614b6e30ac23df21bd9bf7540dad79f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/iter/zipball/9a53e9e4614b6e30ac23df21bd9bf7540dad79f8", + "reference": "9a53e9e4614b6e30ac23df21bd9bf7540dad79f8", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/comparison": "^6.0", + "php-standard-library/either-or-both": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/option": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/collection": "^6.0", + "php-standard-library/dict": "^6.0", + "php-standard-library/math": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Iter\\": "src/Psl/Iter/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Utility functions for inspecting and reducing iterables - arrays, generators, and iterators", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "generator", + "iter", + "iterable", + "iterator" + ], + "support": { + "source": "https://github.com/php-standard-library/iter/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/json", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/json.git", + "reference": "ba52bbe747e91b2a1c905d3c51b8fc0b9f5af27d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/json/zipball/ba52bbe747e91b2a1c905d3c51b8fc0b9f5af27d", + "reference": "ba52bbe747e91b2a1c905d3c51b8fc0b9f5af27d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/type": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/collection": "^6.0", + "php-standard-library/math": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Json\\": "src/Psl/Json/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "JSON encoding and decoding with typed exceptions and sensible defaults", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "decoding", + "encoding", + "json" + ], + "support": { + "source": "https://github.com/php-standard-library/json/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/locale", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/locale.git", + "reference": "9687409700fc2392046f43cc4c1494253f8cf178" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/locale/zipball/9687409700fc2392046f43cc4c1494253f8cf178", + "reference": "9687409700fc2392046f43cc4c1494253f8cf178", + "shasum": "" + }, + "require": { + "ext-intl": "*", + "php": "~8.4.0 || ~8.5.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Locale\\": "src/Psl/Locale/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Backed enum with 700+ locale identifiers for type-safe internationalization", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "i18n", + "internationalization", + "locale" + ], + "support": { + "source": "https://github.com/php-standard-library/locale/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/option", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/option.git", + "reference": "d6cfc43ae2d0ce122d861065f5e6a433737b9e93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/option/zipball/d6cfc43ae2d0ce122d861065f5e6a433737b9e93", + "reference": "d6cfc43ae2d0ce122d861065f5e6a433737b9e93", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/comparison": "^6.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Option\\": "src/Psl/Option/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Option type (Some/None) replacing nullable types with explicit presence semantics", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "monad", + "none", + "option", + "some" + ], + "support": { + "source": "https://github.com/php-standard-library/option/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/process", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/process.git", + "reference": "46895e2c1b19cdd6c48688d1c631f1c23bc524e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/process/zipball/46895e2c1b19cdd6c48688d1c631f1c23bc524e5", + "reference": "46895e2c1b19cdd6c48688d1c631f1c23bc524e5", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.0", + "php-standard-library/date-time": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/io": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/env": "^6.0", + "php-standard-library/filesystem": "^6.0", + "php-standard-library/os": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Process\\": "src/Psl/Process/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Typed, non-blocking API for spawning and managing child processes", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "async", + "process", + "subprocess" + ], + "support": { + "source": "https://github.com/php-standard-library/process/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/promise", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/promise.git", + "reference": "97e5c6ffac07058f0ae408c747b5f5c363f3673b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/promise/zipball/97e5c6ffac07058f0ae408c747b5f5c363f3673b", + "reference": "97e5c6ffac07058f0ae408c747b5f5c363f3673b", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psl\\Promise\\": "src/Psl/Promise/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Promise interface for deferred computations - resolved or rejected", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "async", + "deferred", + "promise" + ], + "support": { + "source": "https://github.com/php-standard-library/promise/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/range", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/range.git", + "reference": "9e08b73482c2c8ee6065e4eb1c6d138ceba56376" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/range/zipball/9e08b73482c2c8ee6065e4eb1c6d138ceba56376", + "reference": "9e08b73482c2c8ee6065e4eb1c6d138ceba56376", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/iter": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/math": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Range\\": "src/Psl/Range/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Range types for integer sequences with iteration support", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "iterator", + "range" + ], + "support": { + "source": "https://github.com/php-standard-library/range/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/regex", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/regex.git", + "reference": "199b6e8f3321e7e5978a3f55963d4bd6fbc57317" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/regex/zipball/199b6e8f3321e7e5978a3f55963d4bd6fbc57317", + "reference": "199b6e8f3321e7e5978a3f55963d4bd6fbc57317", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/type": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Regex\\": "src/Psl/Regex/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Type-safe regular expressions with typed capture groups and proper error handling", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "preg", + "regex", + "regular-expression" + ], + "support": { + "source": "https://github.com/php-standard-library/regex/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/result", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/result.git", + "reference": "8489291e58eb926d3ffc9e48b23d8fa6678a0ff3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/result/zipball/8489291e58eb926d3ffc9e48b23d8fa6678a0ff3", + "reference": "8489291e58eb926d3ffc9e48b23d8fa6678a0ff3", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/promise": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/fun": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Result\\": "src/Psl/Result/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Result type capturing success or failure as a value for controlled error handling", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "error-handling", + "monad", + "result" + ], + "support": { + "source": "https://github.com/php-standard-library/result/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/shell", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/shell.git", + "reference": "e87e4720403c748036166102df5b825f7bb9ecd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/shell/zipball/e87e4720403c748036166102df5b825f7bb9ecd1", + "reference": "e87e4720403c748036166102df5b825f7bb9ecd1", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/process": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/date-time": "^6.0", + "php-standard-library/env": "^6.0", + "php-standard-library/os": "^6.0", + "php-standard-library/secure-random": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Shell\\": "src/Psl/Shell/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Shell command execution with argument escaping and error output management", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "command", + "execute", + "shell" + ], + "support": { + "source": "https://github.com/php-standard-library/shell/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/str", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/str.git", + "reference": "ab722aecf6c45531cdf0bf8ee11aa2370d71dff7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/str/zipball/ab722aecf6c45531cdf0bf8ee11aa2370d71dff7", + "reference": "ab722aecf6c45531cdf0bf8ee11aa2370d71dff7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/default": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/range": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Str\\": "src/Psl/Str/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Unicode-aware string functions replacing PHP mb_* and standard string functions", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "mbstring", + "string", + "unicode", + "utf8" + ], + "support": { + "source": "https://github.com/php-standard-library/str/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/type", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/type.git", + "reference": "80986810f49ba476d90dfeaa9c41bee0997675eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/type/zipball/80986810f49ba476d90dfeaa9c41bee0997675eb", + "reference": "80986810f49ba476d90dfeaa9c41bee0997675eb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/collection": "^6.0", + "php-standard-library/foundation": "^6.0", + "php-standard-library/iter": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "ext-bcmath": "*", + "php-standard-library/dict": "^6.0", + "php-standard-library/math": "^6.0", + "php-standard-library/result": "^6.0", + "php-standard-library/str": "^6.0", + "php-standard-library/vec": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Type\\": "src/Psl/Type/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Runtime type validation implementing Parse, Don't Validate - coerce and assert unstructured input into well-typed data", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "coercion", + "type", + "type-safety", + "validation" + ], + "support": { + "source": "https://github.com/php-standard-library/type/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "php-standard-library/vec", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/vec.git", + "reference": "5251a921c3d06b2045b4371c191bb60ff0ce47d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/vec/zipball/5251a921c3d06b2045b4371c191bb60ff0ce47d3", + "reference": "5251a921c3d06b2045b4371c191bb60ff0ce47d3", + "shasum": "" + }, + "require": { + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/foundation": "^6.0" + }, + "conflict": { + "azjezz/psl": "*" + }, + "require-dev": { + "php-standard-library/collection": "^6.0", + "php-standard-library/fun": "^6.0", + "php-standard-library/iter": "^6.0", + "php-standard-library/str": "^6.0", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-next": "6.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psl/bootstrap.php" + ], + "psr-4": { + "Psl\\Vec\\": "src/Psl/Vec/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Seifeddine Gmati", + "email": "azjezz@carthage.software" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-standard-library/php-standard-library/graphs/contributors" + } + ], + "description": "Functions for creating and transforming sequential, 0-indexed arrays (lists)", + "homepage": "https://php-standard-library.dev", + "keywords": [ + "array", + "list", + "vec", + "vector" + ], + "support": { + "source": "https://github.com/php-standard-library/vec/tree/6.3.0" + }, + "time": "2026-09-25T15:54:11+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "revolt/event-loop", + "version": "v1.0.9", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "44061cf513e53c6200372fc935ac42271566295d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/44061cf513e53c6200372fc935ac42271566295d", + "reference": "44061cf513e53c6200372fc935ac42271566295d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.9" + }, + "time": "2026-05-16T17:55:38+00:00" + }, + { + "name": "roave/backward-compatibility-check", + "version": "8.23.0", + "source": { + "type": "git", + "url": "https://github.com/Roave/BackwardCompatibilityCheck.git", + "reference": "f7573e3d1e3d012096f04aa7fe4de99b42aaf01d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/BackwardCompatibilityCheck/zipball/f7573e3d1e3d012096f04aa7fe4de99b42aaf01d", + "reference": "f7573e3d1e3d012096f04aa7fe4de99b42aaf01d", + "shasum": "" + }, + "require": { + "composer/composer": "^2.9.8", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "nikic/php-parser": "^5.7.0", + "nikolaposa/version": "^4.2.1", + "ocramius/package-versions": "^2.11.0", + "php": "~8.4.0 || ~8.5.0", + "php-standard-library/async": "^6.1.1", + "php-standard-library/dict": "^6.1.1", + "php-standard-library/env": "^6.1.1", + "php-standard-library/file": "^6.1.1", + "php-standard-library/filesystem": "^6.1.1", + "php-standard-library/foundation": "^6.1.1", + "php-standard-library/iter": "^6.1.1", + "php-standard-library/json": "^6.1.1", + "php-standard-library/regex": "^6.1.1", + "php-standard-library/shell": "^6.1.1", + "php-standard-library/str": "^6.1.1", + "php-standard-library/type": "^6.1.1", + "php-standard-library/vec": "^6.1.1", + "roave/better-reflection": "^6.71.0", + "symfony/console": "^7.4.11" + }, + "conflict": { + "marc-mabe/php-enum": "<4.7.2", + "revolt/event-loop": "<0.2.5", + "symfony/process": "<5.3.7" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "justinrainbow/json-schema": "^6.8.2", + "php-standard-library/hash": "^6.1.1", + "php-standard-library/psalm-plugin": "^2.4.0", + "php-standard-library/secure-random": "^6.1.1", + "phpunit/phpunit": "^13.1.10", + "psalm/plugin-phpunit": "^0.19.7", + "roave/infection-static-analysis-plugin": "^1.44.0", + "roave/security-advisories": "dev-master", + "squizlabs/php_codesniffer": "^4.0.1", + "vimeo/psalm": "^6.16.1" + }, + "bin": [ + "bin/roave-backward-compatibility-check" + ], + "type": "library", + "autoload": { + "psr-4": { + "Roave\\BackwardCompatibility\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Titcumb", + "email": "james@asgrim.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Tool to compare two revisions of a public API to check for BC breaks", + "support": { + "issues": "https://github.com/Roave/BackwardCompatibilityCheck/issues", + "source": "https://github.com/Roave/BackwardCompatibilityCheck/tree/8.23.0" + }, + "time": "2026-09-30T20:43:24+00:00" + }, + { + "name": "roave/better-reflection", + "version": "6.73.0", + "source": { + "type": "git", + "url": "https://github.com/Roave/BetterReflection.git", + "reference": "a6a93a3bd7681627f2750bfa8f41e586d32dc67d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/BetterReflection/zipball/a6a93a3bd7681627f2750bfa8f41e586d32dc67d", + "reference": "a6a93a3bd7681627f2750bfa8f41e586d32dc67d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "2026.2", + "nikic/php-parser": "^5.8.0", + "php": "~8.4.1 || ~8.5.0" + }, + "conflict": { + "thecodingmachine/safe": "<1.1.3" + }, + "require-dev": { + "phpbench/phpbench": "^1.7.0", + "phpunit/phpunit": "^13.3.2" + }, + "suggest": { + "composer/composer": "Required to use the ComposerSourceLocator" + }, + "type": "library", + "autoload": { + "psr-4": { + "Roave\\BetterReflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Titcumb", + "email": "james@asgrim.com", + "homepage": "https://github.com/asgrim" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + }, + { + "name": "Gary Hockin", + "email": "gary@roave.com", + "homepage": "https://github.com/geeh" + }, + { + "name": "Jaroslav Hanslík", + "email": "kukulich@kukulich.cz", + "homepage": "https://github.com/kukulich" + } + ], + "description": "Better Reflection - an improved code reflection API", + "support": { + "issues": "https://github.com/Roave/BetterReflection/issues", + "source": "https://github.com/Roave/BetterReflection/tree/6.73.0" + }, + "time": "2026-09-09T19:57:09+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9a90eb5d32d5a500296bf43f946d60246444d5f7", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.12.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2026-06-12T11:32:29+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "65fb381613fd31745d39a740a112a233f978d2d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/65fb381613fd31745d39a740a112a233f978d2d9", + "reference": "65fb381613fd31745d39a740a112a233f978d2d9", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.3" + }, + "time": "2026-10-01T08:16:23+00:00" + }, + { + "name": "seld/signal-handler", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/signal-handler.git", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/signal-handler/zipball/04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^7.5.20 || ^8.5.23", + "psr/log": "^1 || ^2 || ^3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\Signal\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Simple unix signal handler that silently fails where signals are not supported for easy cross-platform development", + "keywords": [ + "posix", + "sigint", + "signal", + "sigterm", + "unix" + ], + "support": { + "issues": "https://github.com/Seldaek/signal-handler/issues", + "source": "https://github.com/Seldaek/signal-handler/tree/2.0.2" + }, + "time": "2023-09-03T09:24:00+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", + "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.20" + }, + "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-09-18T15:55:00+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "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": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-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/filesystem", + "version": "v8.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7599ebb855fede59413ddb7f15198d67bfcae7ba", + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.1.6" + }, + "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-23T10:06:25+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "bd4fed193a3132c91c5c93595cbfc78988915822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/bd4fed193a3132c91c5c93595cbfc78988915822", + "reference": "bd4fed193a3132c91c5c93595cbfc78988915822", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.1.8" + }, + "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-09-26T06:33:49+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/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": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "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 for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.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": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "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 for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.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": "2026-09-23T22:02:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "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 for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.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": "2026-09-24T15:19:04+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/polyfill-php80", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "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\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.43.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": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "reference": "6bfb9c766cacffbc8e118cb87217d08ed84e5cd7", + "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\\Php81\\": "" + }, + "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 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.38.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-05-26T12:45:58+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/22c6548499cbe5c5bb072fc0c42312dff062a015", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015", + "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\\Php84\\": "" + }, + "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 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.43.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": "2026-09-23T22:01:09+00:00" + }, + { + "name": "symfony/process", + "version": "v8.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "10823b09358e690df4ff943e24e8492bb1019fc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/10823b09358e690df4ff943e24e8492bb1019fc3", + "reference": "10823b09358e690df4ff943e24e8492bb1019fc3", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.1.7" + }, + "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-09-02T12:40:29+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "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\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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 writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "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-27T15:39:01+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "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 an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.7" + }, + "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-09-11T14:51:16+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "8.4.1" + }, + "plugin-api-version": "2.6.0" +} diff --git a/tools/infection/composer.json b/tools/infection/composer.json new file mode 100644 index 0000000..52617c9 --- /dev/null +++ b/tools/infection/composer.json @@ -0,0 +1,14 @@ +{ + "require-dev": { + "infection/infection": "^0.35.5" + }, + "config": { + "allow-plugins": { + "infection/extension-installer": true + }, + "platform": { + "php": "8.4.1" + }, + "sort-packages": true + } +} diff --git a/tools/infection/composer.lock b/tools/infection/composer.lock new file mode 100644 index 0000000..80e6338 --- /dev/null +++ b/tools/infection/composer.lock @@ -0,0 +1,2638 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "4c4bc18af84fabd7c885ca4b00ebfadd", + "packages": [], + "packages-dev": [ + { + "name": "colinodell/json5", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/colinodell/json5.git", + "reference": "5724d21bc5c910c2560af1b8915f0cc0163579c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/colinodell/json5/zipball/5724d21bc5c910c2560af1b8915f0cc0163579c8", + "reference": "5724d21bc5c910c2560af1b8915f0cc0163579c8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.0" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "^1.7.0", + "phpstan/phpstan": "^1.10.57", + "scrutinizer/ocular": "^1.9", + "squizlabs/php_codesniffer": "^3.8.1", + "symfony/finder": "^6.0|^7.0", + "symfony/phpunit-bridge": "^7.0.3" + }, + "bin": [ + "bin/json5" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "files": [ + "src/global.php" + ], + "psr-4": { + "ColinODell\\Json5\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Developer" + } + ], + "description": "UTF-8 compatible JSON5 parser for PHP", + "homepage": "https://github.com/colinodell/json5", + "keywords": [ + "JSON5", + "json", + "json5_decode", + "json_decode" + ], + "support": { + "issues": "https://github.com/colinodell/json5/issues", + "source": "https://github.com/colinodell/json5/tree/v3.0.0" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://www.patreon.com/colinodell", + "type": "patreon" + } + ], + "time": "2024-02-09T13:06:12+00:00" + }, + { + "name": "composer/pcre", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "d384a7d0d6e67aa497327260ca95023e92dc2032" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/d384a7d0d6e67aa497327260ca95023e92dc2032", + "reference": "d384a7d0d6e67aa497327260ca95023e92dc2032", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0 || ^1.1.2", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "symfony/polyfill-mbstring": "^1.33", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.4.1" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2026-09-29T22:11:39+00:00" + }, + { + "name": "infection/abstract-testframework-adapter", + "version": "0.5.1", + "source": { + "type": "git", + "url": "https://github.com/infection/abstract-testframework-adapter.git", + "reference": "b24bf3e850f70cd20a10621f08c3cef66f147ac8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/abstract-testframework-adapter/zipball/b24bf3e850f70cd20a10621f08c3cef66f147ac8", + "reference": "b24bf3e850f70cd20a10621f08c3cef66f147ac8", + "shasum": "" + }, + "require": { + "php": "^8.3" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.18", + "fidry/makefile": "^1.0", + "friendsofphp/php-cs-fixer": "^3.95.2", + "phpunit/phpunit": "^12.0 || ^13.0", + "rector/rector": "^2.4.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Infection\\AbstractTestFramework\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } + ], + "description": "Abstract Test Framework Adapter for Infection", + "support": { + "issues": "https://github.com/infection/abstract-testframework-adapter/issues", + "source": "https://github.com/infection/abstract-testframework-adapter/tree/0.5.1" + }, + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" + } + ], + "time": "2026-05-28T19:10:29+00:00" + }, + { + "name": "infection/extension-installer", + "version": "0.1.2", + "source": { + "type": "git", + "url": "https://github.com/infection/extension-installer.git", + "reference": "9b351d2910b9a23ab4815542e93d541e0ca0cdcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/extension-installer/zipball/9b351d2910b9a23ab4815542e93d541e0ca0cdcf", + "reference": "9b351d2910b9a23ab4815542e93d541e0ca0cdcf", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0" + }, + "require-dev": { + "composer/composer": "^1.9 || ^2.0", + "friendsofphp/php-cs-fixer": "^2.18, <2.19", + "infection/infection": "^0.15.2", + "php-coveralls/php-coveralls": "^2.4", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.10", + "phpstan/phpstan-phpunit": "^0.12.6", + "phpstan/phpstan-strict-rules": "^0.12.2", + "phpstan/phpstan-webmozart-assert": "^0.12.2", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.8" + }, + "type": "composer-plugin", + "extra": { + "class": "Infection\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "Infection\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } + ], + "description": "Infection Extension Installer", + "support": { + "issues": "https://github.com/infection/extension-installer/issues", + "source": "https://github.com/infection/extension-installer/tree/0.1.2" + }, + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" + } + ], + "time": "2021-10-20T22:08:34+00:00" + }, + { + "name": "infection/include-interceptor", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/infection/include-interceptor.git", + "reference": "c083331bc0cd1cd319ac1b7a08e3ad4a34aa7992" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/include-interceptor/zipball/c083331bc0cd1cd319ac1b7a08e3ad4a34aa7992", + "reference": "c083331bc0cd1cd319ac1b7a08e3ad4a34aa7992", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "infection/infection": "^0.19.0", + "phan/phan": "^2.4 || ^3", + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "^0.12.8", + "phpunit/phpunit": "^8.5", + "vimeo/psalm": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Infection\\StreamWrapper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } + ], + "description": "Stream Wrapper: Include Interceptor. Allows to replace included (autoloaded) file with another one.", + "support": { + "issues": "https://github.com/infection/include-interceptor/issues", + "source": "https://github.com/infection/include-interceptor/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" + } + ], + "time": "2024-05-03T21:48:06+00:00" + }, + { + "name": "infection/infection", + "version": "0.35.5", + "source": { + "type": "git", + "url": "https://github.com/infection/infection.git", + "reference": "0db852996c8de64d908a4ed2112c36853232d992" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/infection/zipball/0db852996c8de64d908a4ed2112c36853232d992", + "reference": "0db852996c8de64d908a4ed2112c36853232d992", + "shasum": "" + }, + "require": { + "colinodell/json5": "^3.0", + "composer-runtime-api": "^2.0", + "composer/xdebug-handler": "^3.0", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "fidry/cpu-core-counter": "^1.0", + "infection/abstract-testframework-adapter": "^0.5.0", + "infection/extension-installer": "^0.1.0", + "infection/include-interceptor": "^0.2.5 || ^1.0.0", + "infection/mutator": "^0.4", + "justinrainbow/json-schema": "^6.0", + "nikic/php-parser": "^5.6.2", + "ondram/ci-detector": "^4.1.0", + "php": "^8.3", + "psr/log": "^2.0 || ^3.0", + "sanmai/di-container": "^0.1.23 || ^0.2", + "sanmai/duoclock": "^0.1.0", + "sanmai/later": "^0.1.7", + "sanmai/pipeline": "^7.2", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0", + "symfony/console": "^6.4 || ^7.4 || ^8.0", + "symfony/filesystem": "^6.4 || ^7.4 || ^8.0", + "symfony/finder": "^6.4 || ^7.4 || ^8.0", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^6.4 || ^7.4 || ^8.0", + "thecodingmachine/safe": "^v3.4", + "webmozart/assert": "^1.11 || ^2.0" + }, + "conflict": { + "antecedent/patchwork": "<2.1.25", + "dg/bypass-finals": "<1.4.1", + "marc-mabe/php-enum": "<4.4" + }, + "require-dev": { + "carthage-software/mago": "^1.20", + "ext-simplexml": "*", + "fidry/makefile": "^1.0", + "fig/log-test": "^1.2", + "phpat/phpat": "^0.12.4", + "phpbench/phpbench": "^1.4", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-webmozart-assert": "^2.0", + "phpunit/phpunit": "^12.5.29", + "rector/rector": "^2.2.4", + "shipmonk/dead-code-detector": "^1.3", + "shipmonk/name-collision-detector": "^2.1", + "sidz/phpstan-rules": "^0.5.1", + "symfony/yaml": "^6.4 || ^7.4 || ^8.0", + "thecodingmachine/phpstan-safe-rule": "^1.4", + "webmozarts/strict-phpunit": "^7.15" + }, + "bin": [ + "bin/infection" + ], + "type": "library", + "autoload": { + "psr-4": { + "Infection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com", + "homepage": "https://twitter.com/maks_rafalko" + }, + { + "name": "Oleg Zhulnev", + "homepage": "https://github.com/sidz" + }, + { + "name": "Gert de Pagter", + "homepage": "https://github.com/BackEndTea" + }, + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com", + "homepage": "https://twitter.com/tfidry" + }, + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com", + "homepage": "https://www.alexeykopytko.com" + }, + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Infection is a Mutation Testing framework for PHP. The mutation adequacy score can be used to measure the effectiveness of a test set in terms of its ability to detect faults.", + "keywords": [ + "coverage", + "mutant", + "mutation framework", + "mutation testing", + "testing", + "unit testing" + ], + "support": { + "issues": "https://github.com/infection/infection/issues", + "source": "https://github.com/infection/infection/tree/0.35.5" + }, + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" + } + ], + "time": "2026-09-27T14:35:40+00:00" + }, + { + "name": "infection/mutator", + "version": "0.4.1", + "source": { + "type": "git", + "url": "https://github.com/infection/mutator.git", + "reference": "3c976d721b02b32f851ee4e15d553ef1e9186d1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/mutator/zipball/3c976d721b02b32f851ee4e15d553ef1e9186d1d", + "reference": "3c976d721b02b32f851ee4e15d553ef1e9186d1d", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Infection\\Mutator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } + ], + "description": "Mutator interface to implement custom mutators (mutation operators) for Infection", + "support": { + "issues": "https://github.com/infection/mutator/issues", + "source": "https://github.com/infection/mutator/tree/0.4.1" + }, + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" + } + ], + "time": "2025-04-29T08:19:52+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "6.13.1", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "e54aacc4c0657b3a4af4f628428d2702146828c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/e54aacc4c0657b3a4af4f628428d2702146828c6", + "reference": "e54aacc4c0657b3a4af4f628428d2702146828c6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-json": "*", + "marc-mabe/php-enum": "^4.4", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "dev-main", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Danny van der Sluijs", + "email": "danny.vandersluijs@icloud.com", + "role": "Maintainer" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.13.1" + }, + "time": "2026-09-30T07:42:28+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "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.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, + { + "name": "ondram/ci-detector", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/OndraM/ci-detector.git", + "reference": "8b0223b5ed235fd377c75fdd1bfcad05c0f168b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/OndraM/ci-detector/zipball/8b0223b5ed235fd377c75fdd1bfcad05c0f168b8", + "reference": "8b0223b5ed235fd377c75fdd1bfcad05c0f168b8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.13.2", + "lmc/coding-standard": "^3.0.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.1.0", + "phpstan/phpstan": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpunit/phpunit": "^9.6.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "OndraM\\CiDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Machulda", + "email": "ondrej.machulda@gmail.com" + } + ], + "description": "Detect continuous integration environment and provide unified access to properties of current build", + "keywords": [ + "CircleCI", + "Codeship", + "Wercker", + "adapter", + "appveyor", + "aws", + "aws codebuild", + "azure", + "azure devops", + "azure pipelines", + "bamboo", + "bitbucket", + "buddy", + "ci-info", + "codebuild", + "continuous integration", + "continuousphp", + "devops", + "drone", + "github", + "gitlab", + "interface", + "jenkins", + "pipelines", + "sourcehut", + "teamcity", + "travis" + ], + "support": { + "issues": "https://github.com/OndraM/ci-detector/issues", + "source": "https://github.com/OndraM/ci-detector/tree/4.2.0" + }, + "time": "2024-03-12T13:22:30+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "sanmai/di-container", + "version": "0.2", + "source": { + "type": "git", + "url": "https://github.com/sanmai/di-container.git", + "reference": "c14ca816bb5527e2361e3dc554a20829674c7ee1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sanmai/di-container/zipball/c14ca816bb5527e2361e3dc554a20829674c7ee1", + "reference": "c14ca816bb5527e2361e3dc554a20829674c7ee1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1.2 || ^2.0", + "sanmai/pipeline": "^7.10" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^3.17", + "infection/infection": ">=0.31", + "php-coveralls/php-coveralls": "^2.4.1", + "phpbench/phpbench": "^1.4", + "phpstan/extension-installer": "^1.4", + "phpunit/phpunit": "^11.5.25", + "sanmai/phpstan-rules": "^0.3.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.1.x-dev" + }, + "preferred-install": "dist" + }, + "autoload": { + "psr-4": { + "DIContainer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com", + "homepage": "https://github.com/sanmai" + }, + { + "name": "Maks Rafalko", + "homepage": "https://twitter.com/maks_rafalko" + }, + { + "name": "Théo FIDRY", + "homepage": "https://twitter.com/tfidry" + } + ], + "description": "dependency injection container with automatic constructor dependency resolution", + "keywords": [ + "Autowiring", + "constructor di", + "di container", + "psr 11" + ], + "support": { + "issues": "https://github.com/sanmai/di-container/issues", + "source": "https://github.com/sanmai/di-container/tree/0.2" + }, + "funding": [ + { + "url": "https://github.com/sanmai", + "type": "github" + } + ], + "time": "2026-08-11T06:42:18+00:00" + }, + { + "name": "sanmai/duoclock", + "version": "0.1.3", + "source": { + "type": "git", + "url": "https://github.com/sanmai/DuoClock.git", + "reference": "47461e3ff65b7308635047831a55615652e7be1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sanmai/DuoClock/zipball/47461e3ff65b7308635047831a55615652e7be1a", + "reference": "47461e3ff65b7308635047831a55615652e7be1a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^3.17", + "infection/infection": ">=0.29", + "php-coveralls/php-coveralls": "^2.4.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^11.5.25", + "sanmai/phpstan-rules": "^0.3.1" + }, + "type": "library", + "extra": { + "preferred-install": "dist" + }, + "autoload": { + "psr-4": { + "DuoClock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com" + } + ], + "description": "PHP time mocking for tests - PSR-20 clock with mockable sleep(), time(), and TimeSpy for PHPUnit testing", + "support": { + "issues": "https://github.com/sanmai/DuoClock/issues", + "source": "https://github.com/sanmai/DuoClock/tree/0.1.3" + }, + "funding": [ + { + "url": "https://github.com/sanmai", + "type": "github" + } + ], + "time": "2025-12-26T06:12:34+00:00" + }, + { + "name": "sanmai/later", + "version": "0.1.8", + "source": { + "type": "git", + "url": "https://github.com/sanmai/later.git", + "reference": "c56aeb8fa7fdf81eda2640a68b51884685963d13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sanmai/later/zipball/c56aeb8fa7fdf81eda2640a68b51884685963d13", + "reference": "c56aeb8fa7fdf81eda2640a68b51884685963d13", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^3.35.1", + "infection/infection": ">=0.27.6", + "phan/phan": ">=2", + "php-coveralls/php-coveralls": "^2.0", + "phpstan/phpstan": ">=1.4.5", + "phpunit/phpunit": ">=9.5 <10", + "vimeo/psalm": ">=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.1.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Later\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com" + } + ], + "description": "Later: deferred wrapper object", + "support": { + "issues": "https://github.com/sanmai/later/issues", + "source": "https://github.com/sanmai/later/tree/0.1.8" + }, + "funding": [ + { + "url": "https://github.com/sanmai", + "type": "github" + } + ], + "time": "2026-06-29T07:24:33+00:00" + }, + { + "name": "sanmai/pipeline", + "version": "7.12", + "source": { + "type": "git", + "url": "https://github.com/sanmai/pipeline.git", + "reference": "e7c1f115790ae68ea22f30a646578c587a691399" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sanmai/pipeline/zipball/e7c1f115790ae68ea22f30a646578c587a691399", + "reference": "e7c1f115790ae68ea22f30a646578c587a691399", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.8", + "esi/phpunit-coverage-check": ">2", + "friendsofphp/php-cs-fixer": "^3.17", + "infection/infection": ">=0.32.3", + "league/pipeline": "^0.3 || ^1.0", + "php-coveralls/php-coveralls": "^2.4.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^11 || ^12", + "sanmai/phpstan-rules": "^0.3.11", + "vimeo/psalm": ">=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Pipeline\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com" + } + ], + "description": "General-purpose collections pipeline", + "support": { + "issues": "https://github.com/sanmai/pipeline/issues", + "source": "https://github.com/sanmai/pipeline/tree/7.12" + }, + "funding": [ + { + "url": "https://github.com/sanmai", + "type": "github" + } + ], + "time": "2026-09-30T13:46:33+00:00" + }, + { + "name": "sebastian/diff", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2df6626c1baf31d5a88674882a3072f151b5a26", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1", + "symfony/process": "^7.4.17" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-08-25T15:38:55+00:00" + }, + { + "name": "symfony/console", + "version": "v8.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "4b81146e3ee248ea969186f85499b54fbc78db08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/4b81146e3ee248ea969186f85499b54fbc78db08", + "reference": "4b81146e3ee248ea969186f85499b54fbc78db08", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.1.8" + }, + "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-09-18T16:00:27+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "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": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-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/filesystem", + "version": "v8.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7599ebb855fede59413ddb7f15198d67bfcae7ba", + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.1.6" + }, + "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-23T10:06:25+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "bd4fed193a3132c91c5c93595cbfc78988915822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/bd4fed193a3132c91c5c93595cbfc78988915822", + "reference": "bd4fed193a3132c91c5c93595cbfc78988915822", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.1.8" + }, + "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-09-26T06:33:49+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/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": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "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 for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.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": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "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 for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.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": "2026-09-23T22:02:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "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 for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.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": "2026-09-24T15:19:04+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/86a42cbcc07adc331481a5ab310a4024bfc227f6", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6", + "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\\Php85\\": "" + }, + "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 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.43.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": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/process", + "version": "v8.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "10823b09358e690df4ff943e24e8492bb1019fc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/10823b09358e690df4ff943e24e8492bb1019fc3", + "reference": "10823b09358e690df4ff943e24e8492bb1019fc3", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.1.7" + }, + "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-09-02T12:40:29+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "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\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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 writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "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-27T15:39:01+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "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 an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.7" + }, + "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-09-11T14:51:16+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+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": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "platform-overrides": { + "php": "8.4.1" + }, + "plugin-api-version": "2.6.0" +}