From 8063130f5a42377758ddb5ae7f5990f45f19957e Mon Sep 17 00:00:00 2001 From: PedroTroller Date: Wed, 27 Mar 2024 10:43:25 +0100 Subject: [PATCH] feat: initial prototype --- .dockerignore | 7 + .github/dependabot.yaml | 27 + .github/workflows/example-coverage.yaml | 29 + .../workflows/example-github-security.yaml | 59 + .github/workflows/example-phpmetrics.yaml | 57 + .github/workflows/pull-request.yaml | 39 + .github/workflows/push.yaml | 17 + .github/workflows/schedule.yaml | 16 + .gitignore | 5 + .k-pi.dist.yaml | 43 + .php-cs-fixer.dist.php | 115 + .prettierignore | 6 + .prettierrc.yaml | 35 + Dockerfile | 49 + Makefile | 18 + README.md | 42 + action.yml | 42 + bin/console | 15 + composer.json | 29 + composer.lock | 4599 +++++++++++++++++ coverage/phpspec/.gitignore | 1 + entrypoint.sh | 5 + phpspec.yaml.dist | 13 + phpstan-baseline.neon | 2 + phpstan.neon.dist | 7 + spec/K_pi/Data/DiffSpec.php | 80 + spec/K_pi/Data/Github/ResourceUrlSpec.php | 56 + spec/K_pi/Data/ReportSpec.php | 114 + .../Discussion/Storage/ConfigurationSpec.php | 86 + .../Github/Discussion/Storage/FactorySpec.php | 38 + .../CheckReporter/ConfigurationSpec.php | 197 + src/K_pi/CheckReporter.php | 12 + src/K_pi/CheckReporter/Factory.php | 18 + src/K_pi/CheckReporter/Integrations.php | 24 + src/K_pi/Command/AbstractCommand.php | 85 + src/K_pi/Command/CheckCommand.php | 87 + src/K_pi/Command/CompileCommand.php | 79 + src/K_pi/Configuration.php | 51 + .../Exception/AtPathException.php | 35 + src/K_pi/Configuration/Extractor.php | 13 + .../Extractor/StrategyExtractor.php | 33 + .../Extractor/YamlFileExtractor.php | 69 + .../Configuration/ReportConfiguration.php | 282 + src/K_pi/Container.php | 71 + src/K_pi/Container/Definitions.php | 196 + src/K_pi/Data/CheckReporterIntegration.php | 12 + src/K_pi/Data/Diff.php | 27 + src/K_pi/Data/Extra.php | 10 + src/K_pi/Data/Github/ResourceUrl.php | 65 + src/K_pi/Data/Github/StatusState.php | 13 + src/K_pi/Data/Report.php | 130 + src/K_pi/Data/StorageIntegration.php | 10 + src/K_pi/EnvVars.php | 61 + src/K_pi/Integration/Github.php | 46 + .../Github/CheckRun/CheckReporter.php | 36 + .../Github/CheckRun/CheckReporter/Factory.php | 25 + .../Integration/Github/Discussion/Storage.php | 334 ++ .../Discussion/Storage/Configuration.php | 99 + .../Github/Discussion/Storage/Factory.php | 22 + .../Github/Status/CheckReporter.php | 101 + .../Status/CheckReporter/Configuration.php | 191 + .../Github/Status/CheckReporter/Factory.php | 29 + src/K_pi/Integration/Github/Variables.php | 66 + src/K_pi/Libs/KnpGithubApi/Github.php | 202 + src/K_pi/Libs/Lazy.php | 41 + src/K_pi/Libs/Lazy/Github.php | 60 + src/K_pi/Storage.php | 18 + src/K_pi/Storage/Factory.php | 15 + src/K_pi/Storage/Integrations.php | 22 + src/K_pi/ValueNormalizer.php | 25 + tests/compose.yaml | 18 + tests/prettier/.dockerignore | 2 + tests/prettier/.gitignore | 1 + tests/prettier/.prettierignore | 1 + tests/prettier/Dockerfile | 20 + tests/prettier/package.json | 6 + tests/prettier/yarn.lock | 26 + 77 files changed, 8637 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/dependabot.yaml create mode 100644 .github/workflows/example-coverage.yaml create mode 100644 .github/workflows/example-github-security.yaml create mode 100644 .github/workflows/example-phpmetrics.yaml create mode 100644 .github/workflows/pull-request.yaml create mode 100644 .github/workflows/push.yaml create mode 100644 .github/workflows/schedule.yaml create mode 100644 .gitignore create mode 100644 .k-pi.dist.yaml create mode 100644 .php-cs-fixer.dist.php create mode 100644 .prettierignore create mode 100644 .prettierrc.yaml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 action.yml create mode 100755 bin/console create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 coverage/phpspec/.gitignore create mode 100755 entrypoint.sh create mode 100644 phpspec.yaml.dist create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon.dist create mode 100644 spec/K_pi/Data/DiffSpec.php create mode 100644 spec/K_pi/Data/Github/ResourceUrlSpec.php create mode 100644 spec/K_pi/Data/ReportSpec.php create mode 100644 spec/K_pi/Integration/Github/Discussion/Storage/ConfigurationSpec.php create mode 100644 spec/K_pi/Integration/Github/Discussion/Storage/FactorySpec.php create mode 100644 spec/K_pi/Integration/Github/Status/CheckReporter/ConfigurationSpec.php create mode 100644 src/K_pi/CheckReporter.php create mode 100644 src/K_pi/CheckReporter/Factory.php create mode 100644 src/K_pi/CheckReporter/Integrations.php create mode 100644 src/K_pi/Command/AbstractCommand.php create mode 100644 src/K_pi/Command/CheckCommand.php create mode 100644 src/K_pi/Command/CompileCommand.php create mode 100644 src/K_pi/Configuration.php create mode 100644 src/K_pi/Configuration/Exception/AtPathException.php create mode 100644 src/K_pi/Configuration/Extractor.php create mode 100644 src/K_pi/Configuration/Extractor/StrategyExtractor.php create mode 100644 src/K_pi/Configuration/Extractor/YamlFileExtractor.php create mode 100644 src/K_pi/Configuration/ReportConfiguration.php create mode 100644 src/K_pi/Container.php create mode 100644 src/K_pi/Container/Definitions.php create mode 100644 src/K_pi/Data/CheckReporterIntegration.php create mode 100644 src/K_pi/Data/Diff.php create mode 100644 src/K_pi/Data/Extra.php create mode 100644 src/K_pi/Data/Github/ResourceUrl.php create mode 100644 src/K_pi/Data/Github/StatusState.php create mode 100644 src/K_pi/Data/Report.php create mode 100644 src/K_pi/Data/StorageIntegration.php create mode 100644 src/K_pi/EnvVars.php create mode 100644 src/K_pi/Integration/Github.php create mode 100644 src/K_pi/Integration/Github/CheckRun/CheckReporter.php create mode 100644 src/K_pi/Integration/Github/CheckRun/CheckReporter/Factory.php create mode 100644 src/K_pi/Integration/Github/Discussion/Storage.php create mode 100644 src/K_pi/Integration/Github/Discussion/Storage/Configuration.php create mode 100644 src/K_pi/Integration/Github/Discussion/Storage/Factory.php create mode 100644 src/K_pi/Integration/Github/Status/CheckReporter.php create mode 100644 src/K_pi/Integration/Github/Status/CheckReporter/Configuration.php create mode 100644 src/K_pi/Integration/Github/Status/CheckReporter/Factory.php create mode 100644 src/K_pi/Integration/Github/Variables.php create mode 100644 src/K_pi/Libs/KnpGithubApi/Github.php create mode 100644 src/K_pi/Libs/Lazy.php create mode 100644 src/K_pi/Libs/Lazy/Github.php create mode 100644 src/K_pi/Storage.php create mode 100644 src/K_pi/Storage/Factory.php create mode 100644 src/K_pi/Storage/Integrations.php create mode 100644 src/K_pi/ValueNormalizer.php create mode 100644 tests/compose.yaml create mode 100644 tests/prettier/.dockerignore create mode 100644 tests/prettier/.gitignore create mode 100644 tests/prettier/.prettierignore create mode 100644 tests/prettier/Dockerfile create mode 100644 tests/prettier/package.json create mode 100644 tests/prettier/yarn.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..153910a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +/.php-cs-fixer.cache +/.prettier.cache + +/vendor/ +/coverage/ +/phpmetrics/ +/tests/ diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..021835a --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,27 @@ +--- +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' + + - package-ecosystem: 'composer' + directory: '/' + schedule: + interval: 'daily' + + - package-ecosystem: 'docker' + directory: '/' + schedule: + interval: 'daily' + + - package-ecosystem: 'npm' + directory: '/tests/prettier' + schedule: + interval: 'daily' + + - package-ecosystem: 'docker' + directory: '/tests/prettier' + schedule: + interval: 'daily' diff --git a/.github/workflows/example-coverage.yaml b/.github/workflows/example-coverage.yaml new file mode 100644 index 0000000..20f987e --- /dev/null +++ b/.github/workflows/example-coverage.yaml @@ -0,0 +1,29 @@ +--- +on: + workflow_call: + inputs: + command: + type: string + required: true + +jobs: + phpspec: + runs-on: ubuntu-latest + env: + COMPOSE_FILE: tests/compose.yaml + steps: + - uses: actions/checkout@v4 + - run: make phpspec + - name: XPath + id: xpath + run: | + sudo apt-get install -y libxml-xpath-perl + + PERCENT=$(cat coverage/phpspec/index.xml | xpath -q -e '/phpunit/project/directory[@name="/"]/totals/lines/@percent' | cut -f 2 -d "=" | tr -d \") + + echo "phpspec=${PERCENT}" > $GITHUB_OUTPUT + - uses: PedroTroller/K-pi@v1.1.0 + with: + report: coverage + command: ${{ inputs.command }} + values: ${{ toJson(steps.xpath.outputs) }} diff --git a/.github/workflows/example-github-security.yaml b/.github/workflows/example-github-security.yaml new file mode 100644 index 0000000..ce06874 --- /dev/null +++ b/.github/workflows/example-github-security.yaml @@ -0,0 +1,59 @@ +--- +on: workflow_call + +jobs: + metric: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + id: script + with: + script: | + const query = ` + query($cursor: String, $owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + vulnerabilityAlerts(states: OPEN, first: 100, after: $cursor) { + pageInfo { + endCursor + } + nodes { + securityVulnerability { + severity + } + } + } + } + } + `; + + let cursor = null; + const countBySeverity = {CRITICAL: 0, HIGH: 0, MODERATE: 0, LOW: 0}; + + do { + const result = await github.graphql( + query, + { + owner: context.repo.owner, + repo: context.repo.repo, + cursor: cursor, + } + ); + + for(const alert of result.repository.vulnerabilityAlerts.nodes) { + if (!countBySeverity[alert.securityVulnerability.severity]) { + countBySeverity[alert.securityVulnerability.severity] = 0; + } + + countBySeverity[alert.securityVulnerability.severity]++; + } + + cursor = result.repository.vulnerabilityAlerts.pageInfo.endCursor; + } while(null !== cursor) + + return countBySeverity; + - uses: actions/checkout@v4 + - uses: PedroTroller/K-pi@v1.1.0 + with: + report: github-security + command: compile + values: ${{ steps.script.outputs.result }} diff --git a/.github/workflows/example-phpmetrics.yaml b/.github/workflows/example-phpmetrics.yaml new file mode 100644 index 0000000..ce84e55 --- /dev/null +++ b/.github/workflows/example-phpmetrics.yaml @@ -0,0 +1,57 @@ +--- +on: + workflow_call: + inputs: + command: + type: string + required: true + +jobs: + phpmetrics: + runs-on: ubuntu-latest + outputs: + report: ${{ steps.phpmetrics.outputs.result }} + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: latest + tools: phpmetrics/phpmetrics + - name: Compile + run: | + mkdir phpmetrics -p + phpmetrics src --report-html=phpmetrics --report-json=phpmetrics/report.json + + echo "report=$(cat phpmetrics/report.json | jq . --compact-output)" > $GITHUB_OUTPUT + - name: Dump + id: phpmetrics + uses: actions/github-script@v7 + with: + script: return require('./phpmetrics/report.json'); + + report: + name: ${{ matrix.report }} + runs-on: ubuntu-latest + needs: phpmetrics + strategy: + matrix: + include: + - report: outdated-dependencies + query: '{"composer.json": .composer.packages|map(select(.status=="outdated"))|length}' + - report: class-bugs + query: '. | to_entries | map({key: .key, value: .value.bugs}) | map(select(.value != null)) | from_entries' + steps: + - name: jq + id: jq + env: + QUERY: ${{ matrix.query }} + REPORT: ${{ needs.phpmetrics.outputs.report }} + run: | + echo $REPORT > /tmp/report.json + echo report=$(cat /tmp/report.json | jq "$QUERY" --compact-output --raw-output) > $GITHUB_OUTPUT + - uses: actions/checkout@v4 + - uses: PedroTroller/K-pi@v1.1.0 + with: + report: ${{ matrix.report }} + command: ${{ inputs.command }} + values: ${{ steps.jq.outputs.report }} diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml new file mode 100644 index 0000000..7f94789 --- /dev/null +++ b/.github/workflows/pull-request.yaml @@ -0,0 +1,39 @@ +--- +on: pull_request + +permissions: + statuses: write + +jobs: + autoformat: + runs-on: ubuntu-latest + env: + COMPOSE_FILE: tests/compose.yaml + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + - name: make autoformat + run: make autoformat > /dev/null + - name: git status + run: | + if [ -z $(git status --porcelain) ]; + then + echo "Looks good" + else + echo "Some files need to be corrected, so run 'make autoformat' to apply a correction" + echo git status + exit 1 + fi + phpmetrics: + uses: ./.github/workflows/example-phpmetrics.yaml + with: + command: check + coverage: + uses: ./.github/workflows/example-coverage.yaml + with: + command: check diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml new file mode 100644 index 0000000..bddc7a7 --- /dev/null +++ b/.github/workflows/push.yaml @@ -0,0 +1,17 @@ +--- +on: + push: + branches: + - main + +jobs: + github-security: + uses: ./.github/workflows/example-github-security.yaml + phpmetrics: + uses: ./.github/workflows/example-phpmetrics.yaml + with: + command: compile + coverage: + uses: ./.github/workflows/example-coverage.yaml + with: + command: compile diff --git a/.github/workflows/schedule.yaml b/.github/workflows/schedule.yaml new file mode 100644 index 0000000..a8c375e --- /dev/null +++ b/.github/workflows/schedule.yaml @@ -0,0 +1,16 @@ +--- +on: + schedule: + - cron: '0 6 * * 1-5' # Mon to Fri at 8:00 (Paris GMT+2) + +jobs: + github-security: + uses: ./.github/workflows/example-github-security.yaml + phpmetrics: + uses: ./.github/workflows/example-phpmetrics.yaml + with: + command: compile + coverage: + uses: ./.github/workflows/example-coverage.yaml + with: + command: compile diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f189c0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.php-cs-fixer.cache +/.prettier.cache + +/phpmetrics/ +/vendor/ diff --git a/.k-pi.dist.yaml b/.k-pi.dist.yaml new file mode 100644 index 0000000..ad325c7 --- /dev/null +++ b/.k-pi.dist.yaml @@ -0,0 +1,43 @@ +--- +reports: + github-security: + extra: + Total: total + colors: + LOW: '#808080' + MODERATE: '#FFFF00' + HIGH: '#FFC100' + CRITICAL: '#FF0000' + storage: + github-discussion: + url: https://github.com/KnpLabs/K-pi/discussions/4 + coverage: + storage: + github-discussion: + url: https://github.com/KnpLabs/K-pi/discussions/1 + check-reporter: + github-status: + states: higher-is-better + unit: '%' + outdated-dependencies: + storage: + github-discussion: + url: https://github.com/KnpLabs/K-pi/discussions/2 + check-reporter: + github-status: + states: lower-is-better + unit: + singular: ' dependency' + plural: ' dependencies' + class-bugs: + storage: + github-discussion: + url: https://github.com/KnpLabs/K-pi/discussions/3 + check-reporter: + github-status: + states: + on-lower: success + on-higher: success + unit: + singular: ' bug' + plural: ' bugs' diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..1a0d615 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,115 @@ +ignoreVCS(true) + ->ignoreDotFiles(true) + ->in(__DIR__) + ->append([__FILE__]) +; + +$config = new PhpCsFixer\Config(); +$config->setRiskyAllowed(true); +$config->setUsingCache(true); +$config->registerCustomFixers(new Fixers()); +$config->setFinder($finder); +$config->setRules( + RuleSetFactory::create() + ->php(8.3, true) + ->phpCsFixer(true) + ->pedrotroller(true) + ->enable('PedroTroller/line_break_between_method_arguments', [ + 'max-length' => 80, + ]) + ->enable('align_multiline_comment') + ->enable('array_indentation') + ->enable('binary_operator_spaces', [ + 'operators' => [ + '=' => 'align_single_space_minimal', + '=>' => 'align_single_space_minimal', + ], + ]) + ->enable('class_attributes_separation', [ + 'elements' => [ + 'const' => 'one', + 'method' => 'one', + 'property' => 'one', + ], + ]) + ->enable('class_definition', [ + 'single_line' => true, + 'inline_constructor_arguments' => false, + ]) + ->enable('fully_qualified_strict_types') + ->enable('linebreak_after_opening_tag') + ->enable('mb_str_functions') + ->enable('native_function_invocation') + ->enable('no_extra_blank_lines', [ + 'tokens' => [ + 'break', + 'continue', + 'curly_brace_block', + 'default', + 'extra', + 'parenthesis_brace_block', + 'return', + 'square_brace_block', + 'switch', + 'throw', + 'use', + ], + ]) + ->enable('no_superfluous_elseif') + ->enable('no_superfluous_phpdoc_tags', [ + 'allow_mixed' => true, + ]) + ->enable('no_useless_else') + ->enable('ordered_class_elements') + ->enable('ordered_imports') + ->enable('phpdoc_order') + ->enable('concat_space', ['spacing' => 'one']) + ->enable('blank_line_before_statement', [ + 'statements' => [ + 'break', + 'case', + 'continue', + 'declare', + 'default', + 'exit', + 'goto', + 'if', + 'include', + 'include_once', + 'phpdoc', + 'require', + 'require_once', + 'return', + 'switch', + 'throw', + 'try', + 'yield', + ], + ]) + ->enable('trailing_comma_in_multiline', [ + 'after_heredoc' => true, + 'elements' => ['arguments', 'arrays', 'match', 'parameters'], + ]) + ->enable('global_namespace_import', [ + 'import_classes' => true, + 'import_constants' => false, + 'import_functions' => false, + ]) + ->disable('PedroTroller/exceptions_punctuation') + ->disable('phpdoc_add_missing_param_annotation') + ->disable('phpdoc_to_comment') + ->disable('return_assignment') + ->disable('strict_comparison') + ->getRules(), +); + +return $config; diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..35067f8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +/composer.lock + +/.git/ +/coverage/ +/phpmetrics/ +/vendor/ diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..9d3710f --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,35 @@ +--- +overrides: + - files: + - '*.yaml' + - '*.yml' + - '*.neon' + - '*.yaml.dist' + - '*.yml.dist' + - '*.neon.dist' + options: + parser: yaml + tabWidth: 2 + singleQuote: true + - files: + - '*.json' + - '*.json.dist' + options: + parser: json + tabWidth: 4 + - files: + - '*.php' + options: + parser: php + printWidth: 80 + tabWidth: 4 + singleQuote: true + phpVersion: '8.2' + braceStyle: 'per-cs' + trailingCommaPHP: false + - files: + - '*.md' + options: + parser: markdown + printWidth: 80 + proseWrap: always diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6b456cf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +FROM composer:2.7.2 AS composer + +################################## + +FROM composer AS vendor + +WORKDIR /K-pi + +COPY composer.* ./ + +RUN composer install --ignore-platform-reqs --no-scripts --no-plugins --no-dev + +################################## + +FROM vendor AS vendor-dev + +RUN composer install --ignore-platform-reqs --no-scripts --no-plugins + +################################## + +FROM php:8.3.4-cli-alpine3.19 AS base + +WORKDIR /K-pi + +ENTRYPOINT ["php", "/K-pi/bin/console"] + +################################## + +FROM base AS test + +RUN apk add --no-cache $PHPIZE_DEPS linux-headers \ + && pecl install \ + xdebug-3.3.1 \ + && docker-php-ext-enable \ + xdebug + +COPY --from=vendor-dev /K-pi/vendor /K-pi/vendor + +ENV XDEBUG_MODE=coverage + +COPY . . + +################################## + +FROM base AS prod + +COPY --from=vendor /K-pi/vendor /K-pi/vendor + +COPY . . diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6b4e987 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +PWD := $(shell pwd) +UID := $(shell id -u) + +.PHONY: phpspec +phpspec: + @docker compose -f tests/compose.yaml run --rm --build --user=$(UID) --volume="$(PWD)/coverage/phpspec:/K-pi/coverage/phpspec" --entrypoint=php php vendor/bin/phpspec --config=phpspec.yaml.dist run --no-interaction + +.PHONY: prettier +prettier: + @docker compose -f tests/compose.yaml run --rm --build prettier + +.PHONY: php-cs-fixer +php-cs-fixer: + @docker compose -f tests/compose.yaml run --rm --build --user=$(UID) --volume="$(PWD):/K-pi" --workdir=/K-pi composer install + @docker compose -f tests/compose.yaml run --rm --build --user=$(UID) --volume="$(PWD):/K-pi" --entrypoint=php php vendor/bin/php-cs-fixer fix -vvv --diff + +.PHONY: autoformat +autoformat: prettier php-cs-fixer diff --git a/README.md b/README.md new file mode 100644 index 0000000..54cf674 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +The goal of this project is to make it easy to track metrics. + +## Configuration + +Just create a `.k-pi.dist.yaml` file following the following pattern: + +```yaml +--- +reports: + : + storage: + : +``` + +## Integration with CI + +### Github Actions + +## Some examples + +For all examples, please refer to the configuration file +[`.k-pi.dist.yaml`](./.k-pi.dist.yaml). + +### Track test coverage + +> See [`example-coverage.yaml`](./.github/workflows/example-coverage.yaml) + +### Track Github dependabot security alerts + +> See +> [`example-github-security.yaml`](./.github/workflows/example-github-security.yaml) + +The goal of this metric is to count the number of alerts from dependabot's +Github API and project them onto a graph to track their evolution over time. + +In the configuration file, you can see that the configured storage is +`github-discussion`. It means that data will be stored in the discussion but +also projected onto a graph. + +### Track PhpMetrics data + +> See [`example-phpmetrics.yaml`](./.github/workflows/example-phpmetrics.yaml) diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..510c755 --- /dev/null +++ b/action.yml @@ -0,0 +1,42 @@ +--- +name: KnpLabs/K-pi +description: Compile, verify and publish metrics +branding: + color: red + icon: thermometer +inputs: + command: + required: true + type: 'string' + configuration: + description: 'The configuration' + required: false + type: 'string' + configuration_file: + description: 'The path to the configuration file' + required: false + default: '.k-pi.dist.yaml' + type: 'string' + report: + description: 'One of the report names configured in the configuration file' + required: true + type: 'string' + values: + description: 'The metric value read (int|float of list of int|float)' + required: true + github_token: + description: The GitHub token used to create an authenticated client + default: ${{ github.token }} + required: false + github_pull_request: + default: ${{ github.event.pull_request.html_url }} + required: false +runs: + using: 'docker' + image: 'Dockerfile' + args: + - ${{ inputs.command }} + - ${{ inputs.report }} + - ${{ inputs.values }} + - '--configuration-file' + - ${{ inputs.configuration_file }} diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..1fc0945 --- /dev/null +++ b/bin/console @@ -0,0 +1,15 @@ +#!/usr/bin/env php +get(Symfony\Component\Console\Application::class) + ->run(); + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1db61cb --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "require": { + "php": ">=8.3", + "beberlei/assert": "3.3.*", + "ianw/quickchart": "1.3.*", + "knplabs/github-api": "3.14.*", + "nyholm/psr7": "1.8.*", + "symfony/console": "7.0.*", + "symfony/http-client": "7.0.*", + "symfony/yaml": "7.0.*" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "6.3.*", + "friendsofphp/php-cs-fixer": "3.52.*", + "pedrotroller/php-cs-custom-fixer": "2.33.*", + "phpspec/phpspec": "7.5.*" + }, + "autoload": { + "psr-4": { + "K_pi\\": "src/K_pi/" + } + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + }, + "sort-packages": true + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..9daae92 --- /dev/null +++ b/composer.lock @@ -0,0 +1,4599 @@ +{ + "_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": "e96f306efc151144336c2a8394733054", + "packages": [ + { + "name": "beberlei/assert", + "version": "v3.3.2", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/cb70015c04be1baee6f5f5c953703347c0ac1655", + "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7.0 || ^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.2" + }, + "time": "2021-12-16T21:41:27+00:00" + }, + { + "name": "clue/stream-filter", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, + { + "name": "ianw/quickchart", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/typpo/quickchart-php.git", + "reference": "428e8c406d7af9b31214b510d4bbb23d122d417b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/typpo/quickchart-php/zipball/428e8c406d7af9b31214b510d4bbb23d122d417b", + "reference": "428e8c406d7af9b31214b510d4bbb23d122d417b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ian Webster", + "homepage": "https://quickchart.io/" + } + ], + "description": "QuickChart chart API", + "homepage": "http://github.com/typpo/quickchart-php", + "keywords": [ + "bar-chart", + "chart", + "chart-api", + "charts", + "image", + "line-chart" + ], + "support": { + "issues": "https://github.com/typpo/quickchart-php/issues", + "source": "https://github.com/typpo/quickchart-php/tree/v1.3.2" + }, + "time": "2024-01-04T16:46:05+00:00" + }, + { + "name": "knplabs/github-api", + "version": "v3.14.1", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/php-github-api.git", + "reference": "71fec50e228737ec23c0b69801b85bf596fbdaca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/71fec50e228737ec23c0b69801b85bf596fbdaca", + "reference": "71fec50e228737ec23c0b69801b85bf596fbdaca", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.2.5 || ^8.0", + "php-http/cache-plugin": "^1.7.1|^2.0", + "php-http/client-common": "^2.3", + "php-http/discovery": "^1.12", + "php-http/httplug": "^2.2", + "php-http/multipart-stream-builder": "^1.1.2", + "psr/cache": "^1.0|^2.0|^3.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0|^2.0", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.2", + "guzzlehttp/psr7": "^1.7", + "http-interop/http-factory-guzzle": "^1.0", + "php-http/mock-client": "^1.4.1", + "phpstan/extension-installer": "^1.0.5", + "phpstan/phpstan": "^0.12.57", + "phpstan/phpstan-deprecation-rules": "^0.12.5", + "phpunit/phpunit": "^8.5 || ^9.4", + "symfony/cache": "^5.1.8", + "symfony/phpunit-bridge": "^5.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.20.x-dev", + "dev-master": "3.14-dev" + } + }, + "autoload": { + "psr-4": { + "Github\\": "lib/Github/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "http://knplabs.com" + }, + { + "name": "Thibault Duplessis", + "email": "thibault.duplessis@gmail.com", + "homepage": "http://ornicar.github.com" + } + ], + "description": "GitHub API v3 client", + "homepage": "https://github.com/KnpLabs/php-github-api", + "keywords": [ + "api", + "gh", + "gist", + "github" + ], + "support": { + "issues": "https://github.com/KnpLabs/php-github-api/issues", + "source": "https://github.com/KnpLabs/php-github-api/tree/v3.14.1" + }, + "funding": [ + { + "url": "https://github.com/acrobat", + "type": "github" + } + ], + "time": "2024-03-24T18:21:15+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/aa5fc277a4f5508013d571341ade0c3886d4d00e", + "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.1" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-11-13T09:31:12+00:00" + }, + { + "name": "php-http/cache-plugin", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/cache-plugin.git", + "reference": "539b2d1ea0dc1c2f141c8155f888197d4ac5635b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/cache-plugin/zipball/539b2d1ea0dc1c2f141c8155f888197d4ac5635b", + "reference": "539b2d1ea0dc1c2f141c8155f888197d4ac5635b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/client-common": "^1.9 || ^2.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/http-factory-implementation": "^1.0", + "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "require-dev": { + "nyholm/psr7": "^1.6.1", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "PSR-6 Cache plugin for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "cache", + "http", + "httplug", + "plugin" + ], + "support": { + "issues": "https://github.com/php-http/cache-plugin/issues", + "source": "https://github.com/php-http/cache-plugin/tree/2.0.0" + }, + "time": "2024-02-19T17:02:14+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/1e19c059b0e4d5f717bf5d524d616165aeab0612", + "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.1" + }, + "time": "2023-11-30T10:31:25+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.19.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb", + "reference": "61e1a1eb69c92741f5896d9e05fb8e9d7e8bb0cb", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "symfony/phpunit-bridge": "^6.2" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.19.2" + }, + "time": "2023-11-30T16:49:05+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.0" + }, + "time": "2023-04-14T15:10:03+00:00" + }, + { + "name": "php-http/message", + "version": "1.16.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "5997f3289332c699fa2545c427826272498a2088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", + "reference": "5997f3289332c699fa2545c427826272498a2088", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.1" + }, + "time": "2024-03-07T13:22:09+00:00" + }, + { + "name": "php-http/multipart-stream-builder", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/multipart-stream-builder.git", + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/f5938fd135d9fa442cc297dc98481805acfe2b6a", + "reference": "f5938fd135d9fa442cc297dc98481805acfe2b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Message\\MultipartStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "A builder class that help you create a multipart stream", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "multipart stream", + "stream" + ], + "support": { + "issues": "https://github.com/php-http/multipart-stream-builder/issues", + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.0" + }, + "time": "2023-04-28T14:10:22+00:00" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "time": "2024-03-15T13:55:21+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "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/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.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/2.0.0" + }, + "time": "2021-07-14T16:41:46+00:00" + }, + { + "name": "symfony/console", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/6b099f3306f7c9c2d2786ed736d0026b2903205f", + "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.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", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^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/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-22T20:27:20+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "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.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "425f462a59d8030703ee04a9e1c666575ed5db3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/425f462a59d8030703ee04a9e1c666575ed5db3b", + "reference": "425f462a59d8030703ee04a9e1c666575ed5db3b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/http-client-contracts": "^3", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "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 powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-03-02T12:46:12+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "1ee70e699b41909c209a0c930f11034b93578654" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/1ee70e699b41909c209a0c930f11034b93578654", + "reference": "1ee70e699b41909c209a0c930f11034b93578654", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "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 HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-30T20:28:31+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "700ff4096e346f54cb628ea650767c8130f1001f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f", + "reference": "700ff4096e346f54cb628ea650767c8130f1001f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "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/v7.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-08T10:20:21+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "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.4.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-26T14:02:43+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", + "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", + "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/error-handler": "^5.4|^6.0|^7.0", + "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.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-01T13:16:41+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "2d4fca631c00700597e9442a0b2451ce234513d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/2d4fca631c00700597e9442a0b2451ce234513d3", + "reference": "2d4fca631c00700597e9442a0b2451ce234513d3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "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": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T15:02:46+00:00" + } + ], + "packages-dev": [ + { + "name": "composer/pcre", + "version": "3.1.3", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "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.1.3" + }, + "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-03-19T10:26:25+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "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": "Semver 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.4.0" + }, + "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": "2023-08-31T09:50:34+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "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", + "symfony/phpunit-bridge": "^6.0" + }, + "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": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "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": "2022-02-25T21:32:43+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.3" + }, + "time": "2024-01-30T19:34:25+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "friends-of-phpspec/phpspec-code-coverage", + "version": "v6.3.0", + "source": { + "type": "git", + "url": "https://github.com/friends-of-phpspec/phpspec-code-coverage.git", + "reference": "0b878460e7b0ef4bf8e3cb2ee2bd7d3f8202b12d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/friends-of-phpspec/phpspec-code-coverage/zipball/0b878460e7b0ef4bf8e3cb2ee2bd7d3f8202b12d", + "reference": "0b878460e7b0ef4bf8e3cb2ee2bd7d3f8202b12d", + "shasum": "" + }, + "require": { + "php": ">= 7.3", + "phpspec/phpspec": "^6.0 || ^7.0", + "phpunit/php-code-coverage": "^9.2 || ^10.0" + }, + "conflict": { + "sebastian/comparator": "< 2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "phpstan/phpstan": "^1.5" + }, + "suggest": { + "ext-pcov": "Install PCov extension to generate code coverage.", + "ext-xdebug": "Install Xdebug to generate phpspec code coverage." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "files": [ + "src/bootstrap.php" + ], + "psr-4": { + "FriendsOfPhpSpec\\PhpSpec\\CodeCoverage\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "ek9", + "email": "dev@ek9.co", + "homepage": "https://ek9.co" + }, + { + "name": "Henrik Bjornskov" + }, + { + "name": "Stéphane Hulard", + "email": "s.hulard@chstudio.fr", + "homepage": "https://chstudio.fr" + }, + { + "name": "Pol Dellaiera", + "email": "pol.dellaiera@protonmail.com", + "homepage": "https://not-a-number.io/" + }, + { + "name": "Jay Linski", + "homepage": "https://github.com/jaylinski" + } + ], + "description": "Generate Code Coverage reports for PhpSpec tests", + "homepage": "https://github.com/friends-of-phpspec/phpspec-code-coverage", + "keywords": [ + "code-coverage", + "coverage", + "phpspec", + "report", + "spec", + "test", + "tests" + ], + "support": { + "docs": "https://github.com/friends-of-phpspec/phpspec-code-coverage#phpspec-code-coverage", + "issues": "https://github.com/friends-of-phpspec/phpspec-code-coverage/issues", + "source": "https://github.com/friends-of-phpspec/phpspec-code-coverage" + }, + "time": "2023-02-22T15:46:57+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.52.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/6e77207f0d851862ceeb6da63e6e22c01b1587bc", + "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc", + "shasum": "" + }, + "require": { + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.28", + "symfony/polyfill-php80": "^1.28", + "symfony/polyfill-php81": "^1.28", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3 || ^2.0", + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.11", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.4", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.4", + "phpunit/phpunit": "^9.6 || ^10.5.5 || ^11.0.2", + "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "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/" + } + }, + "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.52.1" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2024-03-19T21:02:43+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.0.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-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.0.2" + }, + "time": "2024-03-05T20:51:40+00:00" + }, + { + "name": "pedrotroller/php-cs-custom-fixer", + "version": "v2.33.0", + "source": { + "type": "git", + "url": "https://github.com/PedroTroller/PhpCSFixer-Custom-Fixers.git", + "reference": "a0c9d6d2b764160360ea18b82b296f05c9e9e553" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PedroTroller/PhpCSFixer-Custom-Fixers/zipball/a0c9d6d2b764160360ea18b82b296f05c9e9e553", + "reference": "a0c9d6d2b764160360ea18b82b296f05c9e9e553", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.28", + "phpspec/phpspec": "^7.0", + "sebastian/diff": "^4.0", + "twig/twig": "^3.3", + "webmozart/assert": "^1.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "PedroTroller\\CS\\Fixer\\": "src/PedroTroller/CS/Fixer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP-CS-FIXER : my custom fixers", + "support": { + "issues": "https://github.com/PedroTroller/PhpCSFixer-Custom-Fixers/issues", + "source": "https://github.com/PedroTroller/PhpCSFixer-Custom-Fixers/tree/v2.33.0" + }, + "time": "2023-09-25T12:57:43+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, + "time": "2021-10-19T17:43:47+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "153ae662783729388a584b4361f2545e4d841e3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", + "reference": "153ae662783729388a584b4361f2545e4d841e3c", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" + }, + "time": "2024-02-23T11:10:43+00:00" + }, + { + "name": "phpspec/php-diff", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/phpspec/php-diff.git", + "reference": "fc1156187f9f6c8395886fe85ed88a0a245d72e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/php-diff/zipball/fc1156187f9f6c8395886fe85ed88a0a245d72e9", + "reference": "fc1156187f9f6c8395886fe85ed88a0a245d72e9", + "shasum": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Diff": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Chris Boulton", + "homepage": "http://github.com/chrisboulton" + } + ], + "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", + "support": { + "source": "https://github.com/phpspec/php-diff/tree/v1.1.3" + }, + "time": "2020-09-18T13:47:07+00:00" + }, + { + "name": "phpspec/phpspec", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/phpspec.git", + "reference": "3613651cd36306b5eb04c0e90d197feb68d5f351" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/3613651cd36306b5eb04c0e90d197feb68d5f351", + "reference": "3613651cd36306b5eb04c0e90d197feb68d5f351", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.5 || ^2", + "ext-tokenizer": "*", + "php": "^7.3 || 8.0.* || 8.1.* || 8.2.* || 8.3.*", + "phpspec/php-diff": "^1.0.0", + "phpspec/prophecy": "^1.9", + "sebastian/exporter": "^3.0 || ^4.0 || ^5.0", + "symfony/console": "^3.4 || ^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^3.4 || ^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/finder": "^3.4 || ^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/process": "^3.4 || ^4.4 || ^5.0 || ^6.0 || ^7.0", + "symfony/yaml": "^3.4 || ^4.4 || ^5.0 || ^6.0 || ^7.0" + }, + "conflict": { + "sebastian/comparator": "<1.2.4" + }, + "require-dev": { + "behat/behat": "^3.3", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0", + "symfony/filesystem": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "vimeo/psalm": "^4.3 || ^5.2" + }, + "suggest": { + "phpspec/nyan-formatters": "Adds Nyan formatters" + }, + "bin": [ + "bin/phpspec" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "PhpSpec": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "homepage": "http://marcelloduarte.net/" + }, + { + "name": "Ciaran McNulty", + "homepage": "https://ciaranmcnulty.com/" + } + ], + "description": "Specification-oriented BDD framework for PHP 7.1+", + "homepage": "http://phpspec.net/", + "keywords": [ + "BDD", + "SpecBDD", + "TDD", + "spec", + "specification", + "testing", + "tests" + ], + "support": { + "issues": "https://github.com/phpspec/phpspec/issues", + "source": "https://github.com/phpspec/phpspec/tree/7.5.0" + }, + "time": "2024-01-19T14:20:56+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.19.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/67a759e7d8746d501c41536ba40cd9c0a07d6a87", + "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2 || ^2.0", + "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.*", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0", + "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0 || ^7.0", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "dev", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.19.0" + }, + "time": "2024-02-29T11:52:51+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.26.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "231e3186624c03d7e7c890ec662b81e6b0405227" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/231e3186624c03d7e7c890ec662b81e6b0405227", + "reference": "231e3186624c03d7e7c890ec662b81e6b0405227", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.26.0" + }, + "time": "2024-02-23T16:05:55+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "842f72662d6b9edda84c4b6f13885fd9cd53dc63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/842f72662d6b9edda84c4b6f13885fd9cd53dc63", + "reference": "842f72662d6b9edda84c4b6f13885fd9cd53dc63", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-text-template": "^3.0", + "sebastian/code-unit-reverse-lookup": "^3.0", + "sebastian/complexity": "^3.0", + "sebastian/environment": "^6.0", + "sebastian/lines-of-code": "^2.0", + "sebastian/version": "^4.0", + "theseer/tokenizer": "^1.2.0" + }, + "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-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.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:22:05+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-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": "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": "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.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2db5010a484d53ebf536087a70b4a5423c102372" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", + "reference": "2db5010a484d53ebf536087a70b4a5423c102372", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.3" + }, + "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.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-14T13:18:12+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.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/fbf413a49e54f6b9b17e12d900ac7f6101591b7f", + "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^4.2 || ^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": "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.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T10:55:06+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", + "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "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": "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.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-04-11T05:39:26+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/64f51654862e0f5e318db7e9dcc2292c63cdbddc", + "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "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.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-09-24T13:22:09+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/recursion-context", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "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" + }, + { + "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", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+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/event-dispatcher", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/834c28d533dd0636f910909d01b9ff45cc094b5e", + "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.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": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^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/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T15:02:46+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "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.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2890e3a825bc0c0558526c04499c13f83e1b6b12", + "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "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/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T15:02:46+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/6e5688d69f7cfc4ed4a511e96007e06c2d34ce56", + "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^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/v7.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T17:59:56+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d", + "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/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.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/process", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/0e7727191c3b71ebec6d529fa0e50a01ca5679e9", + "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "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/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-22T20:27:20+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "983900d6fddf2b0cbaacacbbad07610854bd8112" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/983900d6fddf2b0cbaacacbbad07610854bd8112", + "reference": "983900d6fddf2b0cbaacacbbad07610854bd8112", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "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/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T15:02:46+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "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.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@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/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.3" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/coverage/phpspec/.gitignore b/coverage/phpspec/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/coverage/phpspec/.gitignore @@ -0,0 +1 @@ +* diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..605c424 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +echo $@ + +php /K-pi/bin/console $1 $2 $3 $4 diff --git a/phpspec.yaml.dist b/phpspec.yaml.dist new file mode 100644 index 0000000..a24b4d5 --- /dev/null +++ b/phpspec.yaml.dist @@ -0,0 +1,13 @@ +--- +formatter.name: pretty + +extensions: + FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension: + format: + - text + - html + - xml + output: + html: coverage/phpspec + xml: coverage/phpspec + show_only_summary: true diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..b369c75 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,2 @@ +parameters: + ignoreErrors: diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..4bbcb1f --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: max + paths: + - src diff --git a/spec/K_pi/Data/DiffSpec.php b/spec/K_pi/Data/DiffSpec.php new file mode 100644 index 0000000..f229667 --- /dev/null +++ b/spec/K_pi/Data/DiffSpec.php @@ -0,0 +1,80 @@ +beConstructedWith('test', 66.88, 66.85, 2); + } + + function it_is_initializable() + { + $this->shouldHaveType(Diff::class); + } + + function it_has_a_diff() + { + $this->diff->shouldBe(-0.03); + } + + function it_supports_data_provider() + { + foreach ($this->dataProvider() as $index => $data) { + [ + 'from' => $from, + 'to' => $to, + 'precision' => $precision, + 'diff' => $diff, + 'changed' => $changed + ] = $data; + + $self = new Diff( + name: 'test', + from: $from, + to: $to, + precision: $precision, + ); + + $message = "Index #{$index}: " . + 'Value "%s" is not the same as expected value "%s".'; + + Assert::that($self->diff)->same($diff, $message); + Assert::that($self->changed)->same($changed, $message); + } + } + + private function dataProvider(): iterable + { + yield [ + 'from' => 66.88, + 'to' => 66.85, + 'precision' => 2, + 'diff' => -0.03, + 'changed' => true, + ]; + + yield [ + 'from' => 66.85, + 'to' => 66.88, + 'precision' => 2, + 'diff' => 0.03, + 'changed' => true, + ]; + + yield [ + 'from' => 66.88, + 'to' => 66.85, + 'precision' => 1, + 'diff' => 0, + 'changed' => false, + ]; + } +} diff --git a/spec/K_pi/Data/Github/ResourceUrlSpec.php b/spec/K_pi/Data/Github/ResourceUrlSpec.php new file mode 100644 index 0000000..fdd4ad2 --- /dev/null +++ b/spec/K_pi/Data/Github/ResourceUrlSpec.php @@ -0,0 +1,56 @@ +beConstructedWith('https://github.com/KnpLabs/K-pi/pull/11'); + } + + function it_is_initializable() + { + $this->shouldHaveType(ResourceUrl::class); + } + + function it_can_get_data_from_an_url() + { + $this->owner->shouldBe('KnpLabs'); + $this->repository->shouldBe('K-pi'); + $this->type->shouldBe('pull'); + $this->number->shouldBe(11); + } + + function it_detects_bad_resource_urls() + { + foreach ($this->failureDataProvider() as $url => $message) { + $this->beConstructedWith($url); + + $this->shouldThrow( + new InvalidArgumentException($message), + )->duringInstantiation(); + } + } + + private function failureDataProvider(): iterable + { + yield 'https://github.com//K-pi/pull/11' => 'Invalid Github resource url.'; + + yield 'https://github.com/KnpLabs//pull/11' => 'Invalid Github resource url.'; + + yield 'https://github.com/KnpLabs/K-pi//11' => 'Invalid Github resource url.'; + + yield 'https://github.com/KnpLabs/K-pi/pull/0' => 'Invalid Github resource url.'; + + yield 'https://github.com/KnpLabs/K-pi/pull/-1' => 'Invalid Github resource url.'; + + yield 'https://gitlab.org/KnpLabs/K-pi/pull/11' => 'Invalid Github resource url.'; + } +} diff --git a/spec/K_pi/Data/ReportSpec.php b/spec/K_pi/Data/ReportSpec.php new file mode 100644 index 0000000..de0c543 --- /dev/null +++ b/spec/K_pi/Data/ReportSpec.php @@ -0,0 +1,114 @@ +shouldHaveType(Report::class); + } + + function it_is_able_to_store_data() + { + $moment1 = new DateTimeImmutable('1 day ago'); + $moment2 = new DateTimeImmutable(); + + $this->add('foo', $moment2, 5); + $this->add('foo', $moment1, 12); + $this->add('bar', $moment1, 2); + $this->add('baz', $moment2, 12); + $this->add('baz', $moment1, -4); + $this->add('bar', $moment2, 0); + + $this->shouldIterateLike([ + 'foo' => [ + $moment1->format('Y-m-d') => 12, + $moment2->format('Y-m-d') => 5, + ], + 'bar' => [ + $moment1->format('Y-m-d') => 2, + $moment2->format('Y-m-d') => 0, + ], + 'baz' => [ + $moment1->format('Y-m-d') => -4, + $moment2->format('Y-m-d') => 12, + ], + ]); + } + + function it_unifies_data_with_consecutive_dates_but_same_value() + { + $moment0 = new DateTimeImmutable('2 day ago'); + $moment1 = new DateTimeImmutable('1 day ago'); + $moment2 = new DateTimeImmutable(); + + $this->add('foo', $moment2, 5); + $this->add('foo', $moment1, 12); + $this->add('bar', $moment1, 2); + $this->add('baz', $moment2, 12); + $this->add('baz', $moment1, -4); + $this->add('bar', $moment2, 0); + $this->add('foo', $moment0, 12); + + $this->shouldIterateLike([ + 'foo' => [ + $moment0->format('Y-m-d') => 12, + $moment2->format('Y-m-d') => 5, + ], + 'bar' => [ + $moment1->format('Y-m-d') => 2, + $moment2->format('Y-m-d') => 0, + ], + 'baz' => [ + $moment1->format('Y-m-d') => -4, + $moment2->format('Y-m-d') => 12, + ], + ]); + } + + function it_can_provide_last_value() + { + $moment1 = new DateTimeImmutable('1 day ago'); + $moment2 = new DateTimeImmutable(); + + $this->add('foo', $moment2, 5); + $this->add('foo', $moment1, 12); + $this->add('bar', $moment1, 2); + $this->add('baz', $moment2, 12); + $this->add('baz', $moment1, -4); + $this->add('bar', $moment2, 0); + + $this->last('foo')->shouldReturn(5); + $this->last('bar')->shouldReturn(0); + $this->last('baz')->shouldReturn(12); + $this->last('tan')->shouldReturn(null); + } + + function it_can_compile_total() + { + $moment0 = new DateTimeImmutable('2 day ago'); + $moment1 = new DateTimeImmutable('1 day ago'); + $moment2 = new DateTimeImmutable(); + + $this->add('foo', $moment2, 5); + $this->add('bar', $moment1, 2); + $this->add('baz', $moment2, 12); + $this->add('baz', $moment1, -4); + $this->add('bar', $moment2, 0); + $this->add('foo', $moment0, 12); + + $this->getExtra(Extra::TOTAL)->shouldReturn([ + $moment0->format('Y-m-d') => 12, + $moment1->format('Y-m-d') => 12 + 2 - 4, + $moment2->format('Y-m-d') => 5 + 12 + 0, + ]); + } +} diff --git a/spec/K_pi/Integration/Github/Discussion/Storage/ConfigurationSpec.php b/spec/K_pi/Integration/Github/Discussion/Storage/ConfigurationSpec.php new file mode 100644 index 0000000..3c221ed --- /dev/null +++ b/spec/K_pi/Integration/Github/Discussion/Storage/ConfigurationSpec.php @@ -0,0 +1,86 @@ +url = 'https://github.com/KnpLabs/K-pi/discussions/42'; + + $this->beConstructedWith($configuration, 'report-name'); + } + + function it_is_initializable() + { + $this->shouldHaveType(Configuration::class); + } + + function it_is_able_to_load_minimum_configuration() + { + $this->discussion->owner->shouldBe('KnpLabs'); + $this->discussion->repository->shouldBe('K-pi'); + $this->discussion->number->shouldBe(42); + $this->report->shouldBe(true); + $this->persist->shouldBe(true); + } + + function it_supports_full_configuration($configuration) + { + $configuration->report = false; + $configuration->persist = false; + + $this->discussion->owner->shouldBe('KnpLabs'); + $this->discussion->repository->shouldBe('K-pi'); + $this->discussion->number->shouldBe(42); + $this->report->shouldBe(false); + $this->persist->shouldBe(false); + } + + function it_handle_bad_configuration() + { + foreach ($this->invalidDataProvider() as $exception => $configuration) { + $configuration = json_decode(json_encode($configuration), false); + + $this->beConstructedWith($configuration, 'report-name'); + + $this->shouldThrow($exception)->duringInstantiation(); + } + } + + private function invalidDataProvider() + { + yield new AtPathException( + '.reports.report-name.storage.github-discussion.url', + 'Invalid Github resource url.', + ) => [ + 'url' => 'https://github.com/KnpLabs/K-pi/discussions/0', + 'persist' => true, + 'report' => true, + ]; + + yield new AtPathException( + '.reports.report-name.storage.github-discussion.url', + 'Invalid Github resource url.', + ) => [ + 'url' => 'https://github.com/KnpLabs//discussions/42', + 'persist' => true, + 'report' => true, + ]; + + yield new AtPathException( + '.reports.report-name.storage.github-discussion.persist', + 'must be a boolean.', + ) => [ + 'url' => 'https://github.com/KnpLabs/K-pi/discussions/42', + 'persist' => 1, + 'report' => true, + ]; + } +} diff --git a/spec/K_pi/Integration/Github/Discussion/Storage/FactorySpec.php b/spec/K_pi/Integration/Github/Discussion/Storage/FactorySpec.php new file mode 100644 index 0000000..198bf7c --- /dev/null +++ b/spec/K_pi/Integration/Github/Discussion/Storage/FactorySpec.php @@ -0,0 +1,38 @@ +beConstructedWith($github); + } + + function it_is_initializable() + { + $this->shouldHaveType(Factory::class); + } + + function it_is_able_to_parse_configuration(Github $github) + { + $configuration = new stdClass(); + $configuration->url = 'https://github.com/KnpLabs/K-pi/discussions/42'; + + $this->build('report-name', $configuration)->shouldBeLike( + new Storage( + new Configuration($configuration, 'report-name'), + $github->getWrappedObject(), + ), + ); + } +} diff --git a/spec/K_pi/Integration/Github/Status/CheckReporter/ConfigurationSpec.php b/spec/K_pi/Integration/Github/Status/CheckReporter/ConfigurationSpec.php new file mode 100644 index 0000000..6e196d7 --- /dev/null +++ b/spec/K_pi/Integration/Github/Status/CheckReporter/ConfigurationSpec.php @@ -0,0 +1,197 @@ +beConstructedWith($configuration, 'report-name'); + } + + function it_is_initializable() + { + $this->shouldHaveType(Configuration::class); + } + + function it_has_default_value() + { + $this->beConstructedWith(null, 'report-name'); + + $this->onLower->shouldBe(StatusState::SUCCESS); + $this->onHigher->shouldBe(StatusState::SUCCESS); + $this->singularUnit->shouldBe(null); + $this->pluralUnit->shouldBe(null); + } + + function it_has_prebuild_states_higher_is_better($configuration) + { + $configuration->states = 'higher-is-better'; + + $this->beConstructedWith($configuration, 'report-name'); + + $this->onLower->shouldBe(StatusState::ERROR); + $this->onHigher->shouldBe(StatusState::SUCCESS); + } + + function it_has_prebuild_states_lower_is_better($configuration) + { + $configuration->states = 'lower-is-better'; + + $this->beConstructedWith($configuration, 'report-name'); + + $this->onLower->shouldBe(StatusState::SUCCESS); + $this->onHigher->shouldBe(StatusState::ERROR); + } + + function it_can_customize_states($configuration) + { + $onLower = 'on-lower'; + $onHigher = 'on-higher'; + + $configuration->states = new stdClass(); + $configuration->states->{$onLower} = StatusState::FAILURE->value; + $configuration->states->{$onHigher} = StatusState::SUCCESS->value; + + $this->onLower->shouldBe(StatusState::FAILURE); + $this->onHigher->shouldBe(StatusState::SUCCESS); + } + + function it_has_default_states() + { + $this->onLower->shouldBe(StatusState::SUCCESS); + $this->onHigher->shouldBe(StatusState::SUCCESS); + } + + function it_has_a_simple_unit($configuration) + { + $configuration->unit = '%'; + + $this->singularUnit->shouldBe('%'); + $this->pluralUnit->shouldBe('%'); + } + + function it_has_complexe_unit($configuration) + { + $configuration->unit = new stdClass(); + $configuration->unit->singular = ' error'; + $configuration->unit->plural = ' errors'; + + $this->singularUnit->shouldBe(' error'); + $this->pluralUnit->shouldBe(' errors'); + } + + function it_has_default_unit() + { + $this->singularUnit->shouldBe(null); + $this->pluralUnit->shouldBe(null); + } + + function it_handle_bad_configuration($configuration) + { + foreach ($this->invalidDataProvider() as $exception => $configuration) { + $configuration = json_decode(json_encode($configuration), false); + + $this->beConstructedWith($configuration, 'report-name'); + + $this->shouldThrow($exception)->duringInstantiation(); + } + } + + private function invalidDataProvider() + { + yield new AtPathException( + '.reports.report-name.check-reporter.github-status', + 'must be null or an object', + ) => 1; + + yield new AtPathException( + '.reports.report-name.check-reporter.github-status.states', + 'must be "higher-is-better" or "lower-is-better" or an object with "on-lower" and "on-higher" properties', + ) => [ + 'states' => 'unknown', + ]; + + yield new AtPathException( + '.reports.report-name.check-reporter.github-status.states', + 'must be "higher-is-better" or "lower-is-better" or an object with "on-lower" and "on-higher" properties', + ) => [ + 'states' => null, + ]; + + yield new AtPathException( + '.reports.report-name.check-reporter.github-status.states', + 'must be "higher-is-better" or "lower-is-better" or an object with "on-lower" and "on-higher" properties', + ) => [ + 'states' => [ + 'foo' => 'bar', + ], + ]; + + yield new AtPathException( + '.reports.report-name.check-reporter.github-status.states.on-lower', + 'must be "error" or "failure" or "pending" or "success"', + ) => [ + 'states' => [ + 'on-lower' => 'foo', + 'on-higher' => 'success', + ], + ]; + + yield new AtPathException( + '.reports.report-name.check-reporter.github-status.states.on-higher', + 'must be "error" or "failure" or "pending" or "success"', + ) => [ + 'states' => [ + 'on-lower' => 'success', + 'on-higher' => 'foo', + ], + ]; + + yield new AtPathException( + '.reports.report-name.check-reporter.github-status.unit', + 'must be a string or an object with "singular" and "plural" properties', + ) => [ + 'unit' => [ + 'singular' => ' error', + ], + ]; + + yield new AtPathException( + '.reports.report-name.check-reporter.github-status.unit', + 'must be a string or an object with "singular" and "plural" properties', + ) => [ + 'unit' => [ + 'plural' => ' errors', + ], + ]; + + yield new AtPathException( + '.reports.report-name.check-reporter.github-status.unit.singular', + 'must be a string', + ) => [ + 'unit' => [ + 'singular' => null, + 'plural' => ' errors', + ], + ]; + + yield new AtPathException( + '.reports.report-name.check-reporter.github-status.unit.plural', + 'must be a string', + ) => [ + 'unit' => [ + 'singular' => ' error', + 'plural' => null, + ], + ]; + } +} diff --git a/src/K_pi/CheckReporter.php b/src/K_pi/CheckReporter.php new file mode 100644 index 0000000..9d444a7 --- /dev/null +++ b/src/K_pi/CheckReporter.php @@ -0,0 +1,12 @@ + $this->githubCheckRun, + CheckReporterIntegration::GITHUB_STATUS => $this->githubStatus, + }; + } +} diff --git a/src/K_pi/Command/AbstractCommand.php b/src/K_pi/Command/AbstractCommand.php new file mode 100644 index 0000000..b212c4a --- /dev/null +++ b/src/K_pi/Command/AbstractCommand.php @@ -0,0 +1,85 @@ + + */ + protected function getValues(InputInterface $input): array + { + $values = Yaml::parse($this->readArgument($input, 'values')); + + Assert::that($values)->isArray(); + Assert::that(array_keys($values))->all()->string()->notEmpty(); + + return array_map(static function ($value): float|int { + if (\is_int($value) || \is_float($value)) { + return $value; + } + + if (\is_string($value) && is_numeric($value)) { + return (float) $value; + } + + throw new Exception( + sprintf( + '%s is not an integer, a float or a numeric string.', + match (\gettype($value)) { + 'string' => sprintf('"%s"', addslashes($value)), + 'boolean' => $value ? 'true' : 'false', + default => \gettype($value), + }, + ), + ); + }, $values); + } + + /** + * @return non-empty-string + */ + protected function readArgument( + InputInterface $input, + string $argumentName, + ): string { + $argument = $input->getArgument($argumentName); + + Assert::that($argument)->string()->notEmpty(); + + /** + * @var non-empty-string + */ + return $argument; + } + + /** + * @param non-empty-string $reportName + */ + protected function getStorage( + string $reportName, + StorageIntegration $integration, + mixed $configuration, + ): Storage { + return $this->storageIntegrations + ->get($integration) + ->build($reportName, $configuration) + ; + } +} diff --git a/src/K_pi/Command/CheckCommand.php b/src/K_pi/Command/CheckCommand.php new file mode 100644 index 0000000..5064c11 --- /dev/null +++ b/src/K_pi/Command/CheckCommand.php @@ -0,0 +1,87 @@ +setName('check') + ->addArgument('report-name', InputArgument::REQUIRED) + ->addArgument('values', InputArgument::REQUIRED) + ->addOption( + 'configuration-file', + mode: InputOption::VALUE_OPTIONAL, + ) + ; + } + + protected function execute( + InputInterface $input, + OutputInterface $output, + ): int { + $reportName = $this->readArgument($input, 'report-name'); + $configuration = $this->extractor->extract($input); + + if (null === $configuration) { + throw new Exception( + 'Unable to read configuration, no configuration file found.', + ); + } + + $reportConfiguration = $configuration->get($reportName); + $storage = $this->getStorage( + $reportName, + ...$reportConfiguration->getStorageConfiguration(), + ); + $report = $storage->read(); + $values = $this->getValues($input); + + $notifications = array_map( + static fn (float|int $value, string $name): Diff => new Diff( + name: $name, + from: $report->last($name) ?? 0, + to: $value, + precision: $reportConfiguration->getPrecision(), + ), + array_values($values), + array_keys($values), + ); + + if ([] === $notifications) { + return self::SUCCESS; + } + + foreach ( + $reportConfiguration->getCheckReportersConfiguration() as $checkReporterIntegration => $checkReporterConfiguration + ) { + $this->checkReporterIntegrations + ->get($checkReporterIntegration) + ->build($checkReporterConfiguration, $reportName) + ->send(...$notifications) + ; + } + + return self::SUCCESS; + } +} diff --git a/src/K_pi/Command/CompileCommand.php b/src/K_pi/Command/CompileCommand.php new file mode 100644 index 0000000..bea7f2c --- /dev/null +++ b/src/K_pi/Command/CompileCommand.php @@ -0,0 +1,79 @@ +setName('compile') + ->addArgument('report-name', InputArgument::REQUIRED) + ->addArgument('values', InputArgument::REQUIRED) + ->addOption( + 'configuration-file', + mode: InputOption::VALUE_OPTIONAL, + ) + ; + } + + protected function execute( + InputInterface $input, + OutputInterface $output, + ): int { + $reportName = $this->readArgument($input, 'report-name'); + $configuration = $this->extractor->extract($input); + + if (null === $configuration) { + throw new Exception( + 'Unable to read configuration, no configuration file found.', + ); + } + + $reportConfiguration = $configuration->get($reportName); + $storage = $this->getStorage( + $reportName, + ...$reportConfiguration->getStorageConfiguration(), + ); + $report = $storage->read(); + $values = $this->getValues($input); + $now = new DateTimeImmutable(); + + foreach ($values as $name => $value) { + $report->add( + $name, + $now, + ValueNormalizer::normalize( + $value, + $reportConfiguration->getPrecision(), + ), + ); + } + + foreach ($reportConfiguration->getColors() as $name => $color) { + $report->colorize($name, $color); + } + + $storage->write($report, $reportConfiguration); + + return self::SUCCESS; + } +} diff --git a/src/K_pi/Configuration.php b/src/K_pi/Configuration.php new file mode 100644 index 0000000..799c0c8 --- /dev/null +++ b/src/K_pi/Configuration.php @@ -0,0 +1,51 @@ +configuration, 'reports')) { + throw new AtPathException('.', 'property "reports" is mandatory'); + } + + if (false === \is_object($this->configuration->reports)) { + throw new AtPathException('.reports', 'must be an object'); + } + + if ( + false === + property_exists($this->configuration->reports, $reportName) + ) { + throw new AtPathException( + '.reports', + sprintf('property "%s" not found', $reportName), + ); + } + + if ( + false === \is_object($this->configuration->reports->{$reportName}) + ) { + throw new AtPathException( + sprintf('.reports.%s', $reportName), + 'must be an object', + ); + } + + return new ReportConfiguration( + $this->configuration->reports->{$reportName}, + $reportName, + ); + } +} diff --git a/src/K_pi/Configuration/Exception/AtPathException.php b/src/K_pi/Configuration/Exception/AtPathException.php new file mode 100644 index 0000000..6bad872 --- /dev/null +++ b/src/K_pi/Configuration/Exception/AtPathException.php @@ -0,0 +1,35 @@ +getMessage(), $previous); + } +} diff --git a/src/K_pi/Configuration/Extractor.php b/src/K_pi/Configuration/Extractor.php new file mode 100644 index 0000000..41befbf --- /dev/null +++ b/src/K_pi/Configuration/Extractor.php @@ -0,0 +1,13 @@ + + */ + private readonly array $extractors; + + public function __construct(YamlFileExtractor $yamlFileExtractor) + { + $this->extractors = [$yamlFileExtractor]; + } + + public function extract(InputInterface $input): ?Configuration + { + foreach ($this->extractors as $extractor) { + if (null !== ($configuration = $extractor->extract($input))) { + return $configuration; + } + } + + return null; + } +} diff --git a/src/K_pi/Configuration/Extractor/YamlFileExtractor.php b/src/K_pi/Configuration/Extractor/YamlFileExtractor.php new file mode 100644 index 0000000..c2aefb0 --- /dev/null +++ b/src/K_pi/Configuration/Extractor/YamlFileExtractor.php @@ -0,0 +1,69 @@ +hasOption('configuration-file')) { + return null; + } + + $configurationFilePath = $input->getOption('configuration-file'); + + if (false === \is_string($configurationFilePath)) { + return null; + } + + $supported = false; + + foreach (self::EXTENSIONS as $extension) { + if (true === $supported) { + continue; + } + + $supported = str_ends_with($configurationFilePath, $extension); + } + + if (false === $supported) { + return null; + } + + $yaml = Yaml::parseFile($configurationFilePath); + $json = json_decode( + json_encode($yaml, flags: JSON_THROW_ON_ERROR), + false, + flags: JSON_THROW_ON_ERROR, + ); + + if (false === \is_object($json)) { + throw new AtPathException( + '.', + 'configuration root must be an object', + ); + } + + return new Configuration($json); + } +} diff --git a/src/K_pi/Configuration/ReportConfiguration.php b/src/K_pi/Configuration/ReportConfiguration.php new file mode 100644 index 0000000..025276d --- /dev/null +++ b/src/K_pi/Configuration/ReportConfiguration.php @@ -0,0 +1,282 @@ + + */ + public function getPrecision(): int + { + if (false === property_exists($this->configuration, 'precision')) { + return 2; + } + + $precision = $this->configuration->precision; + + if (false === \is_int($precision) || 0 > $precision) { + throw new AtPathException( + sprintf('.reports.%s.precision', $this->reportName), + 'zero or positive integer expected', + ); + } + + return $precision; + } + + /** + * @return array{StorageIntegration, mixed} + */ + public function getStorageConfiguration(): array + { + if (false === property_exists($this->configuration, 'storage')) { + throw new AtPathException( + sprintf('.reports.%s', $this->reportName), + 'property "storage" is mandatory', + ); + } + + foreach ( + get_object_vars($this->configuration->storage) as $integrationName => $integrationConfiguration + ) { + $integration = StorageIntegration::tryFrom($integrationName); + + if (null === $integration) { + throw new AtPathException( + sprintf('.reports.%s.storage', $this->reportName), + sprintf( + 'integration "%s" does not exists, must be %s', + $integration, + implode( + ' or ', + array_map( + static fn ( + StorageIntegration $integration, + ) => '"' . $integration->value . '"', + StorageIntegration::cases(), + ), + ), + ), + ); + } + + return [$integration, $integrationConfiguration]; + } + + throw new AtPathException( + sprintf('.reports.%s.storage', $this->reportName), + sprintf( + 'integration is mandatory, must be %s', + implode( + ' or ', + array_map( + static fn (StorageIntegration $integration) => '"' . + $integration->value . + '"', + StorageIntegration::cases(), + ), + ), + ), + ); + } + + /** + * @return iterable + */ + public function getCheckReportersConfiguration(): iterable + { + $configuration = get_object_vars($this->configuration); + + if (false === \array_key_exists('check-reporter', $configuration)) { + return []; + } + + $checkReporters = $configuration['check-reporter']; + + if (false === \is_object($checkReporters)) { + throw new AtPathException( + sprintf('.reports.%s.check-reporter', $this->reportName), + 'object expected', + ); + } + + $empty = true; + + foreach ( + get_object_vars($checkReporters) as $integrationName => $integrationConfiguration + ) { + $integration = CheckReporterIntegration::tryFrom($integrationName); + + if (null === $integration) { + throw new AtPathException( + sprintf('.reports.%s.check-reporter', $this->reportName), + sprintf( + 'integration "%s" does not exists, must be %s', + $integration, + implode( + ' or ', + array_map( + static fn ( + CheckReporterIntegration $integration, + ) => '"' . $integration->value . '"', + CheckReporterIntegration::cases(), + ), + ), + ), + ); + } + + $empty = false; + + yield $integration => $integrationConfiguration; + } + + if ($empty) { + throw new AtPathException( + sprintf('.reports.%s.check-reporter', $this->reportName), + sprintf( + 'integration is mandatory, must be %s', + implode( + ' or ', + array_map( + static fn ( + CheckReporterIntegration $integration, + ) => '"' . $integration->value . '"', + CheckReporterIntegration::cases(), + ), + ), + ), + ); + } + } + + /** + * @return iterable + */ + public function getColors(): iterable + { + if (false === property_exists($this->configuration, 'colors')) { + return; + } + + $colors = $this->configuration->colors; + + if (false === \is_object($colors)) { + throw new AtPathException( + sprintf('.reports.%s.colors', $this->reportName), + 'non empty object is expected', + ); + } + + $empty = true; + + foreach (get_object_vars($colors) as $name => $value) { + $empty = false; + + if ('' === $name) { + throw new AtPathException( + sprintf('.reports.%s.colors', $this->reportName), + 'property name must be non-empty string', + ); + } + + if (false === \is_string($value) || '' === $value) { + throw new AtPathException( + sprintf('.reports.%s.colors.%s', $this->reportName, $name), + 'color must be non-empty string', + ); + } + + yield $name => $value; + } + + if ($empty) { + throw new AtPathException( + sprintf('.reports.%s.colors', $this->reportName), + 'non empty object is expected', + ); + } + } + + /** + * @return iterable + */ + public function getExtra(): iterable + { + if (false === property_exists($this->configuration, 'extra')) { + return; + } + + $extra = $this->configuration->extra; + + if (false === \is_object($extra)) { + throw new AtPathException( + sprintf('.reports.%s.extra', $this->reportName), + 'non empty object is expected', + ); + } + + $empty = true; + + foreach (get_object_vars($extra) as $name => $value) { + $empty = false; + + if ('' === $name) { + throw new AtPathException( + sprintf('.reports.%s.extra', $this->reportName), + 'property name must be non-empty string', + ); + } + + if (false === \is_string($value)) { + throw new AtPathException( + sprintf('.reports.%s.extra.%s', $this->reportName, $name), + 'must be a string', + ); + } + + $enum = Extra::tryFrom($value); + + if (null === $enum) { + throw new AtPathException( + sprintf('.reports.%s.extra.%s', $this->reportName, $name), + sprintf( + 'extra "%s" does not exists, must be %s', + $value, + implode( + ' or ', + array_map( + static fn (Extra $extra) => '"' . + $extra->value . + '"', + Extra::cases(), + ), + ), + ), + ); + } + + yield $name => $enum; + } + + if ($empty) { + throw new AtPathException( + sprintf('.reports.%s.extra', $this->reportName), + 'non empty object is expected', + ); + } + } +} diff --git a/src/K_pi/Container.php b/src/K_pi/Container.php new file mode 100644 index 0000000..42ffc7c --- /dev/null +++ b/src/K_pi/Container.php @@ -0,0 +1,71 @@ + + */ + private array $definitions; + + /** + * @var array + */ + private array $services; + + public function __construct() + { + $this->definitions = []; + $this->services = []; + } + + /** + * @template T of object + * + * @param class-string $service + * @param callable(Container $container): T $definition + */ + public function define(string $service, callable $definition): void + { + if (\array_key_exists($service, $this->services)) { + throw new Exception("Service {$service} is already built."); + } + + $this->definitions[$service] = $definition; + } + + /** + * @template T of object + * + * @param class-string $service + * + * @return T + */ + public function get(string $service): object + { + if (\array_key_exists($service, $this->services)) { + /** + * @var T + */ + return $this->services[$service]; + } + + if (\array_key_exists($service, $this->definitions)) { + /** + * @var callable(Container $container): T + */ + $definition = $this->definitions[$service]; + + unset($this->definitions[$service]); + + return $this->services[$service] = $definition($this); + } + + throw new Exception("Service {$service} not found."); + } +} diff --git a/src/K_pi/Container/Definitions.php b/src/K_pi/Container/Definitions.php new file mode 100644 index 0000000..629a1f5 --- /dev/null +++ b/src/K_pi/Container/Definitions.php @@ -0,0 +1,196 @@ +define( + K_pi\EnvVars::class, + static fn () => new K_pi\EnvVars(), + ); + + $container->define( + K_pi\Storage\Integrations::class, + static fn ( + K_pi\Container $container, + ) => new K_pi\Storage\Integrations( + $container->get( + K_pi\Integration\Github\Discussion\Storage\Factory::class, + ), + ), + ); + + $container->define( + K_pi\CheckReporter\Integrations::class, + static fn ( + K_pi\Container $container, + ) => new K_pi\CheckReporter\Integrations( + $container->get( + K_pi\Integration\Github\CheckRun\CheckReporter\Factory::class, + ), + $container->get( + K_pi\Integration\Github\Status\CheckReporter\Factory::class, + ), + ), + ); + + self::buildCLI($container); + self::buildExtractor($container); + self::buildGithub($container); + + return $container; + } + + private static function buildCLI(K_pi\Container $container): void + { + $container->define( + Symfony\Component\Console\Application::class, + static function (K_pi\Container $container) { + $application = new Symfony\Component\Console\Application(); + $application->addCommands([ + $container->get(K_pi\Command\CompileCommand::class), + $container->get(K_pi\Command\CheckCommand::class), + ]); + + return $application; + }, + ); + + $container->define( + K_pi\Command\CompileCommand::class, + static fn ( + K_pi\Container $container, + ) => new K_pi\Command\CompileCommand( + $container->get(K_pi\Configuration\Extractor::class), + $container->get(K_pi\Storage\Integrations::class), + ), + ); + + $container->define( + K_pi\Command\CheckCommand::class, + static fn ( + K_pi\Container $container, + ) => new K_pi\Command\CheckCommand( + $container->get(K_pi\CheckReporter\Integrations::class), + $container->get(K_pi\Configuration\Extractor::class), + $container->get(K_pi\Storage\Integrations::class), + ), + ); + } + + private static function buildGithub(K_pi\Container $container): void + { + $container->define( + K_pi\Integration\Github\Variables::class, + static fn ( + K_pi\Container $container, + ) => new K_pi\Integration\Github\Variables( + $container->get(K_pi\EnvVars::class), + ), + ); + + $container->define(Github\Client::class, static function ( + K_pi\Container $container, + ) { + $client = new Github\Client(); + $client->authenticate( + $container + ->get(K_pi\Integration\Github\Variables::class) + ->getToken(), + Github\Client::AUTH_ACCESS_TOKEN, + ); + + return $client; + }); + + $container->define( + K_pi\Libs\KnpGithubApi\Github::class, + static fn ( + K_pi\Container $container, + ) => new K_pi\Libs\KnpGithubApi\Github( + $container->get(Github\Client::class), + ), + ); + + $container->define( + K_pi\Libs\Lazy\Github::class, + static fn (K_pi\Container $container) => new K_pi\Libs\Lazy\Github( + static fn () => $container->get( + K_pi\Libs\KnpGithubApi\Github::class, + ), + ), + ); + + $container->define( + K_pi\Integration\Github::class, + static fn (K_pi\Container $container) => $container->get( + K_pi\Libs\Lazy\Github::class, + ), + ); + + $container->define( + K_pi\Integration\Github\Discussion\Storage\Factory::class, + static fn ( + K_pi\Container $container, + ) => new K_pi\Integration\Github\Discussion\Storage\Factory( + $container->get(K_pi\Integration\Github::class), + ), + ); + + $container->define( + K_pi\Integration\Github\CheckRun\CheckReporter\Factory::class, + static fn ( + K_pi\Container $container, + ) => new K_pi\Integration\Github\CheckRun\CheckReporter\Factory( + $container->get(K_pi\Integration\Github::class), + $container->get(K_pi\Integration\Github\Variables::class), + ), + ); + + $container->define( + K_pi\Integration\Github\Status\CheckReporter\Factory::class, + static fn ( + K_pi\Container $container, + ) => new K_pi\Integration\Github\Status\CheckReporter\Factory( + $container->get(K_pi\Integration\Github::class), + $container->get(K_pi\Integration\Github\Variables::class), + ), + ); + } + + private static function buildExtractor(K_pi\Container $container): void + { + $container->define( + K_pi\Configuration\Extractor::class, + static fn (K_pi\Container $container) => $container->get( + K_pi\Configuration\Extractor\StrategyExtractor::class, + ), + ); + + $container->define( + K_pi\Configuration\Extractor\StrategyExtractor::class, + static fn ( + K_pi\Container $container, + ) => new K_pi\Configuration\Extractor\StrategyExtractor( + $container->get( + K_pi\Configuration\Extractor\YamlFileExtractor::class, + ), + ), + ); + + $container->define( + K_pi\Configuration\Extractor\YamlFileExtractor::class, + static fn () => new K_pi\Configuration\Extractor\YamlFileExtractor(), + ); + } +} diff --git a/src/K_pi/Data/CheckReporterIntegration.php b/src/K_pi/Data/CheckReporterIntegration.php new file mode 100644 index 0000000..3e7223b --- /dev/null +++ b/src/K_pi/Data/CheckReporterIntegration.php @@ -0,0 +1,12 @@ + $precision + */ + public function __construct( + public readonly string $name, + public readonly float|int $from, + public readonly float|int $to, + int $precision, + ) { + $this->diff = ValueNormalizer::normalize($to - $from, $precision); + $this->changed = 0 !== $this->diff; + } +} diff --git a/src/K_pi/Data/Extra.php b/src/K_pi/Data/Extra.php new file mode 100644 index 0000000..ac9b7b9 --- /dev/null +++ b/src/K_pi/Data/Extra.php @@ -0,0 +1,10 @@ +owner = $owner; + $this->repository = $repository; + $this->type = $type; + $number = (int) $number; + + if (0 >= $number) { + throw new InvalidArgumentException('Invalid Github resource url.'); + } + + $this->number = $number; + } +} diff --git a/src/K_pi/Data/Github/StatusState.php b/src/K_pi/Data/Github/StatusState.php new file mode 100644 index 0000000..28fb2d2 --- /dev/null +++ b/src/K_pi/Data/Github/StatusState.php @@ -0,0 +1,13 @@ +> + */ +final class Report implements IteratorAggregate +{ + /** + * @var array> + */ + private array $datasets = []; + + /** + * @var array + */ + private array $colors = []; + + public function getIterator(): Traversable + { + yield from $this->datasets; + } + + /** + * @param non-empty-string $name + */ + public function add( + string $name, + DateTimeImmutable $date, + float|int $value, + ): void { + $index = $date->format('Y-m-d'); + + $this->datasets[$name][$index] = $value; + + ksort($this->datasets[$name]); + + $current = null; + + foreach ($this->datasets[$name] as $date => $value) { + if ($value === $current) { + $dataset = $this->datasets[$name]; + + unset($dataset[$date]); + + if ([] === $dataset) { + unset($this->datasets[$name]); + } else { + $this->datasets[$name] = $dataset; + } + } + + $current = $value; + } + } + + /** + * @param non-empty-string $name + * @param non-empty-string $color + */ + public function colorize(string $name, string $color): void + { + $this->colors[$name] = $color; + } + + /** + * @param non-empty-string $name + * + * @return ?non-empty-string + */ + public function getColor(string $name): ?string + { + return $this->colors[$name] ?? null; + } + + /** + * @param non-empty-string $name + * + * @return null|float|int + */ + public function last(string $name): mixed + { + if (false === \array_key_exists($name, $this->datasets)) { + return null; + } + + return end($this->datasets[$name]); + } + + /** + * @return array + */ + public function getExtra(Extra $extra): array + { + return match ($extra) { + Extra::TOTAL => $this->total(), + }; + } + + /** + * @return array + */ + private function total(): array + { + $totals = []; + + foreach ($this->datasets as $name => $dataset) { + foreach ($dataset as $date => $value) { + $totals[$date][$name] = $value; + } + } + + ksort($totals); + + $current = []; + + foreach ($totals as $date => $total) { + $current = $totals[$date] = array_merge($current, $total); + } + + return array_map(array_sum(...), $totals); + } +} diff --git a/src/K_pi/Data/StorageIntegration.php b/src/K_pi/Data/StorageIntegration.php new file mode 100644 index 0000000..5d883a3 --- /dev/null +++ b/src/K_pi/Data/StorageIntegration.php @@ -0,0 +1,10 @@ + + */ + private array $defaults = []; + + /** + * @param non-empty-string $name + */ + public function get(string $name): string + { + $env = $this->read($name); + + if (null === $env) { + throw new Exception("{$name} env variable not found."); + } + + return $env; + } + + /** + * @param non-empty-string $name + */ + public function has(string $name): bool + { + return null !== $this->read($name); + } + + /** + * @param non-empty-string $name + */ + public function default(string $name, string $env): void + { + $this->defaults[$name] = $env; + } + + /** + * @param non-empty-string $name + */ + private function read(string $name): ?string + { + if (\is_string($env = getenv($name))) { + return $env; + } + + if (\array_key_exists($name, $this->defaults)) { + return $this->defaults[$name]; + } + + return null; + } +} diff --git a/src/K_pi/Integration/Github.php b/src/K_pi/Integration/Github.php new file mode 100644 index 0000000..97d77e5 --- /dev/null +++ b/src/K_pi/Integration/Github.php @@ -0,0 +1,46 @@ +variables->getPullRequestUrl(); + + foreach ($notifications as $notification) { + $this->github->createCheckRun( + owner: $pullRequest->owner, + repository: $pullRequest->repository, + pullRequest: $pullRequest->number, + checkName: "C'est ouf", + ); + } + } +} diff --git a/src/K_pi/Integration/Github/CheckRun/CheckReporter/Factory.php b/src/K_pi/Integration/Github/CheckRun/CheckReporter/Factory.php new file mode 100644 index 0000000..740ab61 --- /dev/null +++ b/src/K_pi/Integration/Github/CheckRun/CheckReporter/Factory.php @@ -0,0 +1,25 @@ +github, $this->variables); + } +} diff --git a/src/K_pi/Integration/Github/Discussion/Storage.php b/src/K_pi/Integration/Github/Discussion/Storage.php new file mode 100644 index 0000000..5db64b7 --- /dev/null +++ b/src/K_pi/Integration/Github/Discussion/Storage.php @@ -0,0 +1,334 @@ + + * } + * > + */ +final class Storage implements StorageInterface +{ + public function __construct( + private readonly Configuration $configuration, + private readonly Github $github, + ) {} + + public function read(): Report + { + if (false === $this->configuration->persist) { + return new Report(); + } + + $discussion = $this->github->readDiscussion( + owner: $this->configuration->discussion->owner, + repository: $this->configuration->discussion->repository, + number: $this->configuration->discussion->number, + ); + + return $this->contentToReport($discussion['body']); + } + + public function write( + Report $report, + ReportConfiguration $configuration, + ): void { + $discussion = $this->github->readDiscussion( + owner: $this->configuration->discussion->owner, + repository: $this->configuration->discussion->repository, + number: $this->configuration->discussion->number, + ); + + $this->github->writeDiscussion( + id: $discussion['id'], + body: $this->reportToContent($report, $configuration), + ); + } + + private function contentToReport(string $content): Report + { + $inJson = false; + $extract = []; + + foreach (explode("\n", $content) as $line) { + if ('```json K-pi' === trim($line, " \n\r")) { + $inJson = true; + + continue; + } + + if ('```' === trim($line, " \n\r")) { + $inJson = false; + + continue; + } + + if ($inJson) { + $extract[] = $line; + } + } + + if ([] === $extract) { + return new Report(); + } + + try { + $export = json_decode( + implode("\n", $extract), + true, + flags: JSON_THROW_ON_ERROR, + ); + + if (false === \is_array($export)) { + return new Report(); + } + + $data = []; + + foreach ($export as $name => $datasetAndColor) { + if (false === \is_string($name)) { + continue; + } + + if ('' === $name) { + continue; + } + + if (false === \is_array($datasetAndColor)) { + continue; + } + + $dataset = $datasetAndColor['dataset'] ?? []; + + if (false === \is_array($dataset) || [] === $dataset) { + continue; + } + + foreach ($dataset as $date => $value) { + if ( + false === \is_int($value) + && false === \is_float($value) + ) { + continue; + } + + if (false === \is_string($date) || '' === $date) { + continue; + } + + $data[$name]['dataset'][$date] = $value; + $data[$name]['color'] = $datasetAndColor['color'] ?? null; + } + } + + return $this->dataToReport($data); + } catch (Exception) { + return new Report(); + } + } + + private function reportToContent( + Report $report, + ReportConfiguration $configuration, + ): string { + $data = null; + $chart = null; + + if ($this->configuration->persist) { + $data = $this->reportToData($report); + } + + if ($this->configuration->report) { + $chart = $this->reportToChart($report, $configuration); + } + + if (null === $data && null !== $chart) { + $template = <<<'MKD' + ![](%s) + MKD; + + return sprintf($template, $chart->getShortUrl()); + } + + if (null !== $data && null === $chart) { + $template = <<<'MKD' + ```json K-pi + %s + ``` + MKD; + + return sprintf( + $template, + json_encode( + $data, + JSON_PRETTY_PRINT | + JSON_UNESCAPED_SLASHES | + JSON_THROW_ON_ERROR, + ), + ); + } + + if (null !== $data && null !== $chart) { + $template = <<<'MKD' + ![](%s) + +
+ Data (do not edit) + + ```json K-pi + %s + ``` +
+ MKD; + + return sprintf( + $template, + $chart->getShortUrl(), + json_encode( + $data, + JSON_PRETTY_PRINT | + JSON_UNESCAPED_SLASHES | + JSON_THROW_ON_ERROR, + ), + ); + } + + return ''; + } + + /** + * @param Data $data + */ + private function dataToReport(array $data): Report + { + $report = new Report(); + + foreach ($data as $name => $datasetAndColor) { + $color = $datasetAndColor['color']; + $dataset = $datasetAndColor['dataset']; + + if (null !== $color) { + $report->colorize($name, $color); + } + + foreach ($dataset as $date => $value) { + $report->add($name, new DateTimeImmutable($date), $value); + } + } + + return $report; + } + + /** + * @return Data + */ + private function reportToData(Report $report): array + { + $data = []; + + foreach ($report as $name => $dataset) { + $data[$name] = [ + 'color' => $report->getColor($name), + 'dataset' => $dataset, + ]; + } + + return $data; + } + + private function reportToChart( + Report $report, + ReportConfiguration $configuration, + ): QuickChart { + $now = (new DateTimeImmutable( + timezone: new DateTimeZone('Europe/Paris'), + ))->format('Y-m-d'); + + $config = [ + 'type' => 'line', + 'options' => [ + 'scales' => [ + 'xAxes' => [ + [ + 'type' => 'time', + 'time' => [ + 'parser' => 'YYYY/MM/DD', + ], + ], + ], + 'yAxes' => [ + [ + 'ticks' => [ + 'min' => 0, + ], + ], + ], + ], + ], + 'data' => [ + 'datasets' => [], + ], + ]; + + $datasets = [...$report]; + + foreach ($configuration->getExtra() as $name => $extra) { + $dataset = $report->getExtra($extra); + + $datasets = [ + $name => $dataset, + ...$datasets, + ]; + } + + foreach ($datasets as $name => $dataset) { + if (false !== ($value = end($dataset))) { + $dataset = array_merge($dataset, [$now => $value], $dataset); + } + + $color = $report->getColor($name); + + $config['data']['datasets'][] = array_merge( + [ + 'label' => $name, + 'fill' => false, + 'data' => array_map( + static fn (string $date, float|int $value) => [ + 'x' => $date, + 'y' => $value, + ], + array_keys($dataset), + array_values($dataset), + ), + ], + null === $color ? [] : ['borderColor' => $color], + ); + } + + $chart = new QuickChart([ + 'width' => 870, + 'height' => 600, + 'version' => '2', + ]); + + $chart->setConfig($config); + + return $chart; + } +} diff --git a/src/K_pi/Integration/Github/Discussion/Storage/Configuration.php b/src/K_pi/Integration/Github/Discussion/Storage/Configuration.php new file mode 100644 index 0000000..41db3ec --- /dev/null +++ b/src/K_pi/Integration/Github/Discussion/Storage/Configuration.php @@ -0,0 +1,99 @@ +reportName, + StorageIntegration::GITHUB_DISCUSSION->value, + ), + 'object is expected.', + ); + } + + $this->discussion = $this->parseUrl($configuration); + $this->report = $this->parseBool($configuration, 'report', true); + $this->persist = $this->parseBool($configuration, 'persist', true); + } + + private function parseUrl(object $configuration): ResourceUrl + { + if (false === property_exists($configuration, 'url')) { + throw new AtPathException( + sprintf( + '.reports.%s.storage.%s', + $this->reportName, + StorageIntegration::GITHUB_DISCUSSION->value, + ), + 'property "url" is mandatory.', + ); + } + + try { + $url = new ResourceUrl($configuration->url); + + if ('discussions' !== $url->type) { + throw new Exception('invalid Github discussion url'); + } + } catch (Throwable $previous) { + throw AtPathException::buildFromThrowable( + sprintf( + '.reports.%s.storage.%s.url', + $this->reportName, + StorageIntegration::GITHUB_DISCUSSION->value, + ), + $previous, + ); + } + + return $url; + } + + private function parseBool( + object $configuration, + string $key, + bool $default, + ): bool { + if (false === property_exists($configuration, $key)) { + return $default; + } + + $bool = $configuration->{$key}; + + if (false === \is_bool($bool)) { + throw new AtPathException( + sprintf( + '.reports.%s.storage.%s.%s', + $this->reportName, + StorageIntegration::GITHUB_DISCUSSION->value, + $key, + ), + 'must be a boolean.', + ); + } + + return $bool; + } +} diff --git a/src/K_pi/Integration/Github/Discussion/Storage/Factory.php b/src/K_pi/Integration/Github/Discussion/Storage/Factory.php new file mode 100644 index 0000000..c804620 --- /dev/null +++ b/src/K_pi/Integration/Github/Discussion/Storage/Factory.php @@ -0,0 +1,22 @@ +github, + ); + } +} diff --git a/src/K_pi/Integration/Github/Status/CheckReporter.php b/src/K_pi/Integration/Github/Status/CheckReporter.php new file mode 100644 index 0000000..b9cf279 --- /dev/null +++ b/src/K_pi/Integration/Github/Status/CheckReporter.php @@ -0,0 +1,101 @@ +variables->getPullRequestUrl(); + + foreach ($notifications as $notification) { + if (false === $notification->changed) { + continue; + } + + $description = sprintf( + '%s%s (%s %s%s%s)', + $notification->to, + $this->getUnit($notification->to), + $this->getIndicator($notification->diff), + $this->getSign($notification->diff), + $notification->diff, + $this->getUnit($notification->diff), + ); + + $this->github->createStatus( + owner: $pullRequest->owner, + repository: $pullRequest->repository, + pullRequest: $pullRequest->number, + state: $this->getState($notification->diff), + context: $this->configuration->reportName . + ': ' . + $notification->name, + description: $description, + ); + } + } + + private function getState(float|int $value): StatusState + { + if ($value >= 0) { + return $this->configuration->onHigher; + } + + return $this->configuration->onLower; + } + + private function getIndicator(float|int $value): ?string + { + if ($value > 0) { + return '⬈'; + } + + if ($value < 0) { + return '⬊'; + } + + return null; + } + + private function getUnit(float|int $value): ?string + { + if ($value >= 2) { + return $this->configuration->pluralUnit; + } + + if ($value <= -2) { + return $this->configuration->pluralUnit; + } + + return $this->configuration->singularUnit; + } + + private function getSign(float|int $value): ?string + { + if ($value >= 0) { + return '+'; + } + + return null; + } +} diff --git a/src/K_pi/Integration/Github/Status/CheckReporter/Configuration.php b/src/K_pi/Integration/Github/Status/CheckReporter/Configuration.php new file mode 100644 index 0000000..d1799be --- /dev/null +++ b/src/K_pi/Integration/Github/Status/CheckReporter/Configuration.php @@ -0,0 +1,191 @@ + [StatusState::ERROR, StatusState::SUCCESS], + 'lower-is-better' => [StatusState::SUCCESS, StatusState::ERROR], + ]; + + public readonly ?string $singularUnit; + + public readonly ?string $pluralUnit; + + public readonly StatusState $onLower; + + public readonly StatusState $onHigher; + + /** + * @param non-empty-string $reportName + */ + public function __construct( + mixed $configuration, + public readonly string $reportName, + ) { + if (null === $configuration) { + $configuration = new stdClass(); + } + + if (false === \is_object($configuration)) { + throw new AtPathException( + sprintf( + '.reports.%s.check-reporter.%s', + $this->reportName, + CheckReporterIntegration::GITHUB_STATUS->value, + ), + 'must be null or an object', + ); + } + + [$this->singularUnit, $this->pluralUnit] = $this->getUnits( + $configuration, + ); + [$this->onLower, $this->onHigher] = $this->getStates($configuration); + } + + /** + * @return array{null, null}|array{string, string} + */ + private function getUnits(object $configuration): array + { + if (false === property_exists($configuration, 'unit')) { + return [null, null]; + } + + $unit = $configuration->unit; + + if (\is_string($unit)) { + return [$unit, $unit]; + } + + if ( + false === property_exists($unit, 'singular') + || false === property_exists($unit, 'plural') + ) { + throw new AtPathException( + sprintf( + '.reports.%s.check-reporter.%s.unit', + $this->reportName, + CheckReporterIntegration::GITHUB_STATUS->value, + ), + 'must be a string or an object with "singular" and "plural" properties', + ); + } + + $singular = $unit->singular; + + if (false === \is_string($singular)) { + throw new AtPathException( + sprintf( + '.reports.%s.check-reporter.%s.unit.singular', + $this->reportName, + CheckReporterIntegration::GITHUB_STATUS->value, + ), + 'must be a string', + ); + } + + $plural = $unit->plural; + + if (false === \is_string($plural)) { + throw new AtPathException( + sprintf( + '.reports.%s.check-reporter.%s.unit.plural', + $this->reportName, + CheckReporterIntegration::GITHUB_STATUS->value, + ), + 'must be a string', + ); + } + + return [$singular, $plural]; + } + + /** + * @return array{StatusState, StatusState} + */ + private function getStates(object $configuration): array + { + if (false === property_exists($configuration, 'states')) { + return [StatusState::SUCCESS, StatusState::SUCCESS]; + } + + $states = $configuration->states; + + $path = sprintf( + '.reports.%s.check-reporter.%s.states', + $this->reportName, + CheckReporterIntegration::GITHUB_STATUS->value, + ); + $message = sprintf( + 'must be %s or an object with "on-lower" and "on-higher" properties', + implode( + ' or ', + array_map( + static fn (string $key) => '"' . $key . '"', + array_keys(self::PREBUILD_STATES), + ), + ), + ); + + if (\is_string($states)) { + if (\array_key_exists($states, self::PREBUILD_STATES)) { + return self::PREBUILD_STATES[$states]; + } + + throw new AtPathException($path, $message); + } + + if (false === \is_object($states)) { + throw new AtPathException($path, $message); + } + + $resolve = static function (string $property) use ( + $states, + $path, + $message + ): StatusState { + $properties = get_object_vars($states); + + if (false === \array_key_exists($property, $properties)) { + throw new AtPathException($path, $message); + } + + $state = $properties[$property]; + + if ( + false === \is_string($state) + || null === ($enum = StatusState::tryFrom($state)) + ) { + throw new AtPathException( + $path . '.' . $property, + sprintf( + 'must be %s', + implode( + ' or ', + array_map( + static fn (StatusState $enum) => '"' . + $enum->value . + '"', + StatusState::cases(), + ), + ), + ), + ); + } + + return $enum; + }; + + return [$resolve('on-lower'), $resolve('on-higher')]; + } +} diff --git a/src/K_pi/Integration/Github/Status/CheckReporter/Factory.php b/src/K_pi/Integration/Github/Status/CheckReporter/Factory.php new file mode 100644 index 0000000..3621a1e --- /dev/null +++ b/src/K_pi/Integration/Github/Status/CheckReporter/Factory.php @@ -0,0 +1,29 @@ +github, + $this->variables, + new Configuration($configuration, $reportName), + ); + } +} diff --git a/src/K_pi/Integration/Github/Variables.php b/src/K_pi/Integration/Github/Variables.php new file mode 100644 index 0000000..f604e56 --- /dev/null +++ b/src/K_pi/Integration/Github/Variables.php @@ -0,0 +1,66 @@ +oneOf(self::VARS_GITHUB_TOKEN); + } + + /** + * @throw InvalidArgumentException|Exception + */ + public function getPullRequestUrl(): ResourceUrl + { + $pullRequestUrl = new ResourceUrl( + $this->oneOf(self::VARS_PULL_REQUEST), + ); + + if ('pull' !== $pullRequestUrl->type) { + throw new Exception('Not a pull-request url'); + } + + return $pullRequestUrl; + } + + /** + * @param non-empty-array $variables + */ + private function oneOf(array $variables): string + { + foreach ($variables as $variable) { + if (false === $this->envVars->has($variable)) { + continue; + } + + return $this->envVars->get($variable); + } + + throw new Exception( + sprintf( + 'None of the following environment variables were found: %s', + implode(',', $variables), + ), + ); + } +} diff --git a/src/K_pi/Libs/KnpGithubApi/Github.php b/src/K_pi/Libs/KnpGithubApi/Github.php new file mode 100644 index 0000000..1a85b2e --- /dev/null +++ b/src/K_pi/Libs/KnpGithubApi/Github.php @@ -0,0 +1,202 @@ +github->graphql()->execute( + <<<'GRAPHQL' + query($owner: String!, $repository: String!, $number: Int!) { + repository(owner: $owner, name: $repository) { + discussion(number: $number) { + id + body + } + } + } + GRAPHQL + , + [ + 'owner' => $owner, + 'repository' => $repository, + 'number' => $number, + ], + ); + + if (isset($response['errors'])) { + throw new Exception( + json_encode($response['errors'], JSON_THROW_ON_ERROR), + ); + } + + $id = $response['data']['repository']['discussion']['id'] ?? null; + + if (false === \is_string($id)) { + throw new Exception( + 'Unable to get discussion ID from ' . + json_encode($response, JSON_THROW_ON_ERROR), + ); + } + + $body = $response['data']['repository']['discussion']['body'] ?? null; + + if (false === \is_string($body)) { + throw new Exception( + 'Unable to get discussion body from ' . + json_encode($response, JSON_THROW_ON_ERROR), + ); + } + + return ['id' => $id, 'body' => $body]; + } + + public function writeDiscussion(string $id, string $body): void + { + $response = $this->github->graphql()->execute( + <<<'GRAPHQL' + mutation($id: ID!, $body: String!) { + updateDiscussion(input: {discussionId: $id, body: $body}) { + clientMutationId + } + } + GRAPHQL + , + [ + 'id' => $id, + 'body' => $body, + ], + ); + + if (isset($response['errors'])) { + throw new Exception( + json_encode($response['errors'], JSON_THROW_ON_ERROR), + ); + } + } + + public function createCheckRun( + string $owner, + string $repository, + int $pullRequest, + string $checkName, + ): void { + $response = $this->github->graphql()->execute( + <<<'GRAPHQL' + query($owner: String!, $repository: String!, $pullRequest: Int!) { + repository(owner: $owner, name: $repository) { + id + pullRequest(number: $pullRequest) { + headRefOid + } + } + } + GRAPHQL + , + [ + 'owner' => $owner, + 'repository' => $repository, + 'pullRequest' => $pullRequest, + ], + ); + + $repositoryId = $response['data']['repository']['id'] ?? null; + + if (false === \is_string($repositoryId)) { + throw new Exception( + 'Unable to get repository ID from ' . json_encode($response), + ); + } + + $commitHash = $response['data']['repository']['pullRequest']['headRefOid'] ?? + null; + + if (false === \is_string($commitHash)) { + throw new Exception( + 'Unable to get pull-request commit hash from ' . + json_encode($response), + ); + } + + $this->github->graphql()->execute( + <<<'GRAPHQL' + mutation ($repositoryId: ID!, $checkName: String!, $commitHash: GitObjectID!, $output: CheckRunOutput!) { + createCheckRun(input: {repositoryId: $repositoryId, name: $checkName, headSha: $commitHash, status: COMPLETED, conclusion: NEUTRAL, output: $output}) { + clientMutationId + } + } + GRAPHQL + , + [ + 'repositoryId' => $repositoryId, + 'commitHash' => $commitHash, + 'checkName' => $checkName, + 'output' => [ + 'title' => 'I need some burger', + 'summary' => 'we are living in a yellow summary', + ], + ], + ); + } + + public function createStatus( + string $owner, + string $repository, + int $pullRequest, + StatusState $state, + string $context, + string $description, + ): void { + $response = $this->github->graphql()->execute( + <<<'GRAPHQL' + query($owner: String!, $repository: String!, $pullRequest: Int!) { + repository(owner: $owner, name: $repository) { + pullRequest(number: $pullRequest) { + headRefOid + } + } + } + GRAPHQL + , + [ + 'owner' => $owner, + 'repository' => $repository, + 'pullRequest' => $pullRequest, + ], + ); + + $commitHash = $response['data']['repository']['pullRequest']['headRefOid'] ?? + null; + + if (false === \is_string($commitHash)) { + throw new Exception( + 'Unable to get pull-request commit hash from ' . + json_encode($response), + ); + } + + $this->github->repository()->statuses()->create( + username: $owner, + repository: $repository, + sha: $commitHash, + params: [ + 'context' => $context, + 'description' => $description, + 'state' => $state->value, + ], + ); + } +} diff --git a/src/K_pi/Libs/Lazy.php b/src/K_pi/Libs/Lazy.php new file mode 100644 index 0000000..313d5f4 --- /dev/null +++ b/src/K_pi/Libs/Lazy.php @@ -0,0 +1,41 @@ +loader = $loader; + } + + /** + * @return T + */ + protected function load(): object + { + if (false === isset($this->loaded)) { + $this->loaded = ($this->loader)(); + } + + return $this->loaded; + } +} diff --git a/src/K_pi/Libs/Lazy/Github.php b/src/K_pi/Libs/Lazy/Github.php new file mode 100644 index 0000000..dc588a5 --- /dev/null +++ b/src/K_pi/Libs/Lazy/Github.php @@ -0,0 +1,60 @@ + + */ +final class Github extends Lazy implements K_pi\Integration\Github +{ + public function readDiscussion( + string $owner, + string $repository, + int $number, + ): array { + return $this->load()->readDiscussion($owner, $repository, $number); + } + + public function writeDiscussion(string $id, string $body): void + { + $this->load()->writeDiscussion($id, $body); + } + + public function createCheckRun( + string $owner, + string $repository, + int $pullRequest, + string $checkName, + ): void { + $this->load()->createCheckRun( + $owner, + $repository, + $pullRequest, + $checkName, + ); + } + + public function createStatus( + string $owner, + string $repository, + int $pullRequest, + StatusState $state, + string $context, + string $description, + ): void { + $this->load()->createStatus( + $owner, + $repository, + $pullRequest, + $state, + $context, + $description, + ); + } +} diff --git a/src/K_pi/Storage.php b/src/K_pi/Storage.php new file mode 100644 index 0000000..fbcad4c --- /dev/null +++ b/src/K_pi/Storage.php @@ -0,0 +1,18 @@ + $this->githubDiscussion, + }; + } +} diff --git a/src/K_pi/ValueNormalizer.php b/src/K_pi/ValueNormalizer.php new file mode 100644 index 0000000..7f4689a --- /dev/null +++ b/src/K_pi/ValueNormalizer.php @@ -0,0 +1,25 @@ +