diff --git a/.env.dist b/.env.dist new file mode 100644 index 00000000..e80ae27f --- /dev/null +++ b/.env.dist @@ -0,0 +1,14 @@ +APP_ENV=develope +USERMAP_UID=1000 +USERMAP_GID=984 +MYSQL_USER=dev +MYSQL_PASSWORD=dev +MYSQL_ROOT_PASSWORD=root +MYSQL_DATABASE=db +MYSQL_PUBLIC_PORT=3306 +MYSQL_TESTING_PORT=3307 +PHP_VERSION=8.3 +MARIADB_VERSION=latest +APACHE_VERSION=2.4-alpine +HTTP_PORT=80 +SHTTP_PORT=443 diff --git a/.github/actions/setup-php-composer/action.yml b/.github/actions/setup-php-composer/action.yml new file mode 100644 index 00000000..aae13386 --- /dev/null +++ b/.github/actions/setup-php-composer/action.yml @@ -0,0 +1,30 @@ +# .github/actions/setup-php-composer/action.yml +name: 'Setup PHP and Composer' +description: 'Common setup for PHP jobs' +inputs: + php-version: + description: 'PHP Version' + default: '8.4' + install-flags: + description: 'Composer install flags' + default: '--prefer-dist --no-progress' + +runs: + using: "composite" + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ inputs.php-version }} + extensions: pdo, mbstring # hier Standard-Extensions ergänzen + + - name: Cache Composer packages + uses: actions/cache@v5 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-php- + + - name: Install dependencies + shell: bash + run: composer install ${{ inputs.install-flags }} diff --git a/.github/workflows/codestyle-and-unittest.yml b/.github/workflows/codestyle-and-unittest.yml index c6be1757..7ab5f60a 100644 --- a/.github/workflows/codestyle-and-unittest.yml +++ b/.github/workflows/codestyle-and-unittest.yml @@ -4,147 +4,107 @@ on: branches: - '*' - '!master' + - '!develop' paths: - "**.php" - pull_request: + workflow_dispatch: jobs: - phplint: - name: PHP Lint + # 1. Job: Statische Analyse (Lint, Stan, CodeSniffer) + # Diese laufen zusammen, da sie keine Datenbank benötigen. + static-analysis: + name: PHP Quality Checks runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.1.1 - - - name: Validate composer.json and composer.lock - run: composer validate --strict + - uses: actions/checkout@v6.0.2 - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer - name: Run PHP Linter run: composer run-script phplint - phpstan: - name: PHP Stan - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4.1.1 - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - name: Run PHP Stan run: composer run-script phpstan - phpcs: - name: PHP CodeSniffer - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4.1.1 - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - name: Run PHP Codesniffer run: composer run-script phpcs - phpunit: - name: PHP Unit Test - needs: - - phplint - - phpstan - - phpcs + - name: Notify Failure + if: failure() + uses: rjstone/discord-webhook-notify@v1.0.4 + with: + severity: error + details: Static Analysis (Lint/Stan/CS) failed. + webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} + + # 2. Job: Tests (Unit & Functional) + # Dieser Job benötigt die MariaDB Service-Container + tests: + name: PHP Tests + needs: static-analysis runs-on: ubuntu-latest - + services: + database-testing: + image: mariadb:latest + env: + MYSQL_ALLOW_EMPTY_PASSWORD: false + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: db + MYSQL_USER: dev + MYSQL_PASSWORD: dev + ports: + - 3306:3306 + options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 + env: + DB_DATABASE: db + DB_USERNAME: dev + DB_PASSWORD: dev + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + APP_ENV: action steps: - - uses: actions/checkout@v4.1.1 + - uses: actions/checkout@v6.0.2 - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress + - name: Setup PHP and Composer + uses: ./.github/actions/setup-php-composer - name: Run PHP Unit Test run: composer run-script unittest - phpfunctional: - name: PHP Functional Test - needs: - - phplint - - phpstan - - phpcs - runs-on: ubuntu-latest + - name: Run PHP Functional Test + run: composer run-script functionaltest + - name: Notify Failure + if: failure() + uses: rjstone/discord-webhook-notify@v1.0.4 + with: + severity: error + details: Unit or Functional Tests failed. + webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} + + # 3. Job: Abschluss-Benachrichtigung + final-status: + name: Final Status Message + needs: [static-analysis, tests] + if: always() + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.1 - - - name: Install PHP with extensions. - uses: shivammathur/setup-php@v2 + - name: Check Info Success + if: ${{ needs.static-analysis.result == 'success' && needs.tests.result == 'success' }} + uses: rjstone/discord-webhook-notify@v1.0.4 with: - php-version: 8.3 - extensions: pdo, pdo_sqlite - ini-values: date.timezone='UTC' - - - name: Validate composer.json and composer.lock - run: composer validate --strict + severity: info + details: Checks successfully executed on API. + webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 + - name: Check Info Failure + if: ${{ needs.static-analysis.result != 'success' || needs.tests.result != 'success' }} + uses: rjstone/discord-webhook-notify@v1.0.4 with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run PHP Unit Test - run: composer run-script functionaltest + severity: error + details: >- + ${{ format('❌ Incorrect checks during the API check. [View Logs](https://github.com/{0}/actions/runs/{1})', + github.repository, + github.run_id) }} + webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} diff --git a/.github/workflows/deploy_build.yml b/.github/workflows/deploy_build.yml new file mode 100644 index 00000000..1bb88d73 --- /dev/null +++ b/.github/workflows/deploy_build.yml @@ -0,0 +1,121 @@ +name: Deployment on Build +on: + push: + branches: [ "master" ] + workflow_dispatch: + +jobs: + # 1. Statische Analyse (Zusammengefasst, da sie keine DB brauchen) + static-analysis: + name: PHP Quality Checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + + - name: Setup Environment + uses: ./.github/actions/setup-php-composer + + - name: Validate Composer + run: composer validate --strict + + - name: Run Checks + run: | + composer run-script phplint + composer run-script phpstan + composer run-script phpcs + + - name: Notify Failure + if: failure() + uses: rjstone/discord-webhook-notify@v1.0.4 + with: + severity: error + details: Static Analysis (Lint/Stan/CS) failed. + webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} + + # 2. Tests (Unit & Functional getrennt, da Functional DB braucht) + tests: + name: PHP Tests + needs: static-analysis + runs-on: ubuntu-latest + services: + mariadb: + image: mariadb:latest + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: db + MYSQL_USER: dev + MYSQL_PASSWORD: dev + ports: ["3306:3306"] + options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 + env: + DB_DATABASE: db + DB_USERNAME: dev + DB_PASSWORD: dev + DB_HOST: 127.0.0.1 + APP_ENV: action + steps: + - uses: actions/checkout@v6.0.2 + - name: Setup Environment + uses: ./.github/actions/setup-php-composer + + - name: Run Unit Tests + run: composer run-script unittest + + - name: Run Functional Tests + run: composer run-script functionaltest + + - name: Notify Failure + if: failure() + uses: rjstone/discord-webhook-notify@v1.0.4 + with: + severity: error + details: PHP Tests failed. + webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} + + # 3. Deployment + deploy: + name: Deploy to Dev + needs: tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + + - name: Setup Environment (No Dev) + uses: ./.github/actions/setup-php-composer + with: + install-flags: '--prefer-dist --no-progress --no-dev' + + - name: Build OpenAPI + run: composer run-script openapi + + - name: Deploy via rsync + uses: burnett01/rsync-deployments@8.0.3 + with: + switches: -avz --no-o --no-g --no-perms --no-t --omit-dir-times --delete --exclude-from='.rsync-exclude' + remote_path: ${{ secrets.DEPLOY_PATH_API_BUILD }} + remote_host: ${{ secrets.DEPLOY_HOST }} + remote_port: ${{ secrets.DEPLOY_PORT }} + remote_user: ${{ secrets.DEPLOY_USER }} + remote_key: ${{ secrets.DEPLOY_KEY }} + remote_key_pass: ${{ secrets.DEPLOY_KEY_PASS }} + + - name: Clear Cache + uses: appleboy/ssh-action@v1.2.2 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_KEY }} + passphrase: ${{ secrets.DEPLOY_KEY_PASS }} + script: rm -rf ${{ secrets.DEPLOY_PATH_API_BUILD }}/data/cache/* + + - name: Deployment Notification + if: always() + uses: rjstone/discord-webhook-notify@v1.0.4 + with: + severity: ${{ job.status == 'success' && 'info' || 'error' }} + details: >- + ${{ job.status == 'success' + && '[🚀 Successful deployment to build!](https://build.ownhackathon.de/api/docs)' + || format('[❌ Deployment failed! View Logs](https://github.com/{0}/actions/runs/{1})', github.repository, github.run_id) + }} + webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} diff --git a/.github/workflows/deploy_dev.yml b/.github/workflows/deploy_dev.yml new file mode 100644 index 00000000..13b9c356 --- /dev/null +++ b/.github/workflows/deploy_dev.yml @@ -0,0 +1,121 @@ +name: Deployment on Development +on: + push: + branches: [ "develop" ] + workflow_dispatch: + +jobs: + # 1. Statische Analyse (Zusammengefasst, da sie keine DB brauchen) + static-analysis: + name: PHP Quality Checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + + - name: Setup Environment + uses: ./.github/actions/setup-php-composer + + - name: Validate Composer + run: composer validate --strict + + - name: Run Checks + run: | + composer run-script phplint + composer run-script phpstan + composer run-script phpcs + + - name: Notify Failure + if: failure() + uses: rjstone/discord-webhook-notify@v1.0.4 + with: + severity: error + details: Static Analysis (Lint/Stan/CS) failed. + webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} + + # 2. Tests (Unit & Functional getrennt, da Functional DB braucht) + tests: + name: PHP Tests + needs: static-analysis + runs-on: ubuntu-latest + services: + mariadb: + image: mariadb:latest + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: db + MYSQL_USER: dev + MYSQL_PASSWORD: dev + ports: ["3306:3306"] + options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3 + env: + DB_DATABASE: db + DB_USERNAME: dev + DB_PASSWORD: dev + DB_HOST: 127.0.0.1 + APP_ENV: action + steps: + - uses: actions/checkout@v6.0.2 + - name: Setup Environment + uses: ./.github/actions/setup-php-composer + + - name: Run Unit Tests + run: composer run-script unittest + + - name: Run Functional Tests + run: composer run-script functionaltest + + - name: Notify Failure + if: failure() + uses: rjstone/discord-webhook-notify@v1.0.4 + with: + severity: error + details: PHP Tests failed. + webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} + + # 3. Deployment + deploy: + name: Deploy to Dev + needs: tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + + - name: Setup Environment (No Dev) + uses: ./.github/actions/setup-php-composer + with: + install-flags: '--prefer-dist --no-progress --no-dev' + + - name: Build OpenAPI + run: composer run-script openapi + + - name: Deploy via rsync + uses: burnett01/rsync-deployments@8.0.3 + with: + switches: -avz --no-o --no-g --no-perms --no-t --omit-dir-times --delete --exclude-from='.rsync-exclude' + remote_path: ${{ secrets.DEPLOY_PATH_API_DEV }} + remote_host: ${{ secrets.DEPLOY_HOST }} + remote_port: ${{ secrets.DEPLOY_PORT }} + remote_user: ${{ secrets.DEPLOY_USER }} + remote_key: ${{ secrets.DEPLOY_KEY }} + remote_key_pass: ${{ secrets.DEPLOY_KEY_PASS }} + + - name: Clear Cache + uses: appleboy/ssh-action@v1.2.2 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_KEY }} + passphrase: ${{ secrets.DEPLOY_KEY_PASS }} + script: rm -rf ${{ secrets.DEPLOY_PATH_API_DEV }}/data/cache/* + + - name: Deployment Notification + if: always() + uses: rjstone/discord-webhook-notify@v1.0.4 + with: + severity: ${{ job.status == 'success' && 'info' || 'error' }} + details: >- + ${{ job.status == 'success' + && '[🚀 Successful deployment to dev!](https://dev.ownhackathon.de/api/docs)' + || format('[❌ Deployment failed! View Logs](https://github.com/{0}/actions/runs/{1})', github.repository, github.run_id) + }} + webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} diff --git a/.github/workflows/deployment_build.yml b/.github/workflows/deployment_build.yml deleted file mode 100644 index e6f1be5f..00000000 --- a/.github/workflows/deployment_build.yml +++ /dev/null @@ -1,234 +0,0 @@ -name: Development Deployment -on: - push: - branches: [ "master" ] - workflow_dispatch: - -jobs: - - phplint: - name: PHP Lint - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4.1.1 - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run PHP Linter - run: composer run-script phplint - - - name: Test Info - uses: rjstone/discord-webhook-notify@v1 - with: - severity: error - details: Code-Style error. - webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} - if: failure() - - phpstan: - name: PHP Stan - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4.1.1 - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run PHP Stan - run: composer run-script phpstan - - - name: Test Info - uses: rjstone/discord-webhook-notify@v1 - with: - severity: error - details: Code-Style error. - webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} - if: failure() - - phpcs: - name: PHP CodeSniffer - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4.1.1 - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run PHP Codesniffer - run: composer run-script phpcs - - - name: Test Info - uses: rjstone/discord-webhook-notify@v1 - with: - severity: error - details: Code-Style error. - webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} - if: failure() - - phpunit: - name: PHP Unit Test - needs: - - phplint - - phpstan - - phpcs - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4.1.1 - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run PHP Unit Test - run: composer run-script unittest - - - name: Test Info - uses: rjstone/discord-webhook-notify@v1 - with: - severity: error - details: Test error. - webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} - if: failure() - - phpfunctional: - name: PHP Functional Test - needs: - - phplint - - phpstan - - phpcs - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4.1.1 - - - name: Install PHP with extensions. - uses: shivammathur/setup-php@v2 - with: - php-version: 8.3 - extensions: pdo, pdo_sqlite - ini-values: date.timezone='UTC' - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run PHP Unit Test - run: composer run-script functionaltest - - - name: Test Info - uses: rjstone/discord-webhook-notify@v1 - with: - severity: error - details: Test error. - webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} - if: failure() - - deployment_of_hackathon_to_build: - name: Deploy Build - needs: - - phpcs - - phplint - - phpstan - - phpunit - - phpfunctional - runs-on: ubuntu-latest - steps: - - name: Get latest code - uses: actions/checkout@v4.1.1 - - - name: Install Composer Dependencies - run: composer install --prefer-dist --no-progress --no-dev - - - name: Run create openapi Data - run: composer run-script openapi - - - name: rsync deployments - uses: burnett01/rsync-deployments@7.0.1 - with: - switches: -avz --no-o --no-g --no-perms --no-t --omit-dir-times --delete --exclude '*' --include-from='.rsync-include' - path: / - remote_path: ${{ secrets.DEPLOY_PATH_API_BUILD }} - remote_host: ${{ secrets.DEPLOY_HOST }} - remote_port: ${{ secrets.DEPLOY_PORT }} - remote_user: ${{ secrets.DEPLOY_USER }} - remote_key: ${{ secrets.DEPLOY_KEY }} - remote_key_pass: ${{ secrets.DEPLOY_KEY_PASS }} - - - name: Test Info Success - uses: rjstone/discord-webhook-notify@v1 - with: - severity: info - details: Successful deployment on build https://build.hackathon.exdrals.de - webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} - if: success() - - - name: Test Info Failure - uses: rjstone/discord-webhook-notify@v1 - with: - severity: error - details: Failure deployment on build. - webhookUrl: ${{ secrets.WEBHOOK_DISCORD_URL }} - if: failure() diff --git a/.github/workflows/deployment_production.yml b/.github/workflows/deployment_production.yml deleted file mode 100644 index 49208751..00000000 --- a/.github/workflows/deployment_production.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Production Deployment - -on: - release: - types: [ published ] - -jobs: - deployment_of_hackathon_to_production: - env: - VUE_APP_API_BASE_URL: 'https://hackathon.exdrals.de' - runs-on: ubuntu-latest - steps: - - name: Get latest code - uses: actions/checkout@v4.1.1 - - - name: Install Composer Dependencies - run: composer install --prefer-dist --no-progress --no-dev - - - name: Run create openapi Data - run: composer run-script openapi - - - name: FTP Deploy to Production Server (Staging) - uses: SamKirkland/FTP-Deploy-Action@4.3.2 - with: - server: ${{ secrets.PROD_HOST }} - username: ${{ secrets.PROD_USER }} - password: ${{ secrets.PROD_PASSWORD }} - exclude: | - **/.git* - **/*.dist - **/*.dist/** - **/.git*/** - **/bin/** - **/tests/** - **/config/autoload/** - **/config/migrations/** - **/node_modules/** - **/client/** - **/database/** - **/scripts/** - **/tests/** - **/docker/** - **/public/assets/** - **/public/index.html - **/*.md - **/*.xml - **/*.neon - **/*.json - **/*.yml - **/*.lock - **/LICENSE diff --git a/.gitignore b/.gitignore index e3dcfbf5..e3d8f26f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /tests/behat.yml /tests/.phpunit.result.cache /vendor/ +/public/assets/* *.cache *.phar .ddev @@ -9,4 +10,11 @@ /database/structure/update*.sql /public/api/doc/swagger.json coverage.xml -/.env +/.phpcs-cache +/.phpunit.result.cache +/clover.xml +/coveralls-upload.json +/phpunit.xml +/public/test.php +/tmp +.env diff --git a/.laminas-ci/pre-run.sh b/.laminas-ci/pre-run.sh new file mode 100755 index 00000000..8b8528d8 --- /dev/null +++ b/.laminas-ci/pre-run.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Due to the fact that we are disabling plugins when installing/updating/downgrading composer dependencies +# we have to manually enable the coding standard here. +composer enable-codestandard diff --git a/.phplint.yml b/.phplint.yml new file mode 100644 index 00000000..2264a4f4 --- /dev/null +++ b/.phplint.yml @@ -0,0 +1,9 @@ +path: ./ +jobs: 10 +cache: .phplint.cache +extensions: + - php +exclude: + - vendor +warning: true +memory_limit: -1 diff --git a/.rsync-exclude b/.rsync-exclude new file mode 100644 index 00000000..97e349aa --- /dev/null +++ b/.rsync-exclude @@ -0,0 +1,33 @@ +/.github +/.laminas-ci +/bin/ +/config/.gitignore +/config/*.dist +/config/*.tpl +/config/autoload/.gitignore +/config/autoload/*.dist +/config/autoload/*.local.* +/config/autoload/*.testing.* +/config/autoload/*.develope.* +/config/autoload/*.action.* +/config/autoload/migrations.* +/database/ +/data/.gitignore +/data/log/** +/docker/ +/docs/ +/public/.* +/public/*.php +/public/*.html +/public/assets/ +/tests/ +/.* +/*.json +/*.lock +/*.yml +/*.yaml +/*.xml +/*.neon +/*.md +/LICENSE +/README diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index ec2148df..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,35 +0,0 @@ - - -# Changelog for the Hackathon evaluation Project - - - - - - - - - - - - - - - -## Unreleased - - - - - - - - - - - - diff --git a/README.md b/README.md index 0e8b24fc..384ea0da 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,103 @@ -# (Black) Hackathon -Evaluation project for the Hackathon Events on the Discord server from BlackScorp +# ownHackathon +Evaluation Project for Hackathons -## Steps for an executable test environment +## Setup: Executable Test Environment -1. Install `git` and `docker` on your maschine -2. Run `git clone git@github.com:ownHackathon/hackathon-api.git` -3. Optional: Check the configurations in `config/autoload`. In case of changes, copy te file and remove the `.dist` file extension and adjust - the configuration file. -4. Copy `.env.dist` and rename it `.env` and set correct your userid and groupid. You can find them out in the terminal via commands `id -u && id -g` -5. Run `docker-compose up -d` -6. Run `docker-compose exec php composer install` -7. Run `docker-compose exec php composer run doctrine migrations:sync-metadata-storage` -8. Run `docker-compose exec php composer run doctrine migrations:migrate` -9. Run `docker-compose exec php composer run openapi` +Follow these steps to set up the project on your local machine. -Done. You can now open http://localhost/api/doc/ Thanks and have fun. +### 1. Prerequisites +Ensure you have `git` and `docker` (including the Docker Compose plugin) installed. -See docker-compose.yml for existing services +### 2. Clone the Repository +```bash +git clone git@github.com:ownHackathon/hackathon-api.git +cd hackathon-api +``` -# unsupportet Script +### 3. Environment Configuration +Copy the environment template and adjust it: +```bash +cp .env.dist .env +``` +Open the `.env` file and set your `USERMAP_UID` and `USERMAP_GID`. This ensures correct file permissions within the container. You can find your IDs by running: +```bash +id -u && id -g +``` -You will find a script called `hackathon` under `/bin`. This offers possibilities to control the project +### 4. Application Configuration (Optional) +Check the configuration files in `config/autoload/`. If changes are needed, copy the desired `.dist` file, remove the extension, and adjust the settings. -- `./bin/hackathon setup` => start the docker container, run composer install and seed Database Data -- `./bin/hackathon start` => start the docker container -- `./bin/hackathon restart` => restart the docker container -- `./bin/hackathon stop` => stop the docker container -- `./bin/hackathon reset` => clean up Database -- `./bin/hackathon reset vendor` => clean up vendor folder -- `./bin/hackathon reset all` => clean up system completely -- `./bin/hackathon composer` => run composer with own param e.g. `./bin/hackathon composer install` -- `./bin/hackathon php` => run commands in php container e.g. `./bin/hackathon php php -v` +### 5. Quick Setup (Recommended) +We provide a management script to automate the entire process (infrastructure start, dependency installation, database migrations, and documentation generation). +**Once finished, the script automatically displays a table with all service URLs, ports, and database credentials.** + +> [!CAUTION] +> **NOTE:** This script is provided **as-is and unsupported**. Use it at your own risk. + +```bash +# Make the script executable +chmod +x bin/hackathon + +# Run the automated setup +./bin/hackathon setup +``` + +*Alternatively, you can run the commands manually:* +
+Show manual steps + +```bash +docker-compose up -d +docker-compose exec php composer install +docker-compose exec php composer run doctrine migrations:sync-metadata-storage +docker-compose exec php composer run doctrine migrations:migrate +docker-compose exec php composer run openapi +``` +
+ +--- + +## Unsupported Management CLI via `./bin/hackathon` + +The management script consolidates all essential developer commands into a single tool. + +### Usage +Run the script from the project root directory: +```bash +./bin/hackathon [COMMAND] +``` + +#### Infrastructure Commands +* **`start`**: Starts the containers. **Note:** It checks for initialization (vendor/ and volumes) and prevents start if the project isn't set up. +* **`stop`** / **`down`**: Pauses containers or stops/removes containers and networks. +* **`setup`**: Complete initial setup (Start, Install, Migrate, OpenAPI, Info). +* **`services`**: Lists all available service names used in this project. +* **`logs [svc]`**: Tails logs for all or a specific service (e.g., `./bin/hackathon logs php`). +* **`info`**: Displays connectivity info (URLs, Ports, and DB Credentials) for running services. +* **`openapi`**: Regenerates the API documentation. + +#### Cleanup & Reset +* **`clean {docker|app|all}`**: + * `docker`: Removes containers, volumes, and **all** project images. + * `app`: Removes `vendor/` and all cache files (`.phplint`, `.phpunit`, etc.). + * `all`: Performs both docker and app cleanup. +* **`reset {database|vendor|all}`**: + * `database`: Wipes database volumes and re-runs migrations. + * `vendor`: Wipes and reinstalls the `vendor/` folder (Database remains untouched). + * `all`: Wipes database, vendor, and all caches, followed by a fresh `setup`. + +#### Development & Utility Commands +* **`composer [...]`**: Run Composer commands in the PHP container. +* **`php [...]`**: Run PHP commands in the PHP container. +* **`test [...]`**: Shortcut to run PHPUnit tests (passes arguments to PHPUnit). +* **`bash`**: Direct interactive shell access to the PHP container. +* **`mysql`**: Direct access to the MariaDB database console. +* **`indocker [service] [command]`**: Access any specific container (`php`, `apache`, `database`, `database-testing`, `mailhog`). + +--- + +### 💡 Pro-Tip +You can create an alias in your `.bashrc` or `.zshrc` to work even faster: +`alias h='./bin/hackathon'` -> Then simply use `h setup`, `h info`, `h bash` or `h test`. +``` diff --git a/bin/hackathon b/bin/hackathon index 93c6f4cc..523b6ccf 100755 --- a/bin/hackathon +++ b/bin/hackathon @@ -1,99 +1,320 @@ -#!/usr/bin/bash -export APP_ENV=${APP_ENV:-develope} +#!/usr/bin/env bash -function composer_install() { - docker-compose exec php composer install +# --- Configuration --- +CONTAINER_PHP="php" +DOCKER_COMPOSE="docker compose" + +# Get project name (directory name, lowercase) +PROJECT_NAME=$(basename "$PWD" | tr '[:upper:]' '[:lower:]') + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Load environment variables from .env if present +if [ -f .env ]; then + export $(grep -v '^#' .env | xargs) +fi + +# --- Helper Functions --- + +log() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Check if a specific container is running +is_running() { + [ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null)" == "true" ] } -function setup_database() { - docker-compose exec --env APP_ENV php composer run doctrine migrations:sync-metadata-storage - docker-compose exec --env APP_ENV php composer run doctrine migrations:migrate --no-interaction +# List all valid service names defined in this project +list_services() { + log "Available services in this project:" + echo "-----------------------------------------------------------" + echo -e " ${GREEN}php${NC} - PHP Application Container" + echo -e " ${GREEN}apache${NC} - Web Server (Apache)" + echo -e " ${GREEN}database${NC} - MariaDB Main Instance" + echo -e " ${GREEN}database-testing${NC} - MariaDB Testing Instance" + echo -e " ${GREEN}mailhog${NC} - Mail Testing Service" + echo "-----------------------------------------------------------" } -function docker_start() { - docker-compose up -d +# Robust check for initialization +check_initialization() { + local initialized=true + + # 1. Check for vendor directory + if [ ! -d "vendor" ]; then + warn "Dependency check failed: directory 'vendor/' is missing." + initialized=false + fi + + # 2. Check for database volumes + local volume_exists=$(docker volume ls -q --filter "label=com.docker.compose.project" --filter "label=com.docker.compose.volume=db") + + if [ -z "$volume_exists" ]; then + warn "Infrastructure check failed: Database volume 'db' not found." + initialized=false + fi + + if [ "$initialized" = false ]; then + error "Project is not fully initialized." + echo -e "Please run ${GREEN}./bin/hackathon setup${NC} to initialize the environment." + exit 1 + fi +} + +show_info() { + echo "" + log "Service Connectivity Information:" + echo "-----------------------------------------------------------" + if is_running "hackathon-apache"; then + echo -e "Web Interface: http://localhost:${HTTP_PORT:-80}" + echo -e "API Documentation: http://localhost:${HTTP_PORT:-80}/api/docs/" + fi + if is_running "hackathon-mailhog"; then + echo -e "Mailhog Web-UI: http://localhost:${MAILHOG_WEBUI_PORT:-8025}" + echo -e "Mailhog SMTP Port: localhost:${MAILHOG_SMTP_PORT:-1025}" + fi + if is_running "hackathon-mariadb"; then + echo -e "Database (App): localhost:${MYSQL_PUBLIC_PORT:-3306}" + echo -e " User: ${MYSQL_USER:-dev} | Pass: ${MYSQL_PASSWORD:-dev}" + fi + if is_running "hackathon-mariadb-testing"; then + echo -e "Database (Test): localhost:${MYSQL_TESTING_PORT:-3307}" + echo -e " User: ${MYSQL_USER:-dev} | Pass: ${MYSQL_PASSWORD:-dev}" + fi + echo "-----------------------------------------------------------" + echo "" +} + +up() { + log "Starting containers..." + $DOCKER_COMPOSE up -d } -function docker_down() { - docker-compose down +down() { + log "Stopping containers and removing networks..." + $DOCKER_COMPOSE down } -function docker_database_cleanup() { - docker volume rm hackathon-api_db +cleanup_docker() { + log "Starting full Docker cleanup (containers, volumes, images)..." + $DOCKER_COMPOSE down -v --rmi all --remove-orphans + log "All Docker resources have been removed." } -function docker_vendor_cleanup() { - rm -rf "./vendor" +cleanup_app() { + log "Cleaning application files and caches (preserving .gitkeep)..." + + # 1. Remove entire directories + local dirs=(".phplint.cache" ".phpunit.cache" ".phpunit.functional.cache" "vendor") + for dir in "${dirs[@]}"; do + if [ -d "$dir" ]; then + rm -rf "$dir" && log "-> $dir/ removed" + fi + done + + # 2. Clear contents of directories, but EXCLUDE .gitkeep + local content_dirs=("data/cache" "data/log") + for c_dir in "${content_dirs[@]}"; do + if [ -d "$c_dir" ]; then + # find: mindepth 1 (only contents), ! -name (not .gitkeep), -delete (remove) + find "$c_dir" -mindepth 1 ! -name ".gitkeep" -delete && log "-> contents of $c_dir/ cleared (except .gitkeep)" + fi + done + + # 3. Remove specific files + if [ -f ".phpcs-cache" ]; then + rm ".phpcs-cache" && log "-> .phpcs-cache removed" + fi } -function docker_cleanup() { - docker image rm -f ghcr.io/ownhackathon/hackathon-api-php:latest mariadb:latest mailhog/mailhog httpd:alpine - docker_database_cleanup - docker_vendor_cleanup +install_deps() { + if [ ! -d "./vendor" ] || [ "$1" == "--force" ]; then + log "Installing PHP dependencies..." + $DOCKER_COMPOSE exec $CONTAINER_PHP composer install + fi } +migrate_db() { + log "Waiting for database readiness..." + $DOCKER_COMPOSE exec $CONTAINER_PHP php -r " + \$start = time(); + while (true) { + try { + new PDO('mysql:host=database;port=3306', 'root', '${MYSQL_ROOT_PASSWORD:-root}'); + break; + } catch (Exception \$e) { + if (time() - \$start > 30) { echo 'Timeout!'; exit(1); } + echo '.'; sleep(1); + } + }" + echo "" + log "Running migrations..." + $DOCKER_COMPOSE exec $CONTAINER_PHP composer run doctrine migrations:sync-metadata-storage -- --no-interaction + $DOCKER_COMPOSE exec $CONTAINER_PHP composer run doctrine migrations:migrate -- --no-interaction +} + +generate_openapi() { + log "Generating OpenAPI documentation..." + $DOCKER_COMPOSE exec $CONTAINER_PHP composer run openapi +} + +build_stack() { + up + install_deps "$1" + migrate_db + generate_openapi + show_info +} + +# --- Command Handler --- + case $1 in "start") - docker_start - ;; - - "restart") - docker-compose restart + check_initialization + up + show_info ;; - "stop") - docker_down + log "Pausing containers..." + $DOCKER_COMPOSE stop + ;; + "down") + down ;; - "setup") - docker_start - composer_install - setup_database - ;; - + build_stack + ;; + "openapi") + generate_openapi + ;; + "info") + show_info + ;; + "services") + list_services + ;; + "logs") + if [ -z "$2" ]; then + $DOCKER_COMPOSE logs -f + else + case $2 in + php|apache|database|database-testing|mailhog) $DOCKER_COMPOSE logs -f "$2" ;; + *) list_services; echo -e "Usage: $0 logs {${GREEN}svc${NC}}"; exit 1 ;; + esac + fi + ;; + "test") + $DOCKER_COMPOSE exec $CONTAINER_PHP vendor/bin/phpunit "${@:2}" + ;; + "mysql") + log "Connecting to Database Console..." + $DOCKER_COMPOSE exec database mariadb -u "${MYSQL_USER:-dev}" -p"${MYSQL_PASSWORD:-dev}" "${MYSQL_DATABASE:-db}" + ;; + "bash") + log "Entering PHP container..." + $DOCKER_COMPOSE exec $CONTAINER_PHP sh -c "which bash > /dev/null && bash || sh" + ;; "clean") - docker_vendor_cleanup - ;; - + case $2 in + "docker") cleanup_docker ;; + "app") cleanup_app ;; + "all") cleanup_docker; cleanup_app ;; + *) + echo -e "Usage: $0 clean {${GREEN}docker${NC}|${GREEN}app${NC}|${GREEN}all${NC}}" + echo "" + echo "Options:" + echo -e " ${GREEN}docker${NC} - Remove all project containers, networks, volumes and ALL images" + echo -e " ${GREEN}app${NC} - Remove vendor/ folder, all cache directories and clear logs (preserves .gitkeep)" + echo -e " ${GREEN}all${NC} - Full cleanup (Combines both docker and app cleanup)" + exit 1 + ;; + esac + ;; "reset") - echo "turning down docker container" - docker_down - case $2 in - "all") - echo "cleanup system completely" - docker_cleanup - ;; + "database") + log "Resetting database..." + $DOCKER_COMPOSE down -v + build_stack + ;; "vendor") - echo "cleanup vendor folder" - docker_vendor_cleanup - ;; + log "Resetting vendor folder..." + [ -d "vendor" ] && rm -rf "vendor" + up + install_deps "--force" + show_info + ;; + "all") + log "Resetting entire application stack..." + $DOCKER_COMPOSE down -v + cleanup_app + build_stack "--force" + ;; *) - echo "cleanup database" - docker_database_cleanup + echo -e "Usage: $0 reset {${GREEN}database${NC}|${GREEN}vendor${NC}|${GREEN}all${NC}}" + echo "" + echo "Options:" + echo -e " ${GREEN}database${NC} - Wipe database volumes and re-run migrations (restarts containers)" + echo -e " ${GREEN}vendor${NC} - Wipe vendor/ folder and reinstall dependencies (DB stays untouched)" + echo -e " ${GREEN}all${NC} - Wipe database, vendor/ and ALL caches, then perform fresh setup" + exit 1 ;; esac - - echo "turning on docker container" - docker_start - - echo "check and install vendor" - composer_install - - echo "wait for services" - sleep 5 - - echo "create database storage information an run migrations" - setup_database - - echo "done" - ;; - + ;; "composer") - docker-compose run --rm --env APP_ENV php composer "${@:2}" + $DOCKER_COMPOSE exec $CONTAINER_PHP composer "${@:2}" ;; - "php") - docker-compose run --rm --env APP_ENV php "${@:2}" + $DOCKER_COMPOSE exec $CONTAINER_PHP php "${@:2}" + ;; + "indocker") + case $2 in + php|apache|database|database-testing|mailhog) + SERVICE=$2 + COMMAND=${@:3} + if [ -z "$COMMAND" ]; then + log "Entering $SERVICE container..." + $DOCKER_COMPOSE exec "$SERVICE" sh -c "which bash > /dev/null && bash || sh" + else + $DOCKER_COMPOSE exec "$SERVICE" $COMMAND + fi + ;; + *) + list_services + echo -e "Usage: $0 indocker {${GREEN}svc${NC}} [command]" + exit 1 + ;; + esac + ;; + *) + echo -e "Usage: $0 {${GREEN}start${NC}|${GREEN}stop${NC}|${GREEN}down${NC}|${GREEN}setup${NC}|${GREEN}reset${NC}|${GREEN}openapi${NC}|${GREEN}info${NC}|${GREEN}clean${NC}|${GREEN}composer${NC}|${GREEN}php${NC}|${GREEN}indocker${NC}}" + echo "" + echo "Infrastructure Commands:" + echo -e " ${GREEN}start${NC} - Start containers (checks for initialization first)" + echo -e " ${GREEN}stop${NC} - Pause containers (preserves state)" + echo -e " ${GREEN}down${NC} - Stop and remove containers and networks" + echo -e " ${GREEN}setup${NC} - Full initial installation (Start, Install, Migrate, OpenAPI)" + echo -e " ${GREEN}info${NC} - Show connectivity info (URLs, Ports, Credentials)" + echo -e " ${GREEN}services${NC} - List all available service names" + echo -e " ${GREEN}openapi${NC} - Regenerate the OpenAPI documentation" + echo "" + echo "Maintenance Commands:" + echo -e " ${GREEN}clean [...]${NC} - Delete resources (run without args for options)" + echo -e " ${GREEN}reset [...]${NC} - Restart with fresh data (run without args for options)" + echo "" + echo "Utility & Development:" + echo -e " ${GREEN}composer [...]${NC} - Execute Composer commands" + echo -e " ${GREEN}php [...]${NC} - Execute PHP scripts" + echo -e " ${GREEN}test [...]${NC} - Run PHPUnit tests" + echo -e " ${GREEN}logs [svc]${NC} - Tail logs for all or a specific service" + echo -e " ${GREEN}bash${NC} - Shortcut to enter PHP container" + echo -e " ${GREEN}mysql${NC} - Shortcut to enter Database CLI" + echo -e " ${GREEN}indocker [svc]${NC} - Access a specific container" + exit 1 ;; esac diff --git a/bin/migrations.php b/bin/migrations.php new file mode 100755 index 00000000..cef980d9 --- /dev/null +++ b/bin/migrations.php @@ -0,0 +1,71 @@ +#!/usr/bin/env php +setCustomTemplate(__DIR__ . '/../config/migrations.template.tpl'); + +$configuration->addMigrationsDirectory('Migrations', $config['migrations_paths']['Migrations']); +if (array_key_exists('TestDataMigrations', $config['migrations_paths'])) { + $configuration->addMigrationsDirectory('TestDataMigrations', $config['migrations_paths']['TestDataMigrations']); +} +$configuration->setAllOrNothing($config['all_or_nothing']); +$configuration->setCheckDatabasePlatform($config['check_database_platform']); +$configuration->setTransactional($config['transactional']); +$configuration->setMigrationOrganization($config['organize_migrations']); + +$storageConfiguration = new TableMetadataStorageConfiguration(); +$storageConfiguration->setTableName($config['table_storage']['table_name']); +$storageConfiguration->setVersionColumnName($config['table_storage']['version_column_name']); +$storageConfiguration->setVersionColumnLength($config['table_storage']['version_column_length']); +$storageConfiguration->setExecutedAtColumnName($config['table_storage']['executed_at_column_name']); +$storageConfiguration->setExecutionTimeColumnName($config['table_storage']['execution_time_column_name']); + +$configuration->setMetadataStorageConfiguration($storageConfiguration); + +$container = require_once __DIR__ . '/../config/container.php'; + +$dependencyFactory = DependencyFactory::fromConnection( + new ExistingConfiguration($configuration), + new ExistingConnection($connection) +); + +$dependencyFactory->setService(Psr\Container\ContainerInterface::class, $container); + +$cli = new Application('Doctrine Migrations'); +$cli->setCatchExceptions(true); + +$cli->addCommands([ + new Command\DumpSchemaCommand($dependencyFactory), + new Command\ExecuteCommand($dependencyFactory), + new Command\GenerateCommand($dependencyFactory), + new Command\LatestCommand($dependencyFactory), + new Command\ListCommand($dependencyFactory), + new Command\MigrateCommand($dependencyFactory), + new Command\RollupCommand($dependencyFactory), + new Command\StatusCommand($dependencyFactory), + new Command\SyncMetadataCommand($dependencyFactory), + new Command\VersionCommand($dependencyFactory), +]); + +$cli->run(); diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..27380e7a --- /dev/null +++ b/composer.json @@ -0,0 +1,120 @@ +{ + "name": "ownhackathon/hackathon-api", + "description": "Evaluation project for self-created hackathon", + "type": "project", + "license": "BSD-3-Clause", + "keywords": [ + "Hackathon", + "OpenSource" + ], + "homepage": "https://github.com/ownHackathon", + "support": { + "docs": "https://github.com/ownHackathon", + "issues": "https://github.com/ownHackathon/hackathon-api/issues", + "source": "https://github.com/ownHackathon/hackathon-api" + }, + "config": { + "allow-plugins": { + "composer/package-versions-deprecated": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "laminas/laminas-component-installer": true + }, + "platform": { + "php": "8.4.16" + }, + "sort-packages": true + }, + "extra": { + "laminas": { + "component-auto-installs": [ + "mezzio/mezzio", + "mezzio/mezzio-helpers", + "mezzio/mezzio-router", + "laminas/laminas-httphandlerrunner", + "mezzio/mezzio-fastroute" + ] + } + }, + "require": { + "php": "~8.4.0", + "ext-json": "*", + "ext-openssl": "*", + "ext-pdo": "*", + "envms/fluentpdo": "^2.2", + "firebase/php-jwt": "^6.0", + "laminas/laminas-config-aggregator": "^1.6", + "laminas/laminas-diactoros": "^3.8.0", + "laminas/laminas-inputfilter": "^2.30", + "laminas/laminas-servicemanager": "^3.4", + "laminas/laminas-stdlib": "^3.6", + "laminas/laminas-validator": "^2.64", + "laminas/laminas-component-installer": "^2.6 || ^3.0", + "laminas/laminas-development-mode": "^3.3.0", + "mezzio/mezzio-tooling": "^2.12", + "mezzio/mezzio": "^3", + "mezzio/mezzio-cors": "^1.13", + "mezzio/mezzio-fastroute": "^3.0.3", + "mezzio/mezzio-helpers": "^5.7", + "monolog/monolog": "^3.9.0", + "ramsey/uuid": "^4.7", + "symfony/mailer": "^8", + "zircote/swagger-php": "^6", + "jetbrains/phpstorm-attributes": "^1.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.2", + "doctrine/migrations": "^3.6", + "filp/whoops": "^2.7.1", + "helmich/phpunit-json-assert": "^3.5", + "helmich/phpunit-psr7-assert": "^4.4", + "overtrue/phplint": "^9.0", + "phpstan/phpstan": "^2.1.33", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpunit/php-code-coverage": "^10.0", + "phpunit/phpunit": "^10.0", + "roave/security-advisories": "dev-master", + "slevomat/coding-standard": "^8.15", + "squizlabs/php_codesniffer": "^4.0" + }, + "autoload": { + "psr-4": { + "App\\": "src/App/", + "Core\\": "src/Core/" + } + }, + "autoload-dev": { + "psr-4": { + "FunctionalTest\\": "tests/FunctionalTest/", + "UnitTest\\": "tests/UnitTest/" + } + }, + "scripts": { + "check": [ + "@phplint", + "@phpcs", + "@phpstan", + "@test" + ], + "clear-config-cache": "php bin/clear-config-cache.php", + "development-disable": "laminas-development-mode disable", + "development-enable": "laminas-development-mode enable", + "development-status": "laminas-development-mode status", + "doctrine": "php ./bin/migrations.php", + "enable-codestandard": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run", + "functionaltest": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --colors=always --display-phpunit-deprecations --configuration phpunit_functionaltest.xml", + "mezzio": "laminas --ansi", + "openapi": "php ./vendor/bin/openapi ./src -f json -o ./public/api/docs/swagger.json", + "phpcbf": "phpcbf --standard=phpcs.xml -p", + "phpcs": "phpcs --standard=phpcs.xml -s -p", + "phplint": "vendor/bin/phplint", + "phpstan": "vendor/bin/phpstan analyse -c phpstan.neon", + "post-create-project-cmd": [ + "@development-enable" + ], + "test": [ + "@unittest", + "@functionaltest" + ], + "unittest": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --colors=always --display-phpunit-deprecations --configuration phpunit_unittest.xml" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..346de3ae --- /dev/null +++ b/composer.lock @@ -0,0 +1,8677 @@ +{ + "_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": "1a6899dfb7acc2c2a218b4d63c4136ec", + "packages": [ + { + "name": "brick/math", + "version": "0.14.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-11-24T14:40:29+00:00" + }, + { + "name": "brick/varexporter", + "version": "0.6.0", + "source": { + "type": "git", + "url": "https://github.com/brick/varexporter.git", + "reference": "af98bfc2b702a312abbcaff37656dbe419cec5bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/varexporter/zipball/af98bfc2b702a312abbcaff37656dbe419cec5bc", + "reference": "af98bfc2b702a312abbcaff37656dbe419cec5bc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "6.8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\VarExporter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A powerful alternative to var_export(), which can export closures and objects without __set_state()", + "keywords": [ + "var_export" + ], + "support": { + "issues": "https://github.com/brick/varexporter/issues", + "source": "https://github.com/brick/varexporter/tree/0.6.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-02-20T17:42:39+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "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%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "envms/fluentpdo", + "version": "v2.2.4", + "source": { + "type": "git", + "url": "https://github.com/envms/fluentpdo.git", + "reference": "1985e0e8406a56140f387bc9bec786b419cbeccc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/envms/fluentpdo/zipball/1985e0e8406a56140f387bc9bec786b419cbeccc", + "reference": "1985e0e8406a56140f387bc9bec786b419cbeccc", + "shasum": "" + }, + "require": { + "ext-pdo": "*", + "php": ">=7.1" + }, + "require-dev": { + "envms/fluent-test": "^1.0", + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Envms\\FluentPDO\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0", + "GPL-2.0+" + ], + "authors": [ + { + "name": "envms", + "homepage": "https://env.ms" + } + ], + "description": "FluentPDO is a quick and light PHP library for rapid query building. It features a smart join builder, which automatically creates table joins.", + "homepage": "https://github.com/envms/fluentpdo", + "keywords": [ + "builder", + "database", + "db", + "dbal", + "fluent", + "mysql", + "oracle", + "pdo", + "query" + ], + "support": { + "issues": "https://github.com/envms/fluentpdo/issues", + "source": "https://github.com/envms/fluentpdo/tree/v2.2.4" + }, + "time": "2022-01-27T21:49:44+00:00" + }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" + }, + { + "name": "jetbrains/phpstorm-attributes", + "version": "1.2", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-attributes.git", + "reference": "64de815a4509c29e00d5e3474087fd24c171afc2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-attributes/zipball/64de815a4509c29e00d5e3474087fd24c171afc2", + "reference": "64de815a4509c29e00d5e3474087fd24c171afc2", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "JetBrains\\PhpStorm\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "JetBrains", + "homepage": "https://www.jetbrains.com" + } + ], + "description": "PhpStorm specific attributes", + "keywords": [ + "attributes", + "jetbrains", + "phpstorm" + ], + "support": { + "issues": "https://youtrack.jetbrains.com/newIssue?project=WI", + "source": "https://github.com/JetBrains/phpstorm-attributes/tree/1.2" + }, + "time": "2024-10-11T10:46:19+00:00" + }, + { + "name": "laminas/laminas-cli", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-cli.git", + "reference": "c84265f644c604f5a70bf5c8fcdb4b0e2aa115d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-cli/zipball/c84265f644c604f5a70bf5c8fcdb4b0e2aa115d5", + "reference": "c84265f644c604f5a70bf5c8fcdb4b0e2aa115d5", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.0 || ^2.0", + "symfony/console": "^6.0 || ^7.0", + "symfony/event-dispatcher": "^6.0 || ^7.0", + "webmozart/assert": "^1.11" + }, + "conflict": { + "amphp/amp": "<2.6.4" + }, + "require-dev": { + "laminas/laminas-coding-standard": "^3.1.0", + "laminas/laminas-mvc": "^3.8.0", + "laminas/laminas-servicemanager": "^3.24.0", + "mikey179/vfsstream": "2.0.x-dev", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "bin": [ + "bin/laminas" + ], + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Cli\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Command-line interface for Laminas projects", + "keywords": [ + "cli", + "command", + "console", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-cli/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/laminas-cli/issues", + "rss": "https://github.com/mezzio/laminas-cli/releases.atom", + "source": "https://github.com/mezzio/laminas-cli" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-14T22:21:28+00:00" + }, + { + "name": "laminas/laminas-code", + "version": "4.17.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-code.git", + "reference": "40d61e2899ec17c5d08bbc0a2d586b3ca17ab9bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/40d61e2899ec17c5d08bbc0a2d586b3ca17ab9bd", + "reference": "40d61e2899ec17c5d08bbc0a2d586b3ca17ab9bd", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0.1", + "ext-phar": "*", + "laminas/laminas-coding-standard": "^3.0.0", + "laminas/laminas-stdlib": "^3.18.0", + "phpunit/phpunit": "^10.5.58", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.15.0" + }, + "suggest": { + "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", + "laminas/laminas-stdlib": "Laminas\\Stdlib component" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Code\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", + "homepage": "https://laminas.dev", + "keywords": [ + "code", + "laminas", + "laminasframework" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-code/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-code/issues", + "rss": "https://github.com/laminas/laminas-code/releases.atom", + "source": "https://github.com/laminas/laminas-code" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-11-01T09:38:14+00:00" + }, + { + "name": "laminas/laminas-component-installer", + "version": "3.7.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-component-installer.git", + "reference": "cd2baf076f8035edca93baef584835f2de1b9e0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-component-installer/zipball/cd2baf076f8035edca93baef584835f2de1b9e0f", + "reference": "cd2baf076f8035edca93baef584835f2de1b9e0f", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.6", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "zendframework/zend-component-installer": "*" + }, + "require-dev": { + "composer/composer": "^2.7.7", + "laminas/laminas-coding-standard": "~3.1.0", + "mikey179/vfsstream": "^1.6.11", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1", + "webmozart/assert": "^1.11.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Laminas\\ComponentInstaller\\ComponentInstaller" + }, + "autoload": { + "psr-4": { + "Laminas\\ComponentInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Composer plugin for injecting modules and configuration providers into application configuration", + "homepage": "https://laminas.dev", + "keywords": [ + "component installer", + "composer", + "laminas", + "plugin" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-component-installer/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-component-installer/issues", + "rss": "https://github.com/laminas/laminas-component-installer/releases.atom", + "source": "https://github.com/laminas/laminas-component-installer" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-16T19:54:21+00:00" + }, + { + "name": "laminas/laminas-config-aggregator", + "version": "1.19.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-config-aggregator.git", + "reference": "612343ce135c340fc667da3615e50d865a86b4d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-config-aggregator/zipball/612343ce135c340fc667da3615e50d865a86b4d9", + "reference": "612343ce135c340fc667da3615e50d865a86b4d9", + "shasum": "" + }, + "require": { + "brick/varexporter": "^0.5.0 || ^0.4.0 || ^0.6.0", + "laminas/laminas-stdlib": "^3.18.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "webimpress/safe-writer": "^2.2.0" + }, + "conflict": { + "nikic/php-parser": "<4.12", + "zendframework/zend-config-aggregator": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-config": "^3.10.1", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "suggest": { + "laminas/laminas-config": "Allows loading configuration from XML, INI, YAML, and JSON files", + "laminas/laminas-config-aggregator-modulemanager": "Allows loading configuration from laminas-mvc Module classes", + "laminas/laminas-config-aggregator-parameters": "Allows usage of templated parameters within your configuration" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\ConfigAggregator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Lightweight library for collecting and merging configuration from different sources", + "homepage": "https://laminas.dev", + "keywords": [ + "config-aggregator", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-config-aggregator/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-config-aggregator/issues", + "rss": "https://github.com/laminas/laminas-config-aggregator/releases.atom", + "source": "https://github.com/laminas/laminas-config-aggregator" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-14T19:57:01+00:00" + }, + { + "name": "laminas/laminas-development-mode", + "version": "3.15.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-development-mode.git", + "reference": "87611d4d742dc314244dcbe4e173a2af11a7c0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-development-mode/zipball/87611d4d742dc314244dcbe4e173a2af11a7c0bc", + "reference": "87611d4d742dc314244dcbe4e173a2af11a7c0bc", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "zfcampus/zf-development-mode": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~3.1.0", + "mikey179/vfsstream": "^1.6.12", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "bin": [ + "bin/laminas-development-mode" + ], + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\DevelopmentMode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Laminas development mode script", + "homepage": "https://laminas.dev", + "keywords": [ + "framework", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-development-mode/issues", + "rss": "https://github.com/laminas/laminas-development-mode/releases.atom", + "source": "https://github.com/laminas/laminas-development-mode" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-14T21:17:32+00:00" + }, + { + "name": "laminas/laminas-diactoros", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "60c182916b2749480895601649563970f3f12ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/60c182916b2749480895601649563970f3f12ec4", + "reference": "60c182916b2749480895601649563970f3f12ec4", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "amphp/amp": "<2.6.4" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^2.2.0", + "laminas/laminas-coding-standard": "~3.1.0", + "php-http/psr7-integration-tests": "^1.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\Diactoros", + "config-provider": "Laminas\\Diactoros\\ConfigProvider" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-12T15:31:36+00:00" + }, + { + "name": "laminas/laminas-escaper", + "version": "2.18.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "06f211dfffff18d91844c1f55250d5d13c007e18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/06f211dfffff18d91844c1f55250d5d13c007e18", + "reference": "06f211dfffff18d91844c1f55250d5d13c007e18", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-mbstring": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "zendframework/zend-escaper": "*" + }, + "require-dev": { + "infection/infection": "^0.31.0", + "laminas/laminas-coding-standard": "~3.1.0", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-escaper/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-escaper/issues", + "rss": "https://github.com/laminas/laminas-escaper/releases.atom", + "source": "https://github.com/laminas/laminas-escaper" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-14T18:31:13+00:00" + }, + { + "name": "laminas/laminas-filter", + "version": "2.42.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-filter.git", + "reference": "985d27bd42daf51b415ce1ee889e0978cc1e59ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-filter/zipball/985d27bd42daf51b415ce1ee889e0978cc1e59ed", + "reference": "985d27bd42daf51b415ce1ee889e0978cc1e59ed", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "laminas/laminas-servicemanager": "^3.21.0", + "laminas/laminas-stdlib": "^3.19.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "laminas/laminas-validator": "<2.10.1", + "zendframework/zend-filter": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "^3.1", + "laminas/laminas-crypt": "^3.12", + "laminas/laminas-i18n": "^2.30.0", + "laminas/laminas-uri": "^2.13", + "pear/archive_tar": "^1.6.0", + "phpunit/phpunit": "^10.5.58", + "psalm/plugin-phpunit": "^0.19.0", + "psr/http-factory": "^1.1.0", + "vimeo/psalm": "^5.26.1" + }, + "suggest": { + "laminas/laminas-crypt": "Laminas\\Crypt component, for encryption filters", + "laminas/laminas-i18n": "Laminas\\I18n component for filters depending on i18n functionality", + "laminas/laminas-uri": "Laminas\\Uri component, for the UriNormalize filter", + "psr/http-factory-implementation": "psr/http-factory-implementation, for creating file upload instances when consuming PSR-7 in file upload filters" + }, + "type": "library", + "extra": { + "laminas": { + "component": "Laminas\\Filter", + "config-provider": "Laminas\\Filter\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Filter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Programmatically filter and normalize data and files", + "homepage": "https://laminas.dev", + "keywords": [ + "filter", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-filter/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-filter/issues", + "rss": "https://github.com/laminas/laminas-filter/releases.atom", + "source": "https://github.com/laminas/laminas-filter" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-13T15:44:52+00:00" + }, + { + "name": "laminas/laminas-httphandlerrunner", + "version": "2.13.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-httphandlerrunner.git", + "reference": "181eaeeb838ad3d80fbbcfb0657a46bc212bbd4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-httphandlerrunner/zipball/181eaeeb838ad3d80fbbcfb0657a46bc212bbd4e", + "reference": "181eaeeb838ad3d80fbbcfb0657a46bc212bbd4e", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-message-implementation": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-diactoros": "^3.6.0", + "phpunit/phpunit": "^10.5.46", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.10.3" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Laminas\\HttpHandlerRunner\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\HttpHandlerRunner\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Execute PSR-15 RequestHandlerInterface instances and emit responses they generate.", + "homepage": "https://laminas.dev", + "keywords": [ + "components", + "laminas", + "mezzio", + "psr-15", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-httphandlerrunner/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-httphandlerrunner/issues", + "rss": "https://github.com/laminas/laminas-httphandlerrunner/releases.atom", + "source": "https://github.com/laminas/laminas-httphandlerrunner" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-12T20:58:29+00:00" + }, + { + "name": "laminas/laminas-inputfilter", + "version": "2.35.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-inputfilter.git", + "reference": "326d2dac38814f70902a3a9e0062f740d06f89c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-inputfilter/zipball/326d2dac38814f70902a3a9e0062f740d06f89c5", + "reference": "326d2dac38814f70902a3a9e0062f740d06f89c5", + "shasum": "" + }, + "require": { + "laminas/laminas-filter": "^2.19", + "laminas/laminas-servicemanager": "^3.21.0", + "laminas/laminas-stdlib": "^3.19", + "laminas/laminas-validator": "^2.60.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.1 || ^2.0" + }, + "conflict": { + "zendframework/zend-inputfilter": "*" + }, + "require-dev": { + "ext-json": "*", + "laminas/laminas-coding-standard": "^3.1.0", + "phpunit/phpunit": "^11.5.46", + "psalm/plugin-phpunit": "^0.19.5", + "psr/http-message": "^2.0", + "vimeo/psalm": "^6.14.3" + }, + "suggest": { + "psr/http-message-implementation": "PSR-7 is required if you wish to validate PSR-7 UploadedFileInterface payloads" + }, + "type": "library", + "extra": { + "laminas": { + "component": "Laminas\\InputFilter", + "config-provider": "Laminas\\InputFilter\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\InputFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Normalize and validate input sets from the web, APIs, the CLI, and more, including files", + "homepage": "https://laminas.dev", + "keywords": [ + "inputfilter", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-inputfilter/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-inputfilter/issues", + "rss": "https://github.com/laminas/laminas-inputfilter/releases.atom", + "source": "https://github.com/laminas/laminas-inputfilter" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2026-01-10T15:07:43+00:00" + }, + { + "name": "laminas/laminas-servicemanager", + "version": "3.24.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-servicemanager.git", + "reference": "b172a0df568bf37ebdfb3658263156eefe3c1e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/b172a0df568bf37ebdfb3658263156eefe3c1e8c", + "reference": "b172a0df568bf37ebdfb3658263156eefe3c1e8c", + "shasum": "" + }, + "require": { + "laminas/laminas-stdlib": "^3.19", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.0" + }, + "conflict": { + "ext-psr": "*", + "laminas/laminas-code": "<4.10.0", + "zendframework/zend-code": "<3.3.1", + "zendframework/zend-servicemanager": "*" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "replace": { + "container-interop/container-interop": "^1.2.0" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11.99.5", + "friendsofphp/proxy-manager-lts": "^1.0.18", + "laminas/laminas-code": "^4.16.0", + "laminas/laminas-coding-standard": "~2.5.0", + "laminas/laminas-container-config-test": "^0.8", + "mikey179/vfsstream": "^1.6.12", + "phpbench/phpbench": "^1.4.1", + "phpunit/phpunit": "^10.5.58", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.26.1" + }, + "suggest": { + "friendsofphp/proxy-manager-lts": "ProxyManager ^2.1.1 to handle lazy initialization of services" + }, + "bin": [ + "bin/generate-deps-for-config-factory", + "bin/generate-factory-for-class" + ], + "type": "library", + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ServiceManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Factory-Driven Dependency Injection Container", + "homepage": "https://laminas.dev", + "keywords": [ + "PSR-11", + "dependency-injection", + "di", + "dic", + "laminas", + "service-manager", + "servicemanager" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-servicemanager/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-servicemanager/issues", + "rss": "https://github.com/laminas/laminas-servicemanager/releases.atom", + "source": "https://github.com/laminas/laminas-servicemanager" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-14T09:03:51+00:00" + }, + { + "name": "laminas/laminas-stdlib", + "version": "3.21.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-stdlib.git", + "reference": "b1c81514cfe158aadf724c42b34d3d0a8164c096" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/b1c81514cfe158aadf724c42b34d3d0a8164c096", + "reference": "b1c81514cfe158aadf724c42b34d3d0a8164c096", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "zendframework/zend-stdlib": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "^3.1.0", + "phpbench/phpbench": "^1.4.1", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Stdlib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "SPL extensions, array utilities, error handlers, and more", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "stdlib" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-stdlib/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-stdlib/issues", + "rss": "https://github.com/laminas/laminas-stdlib/releases.atom", + "source": "https://github.com/laminas/laminas-stdlib" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-11T18:13:12+00:00" + }, + { + "name": "laminas/laminas-stratigility", + "version": "3.14.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-stratigility.git", + "reference": "d23d128a22f79a67e1f9682df4c51719e3553c9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-stratigility/zipball/d23d128a22f79a67e1f9682df4c51719e3553c9d", + "reference": "d23d128a22f79a67e1f9682df4c51719e3553c9d", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1", + "laminas/laminas-escaper": "^2.10.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-middleware": "^1.0.2" + }, + "conflict": { + "zendframework/zend-stratigility": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "^3.1.0", + "laminas/laminas-diactoros": "^2.25 || ^3.8.0", + "phpunit/phpunit": "^10.5.58", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.26.1" + }, + "suggest": { + "psr/http-message-implementation": "Please install a psr/http-message-implementation to consume Stratigility; e.g., laminas/laminas-diactoros" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions/double-pass-middleware.php", + "src/functions/host.php", + "src/functions/middleware.php", + "src/functions/path.php", + "src/functions/double-pass-middleware.legacy.php", + "src/functions/host.legacy.php", + "src/functions/middleware.legacy.php", + "src/functions/path.legacy.php" + ], + "psr-4": { + "Laminas\\Stratigility\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR-7 middleware foundation for building and dispatching middleware pipelines", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "middleware", + "psr-15", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-stratigility/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-stratigility/issues", + "rss": "https://github.com/laminas/laminas-stratigility/releases.atom", + "source": "https://github.com/laminas/laminas-stratigility" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-11-12T05:23:21+00:00" + }, + { + "name": "laminas/laminas-validator", + "version": "2.65.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-validator.git", + "reference": "f0767ca83e0dd91a6f8ccdd4f0887eb132c0ea49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-validator/zipball/f0767ca83e0dd91a6f8ccdd4f0887eb132c0ea49", + "reference": "f0767ca83e0dd91a6f8ccdd4f0887eb132c0ea49", + "shasum": "" + }, + "require": { + "laminas/laminas-servicemanager": "^3.21.0", + "laminas/laminas-stdlib": "^3.19", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-message": "^1.0.1 || ^2.0.0" + }, + "conflict": { + "zendframework/zend-validator": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "^2.5", + "laminas/laminas-db": "^2.20", + "laminas/laminas-filter": "^2.41.0", + "laminas/laminas-i18n": "^2.30.0", + "laminas/laminas-session": "^2.25.1", + "laminas/laminas-uri": "^2.13.0", + "phpunit/phpunit": "^10.5.58", + "psalm/plugin-phpunit": "^0.19.0", + "psr/http-client": "^1.0.3", + "psr/http-factory": "^1.1.0", + "vimeo/psalm": "^5.26.1" + }, + "suggest": { + "laminas/laminas-db": "Laminas\\Db component, required by the (No)RecordExists validator", + "laminas/laminas-filter": "Laminas\\Filter component, required by the Digits validator", + "laminas/laminas-i18n": "Laminas\\I18n component to allow translation of validation error messages", + "laminas/laminas-i18n-resources": "Translations of validator messages", + "laminas/laminas-servicemanager": "Laminas\\ServiceManager component to allow using the ValidatorPluginManager and validator chains", + "laminas/laminas-session": "Laminas\\Session component, ^2.8; required by the Csrf validator", + "laminas/laminas-uri": "Laminas\\Uri component, required by the Uri and Sitemap\\Loc validators", + "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators" + }, + "type": "library", + "extra": { + "laminas": { + "component": "Laminas\\Validator", + "config-provider": "Laminas\\Validator\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Laminas\\Validator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Validation classes for a wide range of domains, and the ability to chain validators to create complex validation criteria", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "validator" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-validator/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-validator/issues", + "rss": "https://github.com/laminas/laminas-validator/releases.atom", + "source": "https://github.com/laminas/laminas-validator" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-13T14:40:30+00:00" + }, + { + "name": "mezzio/mezzio", + "version": "3.23.2", + "source": { + "type": "git", + "url": "https://github.com/mezzio/mezzio.git", + "reference": "988d39687683c9ae70d213c68c75c89965caad30" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mezzio/mezzio/zipball/988d39687683c9ae70d213c68c75c89965caad30", + "reference": "988d39687683c9ae70d213c68c75c89965caad30", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1.5", + "laminas/laminas-httphandlerrunner": "^2.1", + "laminas/laminas-stratigility": "^3.5", + "mezzio/mezzio-router": "^3.15.0", + "mezzio/mezzio-template": "^2.2", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.0||^2.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0.1 || ^2.0.0", + "psr/http-server-middleware": "^1.0", + "webmozart/assert": "^1.11.0" + }, + "conflict": { + "container-interop/container-interop": "<1.2.0", + "filp/whoops": "<2.14.4", + "laminas/laminas-diactoros": "<1.7.1", + "laminas/laminas-http": "<2.15.0", + "laminas/laminas-router": "<3.5.0", + "laminas/laminas-stdlib": "<3.6.0", + "zendframework/zend-expressive": "*" + }, + "require-dev": { + "filp/whoops": "^2.18.4", + "laminas/laminas-coding-standard": "^3.1.0", + "laminas/laminas-diactoros": "^3.8.0", + "laminas/laminas-servicemanager": "^3.23.1", + "mezzio/mezzio-fastroute": "^3.14", + "mezzio/mezzio-laminasrouter": "^3.12", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "suggest": { + "filp/whoops": "^2.1 to use the Whoops error handler", + "laminas/laminas-auradi-config": "^2.0 to use Aura.Di dependency injection container", + "laminas/laminas-pimple-config": "^1.0 to use Pimple for dependency injection container", + "laminas/laminas-servicemanager": "^3.3 to use laminas-servicemanager for dependency injection", + "mezzio/mezzio-helpers": "^3.0 for its UrlHelper, ServerUrlHelper, and BodyParseMiddleware", + "mezzio/mezzio-tooling": "^1.0 for migration and development tools; require it with the --dev flag", + "psr/http-message-implementation": "Please install a psr/http-message-implementation to consume Mezzio; e.g., laminas/laminas-diactoros" + }, + "bin": [ + "bin/mezzio-tooling" + ], + "type": "library", + "extra": { + "laminas": { + "config-provider": "Mezzio\\ConfigProvider" + } + }, + "autoload": { + "files": [ + "src/constants.php", + "src/constants.legacy.php" + ], + "psr-4": { + "Mezzio\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR-15 Middleware Microframework", + "homepage": "https://mezzio.dev", + "keywords": [ + "PSR-11", + "http", + "laminas", + "mezzio", + "middleware", + "psr", + "psr-15", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.mezzio.dev/mezzio/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/mezzio/issues", + "rss": "https://github.com/mezzio/mezzio/releases.atom", + "source": "https://github.com/mezzio/mezzio" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-22T10:56:04+00:00" + }, + { + "name": "mezzio/mezzio-cors", + "version": "1.15.0", + "source": { + "type": "git", + "url": "https://github.com/mezzio/mezzio-cors.git", + "reference": "0465bd920f326033de0191f3a56692ddbd44b994" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mezzio/mezzio-cors/zipball/0465bd920f326033de0191f3a56692ddbd44b994", + "reference": "0465bd920f326033de0191f3a56692ddbd44b994", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1", + "mezzio/mezzio-router": "^3.5 || ^4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "webmozart/assert": "^1.11.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-diactoros": "^3.8.0", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Mezzio\\Cors\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Mezzio\\Cors\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "CORS component for Mezzio and other PSR-15 middleware runners.", + "keywords": [ + "cors", + "mezzio", + "psr-15", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/mezzio-cors/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/mezzio-cors/issues", + "rss": "https://github.com/mezzio/mezzio-cli/releases.atom", + "source": "https://github.com/mezzio/mezzio-cors" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-12T19:30:45+00:00" + }, + { + "name": "mezzio/mezzio-fastroute", + "version": "3.14.0", + "source": { + "type": "git", + "url": "https://github.com/mezzio/mezzio-fastroute.git", + "reference": "00b1dd8560566d745a5a3a18582d1242ad51dd64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mezzio/mezzio-fastroute/zipball/00b1dd8560566d745a5a3a18582d1242ad51dd64", + "reference": "00b1dd8560566d745a5a3a18582d1242ad51dd64", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1.2", + "laminas/laminas-stdlib": "^3.19.0", + "mezzio/mezzio-router": "^3.18 || ^4.0.1", + "nikic/fast-route": "^1.2", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-message": "^1.0.1 || ^2.0.0" + }, + "conflict": { + "container-interop/container-interop": "<1.2.0", + "zendframework/zend-expressive-fastroute": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-diactoros": "^3.6.0", + "laminas/laminas-stratigility": "^4.2.0", + "mikey179/vfsstream": "^1.6.12", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Mezzio\\Router\\FastRouteRouter\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Mezzio\\Router\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "FastRoute integration for Mezzio", + "homepage": "https://mezzio.dev", + "keywords": [ + "FastRoute", + "http", + "laminas", + "mezzio", + "middleware", + "psr", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.mezzio.dev/mezzio/features/router/fast-route/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/mezzio-fastroute/issues", + "rss": "https://github.com/mezzio/mezzio-fastroute/releases.atom", + "source": "https://github.com/mezzio/mezzio-fastroute" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-11T08:43:04+00:00" + }, + { + "name": "mezzio/mezzio-helpers", + "version": "5.20.0", + "source": { + "type": "git", + "url": "https://github.com/mezzio/mezzio-helpers.git", + "reference": "a26ba04bd449d5cdb5ad38b17ce672365dbc9d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mezzio/mezzio-helpers/zipball/a26ba04bd449d5cdb5ad38b17ce672365dbc9d90", + "reference": "a26ba04bd449d5cdb5ad38b17ce672365dbc9d90", + "shasum": "" + }, + "require": { + "mezzio/mezzio-router": "^3.18 || ^4.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-message": "^1.0.1 || ^2.0.0", + "psr/http-server-middleware": "^1.0" + }, + "conflict": { + "amphp/amp": "<2.6.4", + "amphp/dns": "<2.1.2", + "amphp/socket": "<2.3.1", + "zendframework/zend-expressive-helpers": "*" + }, + "require-dev": { + "ext-json": "*", + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-diactoros": "^3.6", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "suggest": { + "ext-json": "If you wish to use the JsonStrategy with BodyParamsMiddleware" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Mezzio\\Helper\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Mezzio\\Helper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Helper/Utility classes for Mezzio", + "homepage": "https://mezzio.dev", + "keywords": [ + "http", + "laminas", + "mezzio", + "middleware", + "psr", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.mezzio.dev/mezzio/features/helpers/intro/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/mezzio-helpers/issues", + "rss": "https://github.com/mezzio/mezzio-helpers/releases.atom", + "source": "https://github.com/mezzio/mezzio-helpers" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-11T08:40:34+00:00" + }, + { + "name": "mezzio/mezzio-router", + "version": "3.19.0", + "source": { + "type": "git", + "url": "https://github.com/mezzio/mezzio-router.git", + "reference": "3df4363e70611ddf096db95c62df6aa98817872c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mezzio/mezzio-router/zipball/3df4363e70611ddf096db95c62df6aa98817872c", + "reference": "3df4363e70611ddf096db95c62df6aa98817872c", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1.5", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.1.2 || ^2.0", + "psr/http-factory": "^1.0.2", + "psr/http-message": "^1.0.1 || ^2.0.0", + "psr/http-server-middleware": "^1.0.2", + "webmozart/assert": "^1.11" + }, + "conflict": { + "mezzio/mezzio": "<3.5", + "zendframework/zend-expressive-router": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~3.1.0", + "laminas/laminas-diactoros": "^3.6.0", + "laminas/laminas-servicemanager": "^4.4.0", + "laminas/laminas-stratigility": "^4.2.0", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^6.13.1" + }, + "suggest": { + "mezzio/mezzio-aurarouter": "^3.0 to use the Aura.Router routing adapter", + "mezzio/mezzio-fastroute": "^3.0 to use the FastRoute routing adapter", + "mezzio/mezzio-laminasrouter": "^3.0 to use the laminas-router routing adapter" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Mezzio\\Router\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Mezzio\\Router\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Router subcomponent for Mezzio", + "homepage": "https://mezzio.dev", + "keywords": [ + "http", + "laminas", + "mezzio", + "middleware", + "psr", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.mezzio.dev/mezzio/features/router/intro/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/mezzio-router/issues", + "rss": "https://github.com/mezzio/mezzio-router/releases.atom", + "source": "https://github.com/mezzio/mezzio-router" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-11T08:41:44+00:00" + }, + { + "name": "mezzio/mezzio-template", + "version": "2.13.0", + "source": { + "type": "git", + "url": "https://github.com/mezzio/mezzio-template.git", + "reference": "ad72bb31036d0639a5c5a502af234217faf6932f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mezzio/mezzio-template/zipball/ad72bb31036d0639a5c5a502af234217faf6932f", + "reference": "ad72bb31036d0639a5c5a502af234217faf6932f", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "conflict": { + "zendframework/zend-expressive-template": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~3.1.0", + "phpunit/phpunit": "^11.5.42", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13.1" + }, + "suggest": { + "mezzio/mezzio-laminasviewrenderer": "^2.0 to use the laminas-view PhpRenderer template renderer", + "mezzio/mezzio-platesrenderer": "^2.0 to use the Plates template renderer", + "mezzio/mezzio-twigrenderer": "^2.0 to use the Twig template renderer" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mezzio\\Template\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Template subcomponent for Mezzio", + "homepage": "https://mezzio.dev", + "keywords": [ + "laminas", + "mezzio", + "template" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.mezzio.dev/mezzio/features/template/intro/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/mezzio-template/issues", + "rss": "https://github.com/mezzio/mezzio-template/releases.atom", + "source": "https://github.com/mezzio/mezzio-template" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-11T08:45:28+00:00" + }, + { + "name": "mezzio/mezzio-tooling", + "version": "2.12.0", + "source": { + "type": "git", + "url": "https://github.com/mezzio/mezzio-tooling.git", + "reference": "41e8242b27398d0511223d48bbd9efc97d6a2e40" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mezzio/mezzio-tooling/zipball/41e8242b27398d0511223d48bbd9efc97d6a2e40", + "reference": "41e8242b27398d0511223d48bbd9efc97d6a2e40", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laminas/laminas-cli": "^1.7.0", + "laminas/laminas-code": "^4.7.1", + "laminas/laminas-stdlib": "^3.15.0", + "laminas/laminas-stratigility": "^3.9.0", + "mezzio/mezzio": "^3.13.0", + "mezzio/mezzio-router": "^3.9.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.1", + "symfony/process": "^6.0.11" + }, + "conflict": { + "amphp/amp": "<2.6.4", + "symfony/console": "<5.4.45", + "symfony/string": "<5.4.45" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.5.0", + "laminas/laminas-diactoros": "^3.3", + "mikey179/vfsstream": "^1.6.12", + "mockery/mockery": "^1.6.10", + "php-mock/php-mock-phpunit": "^2.9.0", + "phpdocumentor/reflection-docblock": "^5.3.0", + "phpunit/phpunit": "^10.5.35", + "psalm/plugin-mockery": "^0.11.0", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.17.0" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Mezzio\\Tooling\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Mezzio\\Tooling\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Migration and development tooling for Mezzio", + "homepage": "https://mezzio.dev", + "keywords": [ + "http", + "laminas", + "mezzio", + "middleware", + "psr", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.mezzio.dev/mezzio/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/mezzio-tooling/issues", + "rss": "https://github.com/mezzio/mezzio-tooling/releases.atom", + "source": "https://github.com/mezzio/mezzio-tooling" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-09-12T08:39:37+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.1", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374", + "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1" + }, + "time": "2026-01-12T11:33:04+00:00" + }, + { + "name": "psr/container", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "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/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "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": "PSR-17: 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" + }, + "time": "2024-04-15T12:06:14+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/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "radebatz/type-info-extras", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/DerManoMann/type-info-extras.git", + "reference": "23b74c4690fb3d147a1b34c4dc090a9ddc6dc31a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DerManoMann/type-info-extras/zipball/23b74c4690fb3d147a1b34c4dc090a9ddc6dc31a", + "reference": "23b74c4690fb3d147a1b34c4dc090a9ddc6dc31a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "phpstan/phpdoc-parser": "^2.0", + "symfony/type-info": "^7.3.8 || ^7.4.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.70", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Radebatz\\TypeInfoExtras\\": "src" + }, + "exclude-from-classmap": [ + "/tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Martin Rademacher", + "email": "mano@radebatz.org" + } + ], + "description": "Extras for symfony/type-info", + "homepage": "http://radebatz.net/mano/", + "keywords": [ + "component", + "symfony", + "type-info", + "types" + ], + "support": { + "issues": "https://github.com/DerManoMann/type-info-extras/issues", + "source": "https://github.com/DerManoMann/type-info-extras/tree/1.0.4" + }, + "time": "2026-01-12T21:15:50+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-23T14:50:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.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-09-25T14:21:43+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "reference": "9dddcddff1ef974ad87b3708e4b442dc38b2261d", + "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|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.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.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-28T09:38:46+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.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-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "fffe05569336549b20a1be64250b40516d6e8d06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/fffe05569336549b20a1be64250b40516d6e8d06", + "reference": "fffe05569336549b20a1be64250b40516d6e8d06", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-23T14:50:43+00:00" + }, + { + "name": "symfony/mailer", + "version": "v8.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "02e033db6e00a42c66b8b8992e4e565ea7464a28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/02e033db6e00a42c66b8b8992e4e565ea7464a28", + "reference": "02e033db6e00a42c66b8b8992e4e565ea7464a28", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.4", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v8.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-16T08:10:18+00:00" + }, + { + "name": "symfony/mime", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40", + "reference": "7576ce3b2b4d3a2a7fe7020a07a392065d6ffd40", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-16T10:17:21+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.31", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "8541b7308fca001320e90bca8a73a28aa5604a6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/8541b7308fca001320e90bca8a73a28aa5604a6e", + "reference": "8541b7308fca001320e90bca8a73a28aa5604a6e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.31" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-15T19:26:35+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-01T09:13:36+00:00" + }, + { + "name": "symfony/type-info", + "version": "v8.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "bb091cec1f70383538c7d000699781813f8d1a6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/bb091cec1f70383538c7d000699781813f8d1a6a", + "reference": "bb091cec1f70383538c7d000699781813f8d1a6a", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v8.0.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-05T14:08:45+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.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.4.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-04T18:11:45+00:00" + }, + { + "name": "webimpress/safe-writer", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/webimpress/safe-writer.git", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "reference": "9d37cc8bee20f7cb2f58f6e23e05097eab5072e6", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.4", + "vimeo/psalm": "^4.7", + "webimpress/coding-standard": "^1.2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev", + "dev-develop": "2.3.x-dev", + "dev-release-1.0": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Webimpress\\SafeWriter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Tool to write files safely, to avoid race conditions", + "keywords": [ + "concurrent write", + "file writer", + "race condition", + "safe writer", + "webimpress" + ], + "support": { + "issues": "https://github.com/webimpress/safe-writer/issues", + "source": "https://github.com/webimpress/safe-writer/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2021-04-19T16:34:45+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^7.2 || ^8.0" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "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.12.1" + }, + "time": "2025-10-29T15:56:20+00:00" + }, + { + "name": "zircote/swagger-php", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/zircote/swagger-php.git", + "reference": "cf332956e6603fe4c8da6223a98b7ba65e20231e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/cf332956e6603fe4c8da6223a98b7ba65e20231e", + "reference": "cf332956e6603fe4c8da6223a98b7ba65e20231e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/php-parser": "^4.19 || ^5.0", + "php": ">=8.2", + "phpstan/phpdoc-parser": "^2.0", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "radebatz/type-info-extras": "^1.0.2", + "symfony/deprecation-contracts": "^2 || ^3", + "symfony/finder": "^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "conflict": { + "symfony/process": ">=6, <6.4.14" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/annotations": "^2.0", + "friendsofphp/php-cs-fixer": "^3.62.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5", + "rector/rector": "^2.3.1" + }, + "bin": [ + "bin/openapi" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenApi\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Robert Allen", + "email": "zircote@gmail.com" + }, + { + "name": "Bob Fanger", + "email": "bfanger@gmail.com", + "homepage": "https://bfanger.nl" + }, + { + "name": "Martin Rademacher", + "email": "mano@radebatz.net", + "homepage": "https://radebatz.net" + } + ], + "description": "Generate interactive documentation for your RESTful API using PHP attributes (preferred) or PHPDoc annotations", + "homepage": "https://github.com/zircote/swagger-php", + "keywords": [ + "api", + "json", + "rest", + "service discovery" + ], + "support": { + "issues": "https://github.com/zircote/swagger-php/issues", + "source": "https://github.com/zircote/swagger-php/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/zircote", + "type": "github" + } + ], + "time": "2026-01-15T19:54:20+00:00" + } + ], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-11T04:32:07+00:00" + }, + { + "name": "doctrine/dbal", + "version": "4.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c", + "reference": "3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "2.0.7", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "11.5.23", + "slevomat/coding-standard": "8.24.0", + "squizlabs/php_codesniffer": "4.0.0", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.4.1" + }, + "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%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-12-04T10:11:03+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "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%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "doctrine/migrations", + "version": "3.9.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "1b823afbc40f932dae8272574faee53f2755eac5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/1b823afbc40f932dae8272574faee53f2755eac5", + "reference": "1b823afbc40f932dae8272574faee53f2755eac5", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/dbal": "^3.6 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2.0", + "php": "^8.1", + "psr/log": "^1.1.3 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/var-exporter": "^6.2 || ^7.0 || ^8.0" + }, + "conflict": { + "doctrine/orm": "<2.12 || >=4" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "doctrine/orm": "^2.13 || ^3", + "doctrine/persistence": "^2 || ^3 || ^4", + "doctrine/sql-formatter": "^1.0", + "ext-pdo_sqlite": "*", + "fig/log-test": "^1", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-symfony": "^2", + "phpunit/phpunit": "^10.3 || ^11.0 || ^12.0", + "symfony/cache": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "suggest": { + "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", + "symfony/yaml": "Allows the use of yaml for migration configuration files." + }, + "bin": [ + "bin/doctrine-migrations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Migrations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" + } + ], + "description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.", + "homepage": "https://www.doctrine-project.org/projects/migrations.html", + "keywords": [ + "database", + "dbal", + "migrations" + ], + "support": { + "issues": "https://github.com/doctrine/migrations/issues", + "source": "https://github.com/doctrine/migrations/tree/3.9.5" + }, + "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%2Fmigrations", + "type": "tidelift" + } + ], + "time": "2025-11-20T11:15:36+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "helmich/phpunit-json-assert", + "version": "v3.5.3", + "source": { + "type": "git", + "url": "https://github.com/martin-helmich/phpunit-json-assert.git", + "reference": "82cedf4ee0a7a2e6a619fbbdab9db77c53fe3793" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/martin-helmich/phpunit-json-assert/zipball/82cedf4ee0a7a2e6a619fbbdab9db77c53fe3793", + "reference": "82cedf4ee0a7a2e6a619fbbdab9db77c53fe3793", + "shasum": "" + }, + "require": { + "justinrainbow/json-schema": "^5.0", + "php": "^8.1", + "softcreatr/jsonpath": "^0.8" + }, + "conflict": { + "phpunit/phpunit": "<8.0 || >= 13.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Helmich\\JsonAssert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Martin Helmich", + "email": "m.helmich@mittwald.de" + } + ], + "description": "PHPUnit assertions for JSON documents", + "support": { + "issues": "https://github.com/martin-helmich/phpunit-json-assert/issues", + "source": "https://github.com/martin-helmich/phpunit-json-assert/tree/v3.5.3" + }, + "funding": [ + { + "url": "https://donate.helmich.me", + "type": "custom" + }, + { + "url": "https://github.com/martin-helmich", + "type": "github" + } + ], + "time": "2025-05-19T22:04:17+00:00" + }, + { + "name": "helmich/phpunit-psr7-assert", + "version": "v4.4.1", + "source": { + "type": "git", + "url": "https://github.com/martin-helmich/phpunit-psr7-assert.git", + "reference": "f35fa69e07cc16977b52805d3abd873cc16747fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/martin-helmich/phpunit-psr7-assert/zipball/f35fa69e07cc16977b52805d3abd873cc16747fd", + "reference": "f35fa69e07cc16977b52805d3abd873cc16747fd", + "shasum": "" + }, + "require": { + "helmich/phpunit-json-assert": "^3.4", + "php": "^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "phpunit/phpunit": "<8.0 || >= 11.0" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.4", + "mockery/mockery": "^1.4.1", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Helmich\\Psr7Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Martin Helmich", + "email": "m.helmich@mittwald.de" + } + ], + "description": "PHPUnit assertions for testing PSR7-compliant applications", + "support": { + "issues": "https://github.com/martin-helmich/phpunit-psr7-assert/issues", + "source": "https://github.com/martin-helmich/phpunit-psr7-assert/tree/v4.4.1" + }, + "funding": [ + { + "url": "https://donate.helmich.me", + "type": "custom" + }, + { + "url": "https://github.com/martin-helmich", + "type": "github" + } + ], + "time": "2023-07-26T19:04:29+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "5.3.1", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "b5a44b6391a3bbb75c9f2b73e1ef03d6045e1e20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/b5a44b6391a3bbb75c9f2b73e1ef03d6045e1e20", + "reference": "b5a44b6391a3bbb75c9f2b73e1ef03d6045e1e20", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.1" + }, + "time": "2025-12-12T08:56:22+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "overtrue/phplint", + "version": "9.6.3", + "source": { + "type": "git", + "url": "https://github.com/overtrue/phplint.git", + "reference": "b0ec1d07b37a37e7fc872c8bdbddacadcecbe047" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/overtrue/phplint/zipball/b0ec1d07b37a37e7fc872c8bdbddacadcecbe047", + "reference": "b0ec1d07b37a37e7fc872c8bdbddacadcecbe047", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "ext-dom": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/cache": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/event-dispatcher": "^6.4 || ^7.0", + "symfony/finder": "^6.4 || ^7.0", + "symfony/options-resolver": "^6.4 || ^7.0", + "symfony/process": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "brainmaestro/composer-git-hooks": "^3.0.0", + "jetbrains/phpstorm-stubs": "^2024.1", + "php-parallel-lint/php-console-highlighter": "^1.0" + }, + "bin": [ + "bin/phplint" + ], + "type": "library", + "extra": { + "hooks": { + "pre-commit": [ + "composer style:fix", + "composer code:check" + ] + }, + "bamarni-bin": { + "bin-links": true, + "forward-command": true, + "target-directory": "vendor-bin" + }, + "branch-alias": { + "dev-main": "9.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Overtrue\\PHPLint\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + }, + { + "name": "Laurent Laville", + "homepage": "https://github.com/llaville" + } + ], + "description": "`phplint` is a tool that can speed up linting of php files by running several lint processes at once.", + "keywords": [ + "check", + "lint", + "phplint", + "static analysis", + "syntax" + ], + "support": { + "issues": "https://github.com/overtrue/phplint/issues", + "source": "https://github.com/overtrue/phplint/tree/9.6.3" + }, + "funding": [ + { + "url": "https://github.com/overtrue", + "type": "github" + } + ], + "time": "2025-11-27T13:49:59+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.33", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-12-05T10:24:31+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/468e02c9176891cc901143da118f09dc9505fc2f", + "reference": "468e02c9176891cc901143da118f09dc9505fc2f", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.15" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.3" + }, + "time": "2025-05-14T10:56:57+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.60", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "f2e26f52f80ef77832e359205f216eeac00e320c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f2e26f52f80ef77832e359205f216eeac00e320c", + "reference": "f2e26f52f80ef77832e359205f216eeac00e320c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.4", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.60" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-12-06T07:50:42+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": "roave/security-advisories", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Roave/SecurityAdvisories.git", + "reference": "95fda149b750941a5d7bd292e712107ca3227a04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/95fda149b750941a5d7bd292e712107ca3227a04", + "reference": "95fda149b750941a5d7bd292e712107ca3227a04", + "shasum": "" + }, + "conflict": { + "3f/pygmentize": "<1.2", + "adaptcms/adaptcms": "<=1.3", + "admidio/admidio": "<=4.3.16", + "adodb/adodb-php": "<=5.22.9", + "aheinze/cockpit": "<2.2", + "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", + "aimeos/ai-admin-jsonadm": "<2020.10.13|>=2021.04.1,<2021.10.6|>=2022.04.1,<2022.10.3|>=2023.04.1,<2023.10.4|==2024.04.1", + "aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.22|>=2022.04.1,<2022.10.13|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.04.7", + "aimeos/ai-cms-grapesjs": ">=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.9|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.10.8|>=2025.04.1,<2025.10.2", + "aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1", + "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7", + "aimeos/aimeos-laravel": "==2021.10", + "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", + "airesvsg/acf-to-rest-api": "<=3.1", + "akaunting/akaunting": "<2.1.13", + "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", + "alextselegidis/easyappointments": "<=1.5.2", + "alexusmai/laravel-file-manager": "<=3.3.1", + "algolia/algoliasearch-magento-2": "<=3.16.1|>=3.17.0.0-beta1,<=3.17.1", + "alt-design/alt-redirect": "<1.6.4", + "altcha-org/altcha": "<1.3.1", + "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", + "amazing/media2click": ">=1,<1.3.3", + "ameos/ameos_tarteaucitron": "<1.2.23", + "amphp/artax": "<1.0.6|>=2,<2.0.6", + "amphp/http": "<=1.7.2|>=2,<=2.1", + "amphp/http-client": ">=4,<4.4", + "anchorcms/anchor-cms": "<=0.12.7", + "andreapollastri/cipi": "<=3.1.15", + "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5", + "aoe/restler": "<1.7.1", + "apache-solr-for-typo3/solr": "<2.8.3", + "apereo/phpcas": "<1.6", + "api-platform/core": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5", + "api-platform/graphql": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5", + "appwrite/server-ce": "<=1.2.1", + "arc/web": "<3", + "area17/twill": "<1.2.5|>=2,<2.5.3", + "artesaos/seotools": "<0.17.2", + "asymmetricrypt/asymmetricrypt": "<9.9.99", + "athlon1600/php-proxy": "<=5.1", + "athlon1600/php-proxy-app": "<=3", + "athlon1600/youtube-downloader": "<=4", + "austintoddj/canvas": "<=3.4.2", + "auth0/auth0-php": ">=3.3,<8.18", + "auth0/login": "<7.20", + "auth0/symfony": "<=5.5", + "auth0/wordpress": "<=5.4", + "automad/automad": "<2.0.0.0-alpha5", + "automattic/jetpack": "<9.8", + "awesome-support/awesome-support": "<=6.0.7", + "aws/aws-sdk-php": "<3.368", + "azuracast/azuracast": "<=0.23.1", + "b13/seo_basics": "<0.8.2", + "backdrop/backdrop": "<=1.32", + "backpack/crud": "<3.4.9", + "backpack/filemanager": "<2.0.2|>=3,<3.0.9", + "bacula-web/bacula-web": "<9.7.1", + "badaso/core": "<=2.9.11", + "bagisto/bagisto": "<2.3.10", + "barrelstrength/sprout-base-email": "<1.2.7", + "barrelstrength/sprout-forms": "<3.9", + "barryvdh/laravel-translation-manager": "<0.6.8", + "barzahlen/barzahlen-php": "<2.0.1", + "baserproject/basercms": "<=5.1.1", + "bassjobsen/bootstrap-3-typeahead": ">4.0.2", + "bbpress/bbpress": "<2.6.5", + "bcit-ci/codeigniter": "<3.1.3", + "bcosca/fatfree": "<3.7.2", + "bedita/bedita": "<4", + "bednee/cooluri": "<1.0.30", + "bigfork/silverstripe-form-capture": ">=3,<3.1.1", + "billz/raspap-webgui": "<3.3.6", + "binarytorch/larecipe": "<2.8.1", + "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3", + "blueimp/jquery-file-upload": "==6.4.4", + "bmarshall511/wordpress_zero_spam": "<5.2.13", + "bolt/bolt": "<3.7.2", + "bolt/core": "<=4.2", + "born05/craft-twofactorauthentication": "<3.3.4", + "bottelet/flarepoint": "<2.2.1", + "bref/bref": "<2.1.17", + "brightlocal/phpwhois": "<=4.2.5", + "brotkrueml/codehighlight": "<2.7", + "brotkrueml/schema": "<1.13.1|>=2,<2.5.1", + "brotkrueml/typo3-matomo-integration": "<1.3.2", + "buddypress/buddypress": "<7.2.1", + "bugsnag/bugsnag-laravel": ">=2,<2.0.2", + "bvbmedia/multishop": "<2.0.39", + "bytefury/crater": "<6.0.2", + "cachethq/cachet": "<2.5.1", + "cadmium-org/cadmium-cms": "<=0.4.9", + "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", + "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", + "cardgate/magento2": "<2.0.33", + "cardgate/woocommerce": "<=3.1.15", + "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", + "cart2quote/module-quotation-encoded": ">=4.1.6,<=4.4.5|>=5,<5.4.4", + "cartalyst/sentry": "<=2.1.6", + "catfan/medoo": "<1.7.5", + "causal/oidc": "<4", + "cecil/cecil": "<7.47.1", + "centreon/centreon": "<22.10.15", + "cesnet/simplesamlphp-module-proxystatistics": "<3.1", + "chriskacerguis/codeigniter-restserver": "<=2.7.1", + "chrome-php/chrome": "<1.14", + "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", + "ckeditor/ckeditor": "<4.25", + "clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3", + "co-stack/fal_sftp": "<0.2.6", + "cockpit-hq/cockpit": "<2.11.4", + "code16/sharp": "<9.11.1", + "codeception/codeception": "<3.1.3|>=4,<4.1.22", + "codeigniter/framework": "<3.1.10", + "codeigniter4/framework": "<4.6.2", + "codeigniter4/shield": "<1.0.0.0-beta8", + "codiad/codiad": "<=2.8.4", + "codingms/additional-tca": ">=1.7,<1.15.17|>=1.16,<1.16.9", + "codingms/modules": "<4.3.11|>=5,<5.7.4|>=6,<6.4.2|>=7,<7.5.5", + "commerceteam/commerce": ">=0.9.6,<0.9.9", + "components/jquery": ">=1.0.3,<3.5", + "composer/composer": "<1.10.27|>=2,<2.2.26|>=2.3,<2.9.3", + "concrete5/concrete5": "<9.4.3", + "concrete5/core": "<8.5.8|>=9,<9.1", + "contao-components/mediaelement": ">=2.14.2,<2.21.1", + "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", + "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.13.56|>=5,<5.3.38|>=5.4.0.0-RC1-dev,<5.6.1", + "contao/core": "<3.5.39", + "contao/core-bundle": "<4.13.57|>=5,<5.3.42|>=5.4,<5.6.5", + "contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8", + "contao/managed-edition": "<=1.5", + "coreshop/core-shop": "<=4.1.7", + "corveda/phpsandbox": "<1.3.5", + "cosenary/instagram": "<=2.3", + "couleurcitron/tarteaucitron-wp": "<0.3", + "craftcms/cms": "<=4.16.16|>=5,<=5.8.20", + "croogo/croogo": "<=4.0.7", + "cuyz/valinor": "<0.12", + "czim/file-handling": "<1.5|>=2,<2.3", + "czproject/git-php": "<4.0.3", + "damienharper/auditor-bundle": "<5.2.6", + "dapphp/securimage": "<3.6.6", + "darylldoyle/safe-svg": "<1.9.10", + "datadog/dd-trace": ">=0.30,<0.30.2", + "datahihi1/tiny-env": "<1.0.3|>=1.0.9,<1.0.11", + "datatables/datatables": "<1.10.10", + "david-garcia/phpwhois": "<=4.3.1", + "dbrisinajumi/d2files": "<1", + "dcat/laravel-admin": "<=2.1.3|==2.2.0.0-beta|==2.2.2.0-beta", + "derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3", + "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4", + "desperado/xml-bundle": "<=0.1.7", + "dev-lancer/minecraft-motd-parser": "<=1.0.5", + "devcode-it/openstamanager": "<=2.9.4", + "devgroup/dotplant": "<2020.09.14-dev", + "digimix/wp-svg-upload": "<=1", + "directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2", + "dl/yag": "<3.0.1", + "dmk/webkitpdf": "<1.1.4", + "dnadesign/silverstripe-elemental": "<5.3.12", + "doctrine/annotations": "<1.2.7", + "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2", + "doctrine/common": "<2.4.3|>=2.5,<2.5.1", + "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2|>=3,<3.1.4", + "doctrine/doctrine-bundle": "<1.5.2", + "doctrine/doctrine-module": "<0.7.2", + "doctrine/mongodb-odm": "<1.0.2", + "doctrine/mongodb-odm-bundle": "<3.0.1", + "doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", + "dolibarr/dolibarr": "<21.0.3", + "dompdf/dompdf": "<2.0.4", + "doublethreedigital/guest-entries": "<3.1.2", + "drupal-pattern-lab/unified-twig-extensions": "<=0.1", + "drupal/access_code": "<2.0.5", + "drupal/acquia_dam": "<1.1.5", + "drupal/admin_audit_trail": "<1.0.5", + "drupal/ai": "<1.0.5", + "drupal/alogin": "<2.0.6", + "drupal/cache_utility": "<1.2.1", + "drupal/civictheme": "<1.12", + "drupal/commerce_alphabank_redirect": "<1.0.3", + "drupal/commerce_eurobank_redirect": "<2.1.1", + "drupal/config_split": "<1.10|>=2,<2.0.2", + "drupal/core": ">=6,<6.38|>=7,<7.103|>=8,<10.4.9|>=10.5,<10.5.6|>=11,<11.1.9|>=11.2,<11.2.8", + "drupal/core-recommended": ">=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", + "drupal/currency": "<3.5", + "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8", + "drupal/email_tfa": "<2.0.6", + "drupal/formatter_suite": "<2.1", + "drupal/gdpr": "<3.0.1|>=3.1,<3.1.2", + "drupal/google_tag": "<1.8|>=2,<2.0.8", + "drupal/ignition": "<1.0.4", + "drupal/json_field": "<1.5", + "drupal/lightgallery": "<1.6", + "drupal/link_field_display_mode_formatter": "<1.6", + "drupal/matomo": "<1.24", + "drupal/oauth2_client": "<4.1.3", + "drupal/oauth2_server": "<2.1", + "drupal/obfuscate": "<2.0.1", + "drupal/plausible_tracking": "<1.0.2", + "drupal/quick_node_block": "<2", + "drupal/rapidoc_elements_field_formatter": "<1.0.1", + "drupal/reverse_proxy_header": "<1.1.2", + "drupal/simple_multistep": "<2", + "drupal/simple_oauth": ">=6,<6.0.7", + "drupal/spamspan": "<3.2.1", + "drupal/tfa": "<1.10", + "drupal/umami_analytics": "<1.0.1", + "duncanmcclean/guest-entries": "<3.1.2", + "dweeves/magmi": "<=0.7.24", + "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2", + "ecodev/newsletter": "<=4", + "ectouch/ectouch": "<=2.7.2", + "egroupware/egroupware": "<23.1.20240624", + "elefant/cms": "<2.0.7", + "elgg/elgg": "<3.3.24|>=4,<4.0.5", + "elijaa/phpmemcacheadmin": "<=1.3", + "elmsln/haxcms": "<11.0.14", + "encore/laravel-admin": "<=1.8.19", + "endroid/qr-code-bundle": "<3.4.2", + "enhavo/enhavo-app": "<=0.13.1", + "enshrined/svg-sanitize": "<0.22", + "erusev/parsedown": "<1.7.2", + "ether/logs": "<3.0.4", + "evolutioncms/evolution": "<=3.2.3", + "exceedone/exment": "<4.4.3|>=5,<5.0.3", + "exceedone/laravel-admin": "<2.2.3|==3", + "ezsystems/demobundle": ">=5.4,<5.4.6.1-dev", + "ezsystems/ez-support-tools": ">=2.2,<2.2.3", + "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1-dev", + "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1-dev|>=5.4,<5.4.11.1-dev|>=2017.12,<2017.12.0.1-dev", + "ezsystems/ezplatform": "<=1.13.6|>=2,<=2.5.24", + "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.39|>=3.3,<3.3.39", + "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1|>=5.3.0.0-beta1,<5.3.5", + "ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12", + "ezsystems/ezplatform-http-cache": "<2.3.16", + "ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.35", + "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8", + "ezsystems/ezplatform-richtext": ">=2.3,<2.3.26|>=3.3,<3.3.40", + "ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15", + "ezsystems/ezplatform-user": ">=1,<1.0.1", + "ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31", + "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1", + "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", + "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", + "ezyang/htmlpurifier": "<=4.2", + "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2", + "facturascripts/facturascripts": "<=2025.4|==2025.11|==2025.41|==2025.43", + "fastly/magento2": "<1.2.26", + "feehi/cms": "<=2.1.1", + "feehi/feehicms": "<=2.1.1", + "fenom/fenom": "<=2.12.1", + "filament/actions": ">=3.2,<3.2.123", + "filament/filament": ">=4,<4.3.1", + "filament/infolists": ">=3,<3.2.115", + "filament/tables": ">=3,<3.2.115", + "filegator/filegator": "<7.8", + "filp/whoops": "<2.1.13", + "fineuploader/php-traditional-server": "<=1.2.2", + "firebase/php-jwt": "<6", + "fisharebest/webtrees": "<=2.1.18", + "fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2", + "fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6", + "flarum/core": "<1.8.10", + "flarum/flarum": "<0.1.0.0-beta8", + "flarum/framework": "<1.8.10", + "flarum/mentions": "<1.6.3", + "flarum/sticky": ">=0.1.0.0-beta14,<=0.1.0.0-beta15", + "flarum/tags": "<=0.1.0.0-beta13", + "floriangaerber/magnesium": "<0.3.1", + "fluidtypo3/vhs": "<5.1.1", + "fof/byobu": ">=0.3.0.0-beta2,<1.1.7", + "fof/pretty-mail": "<=1.1.2", + "fof/upload": "<1.2.3", + "foodcoopshop/foodcoopshop": ">=3.2,<3.6.1", + "fooman/tcpdf": "<6.2.22", + "forkcms/forkcms": "<5.11.1", + "fossar/tcpdf-parser": "<6.2.22", + "francoisjacquet/rosariosis": "<=11.5.1", + "frappant/frp-form-answers": "<3.1.2|>=4,<4.0.2", + "friendsofsymfony/oauth2-php": "<1.3", + "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", + "friendsofsymfony/user-bundle": ">=1,<1.3.5", + "friendsofsymfony1/swiftmailer": ">=4,<5.4.13|>=6,<6.2.5", + "friendsofsymfony1/symfony1": ">=1.1,<1.5.19", + "friendsoftypo3/mediace": ">=7.6.2,<7.6.5", + "friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6", + "froala/wysiwyg-editor": "<=4.3", + "froxlor/froxlor": "<=2.2.5", + "frozennode/administrator": "<=5.0.12", + "fuel/core": "<1.8.1", + "funadmin/funadmin": "<=5.0.2", + "gaoming13/wechat-php-sdk": "<=1.10.2", + "genix/cms": "<=1.1.11", + "georgringer/news": "<1.3.3", + "geshi/geshi": "<=1.0.9.1", + "getformwork/formwork": "<2.2", + "getgrav/grav": "<1.11.0.0-beta1", + "getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1|>=5,<=5.2.1", + "getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", + "getkirby/panel": "<2.5.14", + "getkirby/starterkit": "<=3.7.0.2", + "gilacms/gila": "<=1.15.4", + "gleez/cms": "<=1.3|==2", + "globalpayments/php-sdk": "<2", + "goalgorilla/open_social": "<12.3.11|>=12.4,<12.4.10|>=13.0.0.0-alpha1,<13.0.0.0-alpha11", + "gogentooss/samlbase": "<1.2.7", + "google/protobuf": "<3.4", + "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", + "gp247/core": "<1.1.24", + "gree/jose": "<2.2.1", + "gregwar/rst": "<1.0.3", + "grumpydictator/firefly-iii": "<6.1.17", + "gugoan/economizzer": "<=0.9.0.0-beta1", + "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", + "guzzlehttp/oauth-subscriber": "<0.8.1", + "guzzlehttp/psr7": "<1.9.1|>=2,<2.4.5", + "haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2", + "handcraftedinthealps/goodby-csv": "<1.4.3", + "harvesthq/chosen": "<1.8.7", + "helloxz/imgurl": "<=2.31", + "hhxsv5/laravel-s": "<3.7.36", + "hillelcoren/invoice-ninja": "<5.3.35", + "himiklab/yii2-jqgrid-widget": "<1.0.8", + "hjue/justwriting": "<=1", + "hov/jobfair": "<1.0.13|>=2,<2.0.2", + "httpsoft/http-message": "<1.0.12", + "hyn/multi-tenant": ">=5.6,<5.7.2", + "ibexa/admin-ui": ">=4.2,<4.2.3|>=4.6,<4.6.25|>=5,<5.0.3", + "ibexa/admin-ui-assets": ">=4.6.0.0-alpha1,<4.6.21", + "ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4|>=4.2,<4.2.3|>=4.5,<4.5.6|>=4.6,<4.6.2", + "ibexa/fieldtype-richtext": ">=4.6,<4.6.25|>=5,<5.0.3", + "ibexa/graphql": ">=2.5,<2.5.31|>=3.3,<3.3.28|>=4.2,<4.2.3", + "ibexa/http-cache": ">=4.6,<4.6.14", + "ibexa/post-install": "<1.0.16|>=4.6,<4.6.14", + "ibexa/solr": ">=4.5,<4.5.4", + "ibexa/user": ">=4,<4.4.3|>=5,<5.0.4", + "icecoder/icecoder": "<=8.1", + "idno/known": "<=1.3.1", + "ilicmiljan/secure-props": ">=1.2,<1.2.2", + "illuminate/auth": "<5.5.10", + "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<6.18.31|>=7,<7.22.4", + "illuminate/database": "<6.20.26|>=7,<7.30.5|>=8,<8.40", + "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", + "illuminate/view": "<6.20.42|>=7,<7.30.6|>=8,<8.75", + "imdbphp/imdbphp": "<=5.1.1", + "impresscms/impresscms": "<=1.4.5", + "impresspages/impresspages": "<1.0.13", + "in2code/femanager": "<6.4.2|>=7,<7.5.3|>=8,<8.3.1", + "in2code/ipandlanguageredirect": "<5.1.2", + "in2code/lux": "<17.6.1|>=18,<24.0.2", + "in2code/powermail": "<7.5.1|>=8,<8.5.1|>=9,<10.9.1|>=11,<12.5.3|==13", + "innologi/typo3-appointments": "<2.0.6", + "intelliants/subrion": "<4.2.2", + "inter-mediator/inter-mediator": "==5.5", + "ipl/web": "<0.10.1", + "islandora/crayfish": "<4.1", + "islandora/islandora": ">=2,<2.4.1", + "ivankristianto/phpwhois": "<=4.3", + "jackalope/jackalope-doctrine-dbal": "<1.7.4", + "jambagecom/div2007": "<0.10.2", + "james-heinrich/getid3": "<1.9.21", + "james-heinrich/phpthumb": "<=1.7.23", + "jasig/phpcas": "<1.3.3", + "jbartels/wec-map": "<3.0.3", + "jcbrand/converse.js": "<3.3.3", + "joelbutcher/socialstream": "<5.6|>=6,<6.2", + "johnbillion/wp-crontrol": "<1.16.2|>=1.17,<1.19.2", + "joomla/application": "<1.0.13", + "joomla/archive": "<1.1.12|>=2,<2.0.1", + "joomla/database": ">=1,<2.2|>=3,<3.4", + "joomla/filesystem": "<1.6.2|>=2,<2.0.1", + "joomla/filter": "<2.0.6|>=3,<3.0.5|==4", + "joomla/framework": "<1.5.7|>=2.5.4,<=3.8.12", + "joomla/input": ">=2,<2.0.2", + "joomla/joomla-cms": "<3.9.12|>=4,<4.4.13|>=5,<5.2.6", + "joomla/joomla-platform": "<1.5.4", + "joomla/session": "<1.3.1", + "joyqi/hyper-down": "<=2.4.27", + "jsdecena/laracom": "<2.0.9", + "jsmitty12/phpwhois": "<5.1", + "juzaweb/cms": "<=3.4.2", + "jweiland/events2": "<8.3.8|>=9,<9.0.6", + "jweiland/kk-downloader": "<1.2.2", + "kazist/phpwhois": "<=4.2.6", + "kelvinmo/simplexrd": "<3.1.1", + "kevinpapst/kimai2": "<1.16.7", + "khodakhah/nodcms": "<=3", + "kimai/kimai": "<=2.20.1", + "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", + "klaviyo/magento2-extension": ">=1,<3", + "knplabs/knp-snappy": "<=1.4.2", + "kohana/core": "<3.3.3", + "koillection/koillection": "<1.6.12", + "krayin/laravel-crm": "<=1.3", + "kreait/firebase-php": ">=3.2,<3.8.1", + "kumbiaphp/kumbiapp": "<=1.1.1", + "la-haute-societe/tcpdf": "<6.2.22", + "laminas/laminas-diactoros": "<2.18.1|==2.19|==2.20|==2.21|==2.22|==2.23|>=2.24,<2.24.2|>=2.25,<2.25.2", + "laminas/laminas-form": "<2.17.1|>=3,<3.0.2|>=3.1,<3.1.1", + "laminas/laminas-http": "<2.14.2", + "lara-zeus/artemis": ">=1,<=1.0.6", + "lara-zeus/dynamic-dashboard": ">=3,<=3.0.1", + "laravel/fortify": "<1.11.1", + "laravel/framework": "<10.48.29|>=11,<11.44.1|>=12,<12.1.1", + "laravel/laravel": ">=5.4,<5.4.22", + "laravel/pulse": "<1.3.1", + "laravel/reverb": "<1.4", + "laravel/socialite": ">=1,<2.0.10", + "latte/latte": "<2.10.8", + "lavalite/cms": "<=9|==10.1", + "lavitto/typo3-form-to-database": "<2.2.5|>=3,<3.2.2|>=4,<4.2.3|>=5,<5.0.2", + "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5", + "league/commonmark": "<2.7", + "league/flysystem": "<1.1.4|>=2,<2.1.1", + "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", + "leantime/leantime": "<3.3", + "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", + "libreform/libreform": ">=2,<=2.0.8", + "librenms/librenms": "<25.12", + "liftkit/database": "<2.13.2", + "lightsaml/lightsaml": "<1.3.5", + "limesurvey/limesurvey": "<6.5.12", + "livehelperchat/livehelperchat": "<=3.91", + "livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.6.4", + "livewire/volt": "<1.7", + "lms/routes": "<2.1.1", + "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2", + "lomkit/laravel-rest-api": "<2.13", + "luracast/restler": "<3.1", + "luyadev/yii-helpers": "<1.2.1", + "macropay-solutions/laravel-crud-wizard-free": "<3.4.17", + "maestroerror/php-heic-to-jpg": "<1.0.5", + "magento/community-edition": "<2.4.6.0-patch13|>=2.4.7.0-beta1,<2.4.7.0-patch8|>=2.4.8.0-beta1,<2.4.8.0-patch3|>=2.4.9.0-alpha1,<2.4.9.0-alpha3|==2.4.9", + "magento/core": "<=1.9.4.5", + "magento/magento1ce": "<1.9.4.3-dev", + "magento/magento1ee": ">=1,<1.14.4.3-dev", + "magento/product-community-edition": "<2.4.4.0-patch9|>=2.4.5,<2.4.5.0-patch8|>=2.4.6,<2.4.6.0-patch6|>=2.4.7,<2.4.7.0-patch1", + "magento/project-community-edition": "<=2.0.2", + "magneto/core": "<1.9.4.4-dev", + "mahocommerce/maho": "<25.9", + "maikuolan/phpmussel": ">=1,<1.6", + "mainwp/mainwp": "<=4.4.3.3", + "manogi/nova-tiptap": "<=3.2.6", + "mantisbt/mantisbt": "<2.27.2", + "marcwillmann/turn": "<0.3.3", + "marshmallow/nova-tiptap": "<5.7", + "matomo/matomo": "<1.11", + "matyhtf/framework": "<3.0.6", + "mautic/core": "<5.2.9|>=6,<6.0.7", + "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", + "mautic/grapes-js-builder-bundle": ">=4,<4.4.18|>=5,<5.2.9|>=6,<6.0.7", + "maximebf/debugbar": "<1.19", + "mdanter/ecc": "<2", + "mediawiki/abuse-filter": "<1.39.9|>=1.40,<1.41.3|>=1.42,<1.42.2", + "mediawiki/cargo": "<3.8.3", + "mediawiki/core": "<1.39.5|==1.40", + "mediawiki/data-transfer": ">=1.39,<1.39.11|>=1.41,<1.41.3|>=1.42,<1.42.2", + "mediawiki/matomo": "<2.4.3", + "mediawiki/semantic-media-wiki": "<4.0.2", + "mehrwert/phpmyadmin": "<3.2", + "melisplatform/melis-asset-manager": "<5.0.1", + "melisplatform/melis-cms": "<5.3.4", + "melisplatform/melis-cms-slider": "<5.3.1", + "melisplatform/melis-core": "<5.3.11", + "melisplatform/melis-front": "<5.0.1", + "mezzio/mezzio-swoole": "<3.7|>=4,<4.3", + "mgallegos/laravel-jqgrid": "<=1.3", + "microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1", + "microsoft/microsoft-graph-beta": "<2.0.1", + "microsoft/microsoft-graph-core": "<2.0.2", + "microweber/microweber": "<=2.0.19", + "mikehaertl/php-shellcommand": "<1.6.1", + "mineadmin/mineadmin": "<=3.0.9", + "miniorange/miniorange-saml": "<1.4.3", + "mittwald/typo3_forum": "<1.2.1", + "mobiledetect/mobiledetectlib": "<2.8.32", + "modx/revolution": "<=3.1", + "mojo42/jirafeau": "<4.4", + "mongodb/mongodb": ">=1,<1.9.2", + "mongodb/mongodb-extension": "<1.21.2", + "monolog/monolog": ">=1.8,<1.12", + "moodle/moodle": "<4.4.11|>=4.5.0.0-beta,<4.5.7|>=5.0.0.0-beta,<5.0.3", + "moonshine/moonshine": "<=3.12.5", + "mos/cimage": "<0.7.19", + "movim/moxl": ">=0.8,<=0.10", + "movingbytes/social-network": "<=1.2.1", + "mpdf/mpdf": "<=7.1.7", + "munkireport/comment": "<4", + "munkireport/managedinstalls": "<2.6", + "munkireport/munki_facts": "<1.5", + "munkireport/reportdata": "<3.5", + "munkireport/softwareupdate": "<1.6", + "mustache/mustache": ">=2,<2.14.1", + "mwdelaney/wp-enable-svg": "<=0.2", + "namshi/jose": "<2.2", + "nasirkhan/laravel-starter": "<11.11", + "nategood/httpful": "<1", + "neoan3-apps/template": "<1.1.1", + "neorazorx/facturascripts": "<2022.04", + "neos/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", + "neos/form": ">=1.2,<4.3.3|>=5,<5.0.9|>=5.1,<5.1.3", + "neos/media-browser": "<7.3.19|>=8,<8.0.16|>=8.1,<8.1.11|>=8.2,<8.2.11|>=8.3,<8.3.9", + "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<5.3.10|>=7,<7.0.9|>=7.1,<7.1.7|>=7.2,<7.2.6|>=7.3,<7.3.4|>=8,<8.0.2", + "neos/swiftmailer": "<5.4.5", + "nesbot/carbon": "<2.72.6|>=3,<3.8.4", + "netcarver/textile": "<=4.1.2", + "netgen/tagsbundle": ">=3.4,<3.4.11|>=4,<4.0.15", + "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6", + "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13", + "neuron-core/neuron-ai": "<=2.8.11", + "nilsteampassnet/teampass": "<3.1.3.1-dev", + "nitsan/ns-backup": "<13.0.1", + "nonfiction/nterchange": "<4.1.1", + "notrinos/notrinos-erp": "<=0.7", + "noumo/easyii": "<=0.9", + "novaksolutions/infusionsoft-php-sdk": "<1", + "novosga/novosga": "<=2.2.12", + "nukeviet/nukeviet": "<4.5.02", + "nyholm/psr7": "<1.6.1", + "nystudio107/craft-seomatic": "<3.4.12", + "nzedb/nzedb": "<0.8", + "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", + "october/backend": "<1.1.2", + "october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1", + "october/october": "<3.7.5", + "october/rain": "<1.0.472|>=1.1,<1.1.2", + "october/system": "<=3.7.12|>=4,<=4.0.11", + "oliverklee/phpunit": "<3.5.15", + "omeka/omeka-s": "<4.0.3", + "onelogin/php-saml": "<2.21.1|>=3,<3.8.1|>=4,<4.3.1", + "oneup/uploader-bundle": ">=1,<1.9.3|>=2,<2.1.5", + "open-web-analytics/open-web-analytics": "<1.8.1", + "opencart/opencart": ">=0", + "openid/php-openid": "<2.3", + "openmage/magento-lts": "<20.16", + "opensolutions/vimbadmin": "<=3.0.15", + "opensource-workshop/connect-cms": "<1.8.7|>=2,<2.4.7", + "orchid/platform": ">=8,<14.43", + "oro/calendar-bundle": ">=4.2,<=4.2.6|>=5,<=5.0.6|>=5.1,<5.1.1", + "oro/commerce": ">=4.1,<5.0.11|>=5.1,<5.1.1", + "oro/crm": ">=1.7,<1.7.4|>=3.1,<4.1.17|>=4.2,<4.2.7", + "oro/crm-call-bundle": ">=4.2,<=4.2.5|>=5,<5.0.4|>=5.1,<5.1.1", + "oro/customer-portal": ">=4.1,<=4.1.13|>=4.2,<=4.2.10|>=5,<=5.0.11|>=5.1,<=5.1.3", + "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<=4.2.10|>=5,<=5.0.12|>=5.1,<=5.1.3", + "oveleon/contao-cookiebar": "<1.16.3|>=2,<2.1.3", + "oxid-esales/oxideshop-ce": "<=7.0.5", + "oxid-esales/paymorrow-module": ">=1,<1.0.2|>=2,<2.0.1", + "packbackbooks/lti-1-3-php-library": "<5", + "padraic/humbug_get_contents": "<1.1.2", + "pagarme/pagarme-php": "<3", + "pagekit/pagekit": "<=1.0.18", + "paragonie/ecc": "<2.0.1", + "paragonie/random_compat": "<2", + "paragonie/sodium_compat": "<1.24|>=2,<2.5", + "passbolt/passbolt_api": "<4.6.2", + "paypal/adaptivepayments-sdk-php": "<=3.9.2", + "paypal/invoice-sdk-php": "<=3.9", + "paypal/merchant-sdk-php": "<3.12", + "paypal/permissions-sdk-php": "<=3.9.1", + "pear/archive_tar": "<1.4.14", + "pear/auth": "<1.2.4", + "pear/crypt_gpg": "<1.6.7", + "pear/http_request2": "<2.7", + "pear/pear": "<=1.10.1", + "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1", + "personnummer/personnummer": "<3.0.2", + "phanan/koel": "<5.1.4", + "phenx/php-svg-lib": "<0.5.2", + "php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5", + "php-mod/curl": "<2.3.2", + "phpbb/phpbb": "<3.3.11", + "phpems/phpems": ">=6,<=6.1.3", + "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7", + "phpmailer/phpmailer": "<6.5", + "phpmussel/phpmussel": ">=1,<1.6", + "phpmyadmin/phpmyadmin": "<5.2.2", + "phpmyfaq/phpmyfaq": "<=4.0.13", + "phpoffice/common": "<0.2.9", + "phpoffice/math": "<=0.2", + "phpoffice/phpexcel": "<=1.8.2", + "phpoffice/phpspreadsheet": "<1.30|>=2,<2.1.12|>=2.2,<2.4|>=3,<3.10|>=4,<5", + "phppgadmin/phppgadmin": "<=7.13", + "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", + "phpservermon/phpservermon": "<3.6", + "phpsysinfo/phpsysinfo": "<3.4.3", + "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", + "phpwhois/phpwhois": "<=4.2.5", + "phpxmlrpc/extras": "<0.6.1", + "phpxmlrpc/phpxmlrpc": "<4.9.2", + "pi/pi": "<=2.5", + "pimcore/admin-ui-classic-bundle": "<=1.7.15|>=2.0.0.0-RC1-dev,<=2.2.2", + "pimcore/customer-management-framework-bundle": "<4.2.1", + "pimcore/data-hub": "<1.2.4", + "pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3", + "pimcore/demo": "<10.3", + "pimcore/ecommerce-framework-bundle": "<1.0.10", + "pimcore/perspective-editor": "<1.5.1", + "pimcore/pimcore": "<=11.5.13|>=12.0.0.0-RC1-dev,<12.3.1", + "pimcore/web2print-tools-bundle": "<=5.2.1|>=6.0.0.0-RC1-dev,<=6.1", + "piwik/piwik": "<1.11", + "pixelfed/pixelfed": "<0.12.5", + "plotly/plotly.js": "<2.25.2", + "pocketmine/bedrock-protocol": "<8.0.2", + "pocketmine/pocketmine-mp": "<5.32.1", + "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", + "pressbooks/pressbooks": "<5.18", + "prestashop/autoupgrade": ">=4,<4.10.1", + "prestashop/blockreassurance": "<=5.1.3", + "prestashop/blockwishlist": ">=2,<2.1.1", + "prestashop/contactform": ">=1.0.1,<4.3", + "prestashop/gamification": "<2.3.2", + "prestashop/prestashop": "<8.2.3", + "prestashop/productcomments": "<5.0.2", + "prestashop/ps_checkout": "<4.4.1|>=5,<5.0.5", + "prestashop/ps_contactinfo": "<=3.3.2", + "prestashop/ps_emailsubscription": "<2.6.1", + "prestashop/ps_facetedsearch": "<3.4.1", + "prestashop/ps_linklist": "<3.1", + "privatebin/privatebin": "<1.4|>=1.5,<1.7.4|>=1.7.7,<2.0.3", + "processwire/processwire": "<=3.0.246", + "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", + "propel/propel1": ">=1,<=1.7.1", + "pterodactyl/panel": "<1.12", + "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", + "ptrofimov/beanstalk_console": "<1.7.14", + "pubnub/pubnub": "<6.1", + "punktde/pt_extbase": "<1.5.1", + "pusher/pusher-php-server": "<2.2.1", + "pwweb/laravel-core": "<=0.3.6.0-beta", + "pxlrbt/filament-excel": "<1.1.14|>=2.0.0.0-alpha,<2.3.3", + "pyrocms/pyrocms": "<=3.9.1", + "qcubed/qcubed": "<=3.1.1", + "quickapps/cms": "<=2.0.0.0-beta2", + "rainlab/blog-plugin": "<1.4.1", + "rainlab/debugbar-plugin": "<3.1", + "rainlab/user-plugin": "<=1.4.5", + "rankmath/seo-by-rank-math": "<=1.0.95", + "rap2hpoutre/laravel-log-viewer": "<0.13", + "react/http": ">=0.7,<1.9", + "really-simple-plugins/complianz-gdpr": "<6.4.2", + "redaxo/source": "<=5.20.1", + "remdex/livehelperchat": "<4.29", + "renolit/reint-downloadmanager": "<4.0.2|>=5,<5.0.1", + "reportico-web/reportico": "<=8.1", + "rhukster/dom-sanitizer": "<1.0.7", + "rmccue/requests": ">=1.6,<1.8", + "robrichards/xmlseclibs": "<=3.1.3", + "roots/soil": "<4.1", + "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11", + "rudloff/alltube": "<3.0.3", + "rudloff/rtmpdump-bin": "<=2.3.1", + "s-cart/core": "<=9.0.5", + "s-cart/s-cart": "<6.9", + "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1", + "sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9", + "samwilson/unlinked-wikibase": "<1.42", + "scheb/two-factor-bundle": "<3.26|>=4,<4.11", + "sensiolabs/connect": "<4.2.3", + "serluck/phpwhois": "<=4.2.6", + "setasign/fpdi": "<2.6.4", + "sfroemken/url_redirect": "<=1.2.1", + "sheng/yiicms": "<1.2.1", + "shopware/core": "<6.6.10.9-dev|>=6.7,<6.7.6.1-dev", + "shopware/platform": "<6.6.10.7-dev|>=6.7,<6.7.3.1-dev", + "shopware/production": "<=6.3.5.2", + "shopware/shopware": "<=5.7.17|>=6.4.6,<6.6.10.10-dev|>=6.7,<6.7.6.1-dev", + "shopware/storefront": "<6.6.10.10-dev|>=6.7,<6.7.5.1-dev", + "shopxo/shopxo": "<=6.4", + "showdoc/showdoc": "<2.10.4", + "shuchkin/simplexlsx": ">=1.0.12,<1.1.13", + "silverstripe-australia/advancedreports": ">=1,<=2", + "silverstripe/admin": "<1.13.19|>=2,<2.1.8", + "silverstripe/assets": ">=1,<1.11.1", + "silverstripe/cms": "<4.11.3", + "silverstripe/comments": ">=1.3,<3.1.1", + "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", + "silverstripe/framework": "<5.3.23", + "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.8.2|>=4,<4.3.7|>=5,<5.1.3", + "silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1", + "silverstripe/recipe-cms": ">=4.5,<4.5.3", + "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", + "silverstripe/reports": "<5.2.3", + "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4|>=2.1,<2.1.2", + "silverstripe/silverstripe-omnipay": "<2.5.2|>=3,<3.0.2|>=3.1,<3.1.4|>=3.2,<3.2.1", + "silverstripe/subsites": ">=2,<2.6.1", + "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1", + "silverstripe/userforms": "<3|>=5,<5.4.2", + "silverstripe/versioned-admin": ">=1,<1.11.1", + "simogeo/filemanager": "<=2.5", + "simple-updates/phpwhois": "<=1", + "simplesamlphp/saml2": "<=4.16.15|>=5.0.0.0-alpha1,<=5.0.0.0-alpha19", + "simplesamlphp/saml2-legacy": "<=4.16.15", + "simplesamlphp/simplesamlphp": "<1.18.6", + "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", + "simplesamlphp/simplesamlphp-module-openid": "<1", + "simplesamlphp/simplesamlphp-module-openidprovider": "<0.9", + "simplesamlphp/xml-common": "<1.20", + "simplesamlphp/xml-security": "==1.6.11", + "simplito/elliptic-php": "<1.0.6", + "sitegeist/fluid-components": "<3.5", + "sjbr/sr-feuser-register": "<2.6.2|>=5.1,<12.5", + "sjbr/sr-freecap": "<2.4.6|>=2.5,<2.5.3", + "sjbr/static-info-tables": "<2.3.1", + "slim/psr7": "<1.4.1|>=1.5,<1.5.1|>=1.6,<1.6.1", + "slim/slim": "<2.6", + "slub/slub-events": "<3.0.3", + "smarty/smarty": "<4.5.3|>=5,<5.1.1", + "snipe/snipe-it": "<=8.3.4", + "socalnick/scn-social-auth": "<1.15.2", + "socialiteproviders/steam": "<1.1", + "solspace/craft-freeform": "<4.1.29|>=5,<5.10.16", + "soosyze/soosyze": "<=2", + "spatie/browsershot": "<5.0.5", + "spatie/image-optimizer": "<1.7.3", + "spencer14420/sp-php-email-handler": "<1", + "spipu/html2pdf": "<5.2.8", + "spiral/roadrunner": "<2025.1", + "spoon/library": "<1.4.1", + "spoonity/tcpdf": "<6.2.22", + "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", + "ssddanbrown/bookstack": "<24.05.1", + "starcitizentools/citizen-skin": ">=1.9.4,<3.9", + "starcitizentools/short-description": ">=4,<4.0.1", + "starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1", + "starcitizenwiki/embedvideo": "<=4", + "statamic/cms": "<=5.22", + "stormpath/sdk": "<9.9.99", + "studio-42/elfinder": "<=2.1.64", + "studiomitte/friendlycaptcha": "<0.1.4", + "subhh/libconnect": "<7.0.8|>=8,<8.1", + "sukohi/surpass": "<1", + "sulu/form-bundle": ">=2,<2.5.3", + "sulu/sulu": "<1.6.44|>=2,<2.5.25|>=2.6,<2.6.9|>=3.0.0.0-alpha1,<3.0.0.0-alpha3", + "sumocoders/framework-user-bundle": "<1.4", + "superbig/craft-audit": "<3.0.2", + "svewap/a21glossary": "<=0.4.10", + "swag/paypal": "<5.4.4", + "swiftmailer/swiftmailer": "<6.2.5", + "swiftyedit/swiftyedit": "<1.2", + "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", + "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", + "sylius/grid-bundle": "<1.10.1", + "sylius/paypal-plugin": "<1.6.2|>=1.7,<1.7.2|>=2,<2.0.2", + "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", + "sylius/sylius": "<1.12.19|>=1.13.0.0-alpha1,<1.13.4", + "symbiote/silverstripe-multivaluefield": ">=3,<3.1", + "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", + "symbiote/silverstripe-seed": "<6.0.3", + "symbiote/silverstripe-versionedfiles": "<=2.0.3", + "symfont/process": ">=0", + "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", + "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4", + "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", + "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<5.3.15|>=5.4.3,<5.4.4|>=6.0.3,<6.0.4", + "symfony/http-client": ">=4.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8", + "symfony/http-foundation": "<5.4.50|>=6,<6.4.29|>=7,<7.3.7", + "symfony/http-kernel": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", + "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1", + "symfony/mime": ">=4.3,<4.3.8", + "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/polyfill": ">=1,<1.10", + "symfony/polyfill-php55": ">=1,<1.10", + "symfony/process": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/routing": ">=2,<2.0.19", + "symfony/runtime": ">=5.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8", + "symfony/security-bundle": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.4.10|>=7,<7.0.10|>=7.1,<7.1.3", + "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9", + "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8", + "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8", + "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12", + "symfony/symfony": "<5.4.50|>=6,<6.4.29|>=7,<7.3.7", + "symfony/translation": ">=2,<2.0.17", + "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", + "symfony/ux-autocomplete": "<2.11.2", + "symfony/ux-live-component": "<2.25.1", + "symfony/ux-twig-component": "<2.25.1", + "symfony/validator": "<5.4.43|>=6,<6.4.11|>=7,<7.1.4", + "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", + "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", + "symfony/webhook": ">=6.3,<6.3.8", + "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7|>=2.2.0.0-beta1,<2.2.0.0-beta2", + "symphonycms/symphony-2": "<2.6.4", + "t3/dce": "<0.11.5|>=2.2,<2.6.2", + "t3g/svg-sanitizer": "<1.0.3", + "t3s/content-consent": "<1.0.3|>=2,<2.0.2", + "tastyigniter/tastyigniter": "<4", + "tcg/voyager": "<=1.8", + "tecnickcom/tc-lib-pdf-font": "<2.6.4", + "tecnickcom/tcpdf": "<6.8", + "terminal42/contao-tablelookupwizard": "<3.3.5", + "thelia/backoffice-default-template": ">=2.1,<2.1.2", + "thelia/thelia": ">=2.1,<2.1.3", + "theonedemon/phpwhois": "<=4.2.5", + "thinkcmf/thinkcmf": "<6.0.8", + "thorsten/phpmyfaq": "<4.0.16|>=4.1.0.0-alpha,<=4.1.0.0-beta2", + "tikiwiki/tiki-manager": "<=17.1", + "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", + "tinymce/tinymce": "<7.2", + "tinymighty/wiki-seo": "<1.2.2", + "titon/framework": "<9.9.99", + "tltneon/lgsl": "<7", + "tobiasbg/tablepress": "<=2.0.0.0-RC1", + "topthink/framework": "<6.0.17|>=6.1,<=8.0.4", + "topthink/think": "<=6.1.1", + "topthink/thinkphp": "<=3.2.3|>=6.1.3,<=8.0.4", + "torrentpier/torrentpier": "<=2.8.8", + "tpwd/ke_search": "<4.0.3|>=4.1,<4.6.6|>=5,<5.0.2", + "tribalsystems/zenario": "<=9.7.61188", + "truckersmp/phpwhois": "<=4.3.1", + "ttskch/pagination-service-provider": "<1", + "twbs/bootstrap": "<3.4.1|>=4,<4.3.1", + "twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19", + "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", + "typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", + "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", + "typo3/cms-dashboard": ">=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", + "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", + "typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-felogin": ">=4.2,<4.2.3", + "typo3/cms-fluid": "<4.3.4|>=4.4,<4.4.1", + "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-frontend": "<4.3.9|>=4.4,<4.4.5", + "typo3/cms-indexed-search": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2", + "typo3/cms-lowlevel": ">=11,<=11.5.41", + "typo3/cms-recordlist": ">=11,<11.5.48", + "typo3/cms-recycler": ">=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", + "typo3/cms-redirects": ">=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", + "typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30", + "typo3/cms-scheduler": ">=11,<=11.5.41", + "typo3/cms-setup": ">=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11", + "typo3/cms-webhooks": ">=12,<=12.4.30|>=13,<=13.4.11", + "typo3/cms-workspaces": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", + "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", + "typo3/html-sanitizer": ">=1,<=1.5.2|>=2,<=2.1.3", + "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3", + "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", + "typo3/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5", + "typo3fluid/fluid": ">=2,<2.0.8|>=2.1,<2.1.7|>=2.2,<2.2.4|>=2.3,<2.3.7|>=2.4,<2.4.4|>=2.5,<2.5.11|>=2.6,<2.6.10", + "ua-parser/uap-php": "<3.8", + "uasoft-indonesia/badaso": "<=2.9.7", + "unisharp/laravel-filemanager": "<2.9.1", + "universal-omega/dynamic-page-list3": "<3.6.4", + "unopim/unopim": "<=0.3", + "userfrosting/userfrosting": ">=0.3.1,<4.6.3", + "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2", + "uvdesk/community-skeleton": "<=1.1.1", + "uvdesk/core-framework": "<=1.1.1", + "vanilla/safecurl": "<0.9.2", + "verbb/comments": "<1.5.5", + "verbb/formie": "<=2.1.43", + "verbb/image-resizer": "<2.0.9", + "verbb/knock-knock": "<1.2.8", + "verot/class.upload.php": "<=2.1.6", + "vertexvaar/falsftp": "<0.2.6", + "villagedefrance/opencart-overclocked": "<=1.11.1", + "vova07/yii2-fileapi-widget": "<0.1.9", + "vrana/adminer": "<=4.8.1", + "vufind/vufind": ">=2,<9.1.1", + "waldhacker/hcaptcha": "<2.1.2", + "wallabag/tcpdf": "<6.2.22", + "wallabag/wallabag": "<2.6.11", + "wanglelecc/laracms": "<=1.0.3", + "wapplersystems/a21glossary": "<=0.4.10", + "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9", + "web-auth/webauthn-lib": ">=4.5,<4.9", + "web-feet/coastercms": "==5.5", + "web-tp3/wec_map": "<3.0.3", + "webbuilders-group/silverstripe-kapost-bridge": "<0.4", + "webcoast/deferred-image-processing": "<1.0.2", + "webklex/laravel-imap": "<5.3", + "webklex/php-imap": "<5.3", + "webpa/webpa": "<3.1.2", + "webreinvent/vaahcms": "<=2.3.1", + "wikibase/wikibase": "<=1.39.3", + "wikimedia/parsoid": "<0.12.2", + "willdurand/js-translation-bundle": "<2.1.1", + "winter/wn-backend-module": "<1.2.4", + "winter/wn-cms-module": "<1.0.476|>=1.1,<1.1.11|>=1.2,<1.2.7", + "winter/wn-dusk-plugin": "<2.1", + "winter/wn-system-module": "<1.2.4", + "wintercms/winter": "<=1.2.3", + "wireui/wireui": "<1.19.3|>=2,<2.1.3", + "woocommerce/woocommerce": "<6.6|>=8.8,<8.8.5|>=8.9,<8.9.3", + "wp-cli/wp-cli": ">=0.12,<2.5", + "wp-graphql/wp-graphql": "<=1.14.5", + "wp-premium/gravityforms": "<2.4.21", + "wpanel/wpanel4-cms": "<=4.3.1", + "wpcloud/wp-stateless": "<3.2", + "wpglobus/wpglobus": "<=1.9.6", + "wwbn/avideo": "<14.3", + "xataface/xataface": "<3", + "xpressengine/xpressengine": "<3.0.15", + "yab/quarx": "<2.4.5", + "yeswiki/yeswiki": "<=4.5.4", + "yetiforce/yetiforce-crm": "<6.5", + "yidashi/yii2cmf": "<=2", + "yii2mod/yii2-cms": "<1.9.2", + "yiisoft/yii": "<1.1.31", + "yiisoft/yii2": "<2.0.52", + "yiisoft/yii2-authclient": "<2.2.15", + "yiisoft/yii2-bootstrap": "<2.0.4", + "yiisoft/yii2-dev": "<=2.0.45", + "yiisoft/yii2-elasticsearch": "<2.0.5", + "yiisoft/yii2-gii": "<=2.2.4", + "yiisoft/yii2-jui": "<2.0.4", + "yiisoft/yii2-redis": "<2.0.20", + "yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6", + "yoast-seo-for-typo3/yoast_seo": "<7.2.3", + "yourls/yourls": "<=1.10.2", + "yuan1994/tpadmin": "<=1.3.12", + "yungifez/skuul": "<=2.6.5", + "z-push/z-push-dev": "<2.7.6", + "zencart/zencart": "<=1.5.7.0-beta", + "zendesk/zendesk_api_client_php": "<2.2.11", + "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", + "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", + "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", + "zendframework/zend-db": "<2.2.10|>=2.3,<2.3.5", + "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3", + "zendframework/zend-diactoros": "<1.8.4", + "zendframework/zend-feed": "<2.10.3", + "zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-http": "<2.8.1", + "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6", + "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3", + "zendframework/zend-mail": "<2.4.11|>=2.5,<2.7.2", + "zendframework/zend-navigation": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-session": ">=2,<2.2.9|>=2.3,<2.3.4", + "zendframework/zend-validator": ">=2.3,<2.3.6", + "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6", + "zendframework/zendframework": "<=3", + "zendframework/zendframework1": "<1.12.20", + "zendframework/zendopenid": "<2.0.2", + "zendframework/zendrest": "<2.0.2", + "zendframework/zendservice-amazon": "<2.0.3", + "zendframework/zendservice-api": "<1", + "zendframework/zendservice-audioscrobbler": "<2.0.2", + "zendframework/zendservice-nirvanix": "<2.0.2", + "zendframework/zendservice-slideshare": "<2.0.2", + "zendframework/zendservice-technorati": "<2.0.2", + "zendframework/zendservice-windowsazure": "<2.0.2", + "zendframework/zendxml": ">=1,<1.0.1", + "zenstruck/collection": "<0.2.1", + "zetacomponents/mail": "<1.8.2", + "zf-commons/zfc-user": "<1.2.2", + "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3", + "zfr/zfr-oauth2-server-module": "<0.1.2", + "zoujingli/thinkadmin": "<=6.1.53" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "role": "maintainer" + }, + { + "name": "Ilya Tribusean", + "email": "slash3b@gmail.com", + "role": "maintainer" + } + ], + "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", + "keywords": [ + "dev" + ], + "support": { + "issues": "https://github.com/Roave/SecurityAdvisories/issues", + "source": "https://github.com/Roave/SecurityAdvisories/tree/latest" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories", + "type": "tidelift" + } + ], + "time": "2026-01-15T23:06:28+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-09-07T05:25:07+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "8.26.0", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "d247cdc04b91956bdcfaa0b1313c01960b189d3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/d247cdc04b91956bdcfaa0b1313c01960b189d3c", + "reference": "d247cdc04b91956bdcfaa0b1313c01960b189d3c", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.2.0", + "php": "^7.4 || ^8.0", + "phpstan/phpdoc-parser": "^2.3.0", + "squizlabs/php_codesniffer": "^4.0.1" + }, + "require-dev": { + "phing/phing": "3.0.1|3.1.0", + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/phpstan": "2.1.33", + "phpstan/phpstan-deprecation-rules": "2.0.3", + "phpstan/phpstan-phpunit": "2.0.11", + "phpstan/phpstan-strict-rules": "2.0.7", + "phpunit/phpunit": "9.6.31|10.5.60|11.4.4|11.5.46|12.5.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.26.0" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2025-12-21T18:01:15+00:00" + }, + { + "name": "softcreatr/jsonpath", + "version": "0.8.3", + "source": { + "type": "git", + "url": "https://github.com/SoftCreatR/JSONPath.git", + "reference": "fc12dee0b46f3fa3a175c4051dbab60984acef4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/fc12dee0b46f3fa3a175c4051dbab60984acef4b", + "reference": "fc12dee0b46f3fa3a175c4051dbab60984acef4b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.0" + }, + "replace": { + "flow/jsonpath": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "autoload": { + "psr-4": { + "Flow\\JSONPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stephen Frank", + "email": "stephen@flowsa.com", + "homepage": "https://prismaticbytes.com", + "role": "Developer" + }, + { + "name": "Sascha Greuel", + "email": "hello@1-2.dev", + "homepage": "https://1-2.dev", + "role": "Developer" + } + ], + "description": "JSONPath implementation for parsing, searching and flattening arrays", + "support": { + "email": "hello@1-2.dev", + "forum": "https://github.com/SoftCreatR/JSONPath/discussions", + "issues": "https://github.com/SoftCreatR/JSONPath/issues", + "source": "https://github.com/SoftCreatR/JSONPath" + }, + "funding": [ + { + "url": "https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4", + "type": "custom" + }, + { + "url": "https://github.com/softcreatr", + "type": "github" + } + ], + "time": "2023-08-17T20:14:00+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0525c73950de35ded110cffafb9892946d7771b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5", + "reference": "0525c73950de35ded110cffafb9892946d7771b5", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=7.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-10T16:43:36+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "642117d18bc56832e74b68235359ccefab03dd11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/642117d18bc56832e74b68235359ccefab03dd11", + "reference": "642117d18bc56832e74b68235359ccefab03dd11", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "ext-redis": "<6.1", + "ext-relay": "<0.12.1", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-12-28T10:45:24+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.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": "2025-03-13T15:25:07+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "b38026df55197f9e39a44f3215788edf83187b80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80", + "reference": "b38026df55197f9e39a44f3215788edf83187b80", + "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.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:39:26+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "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/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-04T07:36:47+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T18:53:00+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "roave/security-advisories": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "~8.4.0", + "ext-json": "*", + "ext-openssl": "*", + "ext-pdo": "*" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "8.4.16" + }, + "plugin-api-version": "2.9.0" +} diff --git a/config/.gitignore b/config/.gitignore index d5ce9da0..96043010 100644 --- a/config/.gitignore +++ b/config/.gitignore @@ -1,2 +1 @@ development.config.php -database.php diff --git a/config/autoload/.gitignore b/config/autoload/.gitignore index 65f817e9..1a83fda6 100644 --- a/config/autoload/.gitignore +++ b/config/autoload/.gitignore @@ -1,4 +1,2 @@ -*.production.php local.php -development.config.global.php -development.local.php +*.local.php diff --git a/config/autoload/cors.develope.php b/config/autoload/cors.develope.php new file mode 100644 index 00000000..19c8b962 --- /dev/null +++ b/config/autoload/cors.develope.php @@ -0,0 +1,13 @@ + [ + 'allowed_origins' => ['http://localhost:5173','http://localhost'], + 'allowed_headers' => ['x-ident', 'Authorization', 'Authentication', 'Content-Type'], // No custom headers allowed + 'allowed_max_age' => '3600', // 60 minutes + 'credentials_allowed' => true, // Disallow cookies + 'exposed_headers' => [], // No headers are exposed + ], +]; diff --git a/config/autoload/cors.global.php b/config/autoload/cors.global.php new file mode 100644 index 00000000..2c9c2000 --- /dev/null +++ b/config/autoload/cors.global.php @@ -0,0 +1,13 @@ + [ + 'allowed_origins' => [ConfigurationInterface::ANY_ORIGIN], + 'allowed_headers' => ['x-ident', 'Authorization', 'Authentication', 'Content-Type'], // No custom headers allowed + 'allowed_max_age' => '3600', // 60 minutes + 'credentials_allowed' => true, // Disallow cookies + 'exposed_headers' => [], // No headers are exposed + ], +]; diff --git a/config/autoload/database.action.php b/config/autoload/database.action.php new file mode 100644 index 00000000..13ee96f3 --- /dev/null +++ b/config/autoload/database.action.php @@ -0,0 +1,20 @@ + [ + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'port' => '3306', + 'user' => 'dev', + 'password' => 'dev', + 'dbname' => 'db', + 'charset' => 'utf8mb4', + 'defaultTableOptions' => [ + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_general_ci', + 'engine' => 'InnoDB', + ], + 'error' => PDO::ERRMODE_EXCEPTION, + 'emulate_prepares' => false, + ] +]; diff --git a/config/autoload/database.develope.php b/config/autoload/database.develope.php new file mode 100644 index 00000000..08271b9e --- /dev/null +++ b/config/autoload/database.develope.php @@ -0,0 +1,20 @@ + [ + 'driver' => 'mysql', + 'host' => 'database', + 'port' => '3306', + 'user' => 'dev', + 'password' => 'dev', + 'dbname' => 'db', + 'charset' => 'utf8mb4', + 'defaultTableOptions' => [ + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_general_ci', + 'engine' => 'InnoDB', + ], + 'error' => PDO::ERRMODE_EXCEPTION, + 'emulate_prepares' => false, + ] +]; diff --git a/config/autoload/database.global.php.dist b/config/autoload/database.global.php.dist new file mode 100644 index 00000000..1152a4f8 --- /dev/null +++ b/config/autoload/database.global.php.dist @@ -0,0 +1,20 @@ + [ + 'driver' => 'driver', + 'host' => 'host', + 'port' => 'port', + 'user' => 'user', + 'password' => 'password', + 'dbname' => 'dbname', + 'charset' => 'utf8mb4', + 'defaultTableOptions' => [ + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_general_ci', + 'engine' => 'InnoDB', + ], + 'error' => PDO::ERRMODE_EXCEPTION, + 'emulate_prepares' => false, + ] +]; diff --git a/config/autoload/database.testing.php b/config/autoload/database.testing.php new file mode 100644 index 00000000..73522bc0 --- /dev/null +++ b/config/autoload/database.testing.php @@ -0,0 +1,20 @@ + [ + 'driver' => 'mysql', + 'host' => 'database-testing', + 'port' => '3306', + 'user' => 'dev', + 'password' => 'dev', + 'dbname' => 'db', + 'charset' => 'utf8mb4', + 'defaultTableOptions' => [ + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_general_ci', + 'engine' => 'InnoDB', + ], + 'error' => PDO::ERRMODE_EXCEPTION, + 'emulate_prepares' => false, + ] +]; diff --git a/config/autoload/dependencies.action.php b/config/autoload/dependencies.action.php new file mode 100644 index 00000000..955817c8 --- /dev/null +++ b/config/autoload/dependencies.action.php @@ -0,0 +1,29 @@ + [ + 'aliases' => [ + PDO::class => 'database', + Envms\FluentPDO\Query::class => 'query', + UuidFactoryInterface::class => 'uuid', + Psr\Log\LoggerInterface::class => 'logger', + Symfony\Component\Mailer\MailerInterface::class => 'mailer', + ], + 'invokables' => [ + ], + 'factories' => [ + 'database' => DatabaseFactory::class, + 'query' => QueryFactory::class, + 'logger' => LoggerFactory::class, + 'uuid' => UuidFactory::class, + 'mailer' => NullMailerFactory::class, + ], + ], +]; diff --git a/config/autoload/dependencies.global.php b/config/autoload/dependencies.global.php new file mode 100644 index 00000000..2a172689 --- /dev/null +++ b/config/autoload/dependencies.global.php @@ -0,0 +1,40 @@ + [ + // Use 'aliases' to alias a service name to another service. The + // key is the alias name, the value is the service to which it points. + 'aliases' => [ + PDO::class => 'database', + Envms\FluentPDO\Query::class => 'query', + Psr\Log\LoggerInterface::class => 'logger', + UuidFactoryInterface::class => 'uuid', + Symfony\Component\Mailer\MailerInterface::class => 'mailer', + ], + // Use 'invokables' for constructor-less services, or services that do + // not require arguments to the constructor. Map a service name to the + // class name. + 'invokables' => [ + ], + // Use 'factories' for services provided by callbacks/factory classes. + 'factories' => [ + 'database' => DatabaseFactory::class, + 'query' => QueryFactory::class, + 'logger' => LoggerFactory::class, + 'uuid' => UuidFactory::class, + 'mailer' => MailFactory::class, + ], + ], +]; diff --git a/config/autoload/dependencies.testing.php b/config/autoload/dependencies.testing.php new file mode 100644 index 00000000..955817c8 --- /dev/null +++ b/config/autoload/dependencies.testing.php @@ -0,0 +1,29 @@ + [ + 'aliases' => [ + PDO::class => 'database', + Envms\FluentPDO\Query::class => 'query', + UuidFactoryInterface::class => 'uuid', + Psr\Log\LoggerInterface::class => 'logger', + Symfony\Component\Mailer\MailerInterface::class => 'mailer', + ], + 'invokables' => [ + ], + 'factories' => [ + 'database' => DatabaseFactory::class, + 'query' => QueryFactory::class, + 'logger' => LoggerFactory::class, + 'uuid' => UuidFactory::class, + 'mailer' => NullMailerFactory::class, + ], + ], +]; diff --git a/config/autoload/development.local.php.dist b/config/autoload/development.local.php.dist new file mode 100644 index 00000000..4aca5a7c --- /dev/null +++ b/config/autoload/development.local.php.dist @@ -0,0 +1,35 @@ + [ + 'factories' => [ + ErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class, + 'Mezzio\Whoops' => Container\WhoopsFactory::class, + 'Mezzio\WhoopsPageHandler' => Container\WhoopsPageHandlerFactory::class, + ], + ], + 'whoops' => [ + 'json_exceptions' => [ + 'display' => true, + 'show_trace' => true, + 'ajax_only' => true, + ], + ], +]; diff --git a/config/autoload/local.php.dist b/config/autoload/local.php.dist new file mode 100644 index 00000000..db858d77 --- /dev/null +++ b/config/autoload/local.php.dist @@ -0,0 +1,13 @@ + [ + 'path' => __DIR__ . '/../../data/log/', + ] +]; diff --git a/config/autoload/mail.develope.php b/config/autoload/mail.develope.php new file mode 100644 index 00000000..7a17007e --- /dev/null +++ b/config/autoload/mail.develope.php @@ -0,0 +1,8 @@ + [ + 'dsn' => 'smtp://mailhog:1025', + 'from' => 'hackathon@exdrals.de', + ], +]; diff --git a/config/autoload/mail.testing.php b/config/autoload/mail.testing.php new file mode 100644 index 00000000..7a17007e --- /dev/null +++ b/config/autoload/mail.testing.php @@ -0,0 +1,8 @@ + [ + 'dsn' => 'smtp://mailhog:1025', + 'from' => 'hackathon@exdrals.de', + ], +]; diff --git a/config/autoload/mezzio.global.php b/config/autoload/mezzio.global.php new file mode 100644 index 00000000..64d701e7 --- /dev/null +++ b/config/autoload/mezzio.global.php @@ -0,0 +1,24 @@ + true, + + // Enable debugging; typically used to provide debugging information within templates. + 'debug' => false, + 'mezzio' => [ + // Provide templates for the error handling middleware to use when + // generating responses. + 'error_handler' => [ + 'template_404' => 'error::404', + 'template_error' => 'error::error', + ], + ], +]; diff --git a/config/autoload/migrations.action.php b/config/autoload/migrations.action.php new file mode 100644 index 00000000..166dbab1 --- /dev/null +++ b/config/autoload/migrations.action.php @@ -0,0 +1,23 @@ + [ + 'table_storage' => [ + 'table_name' => 'MigrationVersions', + 'version_column_name' => 'version', + 'version_column_length' => 192, + 'executed_at_column_name' => 'executedAt', + 'execution_time_column_name' => 'executionTime', + ], + + 'migrations_paths' => [ + 'Migrations' => __DIR__ . '/../../database/migrations', + 'TestDataMigrations' => __DIR__ . '/../../tests/FunctionalTest/database/migrations', + ], + + 'all_or_nothing' => true, + 'transactional' => true, + 'check_database_platform' => true, + 'organize_migrations' => 'none', + ], +]; diff --git a/config/autoload/migrations.develope.php b/config/autoload/migrations.develope.php new file mode 100644 index 00000000..0626d103 --- /dev/null +++ b/config/autoload/migrations.develope.php @@ -0,0 +1,22 @@ + [ + 'table_storage' => [ + 'table_name' => 'MigrationVersions', + 'version_column_name' => 'version', + 'version_column_length' => 192, + 'executed_at_column_name' => 'executedAt', + 'execution_time_column_name' => 'executionTime', + ], + + 'migrations_paths' => [ + 'Migrations' => __DIR__ . '/../../database/migrations', + ], + + 'all_or_nothing' => true, + 'transactional' => true, + 'check_database_platform' => true, + 'organize_migrations' => 'none', + ], +]; diff --git a/config/autoload/migrations.global.php b/config/autoload/migrations.global.php new file mode 100644 index 00000000..0626d103 --- /dev/null +++ b/config/autoload/migrations.global.php @@ -0,0 +1,22 @@ + [ + 'table_storage' => [ + 'table_name' => 'MigrationVersions', + 'version_column_name' => 'version', + 'version_column_length' => 192, + 'executed_at_column_name' => 'executedAt', + 'execution_time_column_name' => 'executionTime', + ], + + 'migrations_paths' => [ + 'Migrations' => __DIR__ . '/../../database/migrations', + ], + + 'all_or_nothing' => true, + 'transactional' => true, + 'check_database_platform' => true, + 'organize_migrations' => 'none', + ], +]; diff --git a/config/autoload/migrations.testing.php b/config/autoload/migrations.testing.php new file mode 100644 index 00000000..166dbab1 --- /dev/null +++ b/config/autoload/migrations.testing.php @@ -0,0 +1,23 @@ + [ + 'table_storage' => [ + 'table_name' => 'MigrationVersions', + 'version_column_name' => 'version', + 'version_column_length' => 192, + 'executed_at_column_name' => 'executedAt', + 'execution_time_column_name' => 'executionTime', + ], + + 'migrations_paths' => [ + 'Migrations' => __DIR__ . '/../../database/migrations', + 'TestDataMigrations' => __DIR__ . '/../../tests/FunctionalTest/database/migrations', + ], + + 'all_or_nothing' => true, + 'transactional' => true, + 'check_database_platform' => true, + 'organize_migrations' => 'none', + ], +]; diff --git a/config/autoload/project.global.php b/config/autoload/project.global.php index b99c1907..7ef504b6 100644 --- a/config/autoload/project.global.php +++ b/config/autoload/project.global.php @@ -2,14 +2,15 @@ return [ 'project' => [ - 'uri' => 'build.hackathon.exdrals.de', + 'uri' => 'dev.ownhackathon.de', ], 'api' => [ 'access' => [ 'domain' => [ 'whitelist' => [ - 'build.hackathon.exdrals.de', - 'hackathon.exdrals.de', + 'build.ownhackathon.de', + 'dev.ownhackathon.de', + 'ownhackathon.de', ], ], ], diff --git a/config/autoload/routes.global.php b/config/autoload/routes.global.php new file mode 100644 index 00000000..30521647 --- /dev/null +++ b/config/autoload/routes.global.php @@ -0,0 +1,29 @@ + [ + //.. + 'invokables' => [ + /* ... */ + // Comment out or remove the following line: + // Mezzio\Router\RouterInterface::class => Mezzio\Router\FastRouteRouter::class, + /* ... */ + ], + 'factories' => [ + /* ... */ + // Add this line; the specified factory now creates the router instance: + /* ... */ + ], + ], + + // Add the following to enable caching support: + 'router' => [ + 'fastroute' => [ + // Enable caching support: + 'cache_enabled' => false, + // Optional (but recommended) cache file path: + 'cache_file' => './../../data/cache/fastroute.php.cache', + ], + ], + + 'routes' => [ /* ... */], +]; diff --git a/config/autoload/token.action.php b/config/autoload/token.action.php new file mode 100644 index 00000000..97d7be24 --- /dev/null +++ b/config/autoload/token.action.php @@ -0,0 +1,20 @@ + [ + 'refresh' => [ + 'key' => 'ixo>+W%!Rf/\@)m2UMok:/A_gL 'HS512', + 'duration' => 60 * 60 * 24 * 7 * 12, + 'iss' => 'localhost', + 'aud' => 'localhost', + ], + 'access' => [ + 'key' => 'b:?Y@5JCWF:yi{o>irc(3$HFcR-#b\SA', + 'algorithmus' => 'HS512', + 'duration' => 60 * 15, + 'iss' => 'localhost', + 'aud' => 'localhost', + ], + ], +]; diff --git a/config/autoload/token.develope.php b/config/autoload/token.develope.php new file mode 100644 index 00000000..97d7be24 --- /dev/null +++ b/config/autoload/token.develope.php @@ -0,0 +1,20 @@ + [ + 'refresh' => [ + 'key' => 'ixo>+W%!Rf/\@)m2UMok:/A_gL 'HS512', + 'duration' => 60 * 60 * 24 * 7 * 12, + 'iss' => 'localhost', + 'aud' => 'localhost', + ], + 'access' => [ + 'key' => 'b:?Y@5JCWF:yi{o>irc(3$HFcR-#b\SA', + 'algorithmus' => 'HS512', + 'duration' => 60 * 15, + 'iss' => 'localhost', + 'aud' => 'localhost', + ], + ], +]; diff --git a/config/autoload/token.global.php.dist b/config/autoload/token.global.php.dist new file mode 100644 index 00000000..ac8d71d3 --- /dev/null +++ b/config/autoload/token.global.php.dist @@ -0,0 +1,20 @@ + [ + 'refresh' => [ + 'key' => 'token secret', + 'algorithmus' => 'HS512', + 'duration' => 60 * 60 * 24 * 7 * 12, + 'iss' => 'Issuer of the token', + 'aud' => 'recipients of the token', + ], + 'access' => [ + 'key' =>'token secret', + 'algorithmus' => 'HS512', + 'duration' => 60 * 5, + 'iss' => 'Issuer of the token', + 'aud' => 'recipients of the token', + ], + ], +]; diff --git a/config/autoload/token.testing.php b/config/autoload/token.testing.php new file mode 100644 index 00000000..97d7be24 --- /dev/null +++ b/config/autoload/token.testing.php @@ -0,0 +1,20 @@ + [ + 'refresh' => [ + 'key' => 'ixo>+W%!Rf/\@)m2UMok:/A_gL 'HS512', + 'duration' => 60 * 60 * 24 * 7 * 12, + 'iss' => 'localhost', + 'aud' => 'localhost', + ], + 'access' => [ + 'key' => 'b:?Y@5JCWF:yi{o>irc(3$HFcR-#b\SA', + 'algorithmus' => 'HS512', + 'duration' => 60 * 15, + 'iss' => 'localhost', + 'aud' => 'localhost', + ], + ], +]; diff --git a/config/config.php b/config/config.php new file mode 100644 index 00000000..49142d81 --- /dev/null +++ b/config/config.php @@ -0,0 +1,54 @@ + __DIR__ . '/../data/cache/config-cache.php', +]; + +$aggregator = new ConfigAggregator([ + \Mezzio\Cors\ConfigProvider::class, + \Laminas\InputFilter\ConfigProvider::class, + \Laminas\Filter\ConfigProvider::class, + \Laminas\Validator\ConfigProvider::class, + \Mezzio\Helper\ConfigProvider::class, + \Mezzio\Tooling\ConfigProvider::class, + \Mezzio\Router\FastRouteRouter\ConfigProvider::class, + \Laminas\HttpHandlerRunner\ConfigProvider::class, + // Include cache configuration + new ArrayProvider($cacheConfig), + \Mezzio\ConfigProvider::class, + \Mezzio\Router\ConfigProvider::class, + \Laminas\Diactoros\ConfigProvider::class, + // Swoole config to overwrite some services (if installed) + class_exists(\Mezzio\Swoole\ConfigProvider::class) + ? \Mezzio\Swoole\ConfigProvider::class + : function (): array { + return []; + }, + // Default App module config + \Core\ConfigProvider::class, + \App\ConfigProvider::class, + // Load application config in a pre-defined order in such a way that local settings + // overwrite global settings. (Loaded as first to last): + // - `global.php` + // - `*.global.php` + // - `local.php` + // - `*.local.php` + new PhpFileProvider( + realpath(__DIR__) . sprintf( + '/autoload/{,*.}{global,local,%s}.php', + getenv('APP_ENV') ?: 'production' + ) + ), + // Load development config if it exists + new PhpFileProvider(realpath(__DIR__) . '/development.config.php'), +], $cacheConfig['config_cache_path']); + +return $aggregator->getMergedConfig(); diff --git a/config/container.php b/config/container.php new file mode 100644 index 00000000..b7358d18 --- /dev/null +++ b/config/container.php @@ -0,0 +1,14 @@ + true, + ConfigAggregator::ENABLE_CACHE => false, +]; diff --git a/config/migrations.template.tpl b/config/migrations.template.tpl new file mode 100644 index 00000000..83fc0322 --- /dev/null +++ b/config/migrations.template.tpl @@ -0,0 +1,19 @@ + extends AbstractMigration +{ + public function up(Schema $schema): void + { + + } + + public function down(Schema $schema): void + { + + } +} diff --git a/config/pipeline.php b/config/pipeline.php new file mode 100644 index 00000000..df8f068c --- /dev/null +++ b/config/pipeline.php @@ -0,0 +1,52 @@ +pipe([ + ApiErrorHandlerMiddleware::class, + ServerUrlMiddleware::class, + BodyParamsMiddleware::class, + + CorsMiddleware::class, + RouteMiddleware::class, + + ImplicitHeadMiddleware::class, + ImplicitOptionsMiddleware::class, + MethodNotAllowedMiddleware::class, + + UrlHelperMiddleware::class, + + ClientIdentificationMiddleware::class, + RequestAuthenticationMiddleware::class, + LastAktivityUpdaterMiddleware::class, + + DispatchMiddleware::class, + + RouteNotFoundMiddleware::class, + NotFoundHandler::class, + ]); +}; diff --git a/config/routes.php b/config/routes.php new file mode 100644 index 00000000..6299cde2 --- /dev/null +++ b/config/routes.php @@ -0,0 +1,91 @@ +get( + path: '/api/ping[/]', + middleware: [ + App\Handler\PingHandler::class, + ], + name: RouteIdent::PING->value + ); + $app->get( + path: '/api/token/refresh[/]', + middleware: [ + App\Middleware\Token\RefreshTokenValidationMiddleware::class, + App\Middleware\Token\RefreshTokenDatabaseExistenceMiddleware::class, + App\Middleware\Token\RefreshTokenMatchClientIdentificationMiddleware::class, + App\Middleware\Token\RefreshTokenAccountMiddleware::class, + App\Middleware\Token\GenerateAccessTokenMiddleware::class, + App\Handler\Account\AccessTokenHandler::class, + ], + name: RouteIdent::ACCESS_TOKEN_REFRESH->value + ); + $app->post( + path: '/api/account/authentication[/]', + middleware: [ + App\Middleware\Account\LoginAuthentication\AuthenticationConditionsMiddleware::class, + App\Middleware\Account\LoginAuthentication\AuthenticationValidationMiddleware::class, + App\Middleware\Account\LoginAuthentication\AuthenticationMiddleware::class, + App\Middleware\Token\GenerateRefreshTokenMiddleware::class, + App\Middleware\Token\GenerateAccessTokenMiddleware::class, + App\Middleware\Account\LoginAuthentication\PersistAuthenticationMiddleware::class, + App\Handler\Account\AuthenticationHandler::class, + ], + name: RouteIdent::ACCOUNT_AUTHENTICATE->value + ); + + $app->post( + path: '/api/account', + middleware: [ + App\Middleware\Account\Validation\EmailInputValidatorMiddleware::class, + App\Middleware\Account\RegisterMiddleware::class, + App\Handler\Account\AccountRegisterHandler::class, + ], + name: RouteIdent::ACCOUNT_CREATE->value + ); + + $app->post( + path: '/api/account/activation/[{token}[/]]', + middleware: [ + App\Middleware\Account\Validation\ActivationInputValidatorMiddleware::class, + App\Middleware\Account\ActivationMiddleware::class, + App\Handler\Account\AccountActivationHandler::class, + ], + name: RouteIdent::ACCOUNT_ACTIVATION->value + ); + + $app->post( + path: '/api/account/password/forgotten[/]', + middleware: [ + App\Middleware\Account\Validation\EmailInputValidatorMiddleware::class, + App\Middleware\Account\PasswordForgottenMiddleware::class, + App\Handler\Account\AccountPasswordForgottenHandler::class, + ], + name: RouteIdent::ACCOUNT_PASSWORD_FORGOTTEN->value + ); + + $app->patch( + path: '/api/account/password/[{token}[/]]', + middleware: [ + App\Middleware\Account\Validation\PasswordInputValidatorMiddleware::class, + App\Middleware\Account\PasswordChangeMiddleware::class, + App\Handler\Account\AccountPasswordHandler::class, + ], + name: RouteIdent::ACCOUNT_PASSWORD_SET->value + ); + + $app->get( + path: '/api/account/logout', + middleware: [ + App\Middleware\Token\AccessTokenValidationMiddleware::class, + App\Middleware\Account\LogoutMiddleware::class, + App\Handler\Account\LogoutHandler::class, + ], + name: RouteIdent::ACCOUNT_LOGOUT->value + ); +}; diff --git a/constants.php b/constants.php index 79cd618c..c4e994f0 100644 --- a/constants.php +++ b/constants.php @@ -1,3 +1,5 @@ createTable('Account'); + + $table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'unsigned' => true,]); + $table->addColumn('uuid', Types::STRING, ['length' => 32,]); + $table->addColumn('name', Types::STRING, ['length' => 64, 'notnull' => false,]); + $table->addColumn('password', Types::STRING, ['length' => 255, 'notnull' => false,]); + $table->addColumn('email', Types::STRING, ['length' => 512,]); + $table->addColumn('registeredAt', Types::DATETIME_IMMUTABLE, ['default' => 'CURRENT_TIMESTAMP',]); + $table->addColumn('lastActionAt', Types::DATETIME_IMMUTABLE, ['default' => 'CURRENT_TIMESTAMP',]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['uuid'], 'account_uuid_UNIQUE'); + $table->addUniqueIndex(['name'], 'account_name_UNIQUE'); + $table->addUniqueIndex(['email'], 'account_email_UNIQUE'); + } + + public function down(Schema $schema): void + { + $schema->dropTable('Account'); + } +} diff --git a/database/migrations/Version20231103224045_CreateAccountAccessAuthTable.php b/database/migrations/Version20231103224045_CreateAccountAccessAuthTable.php new file mode 100644 index 00000000..48576a70 --- /dev/null +++ b/database/migrations/Version20231103224045_CreateAccountAccessAuthTable.php @@ -0,0 +1,32 @@ +createTable('AccountAccessAuth'); + + $table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'unsigned' => true,]); + $table->addColumn('accountId', Types::INTEGER, ['unsigned' => true,]); + $table->addColumn('label', Types::STRING, ['length' => 64, 'default' => 'default']); + $table->addColumn('refreshToken', Types::STRING, ['length' => 512,]); + $table->addColumn('userAgent', Types::STRING, ['length' => 255, 'default' => 'unknown']); + $table->addColumn('clientIdentHash', Types::STRING, ['length' => 128,]); + $table->addColumn('createdAt', Types::DATETIME_IMMUTABLE, ['default' => 'CURRENT_TIMESTAMP',]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['refreshToken'], 'account_access_auth_refresh_token_UNIQUE'); + $table->addUniqueIndex(['clientIdentHash'], 'account_access_auth_client_ident_hash_UNIQUE'); + } + + public function down(Schema $schema): void + { + $schema->dropTable('AccountAccessAuth'); + } +} diff --git a/database/migrations/Version20231103224046_CreateAccountActivationTable.php b/database/migrations/Version20231103224046_CreateAccountActivationTable.php new file mode 100644 index 00000000..a2ef6c01 --- /dev/null +++ b/database/migrations/Version20231103224046_CreateAccountActivationTable.php @@ -0,0 +1,28 @@ +createTable('AccountActivation'); + + $table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'unsigned' => true,]); + $table->addColumn('email', Types::STRING, ['length' => 512,]); + $table->addColumn('token', Types::STRING, ['length' => 32,]); + $table->addColumn('createdAt', Types::DATETIME_IMMUTABLE, ['default' => 'CURRENT_TIMESTAMP',]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['token'], 'account_activation_token_UNIQUE'); + } + + public function down(Schema $schema): void + { + $schema->dropTable('AccountActivation'); + } +} diff --git a/database/migrations/Version20231103224047_CreateTokenTable.php b/database/migrations/Version20231103224047_CreateTokenTable.php new file mode 100644 index 00000000..026030d9 --- /dev/null +++ b/database/migrations/Version20231103224047_CreateTokenTable.php @@ -0,0 +1,29 @@ +createTable('Token'); + + $table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'unsigned' => true,]); + $table->addColumn('accountId', Types::INTEGER, ['unsigned' => true,]); + $table->addColumn('token', Types::STRING, ['length' => 32,]); + $table->addColumn('tokenType', Types::SMALLINT, ['unsigned' => true, 'length' => 2,]); + $table->addColumn('createdAt', Types::DATETIME_IMMUTABLE, ['default' => 'CURRENT_TIMESTAMP',]); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['token'], 'token_token_UNIQUE'); + } + + public function down(Schema $schema): void + { + $schema->dropTable('Token'); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index d1f8c1a0..13d8053d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,7 @@ services: container_name: hackathon-php user: "${USERMAP_UID:-1000}:${USERMAP_GID:-1000}" image: "ghcr.io/ownhackathon/hackathon-api-php:latest" + env_file: ".env" volumes: - ./:/var/www/html - ./docker/php/php-ini-overrides.ini:/usr/local/etc/php/conf.d/extra.ini @@ -52,8 +53,8 @@ services: logging: driver: 'none' # disable saving logs ports: - - "1025:1025" # smtp server - - "8025:8025" # web ui + - "${MAILHOG_SMTP_PORT:-1025}:1025" # smtp server + - "${MAILHOG_WEBUI_PORT:-8025}:8025" # web ui volumes: db: diff --git a/docker/php/php-ini-overrides.ini b/docker/php/php-ini-overrides.ini index 68c41dad..92aa2697 100644 --- a/docker/php/php-ini-overrides.ini +++ b/docker/php/php-ini-overrides.ini @@ -5,3 +5,4 @@ xdebug.discover_client_host=On error_reporting=E_ALL error_log=/var/log/php.log sendmail_path = /usr/local/bin/mhsendmail +memory_limit = 1024M diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 00000000..09590230 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,182 @@ + + + + PSR-12 coding standard with Slevomat enhancements and custom declare-line formatting + + + + + + + + + + + src/ + + + */vendor/* + */config/* + */database/* + */ConfigProvider.php + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..e417e175 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,18 @@ +parameters: + level: 6 + phpVersion: 80400 + paths: + - src + ignoreErrors: + - identifier: missingType.iterableValue + - identifier: missingType.generics + bootstrapFiles: + - constants.php + checkUninitializedProperties: true + tips: + treatPhpDocTypesAsCertain: false +rules: + - PHPStan\Rules\Functions\ReturnNullsafeByRefRule +includes: + - vendor/phpstan/phpstan-deprecation-rules/rules.neon + - vendor/phpstan/phpstan/conf/bleedingEdge.neon diff --git a/phpunit_functionaltest.xml b/phpunit_functionaltest.xml new file mode 100644 index 00000000..c6c36a42 --- /dev/null +++ b/phpunit_functionaltest.xml @@ -0,0 +1,32 @@ + + + + + tests/FunctionalTest + + + + + + + + + + + src + + + diff --git a/phpunit_unittest.xml b/phpunit_unittest.xml new file mode 100644 index 00000000..2bf52408 --- /dev/null +++ b/phpunit_unittest.xml @@ -0,0 +1,38 @@ + + + + + tests/UnitTest/AppTest + + + tests/UnitTest/CoreTest + + + tests/UnitTest/GameTest + + + + + + + + + + + src + + + diff --git a/public/api/doc/index.html b/public/api/docs/index.html similarity index 62% rename from public/api/doc/index.html rename to public/api/docs/index.html index 39bbd0fb..da994d3e 100644 --- a/public/api/doc/index.html +++ b/public/api/docs/index.html @@ -7,13 +7,18 @@ name="description" content="SwaggerUI" /> - Hackathon API Overview - + ownHackathon - SwaggerUI + +
- - + + diff --git a/public/api/docs/swagger.json b/public/api/docs/swagger.json new file mode 100644 index 00000000..328503d2 --- /dev/null +++ b/public/api/docs/swagger.json @@ -0,0 +1,466 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "ownHackathon API Overview", + "version": "0.1.0" + }, + "servers": [ + { + "url": "/api" + } + ], + "paths": { + "/token/refresh": { + "get": { + "tags": [ + "Account" + ], + "summary": "Return of a new access token", + "operationId": "1ee7f36021506322a0e202b1fb32f278", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + } + } + }, + "401": { + "description": "Unauthorized access", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpResponseMessage" + } + } + } + } + } + } + }, + "/account/activation/{token}": { + "post": { + "tags": [ + "Account" + ], + "summary": "Activate the account", + "operationId": "db81d65a9e026816e8f2ea40f73a394b", + "parameters": [ + { + "name": "token", + "in": "path", + "description": "Token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "E-Mail for register Account", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountRegistration" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpResponseMessage" + } + } + } + } + } + } + }, + "/account/password/forgotten": { + "post": { + "tags": [ + "Account" + ], + "summary": "Creates a token for password reset. Sending via E-Mail", + "operationId": "faec81c539d68fbab3b442b162ad7c84", + "requestBody": { + "description": "Set Password for a Account", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EMail" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/account/password/{token}": { + "patch": { + "tags": [ + "Account" + ], + "summary": "Set Password to the account", + "operationId": "c5f1c414d69d6f318ab519d1746570e4", + "parameters": [ + { + "name": "token", + "in": "path", + "description": "Token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Set Password for a Account", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountPassword" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpResponseMessage" + } + } + } + } + } + } + }, + "/account": { + "post": { + "tags": [ + "Account" + ], + "summary": "Endpoint to register a new user account.", + "description": "Create Account", + "operationId": "8a212b9a005ff2dd5aeea1756d25bd05", + "requestBody": { + "description": "The email address for the new account", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EMail" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpResponseMessage" + } + } + } + } + } + } + }, + "/account/authentication": { + "post": { + "tags": [ + "Account" + ], + "summary": "Attempts to log in an account using transferred data", + "operationId": "4573a8211c6a27b677250eb46ca168ee", + "requestBody": { + "description": "Account data for authentication", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccountAuthenticationData" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationResponse" + } + } + } + }, + "401": { + "description": "Unauthorized access", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpResponseMessage" + } + } + } + }, + "403": { + "description": "Access denied", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpResponseMessage" + } + } + } + } + } + } + }, + "/account/logout": { + "get": { + "tags": [ + "Account" + ], + "summary": "Attempts to log out an account", + "operationId": "1154528e1da5f90aa3b356c63502b533", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationResponse" + } + } + } + }, + "401": { + "description": "Unauthorized access", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpResponseMessage" + } + } + } + } + } + } + }, + "/ping": { + "get": { + "tags": [ + "System Information" + ], + "summary": "Returns the current time in Unix format", + "operationId": "ffea4e020f3a4868f9f018e280509330", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "properties": { + "ack": { + "description": "actually request time", + "type": "string" + } + }, + "type": "object" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AccountAuthenticationData": { + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "description": "The E-Mail from Account", + "type": "string" + }, + "password": { + "description": "The Password from Account", + "type": "string" + } + }, + "type": "object" + }, + "AccountPassword": { + "properties": { + "password": { + "description": "The Password", + "type": "string" + } + }, + "type": "object" + }, + "AccountRegistration": { + "required": [ + "accountName", + "password" + ], + "properties": { + "accountName": { + "description": "The Display Name for the Account", + "type": "string" + }, + "password": { + "description": "The Password", + "type": "string" + } + }, + "type": "object" + }, + "EMail": { + "properties": { + "email": { + "description": "The E-Mail", + "type": "string" + } + }, + "type": "object" + }, + "AuthenticationResponse": { + "properties": { + "accessToken": { + "description": "The access token after a valid log-in", + "type": "string" + }, + "refreshToken": { + "description": "The refresh token after a valid log-in", + "type": "string" + } + }, + "type": "object" + }, + "HttpResponseMessage": { + "properties": { + "statusCode": { + "description": "The Http Status Code", + "type": "integer", + "example": 400 + }, + "message": { + "description": "The Message", + "type": "string", + "example": "Bad request" + } + }, + "type": "object" + }, + "AccessToken": { + "properties": { + "accessToken": { + "description": "The Token for authorized access", + "type": "string" + } + }, + "type": "object" + }, + "AccountPasswordToken": { + "properties": { + "accountPasswordToken": { + "description": "Token to set a new password", + "type": "string" + } + }, + "type": "object" + }, + "RefreshToken": { + "properties": { + "refreshToken": { + "description": "The token after a valid log-in", + "type": "string" + } + }, + "type": "object" + }, + "Token": { + "properties": { + "token": { + "description": "The Token", + "type": "string" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "accessToken": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "refreshToken": { + "type": "apiKey", + "name": "Authentication", + "in": "header" + }, + "Client-Identification-String": { + "type": "apiKey", + "name": "x-ident", + "in": "header" + } + } + }, + "security": [ + { + "accessToken": [] + }, + { + "Client-Identification-String": [] + }, + { + "refreshToken": [] + } + ], + "tags": [ + { + "name": "Account", + "description": "Account" + }, + { + "name": "System Information", + "description": "System Information" + } + ] +} \ No newline at end of file diff --git a/public/api/index.php b/public/api/index.php index af523c13..2421c2e6 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -12,7 +12,7 @@ chdir(dirname(__DIR__)); require './../vendor/autoload.php'; -if (file_exists('./../.env')){ +if (file_exists('./../.env')) { putenv('APP_ENV=develope'); } diff --git a/src/App/ConfigProvider.php b/src/App/ConfigProvider.php new file mode 100644 index 00000000..407a94f0 --- /dev/null +++ b/src/App/ConfigProvider.php @@ -0,0 +1,268 @@ + $this->getDependencies(), + ConfigAbstractFactory::class => $this->getAbstractFactoryConfig(), + ]; + } + + public function getDependencies(): array + { + return [ + 'aliases' => [ + Hydrator\AccountAccessAuthHydratorInterface::class => Hydrator\AccountAccessAuthHydrator::class, + Hydrator\AccountActivationHydratorInterface::class => Hydrator\AccountActivationHydrator::class, + Hydrator\AccountHydratorInterface::class => Hydrator\AccountHydrator::class, + Hydrator\TokenHydratorInterface::class => Hydrator\TokenHydrator::class, + + AccountRepositoryInterface::class => Repository\AccountRepository::class, + AccountActivationRepositoryInterface::class => Repository\AccountActivationRepository::class, + AccountAccessAuthRepositoryInterface::class => Repository\AccountAccessAuthRepository::class, + TokenRepositoryInterface::class => Repository\TokenRepository::class, + + Store\AccountStoreInterface::class => AccountTable::class, + Store\AccountAccessAuthStoreInterface::class => AccountAccessAuthTable::class, + Store\AccountActivationStoreInterface::class => AccountActivationTable::class, + Store\TokenStoreInterface::class => TokenTable::class, + ], + 'invokables' => [ + ], + 'factories' => [ + Hydrator\AccountAccessAuthHydrator::class => InvokableFactory::class, + Hydrator\AccountActivationHydrator::class => ConfigAbstractFactory::class, + Hydrator\AccountHydrator::class => ConfigAbstractFactory::class, + Hydrator\TokenHydrator::class => ConfigAbstractFactory::class, + + Middleware\Account\LoginAuthentication\AuthenticationConditionsMiddleware::class => InvokableFactory::class, + Middleware\Account\LoginAuthentication\AuthenticationMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\LoginAuthentication\AuthenticationValidationMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\LoginAuthentication\PersistAuthenticationMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\Validation\ActivationInputValidatorMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\Validation\EmailInputValidatorMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\Validation\PasswordInputValidatorMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\ActivationMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\LastAktivityUpdaterMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\LogoutMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\PasswordChangeMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\PasswordForgottenMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\RegisterMiddleware::class => ConfigAbstractFactory::class, + Middleware\Account\RequestAuthenticationMiddleware::class => ConfigAbstractFactory::class, + Middleware\ClientIdentification\ClientIdentificationMiddleware::class => ConfigAbstractFactory::class, + Middleware\Token\AccessTokenValidationMiddleware::class => ConfigAbstractFactory::class, + Middleware\Token\GenerateAccessTokenMiddleware::class => ConfigAbstractFactory::class, + Middleware\Token\GenerateRefreshTokenMiddleware::class => ConfigAbstractFactory::class, + Middleware\Token\RefreshTokenAccountMiddleware::class => ConfigAbstractFactory::class, + Middleware\Token\RefreshTokenDatabaseExistenceMiddleware::class => ConfigAbstractFactory::class, + Middleware\Token\RefreshTokenMatchClientIdentificationMiddleware::class => InvokableFactory::class, + Middleware\Token\RefreshTokenValidationMiddleware::class => ConfigAbstractFactory::class, + Repository\AccountAccessAuthRepository::class => ConfigAbstractFactory::class, + Repository\AccountActivationRepository::class => ConfigAbstractFactory::class, + Repository\AccountRepository::class => ConfigAbstractFactory::class, + Repository\TokenRepository::class => ConfigAbstractFactory::class, + Service\Account\AccountService::class => ConfigAbstractFactory::class, + Service\Authentication\AuthenticationService::class => InvokableFactory::class, + Service\ClientIdentification\ClientIdentificationService::class => InvokableFactory::class, + Service\Token\AccessTokenService::class => Service\Token\AccessTokenServiceFactory::class, + Service\Token\ActivationTokenService::class => ConfigAbstractFactory::class, + Service\Token\PasswordTokenService::class => ConfigAbstractFactory::class, + Service\Token\RefreshTokenService::class => Service\Token\RefreshTokenServiceFactory::class, + Table\AccountAccessAuthTable::class => ConfigAbstractFactory::class, + Table\AccountActivationTable::class => ConfigAbstractFactory::class, + Table\AccountTable::class => ConfigAbstractFactory::class, + Table\TokenTable::class => ConfigAbstractFactory::class, + + Validator\Input\AccountNameInput::class => InvokableFactory::class, + Validator\Input\EmailInput::class => InvokableFactory::class, + Validator\Input\PasswordInput::class => InvokableFactory::class, + Validator\AccountActivationValidator::class => ConfigAbstractFactory::class, + Validator\AuthenticationValidator::class => ConfigAbstractFactory::class, + Validator\EMailValidator::class => ConfigAbstractFactory::class, + Validator\PasswordValidator::class => ConfigAbstractFactory::class, + ], + ]; + } + + public function getAbstractFactoryConfig(): array + { + return [ + Hydrator\AccountActivationHydrator::class => [ + UuidFactoryInterface::class, + ], + Hydrator\AccountHydrator::class => [ + UuidFactoryInterface::class, + ], + Hydrator\TokenHydrator::class => [ + UuidFactoryInterface::class, + ], + + Middleware\Account\LoginAuthentication\AuthenticationMiddleware::class => [ + AuthenticationService::class, + AccountRepositoryInterface::class, + ], + Middleware\Account\LoginAuthentication\AuthenticationValidationMiddleware::class => [ + AuthenticationValidator::class, + ], + Middleware\Account\LoginAuthentication\PersistAuthenticationMiddleware::class => [ + AccountAccessAuthRepositoryInterface::class, + ], + Middleware\Account\Validation\ActivationInputValidatorMiddleware::class => [ + AccountActivationValidator::class, + ], + Middleware\Account\Validation\EmailInputValidatorMiddleware::class => [ + EMailValidator::class, + ], + Middleware\Account\Validation\PasswordInputValidatorMiddleware::class => [ + PasswordValidator::class, + ], + Middleware\Account\ActivationMiddleware::class => [ + AccountActivationRepositoryInterface::class, + AccountRepositoryInterface::class, + UuidFactoryInterface::class, + ], + Middleware\Account\LastAktivityUpdaterMiddleware::class => [ + AccountRepositoryInterface::class, + ], + Middleware\Account\LogoutMiddleware::class => [ + AccountAccessAuthRepositoryInterface::class, + ], + Middleware\Account\PasswordChangeMiddleware::class => [ + AccountRepositoryInterface::class, + TokenRepositoryInterface::class, + AccountService::class, + ], + Middleware\Account\PasswordForgottenMiddleware::class => [ + AccountService::class, + ], + Middleware\Account\RegisterMiddleware::class => [ + AccountService::class, + AccountActivationRepositoryInterface::class, + ActivationTokenService::class, + UuidFactoryInterface::class, + LoggerInterface::class, + ], + Middleware\Account\RequestAuthenticationMiddleware::class => [ + AccessTokenService::class, + AccountRepositoryInterface::class, + UuidFactoryInterface::class, + LoggerInterface::class, + ], + Middleware\ClientIdentification\ClientIdentificationMiddleware::class => [ + ClientIdentificationService::class, + ], + Middleware\Token\AccessTokenValidationMiddleware::class => [ + AccessTokenService::class, + ], + Middleware\Token\GenerateAccessTokenMiddleware::class => [ + AccessTokenService::class, + ], + Middleware\Token\GenerateRefreshTokenMiddleware::class => [ + RefreshTokenService::class, + ], + Middleware\Token\RefreshTokenAccountMiddleware::class => [ + AccountRepositoryInterface::class, + ], + Middleware\Token\RefreshTokenDatabaseExistenceMiddleware::class => [ + AccountAccessAuthRepositoryInterface::class, + ], + Middleware\Token\RefreshTokenValidationMiddleware::class => [ + RefreshTokenService::class, + ], + + Repository\AccountAccessAuthRepository::class => [ + Store\AccountAccessAuthStoreInterface::class, + ], + Repository\AccountActivationRepository::class => [ + Store\AccountActivationStoreInterface::class, + ], + Repository\AccountRepository::class => [ + Store\AccountStoreInterface::class, + ], + Repository\TokenRepository::class => [ + Store\TokenStoreInterface::class, + ], + + Service\Account\AccountService::class => [ + AccountRepositoryInterface::class, + TokenRepositoryInterface::class, + PasswordTokenService::class, + UuidFactoryInterface::class, + ], + Service\Token\ActivationTokenService::class => [ + MailerInterface::class, + ], + Service\Token\PasswordTokenService::class => [ + MailerInterface::class, + ], + Table\AccountAccessAuthTable::class => [ + Query::class, + AccountAccessAuthHydratorInterface::class, + ], + Table\AccountActivationTable::class => [ + Query::class, + AccountActivationHydratorInterface::class, + ], + Table\AccountTable::class => [ + Query::class, + AccountHydratorInterface::class, + ], + Table\TokenTable::class => [ + Query::class, + TokenHydratorInterface::class, + ], + Validator\AccountActivationValidator::class => [ + AccountNameInput::class, + PasswordInput::class, + ], + Validator\AuthenticationValidator::class => [ + EmailInput::class, + PasswordInput::class, + ], + Validator\EMailValidator::class => [ + EmailInput::class, + ], + Validator\PasswordValidator::class => [ + PasswordInput::class, + ], + ]; + } +} diff --git a/src/App/DTO/Account/AccountAuthenticationData.php b/src/App/DTO/Account/AccountAuthenticationData.php new file mode 100644 index 00000000..7c28a769 --- /dev/null +++ b/src/App/DTO/Account/AccountAuthenticationData.php @@ -0,0 +1,28 @@ +value, + )] + public string $email; + + #[OA\Property( + description: 'The Password from Account', + type: DataType::STRING->value, + )] + public string $password; + + public function __construct(string $email, string $password) + { + $this->email = $email; + $this->password = $password; + } +} diff --git a/src/App/DTO/Account/AccountPassword.php b/src/App/DTO/Account/AccountPassword.php new file mode 100644 index 00000000..51ddce68 --- /dev/null +++ b/src/App/DTO/Account/AccountPassword.php @@ -0,0 +1,24 @@ +value, + )] + public string $password, + ) { + } + + public static function fromString(string $password): self + { + return new self($password); + } +} diff --git a/src/App/DTO/Account/AccountRegistration.php b/src/App/DTO/Account/AccountRegistration.php new file mode 100644 index 00000000..bb01d5ad --- /dev/null +++ b/src/App/DTO/Account/AccountRegistration.php @@ -0,0 +1,29 @@ +value, + )] + public string $accountName, + #[OA\Property( + description: 'The Password', + type: DataType::STRING->value, + )] + public string $password, + ) { + } + + public static function fromString(string $accountName, string $password): self + { + return new self($accountName, $password); + } +} diff --git a/src/App/DTO/Client/ClientIdentification.php b/src/App/DTO/Client/ClientIdentification.php new file mode 100644 index 00000000..4a3f86e3 --- /dev/null +++ b/src/App/DTO/Client/ClientIdentification.php @@ -0,0 +1,17 @@ +value, + )] + public string $email, + ) { + } + + public static function fromString(string $email): self + { + return new self($email); + } +} diff --git a/src/App/DTO/Response/AuthenticationResponse.php b/src/App/DTO/Response/AuthenticationResponse.php new file mode 100644 index 00000000..e3b5a4ff --- /dev/null +++ b/src/App/DTO/Response/AuthenticationResponse.php @@ -0,0 +1,31 @@ +value, + )] + public string $accessToken, + #[OA\Property( + description: 'The refresh token after a valid log-in', + type: DataType::STRING->value, + )] + public string $refreshToken, + ) { + } + + public static function from(AccessToken $accessToken, RefreshToken $refreshToken): self + { + return new self($accessToken->accessToken, $refreshToken->refreshToken); + } +} diff --git a/src/App/DTO/Response/HttpResponseMessage.php b/src/App/DTO/Response/HttpResponseMessage.php new file mode 100644 index 00000000..67e91d96 --- /dev/null +++ b/src/App/DTO/Response/HttpResponseMessage.php @@ -0,0 +1,33 @@ +value, + example: HTTP::STATUS_BAD_REQUEST + )] + public int $statusCode, + #[OA\Property( + description: 'The Message', + type: DataType::STRING->value, + example: StatusMessage::BAD_REQUEST->value + )] + public StatusMessage $message, + ) { + } + + public static function create(int $statusCode, StatusMessage $message): self + { + return new self($statusCode, $message); + } +} diff --git a/src/App/DTO/Token/AccessToken.php b/src/App/DTO/Token/AccessToken.php new file mode 100644 index 00000000..edb501bf --- /dev/null +++ b/src/App/DTO/Token/AccessToken.php @@ -0,0 +1,24 @@ +value, + )] + public string $accessToken, + ) { + } + + public static function fromString(string $token): self + { + return new self($token); + } +} diff --git a/src/App/DTO/Token/AccountPasswordToken.php b/src/App/DTO/Token/AccountPasswordToken.php new file mode 100644 index 00000000..ba94635e --- /dev/null +++ b/src/App/DTO/Token/AccountPasswordToken.php @@ -0,0 +1,24 @@ +value, + )] + public string $accountPasswordToken, + ) { + } + + public static function fromString(string $accountPasswordToken): self + { + return new self($accountPasswordToken); + } +} diff --git a/src/App/DTO/Token/JwtTokenConfig.php b/src/App/DTO/Token/JwtTokenConfig.php new file mode 100644 index 00000000..b31c5f6f --- /dev/null +++ b/src/App/DTO/Token/JwtTokenConfig.php @@ -0,0 +1,26 @@ +value, + )] + public string $refreshToken, + ) { + } + + public static function fromString(string $token): self + { + return new self($token); + } +} diff --git a/src/App/DTO/Token/Token.php b/src/App/DTO/Token/Token.php new file mode 100644 index 00000000..92a75603 --- /dev/null +++ b/src/App/DTO/Token/Token.php @@ -0,0 +1,24 @@ +value, + )] + public string $token, + ) { + } + + public static function fromString(string $token): self + { + return new self($token); + } +} diff --git a/src/App/Entity/Account/Account.php b/src/App/Entity/Account/Account.php new file mode 100644 index 00000000..c9679757 --- /dev/null +++ b/src/App/Entity/Account/Account.php @@ -0,0 +1,26 @@ +value, + content: [new OA\JsonContent(ref: AccessToken::class)] + )] + #[OA\Response( + response: HTTP::STATUS_UNAUTHORIZED, + description: StatusMessage::UNAUTHORIZED_ACCESS->value, + content: [new OA\JsonContent(ref: HttpResponseMessage::class)] + )] + public function handle(ServerRequestInterface $request): ResponseInterface + { + $accessToken = $request->getAttribute(AccessToken::class); + + return new JsonResponse($accessToken, HTTP::STATUS_OK); + } +} diff --git a/src/App/Handler/Account/AccountActivationHandler.php b/src/App/Handler/Account/AccountActivationHandler.php new file mode 100644 index 00000000..61c78675 --- /dev/null +++ b/src/App/Handler/Account/AccountActivationHandler.php @@ -0,0 +1,49 @@ +value, + )] + #[OA\Response( + response: HTTP::STATUS_BAD_REQUEST, + description: StatusMessage::BAD_REQUEST->value, + content: [new OA\JsonContent(ref: HttpResponseMessage::class)] + )] + public function handle(ServerRequestInterface $request): ResponseInterface + { + return new JsonResponse([], HTTP::STATUS_OK); + } +} diff --git a/src/App/Handler/Account/AccountPasswordForgottenHandler.php b/src/App/Handler/Account/AccountPasswordForgottenHandler.php new file mode 100644 index 00000000..11afdf9d --- /dev/null +++ b/src/App/Handler/Account/AccountPasswordForgottenHandler.php @@ -0,0 +1,34 @@ +value, + )] + public function handle(ServerRequestInterface $request): ResponseInterface + { + return new JsonResponse([], HTTP::STATUS_OK); + } +} diff --git a/src/App/Handler/Account/AccountPasswordHandler.php b/src/App/Handler/Account/AccountPasswordHandler.php new file mode 100644 index 00000000..e95f749f --- /dev/null +++ b/src/App/Handler/Account/AccountPasswordHandler.php @@ -0,0 +1,49 @@ +value, + )] + #[OA\Response( + response: HTTP::STATUS_BAD_REQUEST, + description: StatusMessage::BAD_REQUEST->value, + content: [new OA\JsonContent(ref: HttpResponseMessage::class)] + )] + public function handle(ServerRequestInterface $request): ResponseInterface + { + return new JsonResponse([], HTTP::STATUS_OK); + } +} diff --git a/src/App/Handler/Account/AccountRegisterHandler.php b/src/App/Handler/Account/AccountRegisterHandler.php new file mode 100644 index 00000000..7a1fd10d --- /dev/null +++ b/src/App/Handler/Account/AccountRegisterHandler.php @@ -0,0 +1,41 @@ +value, + )] + #[OA\Response( + response: HTTP::STATUS_BAD_REQUEST, + description: StatusMessage::BAD_REQUEST->value, + content: [new OA\JsonContent(ref: HttpResponseMessage::class)] + )] + public function handle(ServerRequestInterface $request): ResponseInterface + { + return new JsonResponse([], HTTP::STATUS_OK); + } +} diff --git a/src/App/Handler/Account/AuthenticationHandler.php b/src/App/Handler/Account/AuthenticationHandler.php new file mode 100644 index 00000000..dab59953 --- /dev/null +++ b/src/App/Handler/Account/AuthenticationHandler.php @@ -0,0 +1,54 @@ +value, + content: [new OA\JsonContent(ref: AuthenticationResponse::class)] + )] + #[OA\Response( + response: HTTP::STATUS_UNAUTHORIZED, + description: StatusMessage::UNAUTHORIZED_ACCESS->value, + content: [new OA\JsonContent(ref: HttpResponseMessage::class)] + )] + #[OA\Response( + response: HTTP::STATUS_FORBIDDEN, + description: StatusMessage::FORBIDDEN->value, + content: [new OA\JsonContent(ref: HttpResponseMessage::class)] + )] + public function handle(ServerRequestInterface $request): ResponseInterface + { + $accessToken = $request->getAttribute(AccessToken::class); + $refreshToken = $request->getAttribute(RefreshToken::class); + + $response = AuthenticationResponse::from($accessToken, $refreshToken); + + return new JsonResponse($response, HTTP::STATUS_OK); + } +} diff --git a/src/App/Handler/Account/LogoutHandler.php b/src/App/Handler/Account/LogoutHandler.php new file mode 100644 index 00000000..e67a91ab --- /dev/null +++ b/src/App/Handler/Account/LogoutHandler.php @@ -0,0 +1,38 @@ +value, + content: [new OA\JsonContent(ref: AuthenticationResponse::class)] + )] + #[OA\Response( + response: HTTP::STATUS_UNAUTHORIZED, + description: StatusMessage::UNAUTHORIZED_ACCESS->value, + content: [new OA\JsonContent(ref: HttpResponseMessage::class)] + )] + public function handle(ServerRequestInterface $request): ResponseInterface + { + $response = HttpResponseMessage::create(HTTP::STATUS_OK, StatusMessage::SUCCESS); + + return new JsonResponse($response, $response->statusCode); + } +} diff --git a/src/App/Handler/PingHandler.php b/src/App/Handler/PingHandler.php new file mode 100644 index 00000000..be50fa47 --- /dev/null +++ b/src/App/Handler/PingHandler.php @@ -0,0 +1,44 @@ +value, + content: [ + new OA\JsonContent( + properties: [ + new OA\Property( + property: 'ack', + description: 'actually request time', + type: DataType::STRING->value, + ), + ] + ), + ] + ), + ] + )] + public function handle(ServerRequestInterface $request): ResponseInterface + { + return new JsonResponse(['ack' => time()]); + } +} diff --git a/src/App/Handler/SwaggerUIHandler.php b/src/App/Handler/SwaggerUIHandler.php new file mode 100644 index 00000000..f114ab17 --- /dev/null +++ b/src/App/Handler/SwaggerUIHandler.php @@ -0,0 +1,57 @@ + []], ['Client-Identification-String' => []], ['refreshToken' => []]] +)] +readonly class SwaggerUIHandler implements RequestHandlerInterface +{ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $indexFile = ROOT_DIR . 'public/docs/index.html'; + + if (file_exists($indexFile)) { + return new HtmlResponse(file_get_contents($indexFile)); + } + + return new JsonResponse([], HTTP::STATUS_NO_CONTENT); + } +} diff --git a/src/App/Hydrator/AccountAccessAuthHydrator.php b/src/App/Hydrator/AccountAccessAuthHydrator.php new file mode 100644 index 00000000..882507c4 --- /dev/null +++ b/src/App/Hydrator/AccountAccessAuthHydrator.php @@ -0,0 +1,68 @@ +hydrate($entity); + } + + return $collection; + } + + public function extract(AccountAccessAuthInterface $object): array + { + return [ + 'id' => $object->id, + 'accountId' => $object->accountId, + 'label' => $object->label, + 'refreshToken' => $object->refreshToken, + 'userAgent' => $object->userAgent, + 'clientIdentHash' => $object->clientIdentHash, + 'createdAt' => $object->createdAt->format(DateTimeFormat::DEFAULT->value), + ]; + } + + public function extractCollection(AccountAccessAuthCollectionInterface $collection): array + { + $data = []; + + foreach ($collection as $entity) { + $data[] = $this->extract($entity); + } + + return $data; + } +} diff --git a/src/App/Hydrator/AccountAccessAuthHydratorInterface.php b/src/App/Hydrator/AccountAccessAuthHydratorInterface.php new file mode 100644 index 00000000..1b16b3f7 --- /dev/null +++ b/src/App/Hydrator/AccountAccessAuthHydratorInterface.php @@ -0,0 +1,14 @@ +uuid->fromString($data['token']), + createdAt: new DateTimeImmutable($data['createdAt']), + ); + } + + public function hydrateCollection(array $data): AccountActivationCollectionInterface + { + $collection = new AccountActivationCollection(); + + foreach ($data as $entity) { + $collection[] = $this->hydrate($entity); + } + + return $collection; + } + + public function extract(AccountActivationInterface $object): array + { + return [ + 'id' => $object->id, + 'email' => $object->email->toString(), + 'token' => $object->token->getHex(), + 'createdAt' => $object->createdAt->format(DateTimeFormat::DEFAULT->value), + ]; + } + + public function extractCollection(AccountActivationCollectionInterface $collection): array + { + $data = []; + + foreach ($collection as $entity) { + $data[] = $this->extract($entity); + } + + return $data; + } +} diff --git a/src/App/Hydrator/AccountActivationHydratorInterface.php b/src/App/Hydrator/AccountActivationHydratorInterface.php new file mode 100644 index 00000000..aad28f48 --- /dev/null +++ b/src/App/Hydrator/AccountActivationHydratorInterface.php @@ -0,0 +1,14 @@ +uuid->fromString($data['uuid']), + name: $data['name'], + password: $data['password'], + email: new Email($data['email']), + registeredAt: new DateTimeImmutable($data['registeredAt']), + lastActionAt: new DateTimeImmutable($data['lastActionAt']), + ); + } + + /** + * @throws Exception + */ + public function hydrateCollection(array $data): AccountCollectionInterface + { + $collection = new AccountCollection(); + + foreach ($data as $entity) { + $collection[] = $this->hydrate($entity); + } + + return $collection; + } + + public function extract(AccountInterface $object): array + { + return [ + 'id' => $object->id, + 'uuid' => $object->uuid->getHex()->toString(), + 'name' => $object->name, + 'password' => $object->password, + 'email' => $object->email->toString(), + 'registeredAt' => $object->registeredAt->format(DateTimeFormat::DEFAULT->value), + 'lastActionAt' => $object->lastActionAt->format(DateTimeFormat::DEFAULT->value), + ]; + } + + public function extractCollection(AccountCollectionInterface $collection): array + { + $data = []; + + foreach ($collection as $entity) { + $data[] = $this->extract($entity); + } + + return $data; + } +} diff --git a/src/App/Hydrator/AccountHydratorInterface.php b/src/App/Hydrator/AccountHydratorInterface.php new file mode 100644 index 00000000..3379238b --- /dev/null +++ b/src/App/Hydrator/AccountHydratorInterface.php @@ -0,0 +1,14 @@ +uuid->fromString($data['token']), + createdAt: new DateTimeImmutable($data['createdAt']), + ); + } + + public function hydrateCollection(array $data): TokenCollectionInterface + { + $collection = new TokenCollection(); + + foreach ($data as $entity) { + $collection[] = $this->hydrate($entity); + } + + return $collection; + } + + public function extract(TokenInterface $object): array + { + return [ + 'id' => $object->id, + 'accountId' => $object->accountId, + 'tokenType' => $object->tokenType->value, + 'token' => $object->token->getHex(), + 'createdAt' => $object->createdAt->format(DateTimeFormat::DEFAULT->value), + ]; + } + + public function extractCollection(TokenCollectionInterface $collection): array + { + $data = []; + + foreach ($collection as $entity) { + $data[] = $this->extract($entity); + } + + return $data; + } +} diff --git a/src/App/Hydrator/TokenHydratorInterface.php b/src/App/Hydrator/TokenHydratorInterface.php new file mode 100644 index 00000000..c1ac0358 --- /dev/null +++ b/src/App/Hydrator/TokenHydratorInterface.php @@ -0,0 +1,14 @@ +getAttribute('token'); + + /** @var AccountRegistration $accountData */ + $accountData = $request->getAttribute(AccountRegistration::class); + + if ($activationToken === null) { + throw new HttpInvalidArgumentException( + LogMessage::ACTIVATION_TOKEN_MISSING, + StatusMessage::TOKEN_INVALID, + [ + 'Token:' => $activationToken, + ] + ); + } + + /** @var null|AccountActivationInterface $persistActivationToken */ + $persistActivationToken = $this->accountActivationRepository->findByToken($activationToken); + + if ($persistActivationToken === null) { + throw new HttpInvalidArgumentException( + LogMessage::ACTIVATION_TOKEN_MISSING, + StatusMessage::TOKEN_INVALID, + [ + 'Invalid activation token:' => $activationToken, + ] + ); + } + + $account = new Account( + id: null, + uuid: $this->uuid->uuid7(), + name: $accountData->accountName, + password: password_hash($accountData->password, PASSWORD_BCRYPT), + email: $persistActivationToken->email, + registeredAt: new DateTimeImmutable(), + lastActionAt: new DateTimeImmutable() + ); + + try { + $this->accountRepository->insert($account); + } catch (DuplicateEntryException $e) { + throw new HttpDuplicateEntryException( + LogMessage::ACCOUNT_ALREADY_EXISTS, + StatusMessage::INVALID_DATA, + [ + 'E-Mail' => $account->email->toString(), + 'Exception Message:' => $e->getMessage(), + ] + ); + } + + $this->accountActivationRepository->deleteById($persistActivationToken->id); + + return $handler->handle($request); + } +} diff --git a/src/App/Middleware/Account/LastAktivityUpdaterMiddleware.php b/src/App/Middleware/Account/LastAktivityUpdaterMiddleware.php new file mode 100644 index 00000000..9a533b97 --- /dev/null +++ b/src/App/Middleware/Account/LastAktivityUpdaterMiddleware.php @@ -0,0 +1,35 @@ +getAttribute(AccountInterface::AUTHENTICATED); + + if (!($account instanceof AccountInterface)) { + return $handler->handle($request); + } + + $account = $account->with(lastActionAt: new DateTimeImmutable()); + + $this->accountRepository->update($account); + + return $handler->handle($request->withAttribute(AccountInterface::AUTHENTICATED, $account)); + } +} diff --git a/src/App/Middleware/Account/LoginAuthentication/AuthenticationConditionsMiddleware.php b/src/App/Middleware/Account/LoginAuthentication/AuthenticationConditionsMiddleware.php new file mode 100644 index 00000000..c804b372 --- /dev/null +++ b/src/App/Middleware/Account/LoginAuthentication/AuthenticationConditionsMiddleware.php @@ -0,0 +1,30 @@ +hasHeader('Authentication') || $request->hasHeader('Authorization')) { + throw new HttpUnauthorizedException( + LogMessage::LOGIN_DENIED_AUTH_HEADER_ALREADY_PRESENT, + StatusMessage::ACCOUNT_ALREADY_AUTHENTICATED, + [ + 'uri' => (string)$request->getUri(), + 'ip' => $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown', + ] + ); + } + + return $handler->handle($request); + } +} diff --git a/src/App/Middleware/Account/LoginAuthentication/AuthenticationMiddleware.php b/src/App/Middleware/Account/LoginAuthentication/AuthenticationMiddleware.php new file mode 100644 index 00000000..bfc41663 --- /dev/null +++ b/src/App/Middleware/Account/LoginAuthentication/AuthenticationMiddleware.php @@ -0,0 +1,72 @@ +getParsedBody(); + + if (!array_key_exists('email', $data)) { + throw new HttpUnauthorizedException( + LogMessage::REQUIRED_EMAIL_MISSING, + StatusMessage::INVALID_DATA + ); + } + + $email = new Email($data['email']); + + $account = $this->accountRepository->findByEmail($email); + + if (!($account instanceof AccountInterface)) { + throw new HttpUnauthorizedException( + LogMessage::ACCOUNT_NOT_FOUND, + StatusMessage::INVALID_DATA, + [ + 'E-Mail:' => $email->toString(), + ], + Level::Warning + ); + } + + if (!$this->service->isPasswordMatch($data['password'], $account->password)) { + throw new HttpUnauthorizedException( + LogMessage::PASSWORD_INCORRECT, + StatusMessage::INVALID_DATA, + [ + 'E-Mail:' => $email->toString(), + ], + Level::Warning + ); + } + + $account = $account->with(lastActionAt: new DateTimeImmutable()); + + $this->accountRepository->update($account); + + return $handler->handle($request->withAttribute(AccountInterface::AUTHENTICATED, $account)); + } +} diff --git a/src/App/Middleware/Account/LoginAuthentication/AuthenticationValidationMiddleware.php b/src/App/Middleware/Account/LoginAuthentication/AuthenticationValidationMiddleware.php new file mode 100644 index 00000000..9291538e --- /dev/null +++ b/src/App/Middleware/Account/LoginAuthentication/AuthenticationValidationMiddleware.php @@ -0,0 +1,40 @@ +getParsedBody(); + + $this->validator->setData($data); + + if (!$this->validator->isValid()) { + throw new HttpUnauthorizedException( + LogMessage::EMAIL_INVALID, + StatusMessage::INVALID_DATA, + [ + 'E-Mail:' => $data['email'] ?? null, + 'Validator-Message:' => $this->validator->getMessages(), + ] + ); + } + + return $handler->handle($request->withParsedBody($this->validator->getValues())); + } +} diff --git a/src/App/Middleware/Account/LoginAuthentication/PersistAuthenticationMiddleware.php b/src/App/Middleware/Account/LoginAuthentication/PersistAuthenticationMiddleware.php new file mode 100644 index 00000000..25155700 --- /dev/null +++ b/src/App/Middleware/Account/LoginAuthentication/PersistAuthenticationMiddleware.php @@ -0,0 +1,79 @@ +getAttribute(AccountInterface::AUTHENTICATED); + + /** @var ClientIdentification $clientIdent */ + $clientIdent = $request->getAttribute(ClientIdentification::class); + + /** @var RefreshToken $refreshToken */ + $refreshToken = $request->getAttribute(RefreshToken::class); + + // @phpstan-ignore-next-line + if ($account === null || $clientIdent === null || $refreshToken === null) { + throw new HttpUnauthorizedException( + LogMessage::AUTHENTICATION_PERSISTENCE_ERROR, + StatusMessage::INVALID_DATA, + [ + // @phpstan-ignore-next-line + 'Account:' => $account?->email, + // @phpstan-ignore-next-line + 'Client ID:' => $clientIdent?->identificationHash, + 'Refresh Token:' => $refreshToken ? 'placed' : null, + ] + ); + } + + $accountAccessAuth = new AccountAccessAuth( + 1, + $account->id, + 'default', + $refreshToken->refreshToken, + $clientIdent->clientIdentificationData->userAgent, + $clientIdent->identificationHash, + new DateTimeImmutable() + ); + try { + $this->repository->insert($accountAccessAuth); + } catch (DuplicateEntryException $e) { + throw new HttpDuplicateEntryException( + LogMessage::DUPLICATE_SOURCE_LOGIN, + StatusMessage::INVALID_DATA, + [ + 'Account' => $account->name, + 'ClientID' => $clientIdent->identificationHash, + 'ErrorMessage' => $e->getMessage(), + ], + ); + } + + return $handler->handle($request); + } +} diff --git a/src/App/Middleware/Account/LogoutMiddleware.php b/src/App/Middleware/Account/LogoutMiddleware.php new file mode 100644 index 00000000..ad9d9d46 --- /dev/null +++ b/src/App/Middleware/Account/LogoutMiddleware.php @@ -0,0 +1,63 @@ +getAttribute(AccountInterface::AUTHENTICATED); + + if (!($account instanceof AccountInterface)) { + throw new HttpUnauthorizedException( + LogMessage::LOGOUT_REQUIRES_AUTHENTICATION, + StatusMessage::UNAUTHORIZED_ACCESS, + [], + Level::Warning + ); + } + + /** @var ClientIdentification $clientId */ + $clientId = $request->getAttribute(ClientIdentification::class); + + $accountAccessAuth = $this->authRepository->findByAccountIdAndClientIdHash( + $account->id, + $clientId->identificationHash + ); + + if (!($accountAccessAuth instanceof AccountAccessAuthInterface)) { + throw new HttpUnauthorizedException( + LogMessage::LOGOUT_CLIENT_IDENTITY_MISMATCH, + StatusMessage::UNAUTHORIZED_ACCESS, + [ + 'accountId' => $account->id, + 'clientIdentificationHash' => $clientId->identificationHash, + ], + Level::Warning + ); + } + + $this->authRepository->deleteById($accountAccessAuth->id); + + return $handler->handle($request); + } +} diff --git a/src/App/Middleware/Account/PasswordChangeMiddleware.php b/src/App/Middleware/Account/PasswordChangeMiddleware.php new file mode 100644 index 00000000..31013d8d --- /dev/null +++ b/src/App/Middleware/Account/PasswordChangeMiddleware.php @@ -0,0 +1,68 @@ +getAttribute('token'); + $password = $request->getParsedBody()['password']; + + if ($token === null) { + return $this->errorResponse(LogMessage::PASSWORD_CHANGE_TOKEN_MISSING, $token); + } + + $persistedToken = $this->tokenRepository->findByToken($token); + + if (!($persistedToken instanceof TokenInterface) || $persistedToken->tokenType !== TokenType::EMail) { + return $this->errorResponse(LogMessage::PASSWORD_CHANGE_TOKEN_INVALID, $token); + } + + $account = $this->accountRepository->findById($persistedToken->accountId); + + if (!($account instanceof Account)) { + return $this->errorResponse(LogMessage::PASSWORD_CHANGE_TOKEN_ACCOUNT_NOT_FOUND, $token); + } + + $hashedPassword = $this->accountService->cryptPassword($password); + $account = $account->with(password: $hashedPassword); + + $this->accountRepository->update($account); + $this->tokenRepository->deleteById($persistedToken->id); + + return $handler->handle($request); + } + + private function errorResponse(LogMessage $logMessage, ?string $token): ResponseInterface + { + throw new HttpInvalidArgumentException( + $logMessage, + StatusMessage::TOKEN_INVALID, + [ + 'Token:' => $token, + ] + ); + } +} diff --git a/src/App/Middleware/Account/PasswordForgottenMiddleware.php b/src/App/Middleware/Account/PasswordForgottenMiddleware.php new file mode 100644 index 00000000..f72db6be --- /dev/null +++ b/src/App/Middleware/Account/PasswordForgottenMiddleware.php @@ -0,0 +1,42 @@ +getAttribute(Email::class); + + if (!$this->accountService->isEmailAvailable($email)) { + $this->accountService->sendTokenForPasswordChange($email); + return $handler->handle($request); + } + + throw new HttpHandledInvalidArgumentAsSuccessException( + LogMessage::PASSWORD_REQUEST_MISSING_ACCOUNT, + StatusMessage::INVALID_DATA, + [ + 'email:' => $email->toString(), + ], + Level::Alert + ); + } +} diff --git a/src/App/Middleware/Account/RegisterMiddleware.php b/src/App/Middleware/Account/RegisterMiddleware.php new file mode 100644 index 00000000..3a9e16b9 --- /dev/null +++ b/src/App/Middleware/Account/RegisterMiddleware.php @@ -0,0 +1,58 @@ +getAttribute(Email::class); + + if (!$this->accountService->isEmailAvailable($email)) { + $this->logger->warning(LogMessage::ACCOUNT_ALREADY_EXISTS->value, [ + 'email:' => $email->toString(), + ]); + + $this->accountService->sendTokenForPasswordChange($email); + + return $handler->handle($request); + } + + $activation = new AccountActivation( + id: null, + email: $email, + token: $this->uuid->uuid7(), + createdAt: new DateTimeImmutable() + ); + + $this->accountActivationRepository->insert($activation); + + $this->activationTokenService->sendEmail($activation); + + return $handler->handle($request); + } +} diff --git a/src/App/Middleware/Account/RequestAuthenticationMiddleware.php b/src/App/Middleware/Account/RequestAuthenticationMiddleware.php new file mode 100644 index 00000000..6330b90f --- /dev/null +++ b/src/App/Middleware/Account/RequestAuthenticationMiddleware.php @@ -0,0 +1,76 @@ +getHeaderLine('Authorization'); + + if (strlen($authorization) === 0) { + $this->logger->info('Guest call', [ + 'uri' => (string)$request->getUri(), + ]); + + return $handler->handle($request); + } + + if (!$this->accessTokenService->isValid($authorization)) { + throw new HttpUnauthorizedException( + LogMessage::ACCESS_TOKEN_EXPIRED, + StatusMessage::TOKEN_EXPIRED, + [ + 'uri' => (string)$request->getUri(), + 'ip' => $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown', + ] + ); + } + + $authorization = $this->accessTokenService->decode($authorization); + $uuid = $this->uuid->fromString($authorization->uuid); + $account = $this->accountRepository->findByUuid($uuid); + if (!($account instanceof AccountInterface)) { + throw new HttpUnauthorizedException( + LogMessage::ACCESS_TOKEN_ACCOUNT_NOT_FOUND, + StatusMessage::TOKEN_INVALID, + [ + 'uri' => (string)$request->getUri(), + 'uuid' => $authorization->uuid, + ], + Level::Warning + ); + } + + $this->logger->info('Authenticated user call.', [ + 'Account' => $account->name, + 'uri' => (string)$request->getUri(), + ]); + + return $handler->handle($request->withAttribute(AccountInterface::AUTHENTICATED, $account)); + } +} diff --git a/src/App/Middleware/Account/Validation/ActivationInputValidatorMiddleware.php b/src/App/Middleware/Account/Validation/ActivationInputValidatorMiddleware.php new file mode 100644 index 00000000..7c87bcd2 --- /dev/null +++ b/src/App/Middleware/Account/Validation/ActivationInputValidatorMiddleware.php @@ -0,0 +1,45 @@ +getParsedBody(); + + $this->validator->setData($data); + + if (!$this->validator->isValid()) { + throw new HttpInvalidArgumentException( + LogMessage::ACCOUNT_NAME_INVALID, + StatusMessage::INVALID_DATA, + [ + 'Account Name:' => $data['accountName'] ?? null, + 'Validator-Message:' => $this->validator->getMessages(), + ] + ); + } + + $data = $this->validator->getValues(); + + $response = AccountRegistration::fromString($data['accountName'], $data['password']); + + return $handler->handle($request->withAttribute(AccountRegistration::class, $response)); + } +} diff --git a/src/App/Middleware/Account/Validation/EmailInputValidatorMiddleware.php b/src/App/Middleware/Account/Validation/EmailInputValidatorMiddleware.php new file mode 100644 index 00000000..0913cf04 --- /dev/null +++ b/src/App/Middleware/Account/Validation/EmailInputValidatorMiddleware.php @@ -0,0 +1,47 @@ +getParsedBody(); + + $this->mailValidator->setData($data); + + if (!$this->mailValidator->isValid()) { + throw new HttpInvalidArgumentException( + LogMessage::EMAIL_INVALID, + StatusMessage::INVALID_DATA, + [ + 'E-Mail:' => $data['email'] ?? null, + 'Validator Message:' => $this->mailValidator->getMessages(), + ] + ); + } + + $email = new Email($data['email']); + + return $handler->handle($request->withAttribute(Email::class, $email)); + } +} diff --git a/src/App/Middleware/Account/Validation/PasswordInputValidatorMiddleware.php b/src/App/Middleware/Account/Validation/PasswordInputValidatorMiddleware.php new file mode 100644 index 00000000..8cea3ba0 --- /dev/null +++ b/src/App/Middleware/Account/Validation/PasswordInputValidatorMiddleware.php @@ -0,0 +1,39 @@ +getParsedBody(); + + $this->validator->setData($data); + + if (!$this->validator->isValid()) { + throw new HttpInvalidArgumentException( + LogMessage::PASSWORD_INVALID, + StatusMessage::INVALID_DATA, + [ + 'Validator Message:' => $this->validator->getMessages(), + ] + ); + } + + return $handler->handle($request); + } +} diff --git a/src/App/Middleware/ClientIdentification/ClientIdentificationMiddleware.php b/src/App/Middleware/ClientIdentification/ClientIdentificationMiddleware.php new file mode 100644 index 00000000..a251414a --- /dev/null +++ b/src/App/Middleware/ClientIdentification/ClientIdentificationMiddleware.php @@ -0,0 +1,31 @@ +getHeaderLine('x-ident'); + $userAgent = $request->getHeaderLine('user-agent'); + + $clientIdentificationData = ClientIdentificationData::create($clientIdent, $userAgent); + $identificationHash = $this->clientIdentification->getClientIdentificationHash($clientIdentificationData); + $clientIdentification = ClientIdentification::create($clientIdentificationData, $identificationHash); + + return $handler->handle($request->withAttribute(ClientIdentification::class, $clientIdentification)); + } +} diff --git a/src/App/Middleware/Token/AccessTokenValidationMiddleware.php b/src/App/Middleware/Token/AccessTokenValidationMiddleware.php new file mode 100644 index 00000000..747eb58d --- /dev/null +++ b/src/App/Middleware/Token/AccessTokenValidationMiddleware.php @@ -0,0 +1,47 @@ +getHeaderLine('Authorization'); + + if (empty($accessToken)) { + throw new HttpUnauthorizedException( + LogMessage::ACCESS_TOKEN_MISSING, + StatusMessage::ACCOUNT_UNAUTHORIZED, + [], + Level::Warning + ); + } + + if (!$this->tokenService->isValid($accessToken)) { + throw new HttpUnauthorizedException( + LogMessage::ACCESS_TOKEN_EXPIRED, + StatusMessage::TOKEN_EXPIRED, + [ + 'Access Token:' => $accessToken, + ], + ); + } + + return $handler->handle($request); + } +} diff --git a/src/App/Middleware/Token/GenerateAccessTokenMiddleware.php b/src/App/Middleware/Token/GenerateAccessTokenMiddleware.php new file mode 100644 index 00000000..3c94050c --- /dev/null +++ b/src/App/Middleware/Token/GenerateAccessTokenMiddleware.php @@ -0,0 +1,31 @@ +getAttribute(AccountInterface::AUTHENTICATED); + + $accessToken = $this->accessTokenService->generate($account->uuid); + + $accessToken = AccessToken::fromString($accessToken); + + return $handler->handle($request->withAttribute(AccessToken::class, $accessToken)); + } +} diff --git a/src/App/Middleware/Token/GenerateRefreshTokenMiddleware.php b/src/App/Middleware/Token/GenerateRefreshTokenMiddleware.php new file mode 100644 index 00000000..bbbc10cc --- /dev/null +++ b/src/App/Middleware/Token/GenerateRefreshTokenMiddleware.php @@ -0,0 +1,30 @@ +getAttribute(ClientIdentification::class); + + $refreshToken = $this->tokenService->generate($clientIdentification); + + $refreshToken = RefreshToken::fromString($refreshToken); + + return $handler->handle($request->withAttribute(RefreshToken::class, $refreshToken)); + } +} diff --git a/src/App/Middleware/Token/RefreshTokenAccountMiddleware.php b/src/App/Middleware/Token/RefreshTokenAccountMiddleware.php new file mode 100644 index 00000000..428ea70c --- /dev/null +++ b/src/App/Middleware/Token/RefreshTokenAccountMiddleware.php @@ -0,0 +1,46 @@ +getAttribute(AccountAccessAuthInterface::class); + + /** @var null|AccountInterface $account */ + $account = $this->accountRepository->findById($accountAccessAuth->accountId); + + if ($account === null) { + throw new HttpUnauthorizedException( + LogMessage::REFRESH_TOKEN_ACCOUNT_NOT_FOUND, + StatusMessage::TOKEN_INVALID, + [ + 'AccessAuth ID:' => $accountAccessAuth->id, + 'Account ID:' => $accountAccessAuth->accountId, + ], + Level::Warning + ); + } + return $handler->handle($request->withAttribute(AccountInterface::AUTHENTICATED, $account)); + } +} diff --git a/src/App/Middleware/Token/RefreshTokenDatabaseExistenceMiddleware.php b/src/App/Middleware/Token/RefreshTokenDatabaseExistenceMiddleware.php new file mode 100644 index 00000000..b2883911 --- /dev/null +++ b/src/App/Middleware/Token/RefreshTokenDatabaseExistenceMiddleware.php @@ -0,0 +1,43 @@ +getAttribute(RefreshToken::class); + + $persistToken = $this->accessAuthRepository->findByRefreshToken($refreshToken->refreshToken); + if (!($persistToken instanceof AccountAccessAuthInterface)) { + throw new HttpUnauthorizedException( + LogMessage::REFRESH_TOKEN_NOT_FOUND, + StatusMessage::TOKEN_NOT_PERSISTENT, + [ + 'Refresh Token:' => $refreshToken, + ], + Level::Warning + ); + } + + return $handler->handle($request->withAttribute(AccountAccessAuthInterface::class, $persistToken)); + } +} diff --git a/src/App/Middleware/Token/RefreshTokenMatchClientIdentificationMiddleware.php b/src/App/Middleware/Token/RefreshTokenMatchClientIdentificationMiddleware.php new file mode 100644 index 00000000..265b1a93 --- /dev/null +++ b/src/App/Middleware/Token/RefreshTokenMatchClientIdentificationMiddleware.php @@ -0,0 +1,43 @@ +getAttribute(AccountAccessAuthInterface::class); + + /** @var ClientIdentification $clientIdentification */ + $clientIdentification = $request->getAttribute(ClientIdentification::class); + + if ($accountAccessAuth->clientIdentHash !== $clientIdentification->identificationHash) { + throw new HttpUnauthorizedException( + LogMessage::REFRESH_TOKEN_CLIENT_MISMATCH, + StatusMessage::CLIENT_UNEXPECTED, + [ + 'expected:' => $accountAccessAuth->clientIdentHash, + 'expected UserAgent' => $accountAccessAuth->userAgent, + 'current:' => $clientIdentification->identificationHash, + 'current UserAgent:' => $clientIdentification->clientIdentificationData->userAgent, + ], + Level::Warning + ); + } + + return $handler->handle($request); + } +} diff --git a/src/App/Middleware/Token/RefreshTokenValidationMiddleware.php b/src/App/Middleware/Token/RefreshTokenValidationMiddleware.php new file mode 100644 index 00000000..0ccdc0cf --- /dev/null +++ b/src/App/Middleware/Token/RefreshTokenValidationMiddleware.php @@ -0,0 +1,40 @@ +getHeaderLine('Authentication'); + + if (!$this->tokenService->isValid($refreshToken)) { + throw new HttpUnauthorizedException( + LogMessage::REFRESH_TOKEN_INVALID, + StatusMessage::TOKEN_INVALID, + [ + 'Refresh Token:' => $refreshToken, + ], + ); + } + + $refreshToken = RefreshToken::fromString($refreshToken); + + return $handler->handle($request->withAttribute(RefreshToken::class, $refreshToken)); + } +} diff --git a/src/App/Repository/AccountAccessAuthRepository.php b/src/App/Repository/AccountAccessAuthRepository.php new file mode 100644 index 00000000..ae69bc9c --- /dev/null +++ b/src/App/Repository/AccountAccessAuthRepository.php @@ -0,0 +1,71 @@ +store->insert($accountAccessAuth); + } + + public function update(AccountAccessAuthInterface $accountAccessAuth): true + { + return $this->store->update($accountAccessAuth); + } + + public function deleteById(int $id): true + { + return $this->store->deleteById($id); + } + + public function findById(int $id): ?AccountAccessAuthInterface + { + return $this->store->findById($id); + } + + public function findByAccountId(int $accountId): AccountAccessAuthCollectionInterface + { + return $this->store->findByAccountId($accountId); + } + + public function findByAccountIdAndClientIdHash(int $accountId, string $clientHash): ?AccountAccessAuthInterface + { + return $this->store->findByAccountIdAndClientIdHash($accountId, $clientHash); + } + + public function findByLabel(string $label): AccountAccessAuthCollectionInterface + { + return $this->store->findByLabel($label); + } + + public function findByRefreshToken(string $refreshToken): ?AccountAccessAuthInterface + { + return $this->store->findByRefreshToken($refreshToken); + } + + public function findByUserAgent(string $userAgent): AccountAccessAuthCollectionInterface + { + return $this->store->findByUserAgent($userAgent); + } + + public function findByClientIdentHash(string $clientIdentHash): ?AccountAccessAuthInterface + { + return $this->store->findByClientIdentHash($clientIdentHash); + } + + public function findAll(): AccountAccessAuthCollectionInterface + { + return $this->store->findAll(); + } +} diff --git a/src/App/Repository/AccountActivationRepository.php b/src/App/Repository/AccountActivationRepository.php new file mode 100644 index 00000000..a87a19e0 --- /dev/null +++ b/src/App/Repository/AccountActivationRepository.php @@ -0,0 +1,57 @@ +store->insert($data); + } + + public function update(AccountActivationInterface $data): true + { + return $this->store->update($data); + } + + public function findById(int $id): ?AccountActivationInterface + { + return $this->store->findById($id); + } + + public function findEmail(Email $email): AccountActivationCollectionInterface + { + return $this->store->findByEmail($email); + } + + public function findByToken(string $token): ?AccountActivationInterface + { + return $this->store->findByToken($token); + } + + public function findAll(): AccountActivationCollectionInterface + { + return $this->store->findAll(); + } + + public function deleteById(int $id): true + { + return $this->store->deleteById($id); + } + + public function deleteByEmail(Email $email): true + { + return $this->store->deleteByEmail($email); + } +} diff --git a/src/App/Repository/AccountRepository.php b/src/App/Repository/AccountRepository.php new file mode 100644 index 00000000..1fbe69e4 --- /dev/null +++ b/src/App/Repository/AccountRepository.php @@ -0,0 +1,58 @@ +store->insert($data); + } + + public function update(AccountInterface $data): true + { + return $this->store->update($data); + } + + public function deleteById(int $id): true + { + return $this->store->deleteById($id); + } + + public function findById(int $id): ?AccountInterface + { + return $this->store->findById($id); + } + + public function findByUuid(UuidInterface $uuid): ?AccountInterface + { + return $this->store->findByUuid($uuid); + } + + public function findByName(string $name): ?AccountInterface + { + return $this->store->findByName($name); + } + + public function findByEmail(Email $email): ?AccountInterface + { + return $this->store->findByEmail($email); + } + + public function findAll(): AccountCollectionInterface + { + return $this->store->findAll(); + } +} diff --git a/src/App/Repository/TokenRepository.php b/src/App/Repository/TokenRepository.php new file mode 100644 index 00000000..2eb36068 --- /dev/null +++ b/src/App/Repository/TokenRepository.php @@ -0,0 +1,56 @@ +store->insert($data); + } + + public function update(TokenInterface $data): true + { + return $this->store->update($data); + } + + public function findById(int $id): ?TokenInterface + { + return $this->store->findById($id); + } + + public function findByAccountId(int $accountId): TokenCollectionInterface + { + return $this->store->findByAccountId($accountId); + } + + public function findByToken(string $token): ?TokenInterface + { + return $this->store->findByToken($token); + } + + public function findAll(): TokenCollectionInterface + { + return $this->store->findAll(); + } + + public function deleteById(int $id): true + { + return $this->store->deleteById($id); + } + + public function deleteByAccountId(int $accountId): true + { + return $this->store->deleteByAccountId($accountId); + } +} diff --git a/src/App/Service/Account/AccountService.php b/src/App/Service/Account/AccountService.php new file mode 100644 index 00000000..b2c01645 --- /dev/null +++ b/src/App/Service/Account/AccountService.php @@ -0,0 +1,55 @@ +accountRepository->findByEmail($email); + $token = $this->createPasswordChangeTokenForUserId($account->id); + $this->tokenRepository->insert($token); + $this->tokenService->sendEmail($email, $token); + } + + public function isEmailAvailable(Email $email): bool + { + $account = $this->accountRepository->findByEmail($email); + + return $account === null; + } + + public function createPasswordChangeTokenForUserId(int $userId): TokenInterface + { + return new Token( + id: null, + accountId: $userId, + tokenType: TokenType::EMail, + token: $this->uuid->uuid7(), + createdAt: new DateTimeImmutable() + ); + } + + public function cryptPassword(string $password): string + { + return password_hash($password, PASSWORD_BCRYPT); + } +} diff --git a/src/App/Service/Authentication/AuthenticationService.php b/src/App/Service/Authentication/AuthenticationService.php new file mode 100644 index 00000000..bf48bac5 --- /dev/null +++ b/src/App/Service/Authentication/AuthenticationService.php @@ -0,0 +1,11 @@ +getIdentificationHash($clientIdentificationData); + } + + private function getIdentificationHash(ClientIdentificationData $clientIdentificationData): string + { + return hash('sha512', serialize($clientIdentificationData)); + } +} diff --git a/src/App/Service/Token/AccessTokenService.php b/src/App/Service/Token/AccessTokenService.php new file mode 100644 index 00000000..016a53aa --- /dev/null +++ b/src/App/Service/Token/AccessTokenService.php @@ -0,0 +1,34 @@ + $this->config->iss, + 'aud' => $this->config->aud, + 'iat' => $now, + 'exp' => $now + $this->config->duration, + 'uuid' => $uuid->getHex()->toString(), + ]; + + return JWT::encode($payload, $this->config->key, $this->config->algorithmus); + } +} diff --git a/src/App/Service/Token/AccessTokenServiceFactory.php b/src/App/Service/Token/AccessTokenServiceFactory.php new file mode 100644 index 00000000..ba0b2d36 --- /dev/null +++ b/src/App/Service/Token/AccessTokenServiceFactory.php @@ -0,0 +1,17 @@ +get('config')['jwt_token']['access']; + $jwtTokenConfig = JwtTokenConfig::createFromArray($jwtTokenConfig); + + return new AccessTokenService($jwtTokenConfig); + } +} diff --git a/src/App/Service/Token/ActivationTokenService.php b/src/App/Service/Token/ActivationTokenService.php new file mode 100644 index 00000000..c7444652 --- /dev/null +++ b/src/App/Service/Token/ActivationTokenService.php @@ -0,0 +1,30 @@ +token->getHex()->toString()); + + $email = new Email() + ->from('no-reply@stormannsgal.de') + ->to($activation->email->toString()) + ->subject('Account Activation Code') + ->text($text); + + $this->mailer->send($email); + } +} diff --git a/src/App/Service/Token/JwtTokenTrait.php b/src/App/Service/Token/JwtTokenTrait.php new file mode 100644 index 00000000..db9d14a9 --- /dev/null +++ b/src/App/Service/Token/JwtTokenTrait.php @@ -0,0 +1,42 @@ +config->key, $this->config->algorithmus)); + } catch ( + InvalidArgumentException + | DomainException + | UnexpectedValueException + | SignatureInvalidException + | BeforeValidException + | ExpiredException $e + ) { + return false; + } + + return true; + } + + public function decode(string $token): object + { + if (!$this->isValid($token)) { + return throw new InvalidArgumentException(); + } + + return JWT::decode($token, new Key($this->config->key, $this->config->algorithmus)); + } +} diff --git a/src/App/Service/Token/PasswordTokenService.php b/src/App/Service/Token/PasswordTokenService.php new file mode 100644 index 00000000..962fc064 --- /dev/null +++ b/src/App/Service/Token/PasswordTokenService.php @@ -0,0 +1,31 @@ +token->getHex()->toString()); + + $email = new Email() + ->from('no-reply@stormannsgal.de') + ->to($email->toString()) + ->subject('Password Forgotten Code') + ->text($text); + + $this->mailer->send($email); + } +} diff --git a/src/App/Service/Token/RefreshTokenService.php b/src/App/Service/Token/RefreshTokenService.php new file mode 100644 index 00000000..3863e2bb --- /dev/null +++ b/src/App/Service/Token/RefreshTokenService.php @@ -0,0 +1,34 @@ + $this->config->iss, + 'aud' => $this->config->aud, + 'iat' => $now, + 'exp' => $now + $this->config->duration, + 'ident' => $clientIdentification->identificationHash, + ]; + + return JWT::encode($payload, $this->config->key, $this->config->algorithmus); + } +} diff --git a/src/App/Service/Token/RefreshTokenServiceFactory.php b/src/App/Service/Token/RefreshTokenServiceFactory.php new file mode 100644 index 00000000..9f0877f8 --- /dev/null +++ b/src/App/Service/Token/RefreshTokenServiceFactory.php @@ -0,0 +1,17 @@ +get('config')['jwt_token']['refresh']; + $jwtTokenConfig = JwtTokenConfig::createFromArray($jwtTokenConfig); + + return new RefreshTokenService($jwtTokenConfig); + } +} diff --git a/src/App/Table/AbstractTable.php b/src/App/Table/AbstractTable.php new file mode 100644 index 00000000..859829c5 --- /dev/null +++ b/src/App/Table/AbstractTable.php @@ -0,0 +1,44 @@ +table = substr(new ReflectionClass($this)->getShortName(), 0, -5); + $this->query = $query; + } + + public function getTableName(): string + { + return $this->table; + } + + /** + * @throws Exception + */ + public function deleteById(int $id): true + { + $result = $this->query->delete($this->table, $id)->execute(); + + if ($result === false) { + throw new InvalidArgumentException( + sprintf('Failed to delete %s table with id: `%s`', $this->getTableName(), $id) + ); + } + + return true; + } +} diff --git a/src/App/Table/AccountAccessAuthTable.php b/src/App/Table/AccountAccessAuthTable.php new file mode 100644 index 00000000..f0aa363c --- /dev/null +++ b/src/App/Table/AccountAccessAuthTable.php @@ -0,0 +1,143 @@ +hydrator->extract($data); + + unset($value['id']); + + try { + $lastInsertId = $this->query->insertInto($this->table, $value)->execute(); + } catch (Exception | PDOException $e) { + return throw new DuplicateEntryException($this->getTableName(), $data->id); + } + + return true; + } + + public function update(AccountAccessAuthInterface $data): true + { + $value = $this->hydrator->extract($data); + + $result = $this->query->update($this->table, $value, $data->id)->execute(); + + if ($result === false) { + throw new InvalidArgumentException( + sprintf('Unknown Error while updating %s with id: %s', $this->getTableName(), $data->id) + ); + } + + return true; + } + + public function findById(int $id): ?AccountAccessAuthInterface + { + $result = $this->query->from($this->table) + ->where('id', $id) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findByAccountId(int $accountId): AccountAccessAuthCollectionInterface + { + $result = $this->query->from($this->table) + ->where('userId', $accountId) + ->fetchAll(); + + return is_array($result) + ? $this->hydrator->hydrateCollection($result) + : $this->hydrator->hydrateCollection( + [] + ); + } + + public function findByAccountIdAndClientIdHash(int $accountId, string $clientHash): ?AccountAccessAuthInterface + { + $result = $this->query->from($this->table) + ->where('accountId', $accountId) + ->where('clientIdentHash', $clientHash) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findByLabel(string $label): AccountAccessAuthCollectionInterface + { + $result = $this->query->from($this->table) + ->where('label', $label) + ->fetchAll(); + + return is_array($result) + ? $this->hydrator->hydrateCollection($result) + : $this->hydrator->hydrateCollection( + [] + ); + } + + public function findByRefreshToken(string $refreshToken): ?AccountAccessAuthInterface + { + $result = $this->query->from($this->table) + ->where('refreshToken', $refreshToken) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findByUserAgent(string $userAgent): AccountAccessAuthCollectionInterface + { + $result = $this->query->from($this->table) + ->where('userAgent', $userAgent) + ->fetchAll(); + + return is_array($result) + ? $this->hydrator->hydrateCollection($result) + : $this->hydrator->hydrateCollection( + [] + ); + } + + public function findByClientIdentHash(string $clientIdentHash): ?AccountAccessAuthInterface + { + $result = $this->query->from($this->table) + ->where('clientIdentHash', $clientIdentHash) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findAll(): AccountAccessAuthCollectionInterface + { + $result = $this->query->from($this->table)->fetchAll(); + + return is_array($result) + ? $this->hydrator->hydrateCollection($result) + : $this->hydrator->hydrateCollection( + [] + ); + } +} diff --git a/src/App/Table/AccountActivationTable.php b/src/App/Table/AccountActivationTable.php new file mode 100644 index 00000000..137b1823 --- /dev/null +++ b/src/App/Table/AccountActivationTable.php @@ -0,0 +1,105 @@ +hydrator->extract($data); + + unset($value['id']); + + try { + $this->query->insertInto($this->table, $value)->execute(); + } catch (PDOException $e) { + throw new DuplicateEntryException($this->getTableName(), $data->id); + } + + return true; + } + + public function update(AccountActivationInterface $data): true + { + $value = $this->hydrator->extract($data); + + $result = $this->query->update($this->table, $value, $data->id)->execute(); + + if ($result === false) { + throw new InvalidArgumentException( + sprintf('Unknown Error while updating %s with id: %s', $this->getTableName(), $data->id) + ); + } + + return true; + } + + public function findById(int $id): ?AccountActivationInterface + { + $result = $this->query->from($this->table) + ->where('id', $id) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findByEmail(Email $email): AccountActivationCollectionInterface + { + $result = $this->query->from($this->table) + ->where('email', $email->toString()) + ->fetchAll(); + + return is_array($result) ? $this->hydrator->hydrateCollection($result) : $this->hydrator->hydrateCollection([]); + } + + public function findByToken(string $token): ?AccountActivationInterface + { + $result = $this->query->from($this->table) + ->where('token', $token) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findAll(): AccountActivationCollectionInterface + { + $result = $this->query->from($this->table)->fetchAll(); + + return is_array($result) ? $this->hydrator->hydrateCollection($result) : $this->hydrator->hydrateCollection([]); + } + + public function deleteByEmail(Email $email): true + { + $result = $this->query->delete($this->table) + ->where('email', $email->toString()) + ->execute(); + + if ($result === false) { + throw new InvalidArgumentException( + sprintf('Failed to delete %s table with email: `%s`', $this->getTableName(), $email->toString()) + ); + } + + return true; + } +} diff --git a/src/App/Table/AccountTable.php b/src/App/Table/AccountTable.php new file mode 100644 index 00000000..2bc5dca5 --- /dev/null +++ b/src/App/Table/AccountTable.php @@ -0,0 +1,105 @@ +hydrator->extract($data); + + unset($value['id']); + + try { + $this->query->insertInto($this->table, $value)->execute(); + } catch (PDOException $e) { + throw new DuplicateEntryException($this->getTableName(), $data->id); + } + + return true; + } + + public function update(AccountInterface $data): true + { + $value = $this->hydrator->extract($data); + + $result = $this->query->update($this->table, $value, $data->id)->execute(); + + if ($result === false) { + throw new InvalidArgumentException( + sprintf('Unknown Error while updating %s with id: %s', $this->getTableName(), $data->id) + ); + } + + return true; + } + + public function findById(int $id): ?AccountInterface + { + $result = $this->query->from($this->table) + ->where('id', $id) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findByUuid(UuidInterface $uuid): ?AccountInterface + { + $result = $this->query->from($this->table) + ->where('uuid', $uuid->getHex()->toString()) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findByName(string $name): ?AccountInterface + { + $result = $this->query->from($this->table) + ->where('name', $name) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findByEmail(Email $email): ?AccountInterface + { + $result = $this->query->from($this->table) + ->where('email', $email->toString()) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findAll(): AccountCollectionInterface + { + $result = $this->query->from($this->table)->fetchAll(); + + return is_array($result) ? $this->hydrator->hydrateCollection($result) : $this->hydrator->hydrateCollection([]); + } +} diff --git a/src/App/Table/TokenTable.php b/src/App/Table/TokenTable.php new file mode 100644 index 00000000..63425c1e --- /dev/null +++ b/src/App/Table/TokenTable.php @@ -0,0 +1,104 @@ +hydrator->extract($data); + + unset($value['id']); + + try { + $this->query->insertInto($this->table, $value)->execute(); + } catch (PDOException $e) { + throw new DuplicateEntryException($this->getTableName(), $data->id); + } + + return true; + } + + public function update(TokenInterface $data): true + { + $value = $this->hydrator->extract($data); + + $result = $this->query->update($this->table, $value, $data->id)->execute(); + + if ($result === false) { + throw new InvalidArgumentException( + sprintf('Unknown Error while updating %s with id: %s', $this->getTableName(), $data->id) + ); + } + + return true; + } + + public function findById(int $id): ?TokenInterface + { + $result = $this->query->from($this->table) + ->where('id', $id) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findByAccountId(int $accountId): TokenCollectionInterface + { + $result = $this->query->from($this->table) + ->where('accountId', $accountId) + ->fetchAll(); + + return is_array($result) ? $this->hydrator->hydrateCollection($result) : $this->hydrator->hydrateCollection([]); + } + + public function findByToken(string $token): ?TokenInterface + { + $result = $this->query->from($this->table) + ->where('token', $token) + ->fetch(); + + return is_array($result) ? $this->hydrator->hydrate($result) : null; + } + + public function findAll(): TokenCollectionInterface + { + $result = $this->query->from($this->table)->fetchAll(); + + return is_array($result) ? $this->hydrator->hydrateCollection($result) : $this->hydrator->hydrateCollection([]); + } + + public function deleteByAccountId(int $accountId): true + { + $result = $this->query->delete($this->table) + ->where('accountId', $accountId) + ->execute(); + + if ($result === false) { + throw new InvalidArgumentException( + sprintf('Failed to delete %s table with accountId: `%s`', $this->getTableName(), $accountId) + ); + } + + return true; + } +} diff --git a/src/App/Validator/AccountActivationValidator.php b/src/App/Validator/AccountActivationValidator.php new file mode 100644 index 00000000..9a9ad148 --- /dev/null +++ b/src/App/Validator/AccountActivationValidator.php @@ -0,0 +1,18 @@ +add($this->accountNameInput); + $this->add($this->passwordInput); + } +} diff --git a/src/App/Validator/AuthenticationValidator.php b/src/App/Validator/AuthenticationValidator.php new file mode 100644 index 00000000..f01eca9c --- /dev/null +++ b/src/App/Validator/AuthenticationValidator.php @@ -0,0 +1,18 @@ +add($this->emailInput); + $this->add($this->passwordInput); + } +} diff --git a/src/App/Validator/DateLessNow.php b/src/App/Validator/DateLessNow.php new file mode 100644 index 00000000..f645d1d0 --- /dev/null +++ b/src/App/Validator/DateLessNow.php @@ -0,0 +1,37 @@ + 'Date is in the past', + ]; + + public function isValid($value): bool + { + $dateNow = new DateTime(); + + try { + $dateValue = new DateTime($value); + } catch (Exception $exception) { + return false; + } + + if ($dateValue <= $dateNow) { + $this->error(self::VALID_DATE); + return false; + } + + $this->setValue($value); + + return true; + } +} diff --git a/src/App/Validator/EMailValidator.php b/src/App/Validator/EMailValidator.php new file mode 100644 index 00000000..a0b1e404 --- /dev/null +++ b/src/App/Validator/EMailValidator.php @@ -0,0 +1,15 @@ +add($this->emailInput); + } +} diff --git a/src/App/Validator/Input/AccountNameInput.php b/src/App/Validator/Input/AccountNameInput.php new file mode 100644 index 00000000..efedd759 --- /dev/null +++ b/src/App/Validator/Input/AccountNameInput.php @@ -0,0 +1,26 @@ +setRequired(true); + + $this->getFilterChain()->attachByName('StringTrim'); + + $this->getValidatorChain()->attachByName( + 'StringLength', + [ + 'encoding' => 'UTF-8', + 'min' => 3, + 'max' => 64, + ] + ); + } +} diff --git a/src/App/Validator/Input/EmailInput.php b/src/App/Validator/Input/EmailInput.php new file mode 100644 index 00000000..3103418a --- /dev/null +++ b/src/App/Validator/Input/EmailInput.php @@ -0,0 +1,26 @@ +setRequired(true); + + $this->getFilterChain()->attachByName('StringTrim'); + + $this->getValidatorChain()->attachByName( + 'EmailAddress', + [ + 'hostnameValidator' => new Hostname(), + 'useMxCheck' => true, + ] + ); + } +} diff --git a/src/App/Validator/Input/PasswordInput.php b/src/App/Validator/Input/PasswordInput.php new file mode 100644 index 00000000..02a447d2 --- /dev/null +++ b/src/App/Validator/Input/PasswordInput.php @@ -0,0 +1,26 @@ +setRequired(true); + + $this->getFilterChain()->attachByName('StringTrim'); + + $this->getValidatorChain()->attachByName( + 'StringLength', + [ + 'encoding' => 'UTF-8', + 'min' => 6, + 'max' => 255, + ] + ); + } +} diff --git a/src/App/Validator/PasswordValidator.php b/src/App/Validator/PasswordValidator.php new file mode 100644 index 00000000..bca0e1bc --- /dev/null +++ b/src/App/Validator/PasswordValidator.php @@ -0,0 +1,15 @@ +add($this->passwordInput); + } +} diff --git a/src/Core/ConfigProvider.php b/src/Core/ConfigProvider.php new file mode 100644 index 00000000..17e6d5ca --- /dev/null +++ b/src/Core/ConfigProvider.php @@ -0,0 +1,52 @@ + $this->getDependencies(), + ConfigAbstractFactory::class => $this->getAbstractFactoryConfig(), + ]; + } + + public function getDependencies(): array + { + return [ + 'invokables' => [ + EmailInput::class => EmailInput::class, + PasswordInput::class => PasswordInput::class, + ], + 'aliases' => [ + ], + 'factories' => [ + Factory\ErrorResponseFactory::class => ConfigAbstractFactory::class, + Middleware\ApiErrorHandlerMiddleware::class => ConfigAbstractFactory::class, + Middleware\RouteNotFoundMiddleware::class => ConfigAbstractFactory::class, + + ], + ]; + } + + public function getAbstractFactoryConfig(): array + { + return [ + Factory\ErrorResponseFactory::class => [ + LoggerInterface::class, + ], + Middleware\ApiErrorHandlerMiddleware::class => [ + Factory\ErrorResponseFactory::class, + ], + Middleware\RouteNotFoundMiddleware::class => [ + LoggerInterface::class, + ], + ]; + } +} diff --git a/src/Core/Entity/Account/AccountAccessAuthCollectionInterface.php b/src/Core/Entity/Account/AccountAccessAuthCollectionInterface.php new file mode 100644 index 00000000..173da23d --- /dev/null +++ b/src/Core/Entity/Account/AccountAccessAuthCollectionInterface.php @@ -0,0 +1,16 @@ + 'Eigentümer', + AccountRoles::Administrator => 'Administrator', + AccountRoles::Moderator => 'Moderator', + AccountRoles::User => 'Benutzer', + AccountRoles::Guest => 'Gast' + }; + } +} diff --git a/src/Core/Enum/AccountVisibleStatus.php b/src/Core/Enum/AccountVisibleStatus.php new file mode 100644 index 00000000..f0d1095f --- /dev/null +++ b/src/Core/Enum/AccountVisibleStatus.php @@ -0,0 +1,23 @@ + 'online', + AccountVisibleStatus::NOT_PRESENT => 'Abwesend', + AccountVisibleStatus::DO_NOT_DISTURB => 'Bitte nicht stören', + AccountVisibleStatus::GHOST => 'unsichtbar', + AccountVisibleStatus::PERSONALIZED => 'personalisiert' + }; + } +} diff --git a/src/Core/Enum/DataType.php b/src/Core/Enum/DataType.php new file mode 100644 index 00000000..fca7e15b --- /dev/null +++ b/src/Core/Enum/DataType.php @@ -0,0 +1,19 @@ +value, $this->getHttpStatusCode(), $previous); + $this->context = $context; + $this->responseMessage = $responseMessage; + $this->logLevel = $loglevel; + } + + abstract public function getHttpStatusCode(): int; + + public function getContext(): array + { + return $this->context; + } + + public function getResponseMessage(): StatusMessage + { + return $this->responseMessage; + } + + public function getLogLevel(): Level + { + return $this->logLevel; + } +} diff --git a/src/Core/Exception/HttpHandledInvalidArgumentAsSuccessException.php b/src/Core/Exception/HttpHandledInvalidArgumentAsSuccessException.php new file mode 100644 index 00000000..5ea924b6 --- /dev/null +++ b/src/Core/Exception/HttpHandledInvalidArgumentAsSuccessException.php @@ -0,0 +1,27 @@ +get('config')['database']; + + $dsn = $settings['driver'] === 'mysql' + ? 'mysql:dbname=' . $settings['dbname'] . ';host=' . $settings['host'] . ';port=' . $settings['port'] + . ';charset=utf8mb4' + : 'sqlite:' . $settings['host']; + $user = $settings['user']; + $password = $settings['password']; + $options = [ + PDO::ATTR_ERRMODE => $settings['error'], + PDO::ATTR_EMULATE_PREPARES => $settings['emulate_prepares'], + ]; + + return new PDO($dsn, $user, $password, $options); + } +} diff --git a/src/Core/Factory/ErrorResponseFactory.php b/src/Core/Factory/ErrorResponseFactory.php new file mode 100644 index 00000000..d0c2222a --- /dev/null +++ b/src/Core/Factory/ErrorResponseFactory.php @@ -0,0 +1,51 @@ +getHttpStatusCode(); + $logLevel = $e->getLogLevel(); + $logContext = $e->getContext(); + $responseMessage = $e->getResponseMessage(); + + $this->logger->log( + $logLevel->value, + sprintf('[%d] %s', $statusCode, $e->getMessage()), + $logContext + ); + } else { + $this->logger->log( + Level::Critical, + sprintf('[%d] Unhandled exception %s', $statusCode, $e->getMessage()), + ['exception' => $e] + ); + } + + $message = HttpResponseMessage::create($statusCode, $responseMessage); + return new JsonResponse($message, $message->statusCode); + } +} diff --git a/src/Core/Factory/MailFactory.php b/src/Core/Factory/MailFactory.php new file mode 100644 index 00000000..140c2c3e --- /dev/null +++ b/src/Core/Factory/MailFactory.php @@ -0,0 +1,19 @@ +get('config'); + + $settings = $settings['mailer']; + return new Mailer(Transport::fromDsn($settings['dsn'])); + } +} diff --git a/src/Core/Factory/QueryFactory.php b/src/Core/Factory/QueryFactory.php new file mode 100644 index 00000000..063a6deb --- /dev/null +++ b/src/Core/Factory/QueryFactory.php @@ -0,0 +1,21 @@ +get(PDO::class)); + } +} diff --git a/src/Core/Factory/UuidFactory.php b/src/Core/Factory/UuidFactory.php new file mode 100644 index 00000000..cf9e115e --- /dev/null +++ b/src/Core/Factory/UuidFactory.php @@ -0,0 +1,13 @@ +get('config')['logger']['path']; + + $date = (new DateTime())->format('Y-m-d'); + $path = rtrim($path, '/') . '/' . $date . '/'; + + if (!is_dir($path)) { + mkdir($path, 0775); + } + + $dateFormat = 'Y-m-d H:i:s'; + $output = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n"; + $formatter = new LineFormatter($output, $dateFormat); + $stackTraceFormater = clone $formatter; + $stackTraceFormater->includeStacktraces(true); + + $logger = new Logger('log'); + + $logger->pushHandler(new StreamHandler($path . 'default.log')->setFormatter($formatter)); + + $errorHandler = new StreamHandler($path . 'error.log', Level::Error)->setFormatter($formatter); + $errorHandler = new FilterHandler($errorHandler, Level::Error, Level::Error); + + $logger->pushHandler($errorHandler); + + $errorHandler = new StreamHandler($path . 'warning.log', Level::Warning)->setFormatter($formatter); + $errorHandler = new FilterHandler($errorHandler, Level::Error, Level::Error); + + $logger->pushHandler($errorHandler); + + $logger->pushHandler( + new StreamHandler($path . 'critical.log', Level::Critical)->setFormatter($stackTraceFormater) + ); + $logger->pushProcessor(new PsrLogMessageProcessor()); + $logger->pushProcessor( + new MetaDataProcessor( + filter_input(INPUT_SERVER, 'REMOTE_ADDR'), + filter_input(INPUT_SERVER, 'REQUEST_URI'), + filter_input(INPUT_SERVER, 'REQUEST_METHOD'), + filter_input(INPUT_SERVER, 'REDIRECT_URL'), + filter_input_array(INPUT_GET) + ) + ); + return $logger; + } +} diff --git a/src/Core/Logger/MetaDataProcessor.php b/src/Core/Logger/MetaDataProcessor.php new file mode 100644 index 00000000..ff02b90f --- /dev/null +++ b/src/Core/Logger/MetaDataProcessor.php @@ -0,0 +1,29 @@ +extra['Remote'] = $this->remoteAddr; + $record->extra['URI'] = $this->uri; + $record->extra['Method'] = $this->method; + $record->extra['Redirect'] = $this->redirect; + $record->extra['Query'] = $this->query; + + return $record; + } +} diff --git a/src/Core/Middleware/ApiErrorHandlerMiddleware.php b/src/Core/Middleware/ApiErrorHandlerMiddleware.php new file mode 100644 index 00000000..a9371244 --- /dev/null +++ b/src/Core/Middleware/ApiErrorHandlerMiddleware.php @@ -0,0 +1,27 @@ +handle($request); + } catch (Throwable $e) { + return $this->errorResponseFactory->createFromThrowable($e); + } + } +} diff --git a/src/Core/Middleware/RouteNotFoundMiddleware.php b/src/Core/Middleware/RouteNotFoundMiddleware.php new file mode 100644 index 00000000..aa37efe6 --- /dev/null +++ b/src/Core/Middleware/RouteNotFoundMiddleware.php @@ -0,0 +1,24 @@ +logger->notice('Route not found'); + + return $handler->handle($request); + } +} diff --git a/src/Core/Repository/AccountAccessAuthRepositoryInterface.php b/src/Core/Repository/AccountAccessAuthRepositoryInterface.php new file mode 100644 index 00000000..6258aa2e --- /dev/null +++ b/src/Core/Repository/AccountAccessAuthRepositoryInterface.php @@ -0,0 +1,29 @@ +value = $value instanceof self ? (string)$value : $this->prepareValue($value); + } + + public function toString(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->toString(); + } + + public function serialize(): string + { + return $this->toString(); + } + + public function __serialize(): array + { + return ['string' => $this->toString()]; + } + + public function unserialize(string $data): void + { + $this->__construct($data); + } + + public function __unserialize(array $data): void + { + // @codeCoverageIgnoreStart + if (!isset($data['string'])) { + throw new ValueError(sprintf('%s(): Argument #1 ($data) is invalid', __METHOD__)); + } + // @codeCoverageIgnoreEnd + + $this->unserialize($data['string']); + } + + public function jsonSerialize(): string + { + return $this->toString(); + } + + private function prepareValue(string $value): string + { + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + throw new HttpInvalidArgumentException( + LogMessage::EMAIL_FORMAT_REQUIRED, + StatusMessage::INVALID_DATA, + [ + 'email' => $value, + ] + ); + } + + return $value; + } +} diff --git a/src/Core/Type/TypeInterface.php b/src/Core/Type/TypeInterface.php new file mode 100644 index 00000000..7f7c9ae9 --- /dev/null +++ b/src/Core/Type/TypeInterface.php @@ -0,0 +1,13 @@ + $collection + */ + protected array $collection = []; + private int $position = 0; + + public function offsetExists(mixed $offset): bool + { + return isset($this->collection[$offset]); + } + + /** + * @throws UndefinedOffsetException + */ + public function offsetGet(mixed $offset): mixed + { + if (!$this->offsetExists($offset)) { + throw new UndefinedOffsetException( + sprintf('Undefined offset: %s in Collection %s on Line %s', $offset, __FILE__, __LINE__) + ); + } + + return $this->collection[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + is_null($offset) + ? $this->collection[] = $value + : $this->collection[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + unset($this->collection[$offset]); + } + + public function current(): mixed + { + return $this->collection[$this->position]; + } + + public function next(): void + { + $this->position++; + } + + public function key(): int + { + return $this->position; + } + + public function valid(): bool + { + return $this->offsetExists($this->position); + } + + public function rewind(): void + { + $this->position = 0; + } + + public function count(): int + { + return count($this->collection); + } + + public function first(): mixed + { + $collection = $this->collection; + return array_shift($collection); + } + + public function last(): mixed + { + $collection = $this->collection; + return array_pop($collection); + } + + /** + * @return array + */ + public function filter(Closure $function): array + { + return array_filter($this->collection, $function); + } + + public function getElements(): array + { + return $this->collection; + } + + public function jsonSerialize(): array + { + return $this->getElements(); + } +} diff --git a/src/Core/Utils/CollectionInterface.php b/src/Core/Utils/CollectionInterface.php new file mode 100644 index 00000000..9e838aa7 --- /dev/null +++ b/src/Core/Utils/CollectionInterface.php @@ -0,0 +1,12 @@ +initContainer(); + $this->initApp(); + $this->initPipeline(); + $this->initRoutes(); + } + + public static function tearDownAfterClass(): void + { + system('php ' . dirname(__FILE__) . '/bootstrap.php'); + } + + protected function initContainer(): void + { + $this->container = require __DIR__ . '/../../config/container.php'; + } + + protected function initApp(): void + { + $this->app = $this->container->get(Application::class); + } + + protected function initPipeline(): void + { + $factory = $this->container->get(MiddlewareFactory::class); + (require __DIR__ . '/../../config/pipeline.php')($this->app, $factory, $this->container); + } + + protected function initRoutes(): void + { + $factory = $this->container->get(MiddlewareFactory::class); + (require __DIR__ . '/../../config/routes.php')($this->app, $factory, $this->container); + } + + /** + * Override parent method's hard-coded regex + */ + public static function bodyMatchesJson(array $constraints): Constraint + { + return Assert::logicalAnd( + self::hasHeader( + 'content-type', + Assert::matchesRegularExpression( + ',^application/(.+\+)?json(;.+)?$,' + ) + ), + self::bodyMatches( + Assert::logicalAnd( + Assert::isJson(), + new JsonValueMatchesMany($constraints) + ) + ) + ); + } +} diff --git a/tests/FunctionalTest/Mock/NullLogger.php b/tests/FunctionalTest/Mock/NullLogger.php new file mode 100644 index 00000000..ac8c5631 --- /dev/null +++ b/tests/FunctionalTest/Mock/NullLogger.php @@ -0,0 +1,73 @@ +accountRepository = $this->container->get(AccountRepositoryInterface::class); + $this->accountAccessAuthRepository = $this->container->get(AccountAccessAuthRepository::class); + $this->clientIdentificationService = $this->container->get(ClientIdentificationService::class); + $this->clientIdentificationData = ClientIdentificationData::create( + self::CLIENT_IDENTIFICATION, + self::USER_AGENT + ); + $clientIdentifcationHash = $this->clientIdentificationService->getClientIdentificationHash( + $this->clientIdentificationData + ); + $this->clientIdentification = ClientIdentification::create( + $this->clientIdentificationData, + $clientIdentifcationHash + ); + $this->refreshTokenService = $this->container->get(RefreshTokenService::class); + $this->accessTokenService = $this->container->get(AccessTokenService::class); + $this->refreshToken = $this->refreshTokenService->generate($this->clientIdentification); + /** @var Query $query */ + $query = $this->container->get(Query::class); + $this->PDO = $query->getPdo(); + } + + public function testReturnANewAccessToken(): void + { + $userAccount = $this->accountRepository->findByName('User'); + $accountAccessAuth = new AccountAccessAuth( + null, + $userAccount->id, + 'Testing', + $this->refreshToken, + self::USER_AGENT, + $this->clientIdentificationService->getClientIdentificationHash($this->clientIdentificationData), + new DateTimeImmutable() + ); + $this->accountAccessAuthRepository->insert($accountAccessAuth); + $accountAccessAuthId = (int)$this->PDO->lastInsertId(); + + $request = new ServerRequest( + uri: '/api/token/refresh', + method: 'GET', + headers: [ + 'x-ident' => self::CLIENT_IDENTIFICATION, + 'Authentication' => $this->refreshToken, + 'User-Agent' => self::USER_AGENT, + ] + ); + + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + $this->assertThat($response, $this->bodyMatchesJson([ + 'accessToken' => Assert::isType('string'), + ])); + + $isAccessToken = $this->accessTokenService->isValid($content['accessToken']); + + $this->assertTrue($isAccessToken); + + $this->accountAccessAuthRepository->deleteById($accountAccessAuthId); + } + + public function testGivenRefreshTokenIsInvalid(): void + { + $userAccount = $this->accountRepository->findByName('User'); + $accountAccessAuth = new AccountAccessAuth( + null, + $userAccount->id, + 'Testing', + $this->refreshToken, + self::USER_AGENT, + $this->clientIdentificationService->getClientIdentificationHash($this->clientIdentificationData), + new DateTimeImmutable() + ); + $this->accountAccessAuthRepository->insert($accountAccessAuth); + $accountAccessAuthId = (int)$this->PDO->lastInsertId(); + + $request = new ServerRequest( + uri: '/api/token/refresh', + method: 'GET', + headers: [ + 'x-ident' => self::CLIENT_IDENTIFICATION, + 'Authentication' => self::INVALID_REFRESH_TOKEN, + 'User-Agent' => self::USER_AGENT, + ] + ); + + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_UNAUTHORIZED, $response->getStatusCode()); + $this->assertSame(StatusMessage::TOKEN_INVALID->value, $content['message']); + + $this->accountAccessAuthRepository->deleteById($accountAccessAuthId); + } + + public function testGivenRefreshTokenIsExpired(): void + { + $userAccount = $this->accountRepository->findByName('User'); + $accountAccessAuth = new AccountAccessAuth( + 1, + $userAccount->id, + 'Testing', + $this->refreshToken, + self::USER_AGENT, + $this->clientIdentificationService->getClientIdentificationHash($this->clientIdentificationData), + new DateTimeImmutable() + ); + $this->accountAccessAuthRepository->insert($accountAccessAuth); + $accountAccessAuthId = (int)$this->PDO->lastInsertId(); + + $request = new ServerRequest( + uri: '/api/token/refresh', + method: 'GET', + headers: [ + 'x-ident' => self::CLIENT_IDENTIFICATION, + 'Authentication' => self::EXPIRED_REFRESH_TOKEN, + 'User-Agent' => self::USER_AGENT, + ] + ); + + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_UNAUTHORIZED, $response->getStatusCode()); + $this->assertSame(StatusMessage::TOKEN_INVALID->value, $content['message']); + + $this->accountAccessAuthRepository->deleteById($accountAccessAuthId); + } + + public function testGivenRefreshTokenIsNotPersistenInDatabase(): void + { + $request = new ServerRequest( + uri: '/api/token/refresh', + method: 'GET', + headers: [ + 'x-ident' => self::CLIENT_IDENTIFICATION, + 'Authentication' => $this->refreshToken, + 'User-Agent' => self::USER_AGENT, + ] + ); + + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_UNAUTHORIZED, $response->getStatusCode()); + $this->assertSame(StatusMessage::TOKEN_NOT_PERSISTENT->value, $content['message']); + } + + public function testUnexpectedClientIdentification(): void + { + $userAccount = $this->accountRepository->findByName('User'); + $accountAccessAuth = new AccountAccessAuth( + 1, + $userAccount->id, + 'Testing', + $this->refreshToken, + self::USER_AGENT, + $this->clientIdentificationService->getClientIdentificationHash($this->clientIdentificationData), + new DateTimeImmutable() + ); + $this->accountAccessAuthRepository->insert($accountAccessAuth); + $accountAccessAuthId = (int)$this->PDO->lastInsertId(); + + $request = new ServerRequest( + uri: '/api/token/refresh', + method: 'GET', + headers: [ + 'x-ident' => self::UNEXPECTED_CLIENT_IDENTIFICATION, + 'Authentication' => $this->refreshToken, + 'User-Agent' => self::USER_AGENT, + ] + ); + + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_UNAUTHORIZED, $response->getStatusCode()); + $this->assertSame(StatusMessage::CLIENT_UNEXPECTED->value, $content['message']); + + $this->accountAccessAuthRepository->deleteById($accountAccessAuthId); + } + + public function testUnexpectedUserAgent(): void + { + $userAccount = $this->accountRepository->findByName('User'); + $accountAccessAuth = new AccountAccessAuth( + 1, + $userAccount->id, + 'Testing', + $this->refreshToken, + self::USER_AGENT, + $this->clientIdentificationService->getClientIdentificationHash($this->clientIdentificationData), + new DateTimeImmutable() + ); + $this->accountAccessAuthRepository->insert($accountAccessAuth); + $accountAccessAuthId = (int)$this->PDO->lastInsertId(); + + $request = new ServerRequest( + uri: '/api/token/refresh', + method: 'GET', + headers: [ + 'x-ident' => self::CLIENT_IDENTIFICATION, + 'Authentication' => $this->refreshToken, + 'User-Agent' => self::UNEXPECTED_USER_AGENT, + ] + ); + + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_UNAUTHORIZED, $response->getStatusCode()); + $this->assertSame(StatusMessage::CLIENT_UNEXPECTED->value, $content['message']); + + $this->accountAccessAuthRepository->deleteById($accountAccessAuthId); + } +} diff --git a/tests/FunctionalTest/Root/Account/AccountActivationHandlerTest.php b/tests/FunctionalTest/Root/Account/AccountActivationHandlerTest.php new file mode 100644 index 00000000..1a0e1e82 --- /dev/null +++ b/tests/FunctionalTest/Root/Account/AccountActivationHandlerTest.php @@ -0,0 +1,119 @@ +container->get(UuidFactoryInterface::class); + + /** @var AccountActivationRepositoryInterface $activationRepository */ + $activationRepository = $this->container->get(AccountActivationRepositoryInterface::class); + + $testAccountActivate = new AccountActivation( + id: null, + email: new Email('test@example.com'), + token: $uuid->uuid7(), + createdAt: new DateTimeImmutable() + ); + + $activationRepository->insert($testAccountActivate); + + $request = new ServerRequest( + uri: '/api/account/activation/' . $testAccountActivate->token->getHex()->toString(), + method: 'POST' + ); + $request = $request->withParsedBody([ + 'accountName' => 'Test', + 'password' => 'TestBlaBlubb', + ]); + $response = $this->app->handle($request); + + $emptyAccountActivate = $activationRepository->findByToken( + $testAccountActivate->token->getHex()->toString() + ); + + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + $this->assertNull($emptyAccountActivate); + } + + public function testTokenNotGiven(): void + { + $request = new ServerRequest( + uri: '/api/account/activation/', + method: 'POST' + ); + $request = $request->withParsedBody([ + 'accountName' => 'Test', + 'password' => 'TestBlaBlubb', + ]); + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertThat($response, $this->bodyMatchesJson([ + 'statusCode' => Assert::isType(DataType::INTEGER->value), + 'message' => Assert::isType(DataType::STRING->value), + ])); + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertSame(StatusMessage::TOKEN_INVALID->value, $content['message']); + } + + public function testBodyIsInvalid(): void + { + $request = new ServerRequest( + uri: '/api/account/activation/', + method: 'POST' + ); + + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertThat($response, $this->bodyMatchesJson([ + 'statusCode' => Assert::isType(DataType::INTEGER->value), + 'message' => Assert::isType(DataType::STRING->value), + ])); + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertSame(StatusMessage::INVALID_DATA->value, $content['message']); + } + + public function testTokenIsInvalidOrNotPersistent(): void + { + $request = new ServerRequest( + uri: '/api/account/activation/1ddwrer2', + method: 'POST' + ); + $request = $request->withParsedBody([ + 'accountName' => 'Test', + 'password' => 'TestBlaBlubb', + ]); + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertThat($response, $this->bodyMatchesJson([ + 'statusCode' => Assert::isType(DataType::INTEGER->value), + 'message' => Assert::isType(DataType::STRING->value), + ])); + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertSame(StatusMessage::TOKEN_INVALID->value, $content['message']); + } +} diff --git a/tests/FunctionalTest/Root/Account/AccountPasswordForgottenHandlerTest.php b/tests/FunctionalTest/Root/Account/AccountPasswordForgottenHandlerTest.php new file mode 100644 index 00000000..edf80a39 --- /dev/null +++ b/tests/FunctionalTest/Root/Account/AccountPasswordForgottenHandlerTest.php @@ -0,0 +1,61 @@ +withParsedBody(['email' => self::EMAIL_VALID]); + $response = $this->app->handle($request); + + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = $this->container->get(AccountRepositoryInterface::class); + $account = $accountRepository->findByEmail(new Email(self::EMAIL_VALID)); + + /** @var TokenRepositoryInterface $tokenRepository */ + $tokenRepository = $this->container->get(TokenRepositoryInterface::class); + + $token = $tokenRepository->findByAccountId($account->id); + + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + $this->assertArrayHasKey(0, $token); + $this->assertInstanceOf(UuidInterface::class, $token[0]->token); + } + + public function testDontCreateTokenForPasswordChange(): void + { + $request = new ServerRequest( + uri: '/api/account/password/forgotten', + method: 'POST' + ); + $request = $request->withParsedBody(['email' => self::EMAIL_INVALID]); + $response = $this->app->handle($request); + + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = $this->container->get(AccountRepositoryInterface::class); + $account = $accountRepository->findByEmail(new Email(self::EMAIL_INVALID)); + + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + $this->assertNull($account); + } +} diff --git a/tests/FunctionalTest/Root/Account/AccountPasswordHandlerTest.php b/tests/FunctionalTest/Root/Account/AccountPasswordHandlerTest.php new file mode 100644 index 00000000..4a14ea84 --- /dev/null +++ b/tests/FunctionalTest/Root/Account/AccountPasswordHandlerTest.php @@ -0,0 +1,134 @@ +accountRepository = $this->container->get(AccountRepositoryInterface::class); + $this->tokenRepository = $this->container->get(TokenRepositoryInterface::class); + $this->uuid = $this->container->get(UuidFactoryInterface::class); + + $this->account = $this->accountRepository->findByEmail(new Email('user@example.com')); + $this->token = new Token( + null, + $this->account->id, + TokenType::EMail, + $this->uuid->uuid7(), + new DateTimeImmutable() + ); + } + + public function testChangePasswortHasStatusOk(): void + { + $this->tokenRepository->insert($this->token); + + $request = new ServerRequest( + uri: '/api/account/password/' . $this->token->token->getHex()->toString(), + method: 'PATCH' + ); + $request = $request->withParsedBody(['password' => self::PASSWORD_NEW]); + $response = $this->app->handle($request); + + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + } + + public function testChangedPasswordIsValid(): void + { + $this->tokenRepository->insert($this->token); + + $request = new ServerRequest( + uri: '/api/account/password/' . $this->token->token->getHex()->toString(), + method: 'PATCH' + ); + $request = $request->withParsedBody(['password' => self::PASSWORD_NEW]); + $this->app->handle($request); + + $changedAccount = $this->accountRepository->findById($this->account->id); + + $this->assertNotSame($this->account->password, $changedAccount->password); + $this->assertTrue(password_verify(self::PASSWORD_NEW, $changedAccount->password)); + } + + public function testChangedPasswordIsInvalid(): void + { + $this->tokenRepository->insert($this->token); + + $request = new ServerRequest( + uri: '/api/account/password/' . $this->token->token->getHex()->toString(), + method: 'PATCH' + ); + $request = $request->withParsedBody(['password' => self::PASSWORD_NEW_INVALID]); + $response = $this->app->handle($request); + + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + } + + public function testTokenIsInvalid(): void + { + $request = new ServerRequest( + uri: '/api/account/password/InvalidToken', + method: 'PATCH' + ); + $request = $request->withParsedBody(['password' => self::PASSWORD_NEW]); + $response = $this->app->handle($request); + + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + } + + public function testTokenIsMissed(): void + { + $request = new ServerRequest( + uri: '/api/account/password/', + method: 'PATCH' + ); + $request = $request->withParsedBody(['password' => self::PASSWORD_NEW]); + $response = $this->app->handle($request); + + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + } + + public function testTokenWasDestroyed(): void + { + $this->tokenRepository->insert($this->token); + + $request = new ServerRequest( + uri: '/api/account/password/' . $this->token->token->getHex()->toString(), + method: 'PATCH' + ); + $request = $request->withParsedBody(['password' => self::PASSWORD_NEW]); + $this->app->handle($request); + + $destroyedToken = $this->tokenRepository->findByToken($this->token->token->getHex()->toString()); + + $this->assertNull($destroyedToken); + } +} diff --git a/tests/FunctionalTest/Root/Account/AccountRegisterHandlerTest.php b/tests/FunctionalTest/Root/Account/AccountRegisterHandlerTest.php new file mode 100644 index 00000000..949af802 --- /dev/null +++ b/tests/FunctionalTest/Root/Account/AccountRegisterHandlerTest.php @@ -0,0 +1,104 @@ +app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertThat($response, $this->bodyMatchesJson([ + 'statusCode' => Assert::isType(DataType::INTEGER->value), + 'message' => Assert::isType(DataType::STRING->value), + ])); + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertSame(StatusMessage::INVALID_DATA->value, $content['message']); + } + + public function testBodyHasInvalidParameter(): void + { + $request = new ServerRequest( + uri: '/api/account', + method: 'POST' + ); + + $response = $this->app->handle($request); + $request = $request->withParsedBody(['password' => 'password']); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertThat($response, $this->bodyMatchesJson([ + 'statusCode' => Assert::isType(DataType::INTEGER->value), + 'message' => Assert::isType(DataType::STRING->value), + ])); + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertSame(StatusMessage::INVALID_DATA->value, $content['message']); + } + + public function testActivationDataSetWasCreated(): void + { + $request = new ServerRequest( + uri: '/api/account', + method: 'POST' + ); + $request = $request->withParsedBody(['email' => 'Tester@example.com']); + $response = $this->app->handle($request); + + /** @var AccountActivationRepositoryInterface $repository */ + $repository = $this->container->get(AccountActivationRepositoryInterface::class); + $activationDataSet = $repository->findEmail(new Email('Tester@example.com')); + + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + $this->assertArrayHasKey(0, $activationDataSet); + $this->assertInstanceOf(AccountActivationInterface::class, $activationDataSet[0]); + } + + public function testPasswordDataSetWasCreated(): void + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = $this->container->get(AccountRepositoryInterface::class); + $account = $accountRepository->findByEmail(new Email('user@example.com')); + + /** @var TokenRepositoryInterface $tokenRepository */ + $tokenRepository = $this->container->get(TokenRepositoryInterface::class); + $tokenRepository->deleteByAccountId($account->id); + + $request = new ServerRequest( + uri: '/api/account', + method: 'POST' + ); + $request = $request->withParsedBody(['email' => 'user@example.com']); + $response = $this->app->handle($request); + + $activationDataSet = $tokenRepository->findByAccountId($account->id); + + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + $this->assertArrayHasKey(0, $activationDataSet); + $this->assertInstanceOf(TokenInterface::class, $activationDataSet[0]); + } +} diff --git a/tests/FunctionalTest/Root/Account/AuthenticationHandlerTest.php b/tests/FunctionalTest/Root/Account/AuthenticationHandlerTest.php new file mode 100644 index 00000000..973dee06 --- /dev/null +++ b/tests/FunctionalTest/Root/Account/AuthenticationHandlerTest.php @@ -0,0 +1,180 @@ +withParsedBody(['email' => $email, 'password' => $password]) + ->withAddedHeader('x-ident', (string)rand()); + $response = $this->app->handle($request); + + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + } + + #[DataProvider('invalidAuthenticateDataProvider')] + public function testAuthenticateFailed(string $email, string $password): void + { + $request = new ServerRequest( + uri: '/api/account/authentication', + method: 'POST' + ); + $request = $request->withParsedBody(['email' => $email, 'password' => $password]) + ->withAddedHeader('x-ident', (string)rand()); + $response = $this->app->handle($request); + + $this->assertSame(HTTP::STATUS_UNAUTHORIZED, $response->getStatusCode()); + } + + public function testNoDoubleSameLogins(): void + { + $request = new ServerRequest( + uri: '/api/account/authentication', + method: 'POST' + ); + $request = $request->withParsedBody(['email' => 'owner@example.com', 'password' => 'owner123456']) + ->withAddedHeader('x-ident', (string)rand()); + + $response = $this->app->handle($request); + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + + $response = $this->app->handle($request); + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + } + + public function testAccountIsAlreadyAuthenticated(): void + { + $request = new ServerRequest( + uri: '/api/account/authentication', + method: 'POST' + ); + $request = $request->withParsedBody(['email' => 'owner@example.com', 'password' => 'owner123456']) + ->withAddedHeader('x-ident', (string)rand()) + ->withAddedHeader('Authentication', 'Authentication'); + + $response = $this->app->handle($request); + $this->assertSame(HTTP::STATUS_UNAUTHORIZED, $response->getStatusCode()); + } + + public function testAccountIsAlreadyAuthorized(): void + { + $request = new ServerRequest( + uri: '/api/account/authentication', + method: 'POST' + ); + $request = $request->withParsedBody(['email' => 'admin@example.com', 'password' => 'admin123456']) + ->withAddedHeader('x-ident', (string)rand()) + ->withAddedHeader('Authorization', 'Authorization'); + + $response = $this->app->handle($request); + $this->assertSame(HTTP::STATUS_UNAUTHORIZED, $response->getStatusCode()); + } + + public function testResponseHasValidAccessAndRefreshToken(): void + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = $this->container->get(AccountRepositoryInterface::class); + + /** @var AccessTokenService $accessTokenService */ + $accessTokenService = $this->container->get(AccessTokenService::class); + + /** @var RefreshTokenService $refreshTokenService */ + $refreshTokenService = $this->container->get(RefreshTokenService::class); + + /** @var UuidFactoryInterface $uuid */ + $uuid = $this->container->get(UuidFactoryInterface::class); + + $account = new \App\Entity\Account\Account( + null, + $uuid->uuid7(), + 'I see your Token', + password_hash('I see your Token', PASSWORD_DEFAULT), + new Email('iseeyourtoken@example.com'), + new DateTimeImmutable(), + new DateTimeImmutable() + ); + $accountRepository->insert($account); + + $request = new ServerRequest( + uri: '/api/account/authentication', + method: 'POST' + ); + $request = $request->withParsedBody( + ['email' => $account->email->toString(), 'password' => 'I see your Token'] + ); + + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + $this->assertThat($response, $this->bodyMatchesJson([ + 'accessToken' => Assert::isType('string'), + 'refreshToken' => Assert::isType('string'), + ])); + + $isAccessToken = $accessTokenService->isValid($content['accessToken']); + $isRefreshToken = $refreshTokenService->isValid($content['refreshToken']); + + $this->assertTrue($isAccessToken); + $this->assertTrue($isRefreshToken); + } + + public static function validAccountDataProvider(): array + { + return [ + 'Owner' => ['owner@example.com', 'owner123456'], + 'Administrator' => ['admin@example.com', 'admin123456'], + 'Moderator' => ['moderator@example.com', 'moderator'], + 'User' => ['user@example.com', 'user123456'], + 'Valid fixed Account Constant' => [Account::EMAIL, Account::PASSWORD_STRING,], + ]; + } + + public static function invalidAuthenticateDataProvider(): array + { + return [ + 'Empty Fields' => ['', ''], + 'Empty E-Mail' => ['', '123456'], + 'Empty Password' => ['account@example.com', ''], + 'No E-Mail' => ['no E-Mail', '123456'], + 'Invalid email prefixe' => ['abc..def@mail.com', '123456'], + 'Password too Short' => ['account@example.com', '123'], + 'Password too Long' => [ + 'account@example.com', + '11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' + . '11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' + . '111111111111111111111111111111111111111111111111111111', + ], + 'Account with bad Password' => ['owner@example.com', '123456'], + 'SQL Injection Comment one' => ["owner@example.com'--", '123456'], + 'SQL Injection Comment two' => ["owner@example.com';", '123456'], + ]; + } +} diff --git a/tests/FunctionalTest/Root/Account/InvalidEMailAddressProviderTrait.php b/tests/FunctionalTest/Root/Account/InvalidEMailAddressProviderTrait.php new file mode 100644 index 00000000..36bb2927 --- /dev/null +++ b/tests/FunctionalTest/Root/Account/InvalidEMailAddressProviderTrait.php @@ -0,0 +1,54 @@ +withParsedBody(['email' => $email]); + $response = $this->app->handle($request); + $content = $this->getContentAsJson($response); + + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertThat($response, $this->bodyMatchesJson([ + 'statusCode' => Assert::isType(DataType::INTEGER->value), + 'message' => Assert::isType(DataType::STRING->value), + ])); + $this->assertSame(HTTP::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertSame(StatusMessage::INVALID_DATA->value, $content['message']); + } + + public static function invalidEMailAddressProvider(): array + { + return [ + 'Missing @ symbol' => ['invalidemail.com'], + 'Missing domain' => ['user@'], + 'Missing local part' => ['@domain.com'], + 'Consecutive dots' => ['user..name@domain.com'], + 'Invalid character in local part' => ['user:name@domain.com'], + 'Invalid character in domain' => ['user@domain!.com'], + 'Missing top-level domain' => ['user@domain'], + 'Space in local part' => ['user name@domain.com'], + 'Space in domain' => ['user@domain .com'], + 'Double dot in domain' => ['user@domain..com'], + 'Invalid character' => ['user@-domain.com'], + 'Invalid domain (example.invalid)' => ['test@example.invalid'], + 'Invalid top level domain (example.web)' => ['test@example.web'], + 'Trailing space' => ['test@example '], + 'Leading space' => [' test@example.com'], + ]; + } +} diff --git a/tests/FunctionalTest/Root/PingHandlerTest.php b/tests/FunctionalTest/Root/PingHandlerTest.php new file mode 100644 index 00000000..7c861301 --- /dev/null +++ b/tests/FunctionalTest/Root/PingHandlerTest.php @@ -0,0 +1,29 @@ +app->handle($request); + + $this->assertSame($response->getStatusCode(), HTTP::STATUS_OK); + $this->assertThat($response, $this->bodyMatchesJson([ + 'ack' => Assert::greaterThanOrEqual($timestamp), + ])); + } +} diff --git a/tests/FunctionalTest/bootstrap.php b/tests/FunctionalTest/bootstrap.php new file mode 100644 index 00000000..aba6b5ec --- /dev/null +++ b/tests/FunctionalTest/bootstrap.php @@ -0,0 +1,10 @@ +addSql($sql); + } + + public function down(Schema $schema): void + { + $sql = <<addSql($sql); + } +} diff --git a/tests/UnitTest/AppTest/Handler/AbstractTestHandler.php b/tests/UnitTest/AppTest/Handler/AbstractTestHandler.php new file mode 100644 index 00000000..9a1c05a8 --- /dev/null +++ b/tests/UnitTest/AppTest/Handler/AbstractTestHandler.php @@ -0,0 +1,24 @@ +request = new MockServerRequest(); + + parent::setUp(); + } +} diff --git a/tests/UnitTest/AppTest/Handler/AuthenticationHandlerTest.php b/tests/UnitTest/AppTest/Handler/AuthenticationHandlerTest.php new file mode 100644 index 00000000..c7825b14 --- /dev/null +++ b/tests/UnitTest/AppTest/Handler/AuthenticationHandlerTest.php @@ -0,0 +1,35 @@ +request->withAttribute(AccessToken::class, AccessToken::fromString(Token::ACCESS_TOKEN_VALID)) + ->withAttribute(RefreshToken::class, RefreshToken::fromString(Token::REFRESH_TOKEN_VALID)); + + $authenticationHandler = new AuthenticationHandler(); + + $response = $authenticationHandler->handle($request); + + $json = json_decode((string)$response->getBody(), null, 512, JSON_THROW_ON_ERROR); + + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(HTTP::STATUS_OK, $response->getStatusCode()); + $this->assertTrue(property_exists($json, 'accessToken') && $json->accessToken === Token::ACCESS_TOKEN_VALID); + $this->assertTrue(property_exists($json, 'refreshToken') && $json->refreshToken === Token::REFRESH_TOKEN_VALID); + } +} diff --git a/tests/UnitTest/AppTest/Handler/PingHandlerTest.php b/tests/UnitTest/AppTest/Handler/PingHandlerTest.php new file mode 100644 index 00000000..ba820e46 --- /dev/null +++ b/tests/UnitTest/AppTest/Handler/PingHandlerTest.php @@ -0,0 +1,29 @@ +handle( + $this->createMock(ServerRequestInterface::class) + ); + + $json = json_decode((string)$response->getBody(), null, 512, JSON_THROW_ON_ERROR); + + self::assertInstanceOf(JsonResponse::class, $response); + self::assertTrue(property_exists($json, 'ack') && $json->ack !== null); + } +} diff --git a/tests/UnitTest/AppTest/Hydrator/AccountAccessAuthHydratorTest.php b/tests/UnitTest/AppTest/Hydrator/AccountAccessAuthHydratorTest.php new file mode 100644 index 00000000..5675db67 --- /dev/null +++ b/tests/UnitTest/AppTest/Hydrator/AccountAccessAuthHydratorTest.php @@ -0,0 +1,55 @@ +hydrate(AccountAccessAuth::VALID_DATA); + + $this->assertInstanceOf(AccountAccessAuthInterface::class, $account); + $this->assertSame(AccountAccessAuth::ID, $account->id); + } + + public function testCanHydrateAccountAccessAuthCollection(): void + { + $hydrator = new AccountAccessAuthHydrator(); + + $accounts = $hydrator->hydrateCollection([AccountAccessAuth::VALID_DATA]); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $accounts); + $this->assertInstanceOf(AccountAccessAuthInterface::class, $accounts[0]); + $this->assertSame(AccountAccessAuth::ID, $accounts[0]->id); + } + + public function testCanExtractAccountAccessAuth(): void + { + $hydrator = new AccountAccessAuthHydrator(); + + $account = $hydrator->hydrate(AccountAccessAuth::VALID_DATA); + $account = $hydrator->extract($account); + + $this->assertIsArray($account); + $this->assertSame(AccountAccessAuth::VALID_DATA, $account); + } + + public function testCanExtractAccountAccessAuthCollection(): void + { + $hydrator = new AccountAccessAuthHydrator(); + $accounts = $hydrator->hydrateCollection([AccountAccessAuth::VALID_DATA]); + $accounts = $hydrator->extractCollection($accounts); + + $this->assertIsArray($accounts); + $this->assertArrayHasKey(0, $accounts); + $this->assertSame(AccountAccessAuth::VALID_DATA, $accounts[0]); + } +} diff --git a/tests/UnitTest/AppTest/Hydrator/AccountHydratorTest.php b/tests/UnitTest/AppTest/Hydrator/AccountHydratorTest.php new file mode 100644 index 00000000..1b34dfea --- /dev/null +++ b/tests/UnitTest/AppTest/Hydrator/AccountHydratorTest.php @@ -0,0 +1,66 @@ +uuidFactory = new UuidFactory(); + } + + public function testCanHydrateAccount(): void + { + $hydrator = new AccountHydrator($this->uuidFactory); + + $account = $hydrator->hydrate(Account::VALID_DATA); + + $this->assertInstanceOf(AccountInterface::class, $account); + $this->assertSame(Account::ID, $account->id); + } + + public function testCanHydrateAccountCollection(): void + { + $hydrator = new AccountHydrator($this->uuidFactory); + + /** @var AccountInterface[] $accounts | [] */ + $accounts = $hydrator->hydrateCollection([Account::VALID_DATA]); + + $this->assertInstanceOf(AccountCollectionInterface::class, $accounts); + $this->assertInstanceOf(AccountInterface::class, $accounts[0]); + $this->assertSame(Account::ID, $accounts[0]->id); + } + + public function testCanExtractAccount(): void + { + $hydrator = new AccountHydrator($this->uuidFactory); + + $account = $hydrator->hydrate(Account::VALID_DATA); + $account = $hydrator->extract($account); + + $this->assertIsArray($account); + $this->assertSame(Account::VALID_DATA, $account); + } + + public function testCanExtractAccountCollection(): void + { + $hydrator = new AccountHydrator($this->uuidFactory); + $accounts = $hydrator->hydrateCollection([Account::VALID_DATA]); + $accounts = $hydrator->extractCollection($accounts); + + $this->assertIsArray($accounts); + $this->assertArrayHasKey(0, $accounts); + $this->assertSame(Account::VALID_DATA, $accounts[0]); + } +} diff --git a/tests/UnitTest/AppTest/Middleware/AbstractTestMiddleware.php b/tests/UnitTest/AppTest/Middleware/AbstractTestMiddleware.php new file mode 100644 index 00000000..cdd34f8b --- /dev/null +++ b/tests/UnitTest/AppTest/Middleware/AbstractTestMiddleware.php @@ -0,0 +1,28 @@ +request = new MockServerRequest(); + $this->handler = new MockRequestHandler(); + + parent::setUp(); + } +} diff --git a/tests/UnitTest/AppTest/Middleware/AccountAccessAuthPersistMiddlewareTest.php b/tests/UnitTest/AppTest/Middleware/AccountAccessAuthPersistMiddlewareTest.php new file mode 100644 index 00000000..d51531b2 --- /dev/null +++ b/tests/UnitTest/AppTest/Middleware/AccountAccessAuthPersistMiddlewareTest.php @@ -0,0 +1,108 @@ +repository = new MockAccountAccessAuthRepository(); + $this->hydrator = new AccountHydrator(new UuidFactory()); + } + + public function testCanPersistAccountAccessAuth(): void + { + $middleware = new PersistAuthenticationMiddleware($this->repository); + $account = $this->hydrator->hydrate(Account::VALID_DATA); + $clientData = ClientIdentificationData::create('1', 'default'); + $clientIdent = ClientIdentification::create($clientData, '1234'); + $refreshToken = RefreshToken::fromString('1234'); + + $request = $this->request->withAttribute(AccountInterface::AUTHENTICATED, $account) + ->withAttribute(ClientIdentification::class, $clientIdent) + ->withAttribute(RefreshToken::class, $refreshToken); + + $response = $middleware->process($request, $this->handler); + + $this->assertNotInstanceOf(JsonResponse::class, $response); + } + + public function testFindMissingAccountEntity(): void + { + $middleware = new PersistAuthenticationMiddleware($this->repository); + + $clientData = ClientIdentificationData::create('1', 'default'); + $clientIdent = ClientIdentification::create($clientData, '1234'); + $refreshToken = RefreshToken::fromString('1234'); + + $request = $this->request->withAttribute(ClientIdentification::class, $clientIdent) + ->withAttribute(RefreshToken::class, $refreshToken); + + $this->expectException(HttpUnauthorizedException::class); + $middleware->process($request, $this->handler); + } + + public function testFindMissingClientIdentification(): void + { + $middleware = new PersistAuthenticationMiddleware($this->repository); + + $account = $this->hydrator->hydrate(Account::VALID_DATA); + $refreshToken = RefreshToken::fromString('1234'); + + $request = $this->request->withAttribute(AccountInterface::AUTHENTICATED, $account) + ->withAttribute(RefreshToken::class, $refreshToken); + + $this->expectException(HttpUnauthorizedException::class); + $middleware->process($request, $this->handler); + } + + public function testFindMissingRefreshToken(): void + { + $middleware = new PersistAuthenticationMiddleware($this->repository); + + $account = $this->hydrator->hydrate(Account::VALID_DATA); + $clientData = ClientIdentificationData::create('1', 'default'); + $clientIdent = ClientIdentification::create($clientData, '1234'); + + $request = $this->request->withAttribute(AccountInterface::AUTHENTICATED, $account) + ->withAttribute(ClientIdentification::class, $clientIdent); + + $this->expectException(HttpUnauthorizedException::class); + $middleware->process($request, $this->handler); + } + + public function testAccountAccessAuthHasDuplicat(): void + { + $middleware = new PersistAuthenticationMiddleware($this->repository); + $account = $this->hydrator->hydrate(Account::INVALID_DATA); + $clientData = ClientIdentificationData::create('1', 'default'); + $clientIdent = ClientIdentification::create($clientData, '1234'); + $refreshToken = RefreshToken::fromString('1234'); + + $request = $this->request->withAttribute(AccountInterface::AUTHENTICATED, $account) + ->withAttribute(ClientIdentification::class, $clientIdent) + ->withAttribute(RefreshToken::class, $refreshToken); + + $this->expectException(HttpDuplicateEntryException::class); + $middleware->process($request, $this->handler); + } +} diff --git a/tests/UnitTest/AppTest/Middleware/AccountAuthenticationMiddlewareTest.php b/tests/UnitTest/AppTest/Middleware/AccountAuthenticationMiddlewareTest.php new file mode 100644 index 00000000..a47c317c --- /dev/null +++ b/tests/UnitTest/AppTest/Middleware/AccountAuthenticationMiddlewareTest.php @@ -0,0 +1,107 @@ +accessTokenService = new MockAccessTokenService(); + $this->accountRepository = new MockAccountRepository(); + $this->logger = new NullLogger(); + $this->uuidFactory = new UuidFactory(); + } + + public function testAccountAuthenticatedIsGuest(): void + { + $middleware = new RequestAuthenticationMiddleware( + $this->accessTokenService, + $this->accountRepository, + $this->uuidFactory, + $this->logger, + ); + $handler = new MockAccountAuthenticationMiddlewareRequestHandler(); + $response = $middleware->process($this->request, $handler); + $header = $response->getHeaderLine('Authorization'); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertNotInstanceOf(JsonResponse::class, $response); + $this->assertSame('', $header); + } + + public function testAccountSuccessfulAuthenticated(): void + { + $accessToken = $this->accessTokenService->generate($this->uuidFactory->fromString(Account::UUID)); + $request = $this->request->withHeader('Authorization', $accessToken); + + $middleware = new RequestAuthenticationMiddleware( + $this->accessTokenService, + $this->accountRepository, + $this->uuidFactory, + $this->logger, + ); + + $handler = new MockAccountAuthenticationMiddlewareRequestHandler(); + $response = $middleware->process($request, $handler); + $header = $response->getHeaderLine('Authorization'); + + $this->assertNotInstanceOf(JsonResponse::class, $response); + $this->assertSame('true', $header); + } + + public function testTokenHasExpired(): void + { + $accessTokenService = new MockAccessTokenServiceWithoutDuration(); + $accessToken = $accessTokenService->generate($this->uuidFactory->fromString(Account::UUID)); + $request = $this->request->withHeader('Authorization', $accessToken); + + $middleware = new RequestAuthenticationMiddleware( + $this->accessTokenService, + $this->accountRepository, + $this->uuidFactory, + $this->logger, + ); + + $this->expectException(HttpUnauthorizedException::class); + $middleware->process($request, $this->handler); + } + + public function testTokenHasInvalid(): void + { + $accessToken = $this->accessTokenService->generate($this->uuidFactory->fromString(Account::UUID)); + $request = $this->request->withHeader('Authorization', $accessToken); + $accountRepository = new MockAccountRepositoryAccountAuthenticationMiddlewareInvalidToken(); + $middleware = new RequestAuthenticationMiddleware( + $this->accessTokenService, + $accountRepository, + $this->uuidFactory, + $this->logger, + ); + + $this->expectException(HttpUnauthorizedException::class); + $middleware->process($request, $this->handler); + } +} diff --git a/tests/UnitTest/AppTest/Middleware/AuthenticationConditionsMiddlewareTest.php b/tests/UnitTest/AppTest/Middleware/AuthenticationConditionsMiddlewareTest.php new file mode 100644 index 00000000..2ea196ca --- /dev/null +++ b/tests/UnitTest/AppTest/Middleware/AuthenticationConditionsMiddlewareTest.php @@ -0,0 +1,42 @@ +middleware = new AuthenticationConditionsMiddleware(); + } + + public function testIsSuccessfully(): void + { + $response = $this->middleware->process($this->request, $this->handler); + + $this->assertInstanceOf(ResponseInterface::class, $response); + } + + public function testRequestIsAuthenticated(): void + { + $request = $this->request->withHeader('Authentication', []); + + $this->expectException(HttpUnauthorizedException::class); + $this->middleware->process($request, $this->handler); + } + + public function testRequestIsAuthorized(): void + { + $request = $this->request->withHeader('Authorization', []); + + $this->expectException(HttpUnauthorizedException::class); + $this->middleware->process($request, $this->handler); + } +} diff --git a/tests/UnitTest/AppTest/Middleware/AuthenticationMiddlewareTest.php b/tests/UnitTest/AppTest/Middleware/AuthenticationMiddlewareTest.php new file mode 100644 index 00000000..8c7e8a12 --- /dev/null +++ b/tests/UnitTest/AppTest/Middleware/AuthenticationMiddlewareTest.php @@ -0,0 +1,65 @@ +middleware = new AuthenticationMiddleware( + new MockAuthenticationService(), + new MockAccountRepository(), + ); + } + + public function testCanAuthenticatedAccount(): void + { + $bodyData = [ + 'email' => Account::EMAIL, + 'password' => Account::PASSWORD, + ]; + + $request = $this->request->withParsedBody($bodyData); + $response = $this->middleware->process($request, $this->handler); + + $this->assertNotInstanceOf(JsonResponse::class, $response); + } + + public function testCanNotFoundAccountWithEmail(): void + { + $bodyData = [ + 'email' => Account::EMAIL_INVALID, + 'password' => Account::PASSWORD, + ]; + + $request = $this->request->withParsedBody($bodyData); + + $this->expectException(HttpUnauthorizedException::class); + $this->middleware->process($request, $this->handler); + } + + public function testRequestWithInvalidPassword(): void + { + $bodyData = [ + 'email' => Account::EMAIL, + 'password' => Account::PASSWORD_INVALID, + ]; + + $request = $this->request->withParsedBody($bodyData); + + $this->expectException(HttpUnauthorizedException::class); + $this->middleware->process($request, $this->handler); + } +} diff --git a/tests/UnitTest/AppTest/Middleware/AuthenticationValidationMiddlewareTest.php b/tests/UnitTest/AppTest/Middleware/AuthenticationValidationMiddlewareTest.php new file mode 100644 index 00000000..fc230a00 --- /dev/null +++ b/tests/UnitTest/AppTest/Middleware/AuthenticationValidationMiddlewareTest.php @@ -0,0 +1,33 @@ +process($this->request, $this->handler); + + $this->assertNotInstanceOf(JsonResponse::class, $response); + } + + public function testValidationFailed(): void + { + $middleware = new AuthenticationValidationMiddleware( + new MockAuthenticationValidatorFailed(), + ); + + $this->expectException(HttpUnauthorizedException::class); + $middleware->process($this->request, $this->handler); + } +} diff --git a/tests/UnitTest/AppTest/Middleware/ClientIdentificationMiddlewareTest.php b/tests/UnitTest/AppTest/Middleware/ClientIdentificationMiddlewareTest.php new file mode 100644 index 00000000..5c35eb83 --- /dev/null +++ b/tests/UnitTest/AppTest/Middleware/ClientIdentificationMiddlewareTest.php @@ -0,0 +1,36 @@ +middleware = new ClientIdentificationMiddleware( + new MockClientIdentificationService(), + ); + } + + public function testGenerateClientIdentification(): void + { + $request = $this->request->withHeader('x-ident', '1') + ->withHeader('user-agent', 'Test Browser Agent'); + + $response = $this->middleware->process($request, $this->handler); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertNotInstanceOf(JsonResponse::class, $response); + } + + // ToDo Create test for error cases +} diff --git a/tests/UnitTest/AppTest/Middleware/GenerateAccessTokenMiddlewareTest.php b/tests/UnitTest/AppTest/Middleware/GenerateAccessTokenMiddlewareTest.php new file mode 100644 index 00000000..c9230203 --- /dev/null +++ b/tests/UnitTest/AppTest/Middleware/GenerateAccessTokenMiddlewareTest.php @@ -0,0 +1,34 @@ +middleware = new GenerateAccessTokenMiddleware( + new MockAccessTokenService() + ); + } + + public function testCanGenerateAccessToken(): void + { + $request = $this->request->withAttribute(AccountInterface::AUTHENTICATED, new MockAccount()); + $response = $this->middleware->process($request, $this->handler); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertNotInstanceOf(JsonResponse::class, $response); + } +} diff --git a/tests/UnitTest/AppTest/Middleware/GenerateRefreshTokenMiddlewareTest.php b/tests/UnitTest/AppTest/Middleware/GenerateRefreshTokenMiddlewareTest.php new file mode 100644 index 00000000..94609118 --- /dev/null +++ b/tests/UnitTest/AppTest/Middleware/GenerateRefreshTokenMiddlewareTest.php @@ -0,0 +1,39 @@ +middleware = new GenerateRefreshTokenMiddleware( + new MockRefreshTokenService() + ); + } + + public function testCanGenerateRefreshToken(): void + { + $data = ClientIdentification::create( + ClientIdentificationData::create(null, 'defaul'), + '1' + ); + + $request = $this->request->withAttribute(ClientIdentification::class, $data); + $response = $this->middleware->process($request, $this->handler); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertNotInstanceOf(JsonResponse::class, $response); + } +} diff --git a/tests/UnitTest/AppTest/Repository/AccountAccessAuthRepositoryTest.php b/tests/UnitTest/AppTest/Repository/AccountAccessAuthRepositoryTest.php new file mode 100644 index 00000000..a2d503b3 --- /dev/null +++ b/tests/UnitTest/AppTest/Repository/AccountAccessAuthRepositoryTest.php @@ -0,0 +1,194 @@ +repository = new AccountAccessAuthRepository(new MockAccountAccessAuthTable()); + $this->hydrator = new AccountAccessAuthHydrator(); + } + + public function testCanInsertAccountAccessAuth(): void + { + $result = $this->repository->insert($this->hydrator->hydrate(AccountAccessAuth::VALID_DATA)); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testInsertAccountAccessAuthThrowDuplicateEntryException(): void + { + $this->expectException(DuplicateEntryException::class); + + $this->repository->insert($this->hydrator->hydrate(AccountAccessAuth::INVALID_DATA)); + } + + public function testCanUpdateAccountAccessAuth(): void + { + $result = $this->repository->update($this->hydrator->hydrate(AccountAccessAuth::VALID_DATA)); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testUpdateAccountAccessAuthThrowInvalidArgumentException(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->repository->update($this->hydrator->hydrate(AccountAccessAuth::INVALID_DATA)); + } + + public function testCanDeleteById(): void + { + $result = $this->repository->deleteById(AccountAccessAuth::ID); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testDeleteByIdThrowInvalidArgumentException(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->repository->deleteById(AccountAccessAuth::ID_INVALID); + } + + public function testCanFindById(): void + { + $result = $this->repository->findById(AccountAccessAuth::ID); + + $this->assertInstanceOf(AccountAccessAuthInterface::class, $result); + $this->assertSame(AccountAccessAuth::VALID_DATA, $this->hydrator->extract($result)); + } + + public function testFindByIdIsEmpty(): void + { + $result = $this->repository->findById(AccountAccessAuth::ID_INVALID); + + $this->assertNull($result); + } + + public function testCanFindByUserId(): void + { + $result = $this->repository->findByAccountId(AccountAccessAuth::USER_ID); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertArrayHasKey(0, $result); + $this->assertInstanceOf(AccountAccessAuthInterface::class, $result[0]); + $this->assertSame([0 => AccountAccessAuth::VALID_DATA], $this->hydrator->extractCollection($result)); + } + + public function testFindByUserIdIsEmpty(): void + { + $result = $this->repository->findByAccountId(AccountAccessAuth::USER_ID_INVALID); + + $this->assertInstanceOf(AccountAccessAuthCollection::class, $result); + $this->assertEmpty($result); + } + + public function testCanFindByLabel(): void + { + $result = $this->repository->findByLabel(AccountAccessAuth::LABEL); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertArrayHasKey(0, $result); + $this->assertInstanceOf(AccountAccessAuthInterface::class, $result[0]); + $this->assertSame([0 => AccountAccessAuth::VALID_DATA], $this->hydrator->extractCollection($result)); + } + + public function testFindByLabelIsEmpty(): void + { + $result = $this->repository->findByLabel(AccountAccessAuth::LABEL_INVALID); + + $this->assertInstanceOf(AccountAccessAuthCollection::class, $result); + $this->assertEmpty($result); + } + + public function testCanFindByRefreshToken(): void + { + $result = $this->repository->findByRefreshToken(AccountAccessAuth::REFRESH_TOKEN); + + $this->assertInstanceOf(AccountAccessAuthInterface::class, $result); + $this->assertSame(AccountAccessAuth::VALID_DATA, $this->hydrator->extract($result)); + } + + public function testFindByRefreshTokenIsEmpty(): void + { + $result = $this->repository->findByRefreshToken(AccountAccessAuth::REFRESH_TOKEN_INVALID); + + $this->assertNull($result); + } + + public function testCanFindByUserAgent(): void + { + $result = $this->repository->findByUserAgent(AccountAccessAuth::USER_AGENT); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertArrayHasKey(0, $result); + $this->assertInstanceOf(AccountAccessAuthInterface::class, $result[0]); + $this->assertSame([0 => AccountAccessAuth::VALID_DATA], $this->hydrator->extractCollection($result)); + } + + public function testCanFindByUserAgentIsEmpty(): void + { + $result = $this->repository->findByUserAgent(AccountAccessAuth::USER_AGENT_INVALID); + + $this->assertInstanceOf(AccountAccessAuthCollection::class, $result); + $this->assertEmpty($result); + } + + public function testCanFindByClientIdentHash(): void + { + $result = $this->repository->findByClientIdentHash(AccountAccessAuth::CLIENT_IDENT_HASH); + + $this->assertInstanceOf(AccountAccessAuthInterface::class, $result); + $this->assertSame(AccountAccessAuth::VALID_DATA, $this->hydrator->extract($result)); + } + + public function testFindByClientIdentHashIsEmpty(): void + { + $result = $this->repository->findByClientIdentHash(AccountAccessAuth::CLIENT_IDENT_HASH_INVALID); + + $this->assertNull($result); + } + + public function testCanFindAll(): void + { + $result = $this->repository->findAll(); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertArrayHasKey(0, $result); + $this->assertInstanceOf(AccountAccessAuthInterface::class, $result[0]); + $this->assertSame([0 => AccountAccessAuth::VALID_DATA], $this->hydrator->extractCollection($result)); + } + + public function testFindAllIsEmpty(): void + { + $repository = new AccountAccessAuthRepository(new MockAccountAccessAuthTableFailed()); + + $result = $repository->findAll(); + + $this->assertInstanceOf(AccountAccessAuthCollection::class, $result); + $this->assertEmpty($result); + } +} diff --git a/tests/UnitTest/AppTest/Repository/AccountRepositoryTest.php b/tests/UnitTest/AppTest/Repository/AccountRepositoryTest.php new file mode 100644 index 00000000..a1047935 --- /dev/null +++ b/tests/UnitTest/AppTest/Repository/AccountRepositoryTest.php @@ -0,0 +1,161 @@ +uuidFactory = new UuidFactory(); + $this->repository = new AccountRepository(new MockAccountTable()); + $this->hydrator = new AccountHydrator($this->uuidFactory); + } + + public function testCanInsertAccount(): void + { + $result = $this->repository->insert($this->hydrator->hydrate(Account::VALID_DATA)); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testInsertAccountThrowsException(): void + { + $this->expectException(DuplicateEntryException::class); + + $this->repository->insert($this->hydrator->hydrate(Account::INVALID_DATA)); + } + + public function testCanUpdateAccount(): void + { + $result = $this->repository->update($this->hydrator->hydrate(Account::VALID_DATA)); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testUpdateAccountThrowsException(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->repository->update($this->hydrator->hydrate(Account::INVALID_DATA)); + } + + public function testCanDeleteAccountById(): void + { + $result = $this->repository->deleteById(Account::ID); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testDeleteAccountByIdThrowsInvalidArgumentException(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->repository->deleteById(Account::ID_INVALID); + } + + public function testCanFindById(): void + { + $result = $this->repository->findById(Account::ID); + + $this->assertInstanceOf(AccountInterface::class, $result); + $this->assertSame(Account::VALID_DATA, $this->hydrator->extract($result)); + } + + public function testFindByIdIsEmpty(): void + { + $result = $this->repository->findById(Account::ID_INVALID); + + $this->assertNull($result); + } + + public function testCanFindByUuid(): void + { + $uuid = $this->uuidFactory->fromString(Account::UUID); + $result = $this->repository->findByUuid($uuid); + + $this->assertInstanceOf(AccountInterface::class, $result); + $this->assertSame(Account::VALID_DATA, $this->hydrator->extract($result)); + } + + public function testFindByUuidIsEmtpy(): void + { + $uuid = $this->uuidFactory->fromString(Account::UUID_INVALID); + + $result = $this->repository->findByUuid($uuid); + + $this->assertNull($result); + } + + public function testCanFindByName(): void + { + $result = $this->repository->findByName(Account::NAME); + + $this->assertInstanceOf(AccountInterface::class, $result); + $this->assertSame(Account::VALID_DATA, $this->hydrator->extract($result)); + } + + public function testFindByNameIsEmpty(): void + { + $result = $this->repository->findByName(Account::NAME_INVALID); + + $this->assertNull($result); + } + + public function testCanFindByEmail(): void + { + $result = $this->repository->findByEmail(new Email(Account::EMAIL)); + + $this->assertInstanceOf(AccountInterface::class, $result); + $this->assertSame(Account::VALID_DATA, $this->hydrator->extract($result)); + } + + public function testFindByEmailIsEmpty(): void + { + $result = $this->repository->findByEmail(new Email(Account::EMAIL_INVALID)); + + $this->assertNull($result); + } + + public function testCanFindAll(): void + { + $result = $this->repository->findAll(); + + $this->assertInstanceOf(AccountCollectionInterface::class, $result); + $this->assertArrayHasKey(0, $result); + $this->assertInstanceOf(AccountInterface::class, $result[0]); + $this->assertSame([0 => Account::VALID_DATA], $this->hydrator->extractCollection($result)); + } + + public function testFindAllIsEmpty(): void + { + $repository = new AccountRepository(new MockAccountTableFailed()); + + $result = $repository->findAll(); + + $this->assertInstanceOf(AccountCollectionInterface::class, $result); + $this->assertEmpty($result); + } +} diff --git a/tests/UnitTest/AppTest/Service/AccessTokenServiceTest.php b/tests/UnitTest/AppTest/Service/AccessTokenServiceTest.php new file mode 100644 index 00000000..4a8d0ca0 --- /dev/null +++ b/tests/UnitTest/AppTest/Service/AccessTokenServiceTest.php @@ -0,0 +1,48 @@ +account = new MockAccount(); + } + + public function testGenerateValidAccessToken(): void + { + $config = Token::getTokenStruct(); + $jwtTokenConfig = JwtTokenConfig::createFromArray($config); + + $service = new AccessTokenService($jwtTokenConfig); + + $token = $service->generate($this->account->uuid); + + $isValid = $service->isValid($token); + + $this->assertTrue($isValid); + } + + public function testGenerateValidAccessTokenFails(): void + { + $config = Token::getTokenStruct(); + $config['algorithmus'] = ''; + $jwtTokenConfig = JwtTokenConfig::createFromArray($config); + + $service = new AccessTokenService($jwtTokenConfig); + + $this->expectException(DomainException::class); + + $service->generate($this->account->uuid); + } +} diff --git a/tests/UnitTest/AppTest/Service/AuthenticationServiceTest.php b/tests/UnitTest/AppTest/Service/AuthenticationServiceTest.php new file mode 100644 index 00000000..ebc0eb06 --- /dev/null +++ b/tests/UnitTest/AppTest/Service/AuthenticationServiceTest.php @@ -0,0 +1,31 @@ +service = new AuthenticationService(); + } + + public function testPasswordComparisonIsSuccessful(): void + { + $compare = $this->service->isPasswordMatch(Account::PASSWORD_STRING, Account::PASSWORD); + + $this->assertTrue($compare); + } + + public function testPasswordComparisonFails(): void + { + $compare = $this->service->isPasswordMatch(Account::PASSWORD_STRING, Account::PASSWORD_INVALID); + + $this->assertFalse($compare); + } +} diff --git a/tests/UnitTest/AppTest/Service/RefreshTokenServiceTest.php b/tests/UnitTest/AppTest/Service/RefreshTokenServiceTest.php new file mode 100644 index 00000000..568af821 --- /dev/null +++ b/tests/UnitTest/AppTest/Service/RefreshTokenServiceTest.php @@ -0,0 +1,51 @@ +client = ClientIdentification::create( + ClientIdentificationData::create(null, 'default'), + '1' + ); + } + + public function testGenerateValidRefreshToken(): void + { + $config = Token::getTokenStruct(); + $jwtTokenConfig = JwtTokenConfig::createFromArray($config); + + $service = new RefreshTokenService($jwtTokenConfig); + + $token = $service->generate($this->client); + + $isValid = $service->isValid($token); + + $this->assertTrue($isValid); + } + + public function testGenerateValidRefreshTokenFails(): void + { + $config = Token::getTokenStruct(); + $config['algorithmus'] = ''; + $jwtTokenConfig = JwtTokenConfig::createFromArray($config); + + $service = new RefreshTokenService($jwtTokenConfig); + + $this->expectException(DomainException::class); + + $service->generate($this->client); + } +} diff --git a/tests/UnitTest/AppTest/Table/AccountAccessAuthTableTest.php b/tests/UnitTest/AppTest/Table/AccountAccessAuthTableTest.php new file mode 100644 index 00000000..c8ad6c3a --- /dev/null +++ b/tests/UnitTest/AppTest/Table/AccountAccessAuthTableTest.php @@ -0,0 +1,226 @@ +query = new MockQuery(); + $this->hydrator = new AccountAccessAuthHydrator(); + $this->table = new AccountAccessAuthTable($this->query, $this->hydrator); + } + + public function testCanGetTableName(): void + { + $this->assertSame('AccountAccessAuth', $this->table->getTableName()); + } + + public function testCanInsertAccountAccessAuth(): void + { + $accountAccessAuth = $this->hydrator->hydrate(AccountAccessAuth::VALID_DATA); + + $result = $this->table->insert($accountAccessAuth); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testInsertAccountAccessAuthThrowsException(): void + { + $accountAccessAuth = $this->hydrator->hydrate(AccountAccessAuth::VALID_DATA); + $table = new AccountAccessAuthTable(new MockQueryFailed(), $this->hydrator); + + $this->expectException(DuplicateEntryException::class); + + $table->insert($accountAccessAuth); + } + + public function testCanUpdateAccountAccessAuth(): void + { + $accountAccessAuth = $this->hydrator->hydrate(AccountAccessAuth::VALID_DATA); + + $result = $this->table->update($accountAccessAuth); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testUpdateAccountAccessAuthThrowsException(): void + { + $accountAccessAuth = $this->hydrator->hydrate(AccountAccessAuth::VALID_DATA); + $table = new AccountAccessAuthTable(new MockQueryFailed(), $this->hydrator); + + $this->expectException(InvalidArgumentException::class); + + $table->update($accountAccessAuth); + } + + public function testCanDeleteById(): void + { + $result = $this->table->deleteById(AccountAccessAuth::ID); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testDeleteAccountThrowsException(): void + { + $table = new AccountAccessAuthTable(new MockQueryFailed(), $this->hydrator); + + $this->expectException(InvalidArgumentException::class); + + $table->deleteById(AccountAccessAuth::ID); + } + + public function testCanFindById(): void + { + $result = $this->table->findById(AccountAccessAuth::ID); + + $this->assertInstanceOf(AccountAccessAuthInterface::class, $result); + $this->assertSame(AccountAccessAuth::ID, $result->id); + } + + public function testFindByIdIsEmpty(): void + { + $table = new AccountAccessAuthTable(new MockQueryFailed(), $this->hydrator); + + $result = $table->findById(AccountAccessAuth::ID); + + $this->assertNull($result); + } + + public function testCanFindByUserId(): void + { + /** @var AccountAccessAuthCollectionInterface $result */ + $result = $this->table->findByAccountId(AccountAccessAuth::USER_ID); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertArrayHasKey(0, $result); + $this->assertSame(AccountAccessAuth::USER_ID, $result[0]->accountId); + } + + public function testFindByUserIdIsEmpty(): void + { + $table = new AccountAccessAuthTable(new MockQueryFailed(), $this->hydrator); + + $result = $table->findByAccountId(AccountAccessAuth::USER_ID); + + $this->assertInstanceOf(AccountAccessAuthCollection::class, $result); + $this->assertEmpty($result); + } + + public function testCanFindByLabel(): void + { + /** @var AccountAccessAuthCollectionInterface $result */ + $result = $this->table->findByLabel(AccountAccessAuth::LABEL); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertArrayHasKey(0, $result); + $this->assertSame(AccountAccessAuth::LABEL, $result[0]->label); + } + + public function testFindByLabelIsEmpty(): void + { + $table = new AccountAccessAuthTable(new MockQueryFailed(), $this->hydrator); + + $result = $table->findByLabel(AccountAccessAuth::LABEL); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertEmpty($result); + } + + public function testCanFindByRefreshToken(): void + { + /** @var AccountAccessAuthInterface $result */ + $result = $this->table->findByRefreshToken(AccountAccessAuth::REFRESH_TOKEN); + + $this->assertInstanceOf(AccountAccessAuthInterface::class, $result); + $this->assertSame(AccountAccessAuth::REFRESH_TOKEN, $result->refreshToken); + } + + public function testFindByRefreshTokenIsEmpty(): void + { + $table = new AccountAccessAuthTable(new MockQueryFailed(), $this->hydrator); + + $result = $table->findByRefreshToken(AccountAccessAuth::REFRESH_TOKEN); + + $this->assertNull($result); + } + + public function testCanFindByUserAgent(): void + { + /** @var AccountAccessAuthCollectionInterface $result */ + $result = $this->table->findByUserAgent(AccountAccessAuth::USER_AGENT); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertArrayHasKey(0, $result); + $this->assertSame(AccountAccessAuth::USER_AGENT, $result[0]->userAgent); + } + + public function testFindByUserAgentIsEmpty(): void + { + $table = new AccountAccessAuthTable(new MockQueryFailed(), $this->hydrator); + + $result = $table->findByUserAgent(AccountAccessAuth::USER_AGENT); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertEmpty($result); + } + + public function testCanFindByClientIdentHash(): void + { + /** @var AccountAccessAuthInterface $result */ + $result = $this->table->findByClientIdentHash(AccountAccessAuth::CLIENT_IDENT_HASH); + + $this->assertInstanceOf(AccountAccessAuthInterface::class, $result); + $this->assertSame(AccountAccessAuth::CLIENT_IDENT_HASH, $result->clientIdentHash); + } + + public function testFindByClientIdentHashIsEmpty(): void + { + $table = new AccountAccessAuthTable(new MockQueryFailed(), $this->hydrator); + + $result = $table->findByClientIdentHash(AccountAccessAuth::CLIENT_IDENT_HASH); + + $this->assertNull($result); + } + + public function testCanFindAll(): void + { + /** @var AccountAccessAuthCollectionInterface $result */ + $result = $this->table->findAll(); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertArrayHasKey(0, $result); + $this->assertSame(AccountAccessAuth::ID, $result[0]->id); + } + + public function testFindAllIsEmpty(): void + { + $table = new AccountAccessAuthTable(new MockQueryFailed(), $this->hydrator); + + $result = $table->findAll(); + + $this->assertInstanceOf(AccountAccessAuthCollectionInterface::class, $result); + $this->assertEmpty($result); + } +} diff --git a/tests/UnitTest/AppTest/Table/AccountTableTest.php b/tests/UnitTest/AppTest/Table/AccountTableTest.php new file mode 100644 index 00000000..30d74c66 --- /dev/null +++ b/tests/UnitTest/AppTest/Table/AccountTableTest.php @@ -0,0 +1,189 @@ +uuidFactory = new UuidFactory(); + $this->hydrator = new AccountHydrator($this->uuidFactory); + $this->table = new AccountTable($query, $this->hydrator); + } + + public function testCanGetTableName(): void + { + $this->assertSame('Account', $this->table->getTableName()); + } + + public function testCanInsertAccount(): void + { + $account = $this->hydrator->hydrate(Account::VALID_DATA); + + $result = $this->table->insert($account); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testInsertAccountThrowsException(): void + { + $table = new AccountTable(new MockQueryFailed(), $this->hydrator); + + $account = $this->hydrator->hydrate(Account::VALID_DATA); + + $this->expectException(DuplicateEntryException::class); + + $table->insert($account); + } + + public function testCanUpdateAccount(): void + { + $account = $this->hydrator->hydrate(Account::VALID_DATA); + + $result = $this->table->update($account); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testUpdateAccountThrowsException(): void + { + $table = new AccountTable(new MockQueryFailed(), $this->hydrator); + $account = $this->hydrator->hydrate(Account::VALID_DATA); + + $this->expectException(InvalidArgumentException::class); + + $table->update($account); + } + + public function testCanDeleteById(): void + { + $result = $this->table->deleteById(Account::ID); + + $this->assertIsBool($result); + $this->assertTrue($result); + } + + public function testDeleteAccountThrowsException(): void + { + $table = new AccountTable(new MockQueryFailed(), $this->hydrator); + + $this->expectException(InvalidArgumentException::class); + + $table->deleteById(Account::ID); + } + + /** + * @throws Exception + */ + public function testCanFindById(): void + { + $account = $this->table->findById(Account::ID); + + $this->assertInstanceOf(AccountInterface::class, $account); + $this->assertSame(Account::VALID_DATA, $this->hydrator->extract($account)); + } + + /** + * @throws Exception + */ + public function testFindByIdIsEmpty(): void + { + $result = $this->table->findById(Account::ID_INVALID); + + $this->assertNull($result); + } + + /** + * @throws Exception + */ + public function testCanFindByUuid(): void + { + $uuid = $this->uuidFactory->fromString(Account::UUID); + $account = $this->table->findByUuid($uuid); + + $this->assertInstanceOf(AccountInterface::class, $account); + $this->assertSame(Account::VALID_DATA, $this->hydrator->extract($account)); + } + + /** + * @throws Exception + */ + public function testFindByUuidIsEmpty(): void + { + $uuid = $this->uuidFactory->fromString(Account::UUID_INVALID); + $result = $this->table->findByUuid($uuid); + + $this->assertNull($result); + } + + public function testCanFindByName(): void + { + $account = $this->table->findByName(Account::NAME); + + $this->assertInstanceOf(AccountInterface::class, $account); + $this->assertSame(Account::VALID_DATA, $this->hydrator->extract($account)); + } + + public function testFindByNameIsEmpty(): void + { + $result = $this->table->findByName(Account::NAME_INVALID); + + $this->assertNull($result); + } + + public function testCanFindByEmail(): void + { + $account = $this->table->findByEmail(new Email(Account::EMAIL)); + + $this->assertInstanceOf(AccountInterface::class, $account); + $this->assertSame(Account::VALID_DATA, $this->hydrator->extract($account)); + } + + public function testFindByEmailIsEmpty(): void + { + $result = $this->table->findByEmail(new Email(Account::EMAIL_INVALID)); + + $this->assertNull($result); + } + + public function testCanFindAllAccount(): void + { + $accounts = $this->table->findAll(); + + $this->assertInstanceOf(AccountCollectionInterface::class, $accounts); + $this->assertSame([0 => Account::VALID_DATA], $this->hydrator->extractCollection($accounts)); + } + + public function testFindAllAccountIsEmpty(): void + { + $table = new AccountTable(new MockQueryFailed(), $this->hydrator); + + $result = $table->findAll(); + + $this->assertInstanceOf(AccountCollectionInterface::class, $result); + $this->assertEmpty($result); + } +} diff --git a/tests/UnitTest/CoreTest/Factory/DatabaseFactoryTest.php b/tests/UnitTest/CoreTest/Factory/DatabaseFactoryTest.php new file mode 100644 index 00000000..b0097fc4 --- /dev/null +++ b/tests/UnitTest/CoreTest/Factory/DatabaseFactoryTest.php @@ -0,0 +1,43 @@ +expectException(PDOException::class); + + $config = [ + 'database' => [ + 'driver' => 'mysql', + 'user' => 'testUser', + 'password' => 'testPassword', + 'host' => 'https//example.com', + 'port' => 3306, + 'dbname' => 'example_db', + 'error' => PDO::ERRMODE_EXCEPTION, + 'emulate_prepares' => false, + ], + ]; + + $container = new MockContainer(); + $container->add('config', $config); + + $pdo = (new DatabaseFactory())($container); + + $this->assertInstanceOf(PDO::class, $pdo); + } +} diff --git a/tests/UnitTest/CoreTest/Factory/QueryFactoryTest.php b/tests/UnitTest/CoreTest/Factory/QueryFactoryTest.php new file mode 100644 index 00000000..d42a487b --- /dev/null +++ b/tests/UnitTest/CoreTest/Factory/QueryFactoryTest.php @@ -0,0 +1,29 @@ +add(PDO::class, new MockPDO()); + + $query = (new QueryFactory())($container); + + $this->assertInstanceOf(Query::class, $query); + } +} diff --git a/tests/UnitTest/GameTest/.gitkeep b/tests/UnitTest/GameTest/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/UnitTest/JsonRequestHelper.php b/tests/UnitTest/JsonRequestHelper.php new file mode 100644 index 00000000..265a9fc0 --- /dev/null +++ b/tests/UnitTest/JsonRequestHelper.php @@ -0,0 +1,15 @@ +getBody()->getContents(), true); + } +} diff --git a/tests/UnitTest/Mock/Constants/Account.php b/tests/UnitTest/Mock/Constants/Account.php new file mode 100644 index 00000000..e3242a7a --- /dev/null +++ b/tests/UnitTest/Mock/Constants/Account.php @@ -0,0 +1,60 @@ + self::ID, + 'uuid' => self::UUID, + 'name' => self::NAME, + 'password' => self::PASSWORD, + 'email' => self::EMAIL, + 'registeredAt' => self::REGISTERED, + 'lastActionAt' => self::LAST_ACTION, + ]; + + public const array INVALID_DATA + = [ + 'id' => self::ID_INVALID, + 'uuid' => self::UUID_INVALID, + 'name' => self::NAME_INVALID, + 'password' => self::PASSWORD_INVALID, + 'email' => self::EMAIL_INVALID, + 'registeredAt' => self::REGISTERED, + 'lastActionAt' => self::LAST_ACTION, + ]; +} diff --git a/tests/UnitTest/Mock/Constants/AccountAccessAuth.php b/tests/UnitTest/Mock/Constants/AccountAccessAuth.php new file mode 100644 index 00000000..5bfa113b --- /dev/null +++ b/tests/UnitTest/Mock/Constants/AccountAccessAuth.php @@ -0,0 +1,54 @@ + self::ID, + 'accountId' => self::USER_ID, + 'label' => self::LABEL, + 'refreshToken' => self::REFRESH_TOKEN, + 'userAgent' => self::USER_AGENT, + 'clientIdentHash' => self::CLIENT_IDENT_HASH, + 'createdAt' => self::CREATED_AT, + ]; + + public const array INVALID_DATA + = [ + 'id' => self::ID_INVALID, + 'accountId' => self::USER_ID_INVALID, + 'label' => self::LABEL_INVALID, + 'refreshToken' => self::REFRESH_TOKEN_INVALID, + 'userAgent' => self::USER_AGENT_INVALID, + 'clientIdentHash' => self::CLIENT_IDENT_HASH_INVALID, + 'createdAt' => self::CREATED_AT, + ]; +} diff --git a/tests/UnitTest/Mock/Constants/Token.php b/tests/UnitTest/Mock/Constants/Token.php new file mode 100644 index 00000000..3924a447 --- /dev/null +++ b/tests/UnitTest/Mock/Constants/Token.php @@ -0,0 +1,21 @@ + 'token secret', + 'algorithmus' => 'HS512', + 'duration' => 60 * 60 * 24 * 7 * 12, + 'iss' => 'Issuer of the token', + 'aud' => 'recipients of the token', + ]; + } +} diff --git a/tests/UnitTest/Mock/Database/MockDelete.php b/tests/UnitTest/Mock/Database/MockDelete.php new file mode 100644 index 00000000..e8c9188e --- /dev/null +++ b/tests/UnitTest/Mock/Database/MockDelete.php @@ -0,0 +1,27 @@ +handle($this->statements['DELETE FROM'], $this->statements['WHERE'], $this->parameters['WHERE']); + } + + private function handle(string $table, array $where, array $value): bool + { + return match ($table) { + 'Account', 'AccountAccessAuth' => true, + default => false, + }; + } +} diff --git a/tests/UnitTest/Mock/Database/MockDeleteFailed.php b/tests/UnitTest/Mock/Database/MockDeleteFailed.php new file mode 100644 index 00000000..e72df7a3 --- /dev/null +++ b/tests/UnitTest/Mock/Database/MockDeleteFailed.php @@ -0,0 +1,24 @@ +handle($this->statements['DELETE FROM'], $this->statements['WHERE'], $this->parameters['WHERE']); + } + + private function handle(string $table, array $where, array $value): bool + { + return false; + } +} diff --git a/tests/UnitTest/Mock/Database/MockInsert.php b/tests/UnitTest/Mock/Database/MockInsert.php new file mode 100644 index 00000000..c3e9b2ae --- /dev/null +++ b/tests/UnitTest/Mock/Database/MockInsert.php @@ -0,0 +1,27 @@ +handle($this->statements['INSERT INTO'], $this->statements['VALUES']); + } + + private function handle(string $table, array $values): bool + { + return match($table) { + 'Account', 'AccountAccessAuth' => true, + default => false, + }; + } +} diff --git a/tests/UnitTest/Mock/Database/MockInsertFailed.php b/tests/UnitTest/Mock/Database/MockInsertFailed.php new file mode 100644 index 00000000..f64f2fea --- /dev/null +++ b/tests/UnitTest/Mock/Database/MockInsertFailed.php @@ -0,0 +1,25 @@ +handle($this->statements['INSERT INTO'], $this->statements['VALUES']); + } + + private function handle(string $table, array $values): bool + { + throw new PDOException(); + } +} diff --git a/tests/UnitTest/Mock/Database/MockPDO.php b/tests/UnitTest/Mock/Database/MockPDO.php new file mode 100644 index 00000000..51c8ad13 --- /dev/null +++ b/tests/UnitTest/Mock/Database/MockPDO.php @@ -0,0 +1,18 @@ +statements)) { + return $this->handle($this->statements['FROM'], $this->statements['WHERE'], $this->parameters['WHERE']); + } + + return []; + } + + public function fetchAll($index = '', $selectOnly = ''): false|array + { + return match ($this->getFromTable()) { + 'Account' => [0 => Account::VALID_DATA], + 'AccountAccessAuth' => [0 => AccountAccessAuth::VALID_DATA], + default => false + }; + } + + private function handle(string $from, array $where, array $params): false|array + { + return match ($from) { + 'Account' => $this->handleAccount($where, $params), + 'AccountAccessAuth' => $this->handleAccountAccessAuth($where, $params), + default => false + }; + } + + private function handleAccount(array $where, array $params): false|array + { + if ($where[0][1] === 'id = ?' && $params[0] === Account::ID) { + return Account::VALID_DATA; + } + + if ($where[0][1] === 'uuid = ?' && $params[0] === Account::UUID) { + return Account::VALID_DATA; + } + + if ($where[0][1] === 'name = ?' && $params[0] === Account::NAME) { + return Account::VALID_DATA; + } + + if ($where[0][1] === 'email = ?' && $params[0] === Account::EMAIL) { + return Account::VALID_DATA; + } + + return false; + } + + private function handleAccountAccessAuth(array $where, array $params): false|array + { + if ($where[0][1] === 'id = ?' && $params[0] === AccountAccessAuth::ID) { + return AccountAccessAuth::VALID_DATA; + } + + if ($where[0][1] === 'userId = ?' && $params[0] === AccountAccessAuth::USER_ID) { + return [0 => AccountAccessAuth::VALID_DATA]; + } + + if ($where[0][1] === 'label = ?' && $params[0] === AccountAccessAuth::LABEL) { + return [0 => AccountAccessAuth::VALID_DATA]; + } + + if ($where[0][1] === 'refreshToken = ?' && $params[0] === AccountAccessAuth::REFRESH_TOKEN) { + return AccountAccessAuth::VALID_DATA; + } + + if ($where[0][1] === 'userAgent = ?' && $params[0] === AccountAccessAuth::USER_AGENT) { + return [0 => AccountAccessAuth::VALID_DATA]; + } + + if ($where[0][1] === 'clientIdentHash = ?' && $params[0] === AccountAccessAuth::CLIENT_IDENT_HASH) { + return AccountAccessAuth::VALID_DATA; + } + + return false; + } +} diff --git a/tests/UnitTest/Mock/Database/MockSelectFailed.php b/tests/UnitTest/Mock/Database/MockSelectFailed.php new file mode 100644 index 00000000..860c6e17 --- /dev/null +++ b/tests/UnitTest/Mock/Database/MockSelectFailed.php @@ -0,0 +1,36 @@ +statements)) { + return $this->handle($this->statements['FROM'], $this->statements['WHERE'], $this->parameters['WHERE']); + } + + return []; + } + + public function fetchAll($index = '', $selectOnly = ''): false|array + { + return false; + } + + private function handle(string $from, array $where, array $params): false|array + { + return false; + } +} diff --git a/tests/UnitTest/Mock/Database/MockUpdate.php b/tests/UnitTest/Mock/Database/MockUpdate.php new file mode 100644 index 00000000..0f13c7ef --- /dev/null +++ b/tests/UnitTest/Mock/Database/MockUpdate.php @@ -0,0 +1,22 @@ +statements['UPDATE']) { + 'Account', 'AccountAccessAuth' => true, + default => false + }; + } +} diff --git a/tests/UnitTest/Mock/Database/MockUpdateFailed.php b/tests/UnitTest/Mock/Database/MockUpdateFailed.php new file mode 100644 index 00000000..b755be43 --- /dev/null +++ b/tests/UnitTest/Mock/Database/MockUpdateFailed.php @@ -0,0 +1,19 @@ +getAttribute(AccountInterface::AUTHENTICATED); + $response = new MockResponse(); + + if ($account instanceof AccountInterface) { + return $response->withHeader('Authorization', 'true'); + } + + return $response; + } +} diff --git a/tests/UnitTest/Mock/MockContainer.php b/tests/UnitTest/Mock/MockContainer.php new file mode 100644 index 00000000..0ed0ee37 --- /dev/null +++ b/tests/UnitTest/Mock/MockContainer.php @@ -0,0 +1,32 @@ +has($id)) { + return $this->container[$id]; + } + + return null; + } + + public function has(string $id): bool + { + return array_key_exists($id, $this->container); + } + + public function add(string $id, mixed $value): void + { + $this->container[$id] = $value; + } +} diff --git a/tests/UnitTest/Mock/MockRequestHandler.php b/tests/UnitTest/Mock/MockRequestHandler.php new file mode 100644 index 00000000..da40ff9c --- /dev/null +++ b/tests/UnitTest/Mock/MockRequestHandler.php @@ -0,0 +1,15 @@ +headers[$name] ?? ''; + } + + public function withHeader($name, $value): MessageInterface + { + $header = clone $this; + $header->headers[$name] = $value; + return $header; + } + + public function getProtocolVersion(): string + { + // TODO: Implement getProtocolVersion() method. + } + + public function withProtocolVersion(string $version): MessageInterface + { + // TODO: Implement withProtocolVersion() method. + } + + public function getHeaders(): array + { + // TODO: Implement getHeaders() method. + } + + public function hasHeader(string $name): bool + { + // TODO: Implement hasHeader() method. + } + + public function getHeader(string $name): array + { + // TODO: Implement getHeader() method. + } + + public function withAddedHeader(string $name, $value): MessageInterface + { + // TODO: Implement withAddedHeader() method. + } + + public function withoutHeader(string $name): MessageInterface + { + // TODO: Implement withoutHeader() method. + } + + public function getBody(): StreamInterface + { + // TODO: Implement getBody() method. + } + + public function withBody(StreamInterface $body): MessageInterface + { + // TODO: Implement withBody() method. + } + + public function getStatusCode(): int + { + // TODO: Implement getStatusCode() method. + } + + public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface + { + // TODO: Implement withStatus() method. + } + + public function getReasonPhrase(): string + { + // TODO: Implement getReasonPhrase() method. + } +} diff --git a/tests/UnitTest/Mock/MockServerRequest.php b/tests/UnitTest/Mock/MockServerRequest.php new file mode 100644 index 00000000..0f3c4122 --- /dev/null +++ b/tests/UnitTest/Mock/MockServerRequest.php @@ -0,0 +1,192 @@ +headers; + } + + public function hasHeader($name): bool + { + return array_key_exists($name, $this->headers); + } + + public function getHeader($name): array + { + return array_key_exists($name, $this->headers) ? $this->headers[$name] : []; + } + + public function getHeaderLine($name): string + { + return $this->headers[$name] ?? ''; + } + + public function withHeader($name, $value): MessageInterface + { + $header = clone $this; + $header->headers[$name] = $value; + + return $header; + } + + /** + * @return StreamInterface|array + */ + public function getBody(): StreamInterface + { + return $this->body; + } + + public function getQueryParams(): array + { + return $this->queryParams; + } + + public function withQueryParams(array $query): self + { + $queryParams = clone $this; + + $queryParams->queryParams = $query; + + return $queryParams; + } + + public function getParsedBody(): object|array|null + { + return $this->body; + } + + public function withParsedBody($data): ServerRequestInterface + { + $body = clone $this; + $body->body = $data; + + return $body; + } + + public function getAttribute($name, $default = null) + { + if (array_key_exists($name, $this->attributes)) { + return $this->attributes[$name]; + } + + return $default; + } + + public function withAttribute($name, $value): MockServerRequest + { + $attributes = clone $this; + $attributes->attributes[$name] = $value; + + return $attributes; + } + + public function getProtocolVersion(): string + { + // TODO: Implement getProtocolVersion() method. + } + + public function withProtocolVersion(string $version): MessageInterface + { + // TODO: Implement withProtocolVersion() method. + } + + public function withAddedHeader(string $name, $value): MessageInterface + { + // TODO: Implement withAddedHeader() method. + } + + public function withoutHeader(string $name): MessageInterface + { + // TODO: Implement withoutHeader() method. + } + + public function withBody(StreamInterface $body): MessageInterface + { + // TODO: Implement withBody() method. + } + + public function getRequestTarget(): string + { + // TODO: Implement getRequestTarget() method. + } + + public function withRequestTarget(string $requestTarget): RequestInterface + { + // TODO: Implement withRequestTarget() method. + } + + public function getMethod(): string + { + // TODO: Implement getMethod() method. + } + + public function withMethod(string $method): RequestInterface + { + // TODO: Implement withMethod() method. + } + + public function getUri(): UriInterface + { + return new \Laminas\Diactoros\Uri('http://example.com/'); + } + + public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface + { + // TODO: Implement withUri() method. + } + + public function getServerParams(): array + { + return []; + } + + public function getCookieParams(): array + { + // TODO: Implement getCookieParams() method. + } + + public function withCookieParams(array $cookies): ServerRequestInterface + { + // TODO: Implement withCookieParams() method. + } + + public function getUploadedFiles(): array + { + // TODO: Implement getUploadedFiles() method. + } + + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface + { + // TODO: Implement withUploadedFiles() method. + } + + public function getAttributes(): array + { + // TODO: Implement getAttributes() method. + } + + public function withoutAttribute(string $name): ServerRequestInterface + { + // TODO: Implement withoutAttribute() method. + } +} diff --git a/tests/UnitTest/Mock/Repository/MockAccountAccessAuthRepository.php b/tests/UnitTest/Mock/Repository/MockAccountAccessAuthRepository.php new file mode 100644 index 00000000..8cbcaa52 --- /dev/null +++ b/tests/UnitTest/Mock/Repository/MockAccountAccessAuthRepository.php @@ -0,0 +1,14 @@ + $this->config->iss, + 'aud' => $this->config->aud, + 'iat' => $now, + 'exp' => $now + $this->config->duration, + 'uuid' => $uuid->getHex()->toString(), + ]; + + return JWT::encode($payload, $this->config->key, $this->config->algorithmus); + } +} diff --git a/tests/UnitTest/Mock/Service/MockAccessTokenServiceWithoutDuration.php b/tests/UnitTest/Mock/Service/MockAccessTokenServiceWithoutDuration.php new file mode 100644 index 00000000..dc442e4b --- /dev/null +++ b/tests/UnitTest/Mock/Service/MockAccessTokenServiceWithoutDuration.php @@ -0,0 +1,35 @@ + $this->config->iss, + 'aud' => $this->config->aud, + 'iat' => $now, + 'exp' => $now + $this->config->duration, + 'uuid' => $uuid->getHex()->toString(), + ]; + + return JWT::encode($payload, $this->config->key, $this->config->algorithmus); + } +} diff --git a/tests/UnitTest/Mock/Service/MockAuthenticationService.php b/tests/UnitTest/Mock/Service/MockAuthenticationService.php new file mode 100644 index 00000000..ed819dba --- /dev/null +++ b/tests/UnitTest/Mock/Service/MockAuthenticationService.php @@ -0,0 +1,14 @@ +accountId !== Account::ID) { + throw new DuplicateEntryException('AccountAccessAuth', $data->id); + } + + return true; + } + + public function update(AccountAccessAuthInterface $data): true + { + if ($data->id !== AccountAccessAuth::ID) { + throw new InvalidArgumentException(); + } + + return true; + } + + public function deleteById(int $id): true + { + if ($id !== AccountAccessAuth::ID) { + throw new InvalidArgumentException(); + } + + return true; + } + + public function findById(int $id): ?AccountAccessAuthInterface + { + return $id === AccountAccessAuth::ID ? $this->hydrator->hydrate(AccountAccessAuth::VALID_DATA) : null; + } + + public function findByAccountId(int $accountId): AccountAccessAuthCollectionInterface + { + return $accountId === AccountAccessAuth::USER_ID + ? $this->hydrator->hydrateCollection([0 => AccountAccessAuth::VALID_DATA]) + : $this->hydrator->hydrateCollection( + [] + ); + } + + public function findByLabel(string $label): AccountAccessAuthCollectionInterface + { + return $label === AccountAccessAuth::LABEL + ? $this->hydrator->hydrateCollection([0 => AccountAccessAuth::VALID_DATA]) + : $this->hydrator->hydrateCollection( + [] + ); + } + + public function findByRefreshToken(string $refreshToken): ?AccountAccessAuthInterface + { + return $refreshToken === AccountAccessAuth::REFRESH_TOKEN ? $this->hydrator->hydrate( + AccountAccessAuth::VALID_DATA + ) : null; + } + + public function findByUserAgent(string $userAgent): AccountAccessAuthCollectionInterface + { + return $userAgent === AccountAccessAuth::USER_AGENT + ? $this->hydrator->hydrateCollection([0 => AccountAccessAuth::VALID_DATA]) + : $this->hydrator->hydrateCollection( + [] + ); + } + + public function findByClientIdentHash(string $clientIdentHash): ?AccountAccessAuthInterface + { + return $clientIdentHash === AccountAccessAuth::CLIENT_IDENT_HASH ? $this->hydrator->hydrate( + AccountAccessAuth::VALID_DATA + ) : null; + } + + public function findAll(): AccountAccessAuthCollectionInterface + { + return $this->hydrator->hydrateCollection([0 => AccountAccessAuth::VALID_DATA]); + } +} diff --git a/tests/UnitTest/Mock/Table/MockAccountAccessAuthTableFailed.php b/tests/UnitTest/Mock/Table/MockAccountAccessAuthTableFailed.php new file mode 100644 index 00000000..d184590d --- /dev/null +++ b/tests/UnitTest/Mock/Table/MockAccountAccessAuthTableFailed.php @@ -0,0 +1,25 @@ +hydrator->hydrateCollection([]); + } +} diff --git a/tests/UnitTest/Mock/Table/MockAccountTable.php b/tests/UnitTest/Mock/Table/MockAccountTable.php new file mode 100644 index 00000000..8b9e6a63 --- /dev/null +++ b/tests/UnitTest/Mock/Table/MockAccountTable.php @@ -0,0 +1,84 @@ +id !== Account::ID) { + throw new DuplicateEntryException('Account', $data->id); + } + + return true; + } + + public function update(AccountInterface $data): true + { + if ($data->id !== Account::ID) { + throw new InvalidArgumentException(); + } + + return true; + } + + public function deleteById(int $id): true + { + if ($id !== Account::ID) { + throw new InvalidArgumentException(); + } + + return true; + } + + public function findById(int $id): ?AccountInterface + { + return $id === Account::ID ? $this->hydrator->hydrate(Account::VALID_DATA) : null; + } + + public function findByUuid(UuidInterface $uuid): ?AccountInterface + { + return $uuid->getHex()->toString() === Account::UUID ? $this->hydrator->hydrate(Account::VALID_DATA) : null; + } + + public function findByName(string $name): ?AccountInterface + { + return $name === Account::NAME ? $this->hydrator->hydrate(Account::VALID_DATA) : null; + } + + public function findByEmail(Email $email): ?AccountInterface + { + return $email->toString() === Account::EMAIL ? $this->hydrator->hydrate(Account::VALID_DATA) : null; + } + + public function findAll(): AccountCollection + { + return $this->hydrator->hydrateCollection([Account::VALID_DATA]); + } +} diff --git a/tests/UnitTest/Mock/Table/MockAccountTableAccountAuthenticationMiddlewareInvalidToken.php b/tests/UnitTest/Mock/Table/MockAccountTableAccountAuthenticationMiddlewareInvalidToken.php new file mode 100644 index 00000000..8edb8323 --- /dev/null +++ b/tests/UnitTest/Mock/Table/MockAccountTableAccountAuthenticationMiddlewareInvalidToken.php @@ -0,0 +1,84 @@ +id !== Account::ID) { + throw new DuplicateEntryException('Account', $data->id); + } + + return true; + } + + public function update(AccountInterface $data): true + { + if ($data->id !== Account::ID) { + throw new InvalidArgumentException(); + } + + return true; + } + + public function deleteById(int $id): true + { + if ($id !== Account::ID) { + throw new InvalidArgumentException(); + } + + return true; + } + + public function findById(int $id): ?AccountInterface + { + return $id === Account::ID ? $this->hydrator->hydrate(Account::VALID_DATA) : null; + } + + public function findByUuid(UuidInterface $uuid): ?AccountInterface + { + return null; + } + + public function findByName(string $name): ?AccountInterface + { + return $name === Account::NAME ? $this->hydrator->hydrate(Account::VALID_DATA) : null; + } + + public function findByEmail(Email $email): ?AccountInterface + { + return $email->toString() === Account::EMAIL ? $this->hydrator->hydrate(Account::VALID_DATA) : null; + } + + public function findAll(): AccountCollection + { + return $this->hydrator->hydrateCollection([Account::VALID_DATA]); + } +} diff --git a/tests/UnitTest/Mock/Table/MockAccountTableFailed.php b/tests/UnitTest/Mock/Table/MockAccountTableFailed.php new file mode 100644 index 00000000..c5b78ad8 --- /dev/null +++ b/tests/UnitTest/Mock/Table/MockAccountTableFailed.php @@ -0,0 +1,26 @@ +hydrator->hydrateCollection([]); + } +} diff --git a/tests/UnitTest/Mock/Validator/MockAuthenticationValidator.php b/tests/UnitTest/Mock/Validator/MockAuthenticationValidator.php new file mode 100644 index 00000000..c5b696ff --- /dev/null +++ b/tests/UnitTest/Mock/Validator/MockAuthenticationValidator.php @@ -0,0 +1,20 @@ +