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 @@
+