diff --git a/Makefile b/Makefile
index 371410f..07d0e76 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: help up down build logs ps test lint phpstan cs-fix frontend-lint frontend-test e2e clean
+.PHONY: help up down restart rebuild logs ps test lint phpstan arch cs-fix warmup frontend-lint frontend-test e2e clean
# Default target
help:
@@ -7,6 +7,8 @@ help:
@echo "Docker:"
@echo " make up - Lancer tous les services"
@echo " make down - Arreter tous les services"
+ @echo " make restart - Redemarrer tous les services"
+ @echo " make rebuild - Reconstruire et relancer les services"
@echo " make build - Reconstruire les images"
@echo " make logs - Voir les logs (Ctrl+C pour quitter)"
@echo " make ps - Statut des services"
@@ -14,8 +16,10 @@ help:
@echo ""
@echo "Backend:"
@echo " make phpstan - Analyse statique PHPStan"
+ @echo " make arch - Tests d'architecture (PHPat)"
@echo " make cs-fix - Correction code style PHP"
@echo " make test-php - Tests PHPUnit"
+ @echo " make warmup - Warmup du cache Symfony"
@echo ""
@echo "Frontend:"
@echo " make lint - ESLint frontend"
@@ -36,6 +40,15 @@ up:
down:
docker compose down
+restart:
+ docker compose down
+ docker compose up -d
+
+rebuild:
+ docker compose down
+ docker compose build --no-cache
+ docker compose up -d
+
build:
docker compose build --no-cache
@@ -55,6 +68,9 @@ clean:
phpstan:
docker compose exec php composer phpstan
+arch:
+ docker compose exec php composer arch
+
cs-fix:
docker compose exec php composer cs-fix
@@ -64,6 +80,9 @@ cs-check:
test-php:
docker compose exec php composer test
+warmup:
+ docker compose exec php php bin/console cache:warmup
+
# =============================================================================
# Frontend
# =============================================================================
@@ -97,3 +116,6 @@ check-bc:
check-naming:
./scripts/check-naming.sh
+
+check-tenants:
+ ./scripts/check-tenants.sh
diff --git a/README.md b/README.md
index de836d9..02a990f 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,14 @@ Application de gestion scolaire moderne - Backend Symfony 8 + Frontend SvelteKit
- Docker Desktop 24+ avec Docker Compose 2.20+
- Git
+### Configuration /etc/hosts (multi-tenant)
+
+Classeo utilise une architecture multi-tenant basée sur les sous-domaines. Ajoutez cette ligne à `/etc/hosts` :
+
+```bash
+sudo sh -c 'echo "127.0.0.1 classeo.local ecole-alpha.classeo.local ecole-beta.classeo.local" >> /etc/hosts'
+```
+
### Lancement
```bash
@@ -21,15 +29,27 @@ docker compose up -d
# Verifier le statut
docker compose ps
+
+# Verifier que les tenants répondent
+make check-tenants
```
### URLs
+#### Multi-tenant (recommandé)
+
+| Service | URL | Description |
+|---------|-----|-------------|
+| Frontend Alpha | http://ecole-alpha.classeo.local:5174 | Tenant ecole-alpha |
+| Frontend Beta | http://ecole-beta.classeo.local:5174 | Tenant ecole-beta |
+| API Alpha | http://ecole-alpha.classeo.local:18000/api | API tenant ecole-alpha |
+| API Beta | http://ecole-beta.classeo.local:18000/api | API tenant ecole-beta |
+| API Docs | http://ecole-alpha.classeo.local:18000/api/docs | Documentation OpenAPI |
+
+#### Services partagés
+
| Service | URL | Description |
|---------|-----|-------------|
-| Frontend | http://localhost:5174 | Application SvelteKit |
-| Backend API | http://localhost:18000/api | API REST (API Platform) |
-| API Docs | http://localhost:18000/api/docs | Documentation OpenAPI |
| RabbitMQ | http://localhost:15672 | Admin (guest/guest) |
| Meilisearch | http://localhost:7700 | Dashboard recherche |
| Mailpit | http://localhost:8025 | Emails de test |
diff --git a/backend/.editorconfig b/backend/.editorconfig
new file mode 100644
index 0000000..6699076
--- /dev/null
+++ b/backend/.editorconfig
@@ -0,0 +1,17 @@
+# editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[{compose.yaml,compose.*.yaml}]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/backend/.env b/backend/.env
index ba0ed41..b1a9c03 100644
--- a/backend/.env
+++ b/backend/.env
@@ -17,7 +17,7 @@
APP_ENV=dev
APP_SECRET=change_me_in_production_12345678
TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
-TRUSTED_HOSTS='^(localhost|php|127\.0\.0\.1)$'
+TRUSTED_HOSTS=^(localhost|php|127\.0\.0\.1|(.+\.)?classeo\.local)$
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
@@ -52,3 +52,14 @@ MEILISEARCH_API_KEY=masterKey
###> symfony/mailer ###
MAILER_DSN=smtp://mailpit:1025
###< symfony/mailer ###
+
+###> symfony/routing ###
+# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
+# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
+DEFAULT_URI=http://localhost
+###< symfony/routing ###
+
+###> multi-tenant ###
+# Base domain for tenant resolution (e.g., classeo.fr, classeo.local)
+TENANT_BASE_DOMAIN=classeo.local
+###< multi-tenant ###
diff --git a/backend/.env.dev b/backend/.env.dev
new file mode 100644
index 0000000..a998851
--- /dev/null
+++ b/backend/.env.dev
@@ -0,0 +1,4 @@
+
+###> symfony/framework-bundle ###
+APP_SECRET=88a37335e55d5adf4169161f49f4b596
+###< symfony/framework-bundle ###
diff --git a/backend/.env.test b/backend/.env.test
new file mode 100644
index 0000000..64bd111
--- /dev/null
+++ b/backend/.env.test
@@ -0,0 +1,3 @@
+# define your env variables for the test env here
+KERNEL_CLASS='App\Kernel'
+APP_SECRET='$ecretf0rt3st'
diff --git a/backend/.gitignore b/backend/.gitignore
index 5259a9c..1651720 100644
--- a/backend/.gitignore
+++ b/backend/.gitignore
@@ -10,7 +10,6 @@
/public/bundles/
# Fichiers auto-générés par Symfony
-/config/bundles.php
/config/preload.php
/config/reference.php
@@ -45,7 +44,6 @@ phpstan.neon.dist
# Composer
# =============================================================================
composer.phar
-composer.lock
###> symfony/framework-bundle ###
/.env.local
@@ -70,3 +68,14 @@ composer.lock
/phpunit.xml
/.phpunit.cache/
###< phpunit/phpunit ###
+
+# =============================================================================
+# Symfony Flex Docker (redondant avec compose.yaml racine)
+# =============================================================================
+/compose.yaml
+/compose.override.yaml
+
+# =============================================================================
+# System
+# =============================================================================
+core
diff --git a/backend/.php-cs-fixer.php b/backend/.php-cs-fixer.dist.php
similarity index 90%
rename from backend/.php-cs-fixer.php
rename to backend/.php-cs-fixer.dist.php
index 7bed316..52c43bd 100644
--- a/backend/.php-cs-fixer.php
+++ b/backend/.php-cs-fixer.dist.php
@@ -14,6 +14,8 @@ $finder = (new PhpCsFixer\Finder())
// Exclusions spécifiques
->notPath('src/Shared/Domain/AggregateRoot.php')
->notPath('src/Shared/Domain/EntityId.php')
+ // Classes that need to be mocked in tests (cannot be final)
+ ->notPath('src/Shared/Infrastructure/Tenant/TenantResolver.php')
;
return (new PhpCsFixer\Config())
@@ -25,7 +27,7 @@ return (new PhpCsFixer\Config())
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
- 'not_operator_with_successor_space' => true,
+ 'not_operator_with_successor_space' => false,
'trailing_comma_in_multiline' => true,
'phpdoc_order' => true,
'phpdoc_separation' => true,
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 2d7413e..4b8fc84 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -52,6 +52,17 @@ RUN echo "opcache.enable=1" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
# =============================================================================
FROM base AS dev
+# Install gosu for proper user switching
+ENV GOSU_VERSION=1.17
+RUN set -eux; \
+ apk add --no-cache --virtual .gosu-deps dpkg gnupg; \
+ dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
+ wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
+ chmod +x /usr/local/bin/gosu; \
+ gosu --version; \
+ gosu nobody true; \
+ apk del --no-network .gosu-deps
+
# Enable opcache revalidation for dev (zz- prefix loads last alphabetically)
RUN echo "opcache.validate_timestamps=1" >> "$PHP_INI_DIR/conf.d/zz-opcache-dev.ini"
@@ -65,16 +76,40 @@ RUN echo "xdebug.mode=develop,debug,coverage" >> "$PHP_INI_DIR/conf.d/xdebug.ini
ENV SERVER_NAME=:8000
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
-# Create entrypoint script for dev (installs deps if needed)
-RUN echo '#!/bin/sh' > /usr/local/bin/docker-entrypoint.sh && \
- echo 'set -e' >> /usr/local/bin/docker-entrypoint.sh && \
- echo 'if [ ! -f /app/vendor/autoload.php ]; then' >> /usr/local/bin/docker-entrypoint.sh && \
- echo ' echo "Installing Composer dependencies..."' >> /usr/local/bin/docker-entrypoint.sh && \
- echo ' composer install --prefer-dist --no-progress --no-interaction' >> /usr/local/bin/docker-entrypoint.sh && \
- echo 'fi' >> /usr/local/bin/docker-entrypoint.sh && \
- echo 'mkdir -p var/cache var/log && chmod -R 777 var' >> /usr/local/bin/docker-entrypoint.sh && \
- echo 'exec "$@"' >> /usr/local/bin/docker-entrypoint.sh && \
- chmod +x /usr/local/bin/docker-entrypoint.sh
+# Entrypoint: detect host UID/GID and run as matching user
+# Uses gosu with UID:GID directly (no need to create user in Dockerfile)
+COPY --chmod=755 <<'EOF' /usr/local/bin/docker-entrypoint.sh
+#!/bin/sh
+set -e
+
+# Detect UID/GID from mounted /app directory
+HOST_UID=$(stat -c %u /app)
+HOST_GID=$(stat -c %g /app)
+
+# If root owns /app, run as root (CI environment or volume not mounted)
+if [ "$HOST_UID" = "0" ]; then
+ # Install dependencies if not present
+ if [ ! -f /app/vendor/autoload.php ]; then
+ echo "Installing Composer dependencies..."
+ composer install --prefer-dist --no-progress --no-interaction
+ fi
+ mkdir -p /app/var/cache /app/var/log
+ exec "$@"
+fi
+
+# Ensure directories exist with correct ownership
+mkdir -p /app/var/cache /app/var/log /data /config
+chown -R "$HOST_UID:$HOST_GID" /app/var /data /config 2>/dev/null || true
+
+# Install Composer dependencies if not present (as host user)
+if [ ! -f /app/vendor/autoload.php ]; then
+ echo "Installing Composer dependencies..."
+ gosu "$HOST_UID:$HOST_GID" composer install --prefer-dist --no-progress --no-interaction
+fi
+
+# Run command as host user via gosu (using UID:GID directly)
+exec gosu "$HOST_UID:$HOST_GID" "$@"
+EOF
EXPOSE 8000
diff --git a/backend/Makefile b/backend/Makefile
new file mode 100644
index 0000000..fd5a048
--- /dev/null
+++ b/backend/Makefile
@@ -0,0 +1,29 @@
+.PHONY: help test arch lint warmup cache-clear
+
+# Default PHP command (can be overridden)
+PHP ?= docker run --rm -v "$$(pwd):/app" -w /app php:8.5-cli php
+
+help: ## Display this help
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
+
+test: ## Run PHPUnit tests
+ $(PHP) ./vendor/bin/phpunit --testdox
+
+arch: ## Run architecture tests (PHPat via PHPStan)
+ $(PHP) ./vendor/bin/phpstan analyse tests/Architecture --configuration=phpstan.neon --level=1
+
+lint: ## Run PHPStan static analysis
+ $(PHP) ./vendor/bin/phpstan analyse --configuration=phpstan.neon
+
+warmup: ## Warmup Symfony cache (avoids timeout in web requests)
+ $(PHP) ./bin/console cache:warmup --env=dev
+ $(PHP) ./bin/console cache:warmup --env=prod
+
+cache-clear: ## Clear Symfony cache
+ $(PHP) ./bin/console cache:clear --env=dev
+ $(PHP) ./bin/console cache:clear --env=prod
+
+cs-fix: ## Run PHP-CS-Fixer
+ $(PHP) ./vendor/bin/php-cs-fixer fix
+
+ci: lint arch test ## Run all CI checks (lint, arch, test)
diff --git a/backend/bin/phpunit b/backend/bin/phpunit
new file mode 100755
index 0000000..ac5eef1
--- /dev/null
+++ b/backend/bin/phpunit
@@ -0,0 +1,4 @@
+#!/usr/bin/env php
+=8.2",
+ "psr/cache": "^1.0 || ^2.0 || ^3.0",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/deprecation-contracts": "^3.1",
+ "symfony/http-foundation": "^6.4.14 || ^7.0 || ^8.0",
+ "symfony/http-kernel": "^6.4 || ^7.0 || ^8.0",
+ "symfony/property-access": "^6.4 || ^7.0 || ^8.0",
+ "symfony/property-info": "^6.4 || ^7.1 || ^8.0",
+ "symfony/serializer": "^6.4 || ^7.0 || ^8.0",
+ "symfony/translation-contracts": "^3.3",
+ "symfony/type-info": "^7.4 || ^8.0",
+ "symfony/validator": "^6.4.11 || ^7.1 || ^8.0",
+ "symfony/web-link": "^6.4 || ^7.1 || ^8.0",
+ "willdurand/negotiation": "^3.1"
+ },
+ "conflict": {
+ "doctrine/common": "<3.2.2",
+ "doctrine/dbal": "<2.10",
+ "doctrine/mongodb-odm": "<2.4",
+ "doctrine/orm": "<2.14.0",
+ "doctrine/persistence": "<1.3",
+ "phpspec/prophecy": "<1.15",
+ "phpunit/phpunit": "<9.5",
+ "symfony/framework-bundle": "6.4.6 || 7.0.6",
+ "symfony/object-mapper": "<7.3.4",
+ "symfony/var-exporter": "<6.1.1"
+ },
+ "replace": {
+ "api-platform/doctrine-common": "self.version",
+ "api-platform/doctrine-odm": "self.version",
+ "api-platform/doctrine-orm": "self.version",
+ "api-platform/documentation": "self.version",
+ "api-platform/elasticsearch": "self.version",
+ "api-platform/graphql": "self.version",
+ "api-platform/hal": "self.version",
+ "api-platform/http-cache": "self.version",
+ "api-platform/hydra": "self.version",
+ "api-platform/json-api": "self.version",
+ "api-platform/json-schema": "self.version",
+ "api-platform/jsonld": "self.version",
+ "api-platform/laravel": "self.version",
+ "api-platform/metadata": "self.version",
+ "api-platform/openapi": "self.version",
+ "api-platform/parameter-validator": "self.version",
+ "api-platform/ramsey-uuid": "self.version",
+ "api-platform/serializer": "self.version",
+ "api-platform/state": "self.version",
+ "api-platform/symfony": "self.version",
+ "api-platform/validator": "self.version"
+ },
+ "require-dev": {
+ "behat/behat": "^3.11",
+ "behat/mink": "^1.9",
+ "doctrine/common": "^3.2.2",
+ "doctrine/dbal": "^4.0",
+ "doctrine/doctrine-bundle": "^2.11 || ^3.1",
+ "doctrine/orm": "^2.17 || ^3.0",
+ "elasticsearch/elasticsearch": "^7.17 || ^8.4 || ^9.0",
+ "friends-of-behat/mink-browserkit-driver": "^1.3.1",
+ "friends-of-behat/mink-extension": "^2.2",
+ "friends-of-behat/symfony-extension": "^2.1",
+ "guzzlehttp/guzzle": "^6.0 || ^7.0",
+ "illuminate/config": "^11.0 || ^12.0",
+ "illuminate/contracts": "^11.0 || ^12.0",
+ "illuminate/database": "^11.0 || ^12.0",
+ "illuminate/http": "^11.0 || ^12.0",
+ "illuminate/pagination": "^11.0 || ^12.0",
+ "illuminate/routing": "^11.0 || ^12.0",
+ "illuminate/support": "^11.0 || ^12.0",
+ "jangregor/phpstan-prophecy": "^2.1.11",
+ "justinrainbow/json-schema": "^6.5.2",
+ "laravel/framework": "^11.0 || ^12.0",
+ "orchestra/testbench": "^9.1",
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpdoc-parser": "^1.29 || ^2.0",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-doctrine": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-symfony": "^2.0",
+ "phpunit/phpunit": "^12.2",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "ramsey/uuid": "^4.7",
+ "ramsey/uuid-doctrine": "^2.0",
+ "soyuka/contexts": "^3.3.10",
+ "soyuka/pmu": "^0.2.0",
+ "soyuka/stubs-mongodb": "^1.0",
+ "symfony/asset": "^6.4 || ^7.0 || ^8.0",
+ "symfony/browser-kit": "^6.4 || ^7.0 || ^8.0",
+ "symfony/cache": "^6.4 || ^7.0 || ^8.0",
+ "symfony/config": "^6.4 || ^7.0 || ^8.0",
+ "symfony/console": "^6.4 || ^7.0 || ^8.0",
+ "symfony/css-selector": "^6.4 || ^7.0 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0",
+ "symfony/doctrine-bridge": "^6.4.2 || ^7.1 || ^8.0",
+ "symfony/dom-crawler": "^6.4 || ^7.0 || ^8.0",
+ "symfony/error-handler": "^6.4 || ^7.0 || ^8.0",
+ "symfony/event-dispatcher": "^6.4 || ^7.0 || ^8.0",
+ "symfony/expression-language": "^6.4 || ^7.0 || ^8.0",
+ "symfony/finder": "^6.4 || ^7.0 || ^8.0",
+ "symfony/form": "^6.4 || ^7.0 || ^8.0",
+ "symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0",
+ "symfony/http-client": "^6.4 || ^7.0 || ^8.0",
+ "symfony/intl": "^6.4 || ^7.0 || ^8.0",
+ "symfony/json-streamer": "^7.4 || ^8.0",
+ "symfony/maker-bundle": "^1.24",
+ "symfony/mercure-bundle": "*",
+ "symfony/messenger": "^6.4 || ^7.0 || ^8.0",
+ "symfony/object-mapper": "^7.4 || ^8.0",
+ "symfony/routing": "^6.4 || ^7.0 || ^8.0",
+ "symfony/security-bundle": "^6.4 || ^7.0 || ^8.0",
+ "symfony/security-core": "^6.4 || ^7.0 || ^8.0",
+ "symfony/stopwatch": "^6.4 || ^7.0 || ^8.0",
+ "symfony/string": "^6.4 || ^7.0 || ^8.0",
+ "symfony/twig-bundle": "^6.4 || ^7.0 || ^8.0",
+ "symfony/uid": "^6.4 || ^7.0 || ^8.0",
+ "symfony/var-exporter": "^7.4 || ^8.0",
+ "symfony/web-profiler-bundle": "^6.4 || ^7.0 || ^8.0",
+ "symfony/yaml": "^6.4 || ^7.0 || ^8.0",
+ "twig/twig": "^1.42.3 || ^2.12 || ^3.0",
+ "webonyx/graphql-php": "^15.0"
+ },
+ "suggest": {
+ "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.",
+ "elasticsearch/elasticsearch": "To support Elasticsearch.",
+ "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.",
+ "psr/cache-implementation": "To use metadata caching.",
+ "ramsey/uuid": "To support Ramsey's UUID identifiers.",
+ "symfony/cache": "To have metadata caching when using Symfony integration.",
+ "symfony/config": "To load XML configuration files.",
+ "symfony/expression-language": "To use authorization features.",
+ "symfony/http-client": "To use the HTTP cache invalidation system.",
+ "symfony/json-streamer": "To use the JSON Streamer component.",
+ "symfony/messenger": "To support messenger integration.",
+ "symfony/security": "To use authorization features.",
+ "symfony/twig-bundle": "To use the Swagger UI integration.",
+ "symfony/uid": "To support Symfony UUID/ULID identifiers.",
+ "symfony/web-profiler-bundle": "To use the data collector.",
+ "webonyx/graphql-php": "To support GraphQL."
+ },
+ "type": "library",
+ "extra": {
+ "pmu": {
+ "projects": [
+ "./src/*/composer.json",
+ "src/Doctrine/*/composer.json"
+ ]
+ },
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.1 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.3.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/JsonLd/HydraContext.php"
+ ],
+ "psr-4": {
+ "ApiPlatform\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ }
+ ],
+ "description": "Build a fully-featured hypermedia or GraphQL API in minutes!",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "Hydra",
+ "JSON-LD",
+ "api",
+ "graphql",
+ "hal",
+ "jsonapi",
+ "laravel",
+ "openapi",
+ "rest",
+ "swagger",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/api-platform/core/issues",
+ "source": "https://github.com/api-platform/core/tree/v4.2.14"
+ },
+ "time": "2026-01-23T15:23:18+00:00"
+ },
+ {
+ "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": "doctrine/collections",
+ "version": "2.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/collections.git",
+ "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2",
+ "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1",
+ "php": "^8.1",
+ "symfony/polyfill-php84": "^1.30"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "ext-json": "*",
+ "phpstan/phpstan": "^2.1.30",
+ "phpstan/phpstan-phpunit": "^2.0.7",
+ "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Collections\\": "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"
+ }
+ ],
+ "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.",
+ "homepage": "https://www.doctrine-project.org/projects/collections.html",
+ "keywords": [
+ "array",
+ "collections",
+ "iterators",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/collections/issues",
+ "source": "https://github.com/doctrine/collections/tree/2.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-15T10:01:58+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/doctrine-bundle",
+ "version": "3.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/DoctrineBundle.git",
+ "reference": "af84173db6978c3d2688ea3bcf3a91720b0704ce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/af84173db6978c3d2688ea3bcf3a91720b0704ce",
+ "reference": "af84173db6978c3d2688ea3bcf3a91720b0704ce",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/dbal": "^4.0",
+ "doctrine/deprecations": "^1.0",
+ "doctrine/persistence": "^4",
+ "doctrine/sql-formatter": "^1.0.1",
+ "php": "^8.4",
+ "symfony/cache": "^6.4 || ^7.0 || ^8.0",
+ "symfony/config": "^6.4 || ^7.0 || ^8.0",
+ "symfony/console": "^6.4 || ^7.0 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0",
+ "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3 || ^8.0",
+ "symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0",
+ "symfony/service-contracts": "^3"
+ },
+ "conflict": {
+ "doctrine/orm": "<3.0 || >=4.0",
+ "twig/twig": "<3.0.4"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "doctrine/orm": "^3.4.4",
+ "phpstan/phpstan": "2.1.1",
+ "phpstan/phpstan-phpunit": "2.0.3",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpstan/phpstan-symfony": "^2.0",
+ "phpunit/phpunit": "^12.3.10",
+ "psr/log": "^3.0",
+ "symfony/doctrine-messenger": "^6.4 || ^7.0 || ^8.0",
+ "symfony/expression-language": "^6.4 || ^7.0 || ^8.0",
+ "symfony/messenger": "^6.4 || ^7.0 || ^8.0",
+ "symfony/property-info": "^6.4 || ^7.0 || ^8.0",
+ "symfony/security-bundle": "^6.4 || ^7.0 || ^8.0",
+ "symfony/stopwatch": "^6.4 || ^7.0 || ^8.0",
+ "symfony/string": "^6.4 || ^7.0 || ^8.0",
+ "symfony/twig-bridge": "^6.4 || ^7.0 || ^8.0",
+ "symfony/validator": "^6.4 || ^7.0 || ^8.0",
+ "symfony/web-profiler-bundle": "^6.4 || ^7.0 || ^8.0",
+ "symfony/yaml": "^6.4 || ^7.0 || ^8.0",
+ "twig/twig": "^3.21.1"
+ },
+ "suggest": {
+ "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.",
+ "ext-pdo": "*",
+ "symfony/web-profiler-bundle": "To use the data collector."
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Bundle\\DoctrineBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ },
+ {
+ "name": "Doctrine Project",
+ "homepage": "https://www.doctrine-project.org/"
+ }
+ ],
+ "description": "Symfony DoctrineBundle",
+ "homepage": "https://www.doctrine-project.org",
+ "keywords": [
+ "database",
+ "dbal",
+ "orm",
+ "persistence"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/DoctrineBundle/issues",
+ "source": "https://github.com/doctrine/DoctrineBundle/tree/3.2.2"
+ },
+ "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%2Fdoctrine-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-24T12:24:29+00:00"
+ },
+ {
+ "name": "doctrine/doctrine-migrations-bundle",
+ "version": "3.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git",
+ "reference": "1e380c6dd8ac8488217f39cff6b77e367f1a644b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/1e380c6dd8ac8488217f39cff6b77e367f1a644b",
+ "reference": "1e380c6dd8ac8488217f39cff6b77e367f1a644b",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/doctrine-bundle": "^2.4 || ^3.0",
+ "doctrine/migrations": "^3.2",
+ "php": "^7.2 || ^8.0",
+ "symfony/deprecation-contracts": "^2.1 || ^3",
+ "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "composer/semver": "^3.0",
+ "doctrine/coding-standard": "^12 || ^14",
+ "doctrine/orm": "^2.6 || ^3",
+ "phpstan/phpstan": "^1.4 || ^2",
+ "phpstan/phpstan-deprecation-rules": "^1 || ^2",
+ "phpstan/phpstan-phpunit": "^1 || ^2",
+ "phpstan/phpstan-strict-rules": "^1.1 || ^2",
+ "phpstan/phpstan-symfony": "^1.3 || ^2",
+ "phpunit/phpunit": "^8.5 || ^9.5",
+ "symfony/phpunit-bridge": "^6.3 || ^7 || ^8",
+ "symfony/var-exporter": "^5.4 || ^6 || ^7 || ^8"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Bundle\\MigrationsBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Doctrine Project",
+ "homepage": "https://www.doctrine-project.org"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony DoctrineMigrationsBundle",
+ "homepage": "https://www.doctrine-project.org",
+ "keywords": [
+ "dbal",
+ "migrations",
+ "schema"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues",
+ "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-11-15T19:02:59+00:00"
+ },
+ {
+ "name": "doctrine/event-manager",
+ "version": "2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/event-manager.git",
+ "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf",
+ "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "conflict": {
+ "doctrine/common": "<2.9"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "phpdocumentor/guides-cli": "^1.4",
+ "phpstan/phpstan": "^2.1.32",
+ "phpunit/phpunit": "^10.5.58"
+ },
+ "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.1.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": "2026-01-29T07:11:08+00:00"
+ },
+ {
+ "name": "doctrine/inflector",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/inflector.git",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^12.0 || ^13.0",
+ "phpstan/phpstan": "^1.12 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.4 || ^2.0",
+ "phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
+ "phpunit/phpunit": "^8.5 || ^12.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Inflector\\": "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"
+ }
+ ],
+ "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.",
+ "homepage": "https://www.doctrine-project.org/projects/inflector.html",
+ "keywords": [
+ "inflection",
+ "inflector",
+ "lowercase",
+ "manipulation",
+ "php",
+ "plural",
+ "singular",
+ "strings",
+ "uppercase",
+ "words"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/inflector/issues",
+ "source": "https://github.com/doctrine/inflector/tree/2.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-10T19:31:58+00:00"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7",
+ "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.4"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^10.5.58"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/2.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-05T06:47:08+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": "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": "doctrine/orm",
+ "version": "3.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/orm.git",
+ "reference": "2148940290e4c44b9101095707e71fb590832fa5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/orm/zipball/2148940290e4c44b9101095707e71fb590832fa5",
+ "reference": "2148940290e4c44b9101095707e71fb590832fa5",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": "^2",
+ "doctrine/collections": "^2.2",
+ "doctrine/dbal": "^3.8.2 || ^4",
+ "doctrine/deprecations": "^0.5.3 || ^1",
+ "doctrine/event-manager": "^1.2 || ^2",
+ "doctrine/inflector": "^1.4 || ^2.0",
+ "doctrine/instantiator": "^1.3 || ^2",
+ "doctrine/lexer": "^3",
+ "doctrine/persistence": "^3.3.1 || ^4",
+ "ext-ctype": "*",
+ "php": "^8.1",
+ "psr/cache": "^1 || ^2 || ^3",
+ "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/var-exporter": "^6.3.9 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14.0",
+ "phpbench/phpbench": "^1.0",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "2.1.23",
+ "phpstan/phpstan-deprecation-rules": "^2",
+ "phpunit/phpunit": "^10.5.0 || ^11.5",
+ "psr/log": "^1 || ^2 || ^3",
+ "symfony/cache": "^5.4 || ^6.2 || ^7.0 || ^8.0"
+ },
+ "suggest": {
+ "ext-dom": "Provides support for XSD validation for XML mapping files",
+ "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\ORM\\": "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": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ }
+ ],
+ "description": "Object-Relational-Mapper for PHP",
+ "homepage": "https://www.doctrine-project.org/projects/orm.html",
+ "keywords": [
+ "database",
+ "orm"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/orm/issues",
+ "source": "https://github.com/doctrine/orm/tree/3.6.1"
+ },
+ "time": "2026-01-09T05:28:15+00:00"
+ },
+ {
+ "name": "doctrine/persistence",
+ "version": "4.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/persistence.git",
+ "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/persistence/zipball/b9c49ad3558bb77ef973f4e173f2e9c2eca9be09",
+ "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/event-manager": "^1 || ^2",
+ "php": "^8.1",
+ "psr/cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "phpstan/phpstan": "2.1.30",
+ "phpstan/phpstan-phpunit": "^2",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "^10.5.58 || ^12",
+ "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0",
+ "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Persistence\\": "src/Persistence"
+ }
+ },
+ "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 Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.",
+ "homepage": "https://www.doctrine-project.org/projects/persistence.html",
+ "keywords": [
+ "mapper",
+ "object",
+ "odm",
+ "orm",
+ "persistence"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/persistence/issues",
+ "source": "https://github.com/doctrine/persistence/tree/4.1.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%2Fpersistence",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-10-16T20:13:18+00:00"
+ },
+ {
+ "name": "doctrine/sql-formatter",
+ "version": "1.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/sql-formatter.git",
+ "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/a8af23a8e9d622505baa2997465782cbe8bb7fc7",
+ "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "ergebnis/phpunit-slow-test-detector": "^2.20",
+ "phpstan/phpstan": "^2.1.31",
+ "phpunit/phpunit": "^10.5.58"
+ },
+ "bin": [
+ "bin/sql-formatter"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\SqlFormatter\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jeremy Dorn",
+ "email": "jeremy@jeremydorn.com",
+ "homepage": "https://jeremydorn.com/"
+ }
+ ],
+ "description": "a PHP SQL highlighting library",
+ "homepage": "https://github.com/doctrine/sql-formatter/",
+ "keywords": [
+ "highlight",
+ "sql"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/sql-formatter/issues",
+ "source": "https://github.com/doctrine/sql-formatter/tree/1.5.3"
+ },
+ "time": "2025-10-26T09:35:14+00:00"
+ },
+ {
+ "name": "lcobucci/jwt",
+ "version": "5.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/lcobucci/jwt.git",
+ "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e",
+ "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "ext-sodium": "*",
+ "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+ "psr/clock": "^1.0"
+ },
+ "require-dev": {
+ "infection/infection": "^0.29",
+ "lcobucci/clock": "^3.2",
+ "lcobucci/coding-standard": "^11.0",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/extension-installer": "^1.2",
+ "phpstan/phpstan": "^1.10.7",
+ "phpstan/phpstan-deprecation-rules": "^1.1.3",
+ "phpstan/phpstan-phpunit": "^1.3.10",
+ "phpstan/phpstan-strict-rules": "^1.5.0",
+ "phpunit/phpunit": "^11.1"
+ },
+ "suggest": {
+ "lcobucci/clock": ">= 3.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Lcobucci\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Luís Cobucci",
+ "email": "lcobucci@gmail.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to work with JSON Web Token and JSON Web Signature",
+ "keywords": [
+ "JWS",
+ "jwt"
+ ],
+ "support": {
+ "issues": "https://github.com/lcobucci/jwt/issues",
+ "source": "https://github.com/lcobucci/jwt/tree/5.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/lcobucci",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/lcobucci",
+ "type": "patreon"
+ }
+ ],
+ "time": "2025-10-17T11:30:53+00:00"
+ },
+ {
+ "name": "lexik/jwt-authentication-bundle",
+ "version": "v3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git",
+ "reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/60df75dc70ee6f597929cb2f0812adda591dfa4b",
+ "reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "lcobucci/jwt": "^5.0",
+ "php": ">=8.2",
+ "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/deprecation-contracts": "^2.4|^3.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/property-access": "^6.4|^7.0|^8.0",
+ "symfony/security-bundle": "^6.4|^7.0|^8.0",
+ "symfony/translation-contracts": "^1.0|^2.0|^3.0"
+ },
+ "require-dev": {
+ "api-platform/core": "^3.0|^4.0",
+ "rector/rector": "^1.2",
+ "symfony/browser-kit": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dom-crawler": "^6.4|^7.0|^8.0",
+ "symfony/filesystem": "^6.4|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/phpunit-bridge": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0"
+ },
+ "suggest": {
+ "gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony",
+ "spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Lexik\\Bundle\\JWTAuthenticationBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jeremy Barthe",
+ "email": "j.barthe@lexik.fr",
+ "homepage": "https://github.com/jeremyb"
+ },
+ {
+ "name": "Nicolas Cabot",
+ "email": "n.cabot@lexik.fr",
+ "homepage": "https://github.com/slashfan"
+ },
+ {
+ "name": "Cedric Girard",
+ "email": "c.girard@lexik.fr",
+ "homepage": "https://github.com/cedric-g"
+ },
+ {
+ "name": "Dev Lexik",
+ "email": "dev@lexik.fr",
+ "homepage": "https://github.com/lexik"
+ },
+ {
+ "name": "Robin Chalas",
+ "email": "robin.chalas@gmail.com",
+ "homepage": "https://github.com/chalasr"
+ },
+ {
+ "name": "Lexik Community",
+ "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors"
+ }
+ ],
+ "description": "This bundle provides JWT authentication for your Symfony REST API",
+ "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle",
+ "keywords": [
+ "Authentication",
+ "JWS",
+ "api",
+ "bundle",
+ "jwt",
+ "rest",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues",
+ "source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v3.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/chalasr",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-20T17:47:00+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": "psr/cache",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/cache.git",
+ "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+ "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for caching libraries",
+ "keywords": [
+ "cache",
+ "psr",
+ "psr-6"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/cache/tree/3.0.0"
+ },
+ "time": "2021-02-03T23:26:27+00:00"
+ },
+ {
+ "name": "psr/clock",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/clock.git",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Clock\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for reading the clock.",
+ "homepage": "https://github.com/php-fig/clock",
+ "keywords": [
+ "clock",
+ "now",
+ "psr",
+ "psr-20",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/clock/issues",
+ "source": "https://github.com/php-fig/clock/tree/1.0.0"
+ },
+ "time": "2022-11-25T14:36:26+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/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/link",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/link.git",
+ "reference": "84b159194ecfd7eaa472280213976e96415433f7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/link/zipball/84b159194ecfd7eaa472280213976e96415433f7",
+ "reference": "84b159194ecfd7eaa472280213976e96415433f7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "suggest": {
+ "fig/link-util": "Provides some useful PSR-13 utilities"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Link\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for HTTP links",
+ "homepage": "https://github.com/php-fig/link",
+ "keywords": [
+ "http",
+ "http-link",
+ "link",
+ "psr",
+ "psr-13",
+ "rest"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/link/tree/2.0.1"
+ },
+ "time": "2021-03-11T23:00:27+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": "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/amqp-messenger",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/amqp-messenger.git",
+ "reference": "26e49e6df2020da4bf72bc44cbcc146f1bb7a167"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/amqp-messenger/zipball/26e49e6df2020da4bf72bc44cbcc146f1bb7a167",
+ "reference": "26e49e6df2020da4bf72bc44cbcc146f1bb7a167",
+ "shasum": ""
+ },
+ "require": {
+ "ext-amqp": "*",
+ "php": ">=8.4",
+ "symfony/messenger": "^7.4|^8.0"
+ },
+ "require-dev": {
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/property-access": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0"
+ },
+ "type": "symfony-messenger-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Messenger\\Bridge\\Amqp\\": ""
+ },
+ "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": "Symfony AMQP extension Messenger Bridge",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/amqp-messenger/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-08T22:36:47+00:00"
+ },
+ {
+ "name": "symfony/asset",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/asset.git",
+ "reference": "2401c7e9f223969f0979eeb884a09fa6f8d7e49b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/asset/zipball/2401c7e9f223969f0979eeb884a09fa6f8d7e49b",
+ "reference": "2401c7e9f223969f0979eeb884a09fa6f8d7e49b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4"
+ },
+ "require-dev": {
+ "symfony/http-client": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Asset\\": ""
+ },
+ "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": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/asset/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-13T13:06:50+00:00"
+ },
+ {
+ "name": "symfony/cache",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/cache.git",
+ "reference": "92e9960386c7e01f58198038c199d522959a843c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/92e9960386c7e01f58198038c199d522959a843c",
+ "reference": "92e9960386c7e01f58198038c199d522959a843c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "psr/cache": "^2.0|^3.0",
+ "psr/log": "^1.1|^2|^3",
+ "symfony/cache-contracts": "^3.6",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/var-exporter": "^7.4|^8.0"
+ },
+ "conflict": {
+ "doctrine/dbal": "<4.3",
+ "ext-redis": "<6.1",
+ "ext-relay": "<0.12.1"
+ },
+ "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": "^4.3",
+ "predis/predis": "^1.1|^2.0",
+ "psr/simple-cache": "^1.0|^2.0|^3.0",
+ "symfony/clock": "^7.4|^8.0",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/filesystem": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^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/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T16:18:07+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/clock",
+ "version": "v8.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/clock.git",
+ "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f",
+ "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "psr/clock": "^1.0"
+ },
+ "provide": {
+ "psr/clock-implementation": "1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/now.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Clock\\": ""
+ },
+ "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": "Decouples applications from the system clock",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clock",
+ "psr20",
+ "time"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/clock/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-12T15:46:48+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "8f45af92f08f82902827a8b6f403aaf49d893539"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/8f45af92f08f82902827a8b6f403aaf49d893539",
+ "reference": "8f45af92f08f82902827a8b6f403aaf49d893539",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/filesystem": "^7.4|^8.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/service-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/finder": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Config\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/config/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-13T13:06:50+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
+ "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/string": "^7.4|^8.0"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/lock": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases the creation of beautiful and testable command line interfaces",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "cli",
+ "command-line",
+ "console",
+ "terminal"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-13T13:06:50+00:00"
+ },
+ {
+ "name": "symfony/dependency-injection",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dependency-injection.git",
+ "reference": "40a6c455ade7e3bf25900d6b746d40cfa2573e26"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/40a6c455ade7e3bf25900d6b746d40cfa2573e26",
+ "reference": "40a6c455ade7e3bf25900d6b746d40cfa2573e26",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/service-contracts": "^3.6",
+ "symfony/var-exporter": "^7.4|^8.0"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "provide": {
+ "psr/container-implementation": "1.1|2.0",
+ "symfony/service-implementation": "1.1|2.0|3.0"
+ },
+ "require-dev": {
+ "symfony/config": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/yaml": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DependencyInjection\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows you to standardize and centralize the way objects are constructed in your application",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dependency-injection/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T16:18:07+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/doctrine-bridge",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/doctrine-bridge.git",
+ "reference": "0d07589d03ed7db1833bfe943635872a2e8aebb2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/0d07589d03ed7db1833bfe943635872a2e8aebb2",
+ "reference": "0d07589d03ed7db1833bfe943635872a2e8aebb2",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/event-manager": "^2",
+ "doctrine/persistence": "^3.1|^4",
+ "php": ">=8.4",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.8",
+ "doctrine/dbal": "<4.3",
+ "doctrine/lexer": "<1.1",
+ "doctrine/orm": "<3.4",
+ "symfony/property-info": "<8.0"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.8|^2.0",
+ "doctrine/data-fixtures": "^1.1|^2",
+ "doctrine/dbal": "^4.3",
+ "doctrine/orm": "^3.4",
+ "psr/log": "^1|^2|^3",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/doctrine-messenger": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/form": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/lock": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/property-access": "^7.4|^8.0",
+ "symfony/property-info": "^8.0",
+ "symfony/security-core": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0",
+ "symfony/translation": "^7.4|^8.0",
+ "symfony/type-info": "^7.4|^8.0",
+ "symfony/uid": "^7.4|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^8.0"
+ },
+ "type": "symfony-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\Doctrine\\": ""
+ },
+ "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 integration for Doctrine with various Symfony components",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/doctrine-bridge/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-23T11:07:10+00:00"
+ },
+ {
+ "name": "symfony/doctrine-messenger",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/doctrine-messenger.git",
+ "reference": "81d0b288e90d462896d1dffcff99571dd9d1618c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/81d0b288e90d462896d1dffcff99571dd9d1618c",
+ "reference": "81d0b288e90d462896d1dffcff99571dd9d1618c",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/dbal": "^4.3",
+ "php": ">=8.4",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "doctrine/persistence": "<1.3"
+ },
+ "require-dev": {
+ "doctrine/persistence": "^1.3|^2|^3",
+ "symfony/property-access": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0"
+ },
+ "type": "symfony-messenger-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": ""
+ },
+ "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": "Symfony Doctrine Messenger Bridge",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/doctrine-messenger/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T16:18:07+00:00"
+ },
+ {
+ "name": "symfony/dotenv",
+ "version": "v8.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dotenv.git",
+ "reference": "460b4067a85288c59a59ce8c1bfb3942e71fd85c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dotenv/zipball/460b4067a85288c59a59ce8c1bfb3942e71fd85c",
+ "reference": "460b4067a85288c59a59ce8c1bfb3942e71fd85c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Dotenv\\": ""
+ },
+ "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": "Registers environment variables from a .env file",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "dotenv",
+ "env",
+ "environment"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/dotenv/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/error-handler",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/error-handler.git",
+ "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/7620b97ec0ab1d2d6c7fb737aa55da411bea776a",
+ "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "psr/log": "^1|^2|^3",
+ "symfony/polyfill-php85": "^1.32",
+ "symfony/var-dumper": "^7.4|^8.0"
+ },
+ "conflict": {
+ "symfony/deprecation-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0",
+ "symfony/webpack-encore-bundle": "^1.0|^2.0"
+ },
+ "bin": [
+ "Resources/bin/patch-type-declarations"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\ErrorHandler\\": ""
+ },
+ "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 to manage errors and ease debugging PHP code",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/error-handler/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-23T11:07:10+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "99301401da182b6cfaa4700dbe9987bb75474b47"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47",
+ "reference": "99301401da182b6cfaa4700dbe9987bb75474b47",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/event-dispatcher-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/security-http": "<7.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": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/error-handler": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/framework-bundle": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/stopwatch": "^7.4|^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/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-05T11:45:55+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/filesystem",
+ "version": "v8.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "d937d400b980523dc9ee946bb69972b5e619058d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d",
+ "reference": "d937d400b980523dc9ee946bb69972b5e619058d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.8"
+ },
+ "require-dev": {
+ "symfony/process": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides basic utilities for the filesystem",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/filesystem/tree/v8.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/finder",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
+ "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4"
+ },
+ "require-dev": {
+ "symfony/filesystem": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Finds files and directories via an intuitive fluent interface",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-26T15:08:38+00:00"
+ },
+ {
+ "name": "symfony/flex",
+ "version": "v2.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/flex.git",
+ "reference": "9cd384775973eabbf6e8b05784dda279fc67c28d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/flex/zipball/9cd384775973eabbf6e8b05784dda279fc67c28d",
+ "reference": "9cd384775973eabbf6e8b05784dda279fc67c28d",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.1",
+ "php": ">=8.1"
+ },
+ "conflict": {
+ "composer/semver": "<1.7.2",
+ "symfony/dotenv": "<5.4"
+ },
+ "require-dev": {
+ "composer/composer": "^2.1",
+ "symfony/dotenv": "^6.4|^7.4|^8.0",
+ "symfony/filesystem": "^6.4|^7.4|^8.0",
+ "symfony/phpunit-bridge": "^6.4|^7.4|^8.0",
+ "symfony/process": "^6.4|^7.4|^8.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Symfony\\Flex\\Flex"
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Flex\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien.potencier@gmail.com"
+ }
+ ],
+ "description": "Composer plugin for Symfony",
+ "support": {
+ "issues": "https://github.com/symfony/flex/issues",
+ "source": "https://github.com/symfony/flex/tree/v2.10.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-16T09:38:19+00:00"
+ },
+ {
+ "name": "symfony/framework-bundle",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/framework-bundle.git",
+ "reference": "e2f9469e7a802dd7c0d193792afc494d68177c54"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/e2f9469e7a802dd7c0d193792afc494d68177c54",
+ "reference": "e2f9469e7a802dd7c0d193792afc494d68177c54",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "ext-xml": "*",
+ "php": ">=8.4",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/config": "^7.4.4|^8.0.4",
+ "symfony/dependency-injection": "^7.4.4|^8.0.4",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/error-handler": "^7.4|^8.0",
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/filesystem": "^7.4|^8.0",
+ "symfony/finder": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/polyfill-php85": "^1.32",
+ "symfony/routing": "^7.4|^8.0"
+ },
+ "conflict": {
+ "doctrine/persistence": "<1.3",
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
+ "phpdocumentor/type-resolver": "<1.5.1",
+ "symfony/console": "<7.4",
+ "symfony/form": "<7.4",
+ "symfony/json-streamer": "<7.4",
+ "symfony/messenger": "<7.4",
+ "symfony/security-csrf": "<7.4",
+ "symfony/serializer": "<7.4",
+ "symfony/translation": "<7.4",
+ "symfony/webhook": "<7.4",
+ "symfony/workflow": "<7.4"
+ },
+ "require-dev": {
+ "doctrine/persistence": "^1.3|^2|^3",
+ "dragonmantank/cron-expression": "^3.1",
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "phpstan/phpdoc-parser": "^1.0|^2.0",
+ "seld/jsonlint": "^1.10",
+ "symfony/asset": "^7.4|^8.0",
+ "symfony/asset-mapper": "^7.4|^8.0",
+ "symfony/browser-kit": "^7.4|^8.0",
+ "symfony/clock": "^7.4|^8.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/css-selector": "^7.4|^8.0",
+ "symfony/dom-crawler": "^7.4|^8.0",
+ "symfony/dotenv": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/form": "^7.4|^8.0",
+ "symfony/html-sanitizer": "^7.4|^8.0",
+ "symfony/http-client": "^7.4|^8.0",
+ "symfony/json-streamer": "^7.4|^8.0",
+ "symfony/lock": "^7.4|^8.0",
+ "symfony/mailer": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/mime": "^7.4|^8.0",
+ "symfony/notifier": "^7.4|^8.0",
+ "symfony/object-mapper": "^7.4|^8.0",
+ "symfony/polyfill-intl-icu": "^1.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/property-info": "^7.4|^8.0",
+ "symfony/rate-limiter": "^7.4|^8.0",
+ "symfony/runtime": "^7.4|^8.0",
+ "symfony/scheduler": "^7.4|^8.0",
+ "symfony/security-bundle": "^7.4|^8.0",
+ "symfony/semaphore": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0",
+ "symfony/string": "^7.4|^8.0",
+ "symfony/translation": "^7.4|^8.0",
+ "symfony/twig-bundle": "^7.4|^8.0",
+ "symfony/type-info": "^7.4.1|^8.0.1",
+ "symfony/uid": "^7.4|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/web-link": "^7.4|^8.0",
+ "symfony/webhook": "^7.4|^8.0",
+ "symfony/workflow": "^7.4|^8.0",
+ "symfony/yaml": "^7.4|^8.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\FrameworkBundle\\": ""
+ },
+ "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 tight integration between Symfony components and the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/framework-bundle/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T09:06:10+00:00"
+ },
+ {
+ "name": "symfony/http-foundation",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-foundation.git",
+ "reference": "e3422806e6f6760dbed0ddbc0a7fbfb6b5ce96bb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e3422806e6f6760dbed0ddbc0a7fbfb6b5ce96bb",
+ "reference": "e3422806e6f6760dbed0ddbc0a7fbfb6b5ce96bb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/polyfill-mbstring": "^1.1"
+ },
+ "conflict": {
+ "doctrine/dbal": "<4.3"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^4.3",
+ "predis/predis": "^1.1|^2.0",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/clock": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/mime": "^7.4|^8.0",
+ "symfony/rate-limiter": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpFoundation\\": ""
+ },
+ "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": "Defines an object-oriented layer for the HTTP specification",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-foundation/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T16:18:07+00:00"
+ },
+ {
+ "name": "symfony/http-kernel",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-kernel.git",
+ "reference": "20c1c5e41fc53928dbb670088f544f2d460d497d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/20c1c5e41fc53928dbb670088f544f2d460d497d",
+ "reference": "20c1c5e41fc53928dbb670088f544f2d460d497d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "psr/log": "^1|^2|^3",
+ "symfony/error-handler": "^7.4|^8.0",
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/flex": "<2.10",
+ "symfony/http-client-contracts": "<2.5",
+ "symfony/translation-contracts": "<2.5",
+ "twig/twig": "<3.21"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "symfony/browser-kit": "^7.4|^8.0",
+ "symfony/clock": "^7.4|^8.0",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/css-selector": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/dom-crawler": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/finder": "^7.4|^8.0",
+ "symfony/http-client-contracts": "^2.5|^3",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/property-access": "^7.4|^8.0",
+ "symfony/routing": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0",
+ "symfony/translation": "^7.4|^8.0",
+ "symfony/translation-contracts": "^2.5|^3",
+ "symfony/uid": "^7.4|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^8.0",
+ "symfony/var-exporter": "^7.4|^8.0",
+ "twig/twig": "^3.21"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpKernel\\": ""
+ },
+ "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 structured process for converting a Request into a Response",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-kernel/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-28T10:46:31+00:00"
+ },
+ {
+ "name": "symfony/messenger",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/messenger.git",
+ "reference": "3483db96bcc33310cd1807d2b962e7e01d9f41c2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/messenger/zipball/3483db96bcc33310cd1807d2b962e7e01d9f41c2",
+ "reference": "3483db96bcc33310cd1807d2b962e7e01d9f41c2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "psr/log": "^1|^2|^3",
+ "symfony/clock": "^7.4|^8.0"
+ },
+ "conflict": {
+ "symfony/console": "<7.4",
+ "symfony/event-dispatcher-contracts": "<2.5",
+ "symfony/lock": "<7.4",
+ "symfony/serializer": "<7.4.4|>=8.0,<8.0.4"
+ },
+ "require-dev": {
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/lock": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/property-access": "^7.4|^8.0",
+ "symfony/rate-limiter": "^7.4|^8.0",
+ "symfony/routing": "^7.4|^8.0",
+ "symfony/serializer": "^7.4.4|^8.0.4",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/stopwatch": "^7.4|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Messenger\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Samuel Roze",
+ "email": "samuel.roze@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Helps applications send and receive messages to/from other applications or via message queues",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/messenger/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-08T22:36:47+00:00"
+ },
+ {
+ "name": "symfony/monolog-bridge",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/monolog-bridge.git",
+ "reference": "7c3da570ec252d5ca1212945ddbbf1dac4a0d779"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/7c3da570ec252d5ca1212945ddbbf1dac4a0d779",
+ "reference": "7c3da570ec252d5ca1212945ddbbf1dac4a0d779",
+ "shasum": ""
+ },
+ "require": {
+ "monolog/monolog": "^3",
+ "php": ">=8.4",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0",
+ "symfony/http-client": "^7.4|^8.0",
+ "symfony/mailer": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/mime": "^7.4|^8.0",
+ "symfony/security-core": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^8.0"
+ },
+ "type": "symfony-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\Monolog\\": ""
+ },
+ "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 integration for Monolog with various Symfony components",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/monolog-bridge/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-07T12:23:22+00:00"
+ },
+ {
+ "name": "symfony/monolog-bundle",
+ "version": "v4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/monolog-bundle.git",
+ "reference": "3b4ee2717ee56c5e1edb516140a175eb2a72bc66"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/3b4ee2717ee56c5e1edb516140a175eb2a72bc66",
+ "reference": "3b4ee2717ee56c5e1edb516140a175eb2a72bc66",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": "^2.0",
+ "monolog/monolog": "^3.5",
+ "php": ">=8.2",
+ "symfony/config": "^7.3 || ^8.0",
+ "symfony/dependency-injection": "^7.3 || ^8.0",
+ "symfony/http-kernel": "^7.3 || ^8.0",
+ "symfony/monolog-bridge": "^7.3 || ^8.0",
+ "symfony/polyfill-php84": "^1.30"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5.41 || ^12.3",
+ "symfony/console": "^7.3 || ^8.0",
+ "symfony/yaml": "^7.3 || ^8.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\MonologBundle\\": "src"
+ }
+ },
+ "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": "Symfony MonologBundle",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "log",
+ "logging"
+ ],
+ "support": {
+ "issues": "https://github.com/symfony/monolog-bundle/issues",
+ "source": "https://github.com/symfony/monolog-bundle/tree/v4.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-08T08:00:13+00:00"
+ },
+ {
+ "name": "symfony/password-hasher",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/password-hasher.git",
+ "reference": "ca6af4e20357d58d50c818d676cf2e2dd5e53b02"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/password-hasher/zipball/ca6af4e20357d58d50c818d676cf2e2dd5e53b02",
+ "reference": "ca6af4e20357d58d50c818d676cf2e2dd5e53b02",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0",
+ "symfony/security-core": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\PasswordHasher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Robin Chalas",
+ "email": "robin.chalas@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides password hashing utilities",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "hashing",
+ "password"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/password-hasher/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-01T23:07:29+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-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/polyfill-php85",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php85.git",
+ "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
+ "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php85\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php85/tree/v1.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-23T16:12:55+00:00"
+ },
+ {
+ "name": "symfony/polyfill-uuid",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-uuid.git",
+ "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
+ "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-uuid": "*"
+ },
+ "suggest": {
+ "ext-uuid": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Uuid\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for uuid functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-uuid/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/property-access",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/property-access.git",
+ "reference": "a35a5ec85b605d0d1a9fd802cb44d87682c746fd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/property-access/zipball/a35a5ec85b605d0d1a9fd802cb44d87682c746fd",
+ "reference": "a35a5ec85b605d0d1a9fd802cb44d87682c746fd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/property-info": "^7.4.4|^8.0.4"
+ },
+ "require-dev": {
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/var-exporter": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\PropertyAccess\\": ""
+ },
+ "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 functions to read and write from/to an object or array using a simple string notation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "access",
+ "array",
+ "extraction",
+ "index",
+ "injection",
+ "object",
+ "property",
+ "property-path",
+ "reflection"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/property-access/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-05T09:27:50+00:00"
+ },
+ {
+ "name": "symfony/property-info",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/property-info.git",
+ "reference": "9d987224b54758240e80a062c5e414431bbf84de"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/property-info/zipball/9d987224b54758240e80a062c5e414431bbf84de",
+ "reference": "9d987224b54758240e80a062c5e414431bbf84de",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/string": "^7.4|^8.0",
+ "symfony/type-info": "^7.4.4|^8.0.4"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
+ "phpdocumentor/type-resolver": "<1.5.1"
+ },
+ "require-dev": {
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "phpstan/phpdoc-parser": "^1.0|^2.0",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\PropertyInfo\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "dunglas@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Extracts information about PHP class' properties using metadata of popular sources",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "doctrine",
+ "phpdoc",
+ "property",
+ "symfony",
+ "type",
+ "validator"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/property-info/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T16:18:07+00:00"
+ },
+ {
+ "name": "symfony/routing",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/routing.git",
+ "reference": "4a2bc08d1c35307239329f434d45c2bfe8241fa9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/4a2bc08d1c35307239329f434d45c2bfe8241fa9",
+ "reference": "4a2bc08d1c35307239329f434d45c2bfe8241fa9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/yaml": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Routing\\": ""
+ },
+ "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": "Maps an HTTP request to a set of configuration variables",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "router",
+ "routing",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/routing/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-12T12:37:40+00:00"
+ },
+ {
+ "name": "symfony/runtime",
+ "version": "v8.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/runtime.git",
+ "reference": "73b34037b23db051048ba2873031ddb89be9f19d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/runtime/zipball/73b34037b23db051048ba2873031ddb89be9f19d",
+ "reference": "73b34037b23db051048ba2873031ddb89be9f19d",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0|^2.0",
+ "php": ">=8.4"
+ },
+ "conflict": {
+ "symfony/error-handler": "<7.4"
+ },
+ "require-dev": {
+ "composer/composer": "^2.6",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/dotenv": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Runtime\\": "",
+ "Symfony\\Runtime\\Symfony\\Component\\": "Internal/"
+ },
+ "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": "Enables decoupling PHP applications from global state",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "runtime"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/runtime/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/security-bundle",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/security-bundle.git",
+ "reference": "c170650a00ba724be3455852747af600a2f042b4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/security-bundle/zipball/c170650a00ba724be3455852747af600a2f042b4",
+ "reference": "c170650a00ba724be3455852747af600a2f042b4",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "ext-xml": "*",
+ "php": ">=8.4",
+ "symfony/clock": "^7.4|^8.0",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/password-hasher": "^7.4|^8.0",
+ "symfony/security-core": "^7.4|^8.0",
+ "symfony/security-csrf": "^7.4|^8.0",
+ "symfony/security-http": "^7.4|^8.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "require-dev": {
+ "symfony/asset": "^7.4|^8.0",
+ "symfony/browser-kit": "^7.4|^8.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/css-selector": "^7.4|^8.0",
+ "symfony/dom-crawler": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/form": "^7.4|^8.0",
+ "symfony/framework-bundle": "^7.4|^8.0",
+ "symfony/http-client": "^7.4|^8.0",
+ "symfony/ldap": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/rate-limiter": "^7.4|^8.0",
+ "symfony/runtime": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0",
+ "symfony/translation": "^7.4|^8.0",
+ "symfony/twig-bridge": "^7.4|^8.0",
+ "symfony/twig-bundle": "^7.4|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/yaml": "^7.4|^8.0",
+ "web-token/jwt-library": "^3.3.2|^4.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\SecurityBundle\\": ""
+ },
+ "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 tight integration of the Security component into the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/security-bundle/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-10T13:58:55+00:00"
+ },
+ {
+ "name": "symfony/security-core",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/security-core.git",
+ "reference": "c62565de41a136535ffa79a4db0373a7173b4d02"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/security-core/zipball/c62565de41a136535ffa79a4db0373a7173b4d02",
+ "reference": "c62565de41a136535ffa79a4db0373a7173b4d02",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/event-dispatcher-contracts": "^2.5|^3",
+ "symfony/password-hasher": "^7.4|^8.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "require-dev": {
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "psr/container": "^1.1|^2.0",
+ "psr/log": "^1|^2|^3",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/ldap": "^7.4|^8.0",
+ "symfony/string": "^7.4|^8.0",
+ "symfony/translation": "^7.4|^8.0",
+ "symfony/validator": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Security\\Core\\": ""
+ },
+ "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": "Symfony Security Component - Core Library",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/security-core/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-23T11:07:10+00:00"
+ },
+ {
+ "name": "symfony/security-csrf",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/security-csrf.git",
+ "reference": "8be8bc615044c5911e6d15a5b0a80132068170c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/security-csrf/zipball/8be8bc615044c5911e6d15a5b0a80132068170c5",
+ "reference": "8be8bc615044c5911e6d15a5b0a80132068170c5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/security-core": "^7.4|^8.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Security\\Csrf\\": ""
+ },
+ "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": "Symfony Security Component - CSRF Library",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/security-csrf/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-23T11:07:10+00:00"
+ },
+ {
+ "name": "symfony/security-http",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/security-http.git",
+ "reference": "02f37c050db6e997052916194086d1a0a8790b8f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/security-http/zipball/02f37c050db6e997052916194086d1a0a8790b8f",
+ "reference": "02f37c050db6e997052916194086d1a0a8790b8f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/property-access": "^7.4|^8.0",
+ "symfony/security-core": "^7.4|^8.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/http-client-contracts": "<3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/clock": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/http-client": "^7.4|^8.0",
+ "symfony/http-client-contracts": "^3.0",
+ "symfony/rate-limiter": "^7.4|^8.0",
+ "symfony/routing": "^7.4|^8.0",
+ "symfony/security-csrf": "^7.4|^8.0",
+ "symfony/translation": "^7.4|^8.0",
+ "web-token/jwt-library": "^3.3.2|^4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Security\\Http\\": ""
+ },
+ "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": "Symfony Security Component - HTTP Integration",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/security-http/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-23T11:07:10+00:00"
+ },
+ {
+ "name": "symfony/serializer",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/serializer.git",
+ "reference": "867a38a1927d23a503f7248aa182032c6ea42702"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/serializer/zipball/867a38a1927d23a503f7248aa182032c6ea42702",
+ "reference": "867a38a1927d23a503f7248aa182032c6ea42702",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
+ "phpdocumentor/type-resolver": "<1.5.1",
+ "symfony/property-info": "<7.3"
+ },
+ "require-dev": {
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "phpstan/phpdoc-parser": "^1.0|^2.0",
+ "seld/jsonlint": "^1.10",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/error-handler": "^7.4|^8.0",
+ "symfony/filesystem": "^7.4|^8.0",
+ "symfony/form": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/mime": "^7.4|^8.0",
+ "symfony/property-access": "^7.4|^8.0",
+ "symfony/property-info": "^7.4|^8.0",
+ "symfony/translation-contracts": "^2.5|^3",
+ "symfony/type-info": "^7.4|^8.0",
+ "symfony/uid": "^7.4|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^8.0",
+ "symfony/var-exporter": "^7.4|^8.0",
+ "symfony/yaml": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Serializer\\": ""
+ },
+ "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": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/serializer/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T09:06:43+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/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/string",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "758b372d6882506821ed666032e43020c4f57194"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
+ "reference": "758b372d6882506821ed666032e43020c4f57194",
+ "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.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-12T12:37:40+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v3.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "65a8bc82080447fae78373aa10f8d13b38338977"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977",
+ "reference": "65a8bc82080447fae78373aa10f8d13b38338977",
+ "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": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ },
+ "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 translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/translation-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-15T13:41:35+00:00"
+ },
+ {
+ "name": "symfony/twig-bridge",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/twig-bridge.git",
+ "reference": "3e60c35cb47b1077524c066ec277eaf92cdc2393"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/3e60c35cb47b1077524c066ec277eaf92cdc2393",
+ "reference": "3e60c35cb47b1077524c066ec277eaf92cdc2393",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/translation-contracts": "^2.5|^3",
+ "twig/twig": "^3.21"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
+ "phpdocumentor/type-resolver": "<1.5.1",
+ "symfony/form": "<7.4.4|>8.0,<8.0.4"
+ },
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10|^3|^4",
+ "league/html-to-markdown": "^5.0",
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "symfony/asset": "^7.4|^8.0",
+ "symfony/asset-mapper": "^7.4|^8.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/emoji": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/finder": "^7.4|^8.0",
+ "symfony/form": "^7.4.4|^8.0.4",
+ "symfony/html-sanitizer": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/intl": "^7.4|^8.0",
+ "symfony/mime": "^7.4|^8.0",
+ "symfony/polyfill-intl-icu": "^1.0",
+ "symfony/property-info": "^7.4|^8.0",
+ "symfony/routing": "^7.4|^8.0",
+ "symfony/security-acl": "^2.8|^3.0",
+ "symfony/security-core": "^7.4|^8.0",
+ "symfony/security-csrf": "^7.4|^8.0",
+ "symfony/security-http": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0",
+ "symfony/translation": "^7.4|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/web-link": "^7.4|^8.0",
+ "symfony/workflow": "^7.4|^8.0",
+ "symfony/yaml": "^7.4|^8.0",
+ "twig/cssinliner-extra": "^3",
+ "twig/inky-extra": "^3",
+ "twig/markdown-extra": "^3"
+ },
+ "type": "symfony-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\Twig\\": ""
+ },
+ "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 integration for Twig with various Symfony components",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/twig-bridge/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T09:06:10+00:00"
+ },
+ {
+ "name": "symfony/twig-bundle",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/twig-bundle.git",
+ "reference": "5a68f2e0e06996514bf04900c3982b93b42487af"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/5a68f2e0e06996514bf04900c3982b93b42487af",
+ "reference": "5a68f2e0e06996514bf04900c3982b93b42487af",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "php": ">=8.4",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/twig-bridge": "^7.4|^8.0"
+ },
+ "require-dev": {
+ "symfony/asset": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/finder": "^7.4|^8.0",
+ "symfony/form": "^7.4|^8.0",
+ "symfony/framework-bundle": "^7.4|^8.0",
+ "symfony/routing": "^7.4|^8.0",
+ "symfony/runtime": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0",
+ "symfony/translation": "^7.4|^8.0",
+ "symfony/web-link": "^7.4|^8.0",
+ "symfony/yaml": "^7.4|^8.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\TwigBundle\\": ""
+ },
+ "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 tight integration of Twig into the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/twig-bundle/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-06T12:43:21+00:00"
+ },
+ {
+ "name": "symfony/type-info",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/type-info.git",
+ "reference": "106a2d3bbf0d4576b2f70e6ca866fa420956ed0d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/type-info/zipball/106a2d3bbf0d4576b2f70e6ca866fa420956ed0d",
+ "reference": "106a2d3bbf0d4576b2f70e6ca866fa420956ed0d",
+ "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.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-09T12:15:10+00:00"
+ },
+ {
+ "name": "symfony/uid",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/uid.git",
+ "reference": "8b81bd3700f5c1913c22a3266a647aa1bb974435"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/8b81bd3700f5c1913c22a3266a647aa1bb974435",
+ "reference": "8b81bd3700f5c1913c22a3266a647aa1bb974435",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/polyfill-uuid": "^1.15"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Uid\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to generate and represent UIDs",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "UID",
+ "ulid",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/uid/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-03T23:40:55+00:00"
+ },
+ {
+ "name": "symfony/validator",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/validator.git",
+ "reference": "ba171e89ee2d01c24c1d8201d59ec595ef4adba1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/validator/zipball/ba171e89ee2d01c24c1d8201d59ec595ef4adba1",
+ "reference": "ba171e89ee2d01c24c1d8201d59ec595ef4adba1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/translation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "doctrine/lexer": "<1.1",
+ "symfony/doctrine-bridge": "<7.4"
+ },
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10|^3|^4",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/finder": "^7.4|^8.0",
+ "symfony/http-client": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/intl": "^7.4|^8.0",
+ "symfony/mime": "^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/string": "^7.4|^8.0",
+ "symfony/translation": "^7.4|^8.0",
+ "symfony/type-info": "^7.4|^8.0",
+ "symfony/yaml": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Validator\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/",
+ "/Resources/bin/"
+ ]
+ },
+ "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 to validate values",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/validator/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T09:06:10+00:00"
+ },
+ {
+ "name": "symfony/var-dumper",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-dumper.git",
+ "reference": "326e0406fc315eca57ef5740fa4a280b7a068c82"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/326e0406fc315eca57ef5740fa4a280b7a068c82",
+ "reference": "326e0406fc315eca57ef5740fa4a280b7a068c82",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "conflict": {
+ "symfony/console": "<7.4",
+ "symfony/error-handler": "<7.4"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/uid": "^7.4|^8.0",
+ "twig/twig": "^3.12"
+ },
+ "bin": [
+ "Resources/bin/var-dump-server"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions/dump.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\VarDumper\\": ""
+ },
+ "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 mechanisms for walking through any arbitrary PHP variable",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "debug",
+ "dump"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-dumper/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-01T23:07:29+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": "symfony/web-link",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/web-link.git",
+ "reference": "0f79e9e89c4a8ecf4964cac25a12e152bcb23a99"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/web-link/zipball/0f79e9e89c4a8ecf4964cac25a12e152bcb23a99",
+ "reference": "0f79e9e89c4a8ecf4964cac25a12e152bcb23a99",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "psr/link": "^1.1|^2.0"
+ },
+ "provide": {
+ "psr/link-implementation": "1.0|2.0"
+ },
+ "require-dev": {
+ "symfony/http-kernel": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\WebLink\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "dunglas@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Manages links between resources",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "dns-prefetch",
+ "http",
+ "http2",
+ "link",
+ "performance",
+ "prefetch",
+ "preload",
+ "prerender",
+ "psr13",
+ "push"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/web-link/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-01T23:07:29+00:00"
+ },
+ {
+ "name": "symfony/yaml",
+ "version": "v8.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14",
+ "reference": "7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/console": "<7.4"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^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/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-04T18:17:06+00:00"
+ },
+ {
+ "name": "twig/twig",
+ "version": "v3.23.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/Twig.git",
+ "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
+ "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.3"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.0",
+ "psr/container": "^1.0|^2.0",
+ "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/Resources/core.php",
+ "src/Resources/debug.php",
+ "src/Resources/escaper.php",
+ "src/Resources/string_loader.php"
+ ],
+ "psr-4": {
+ "Twig\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Twig Team",
+ "role": "Contributors"
+ },
+ {
+ "name": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com",
+ "role": "Project Founder"
+ }
+ ],
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "templating"
+ ],
+ "support": {
+ "issues": "https://github.com/twigphp/Twig/issues",
+ "source": "https://github.com/twigphp/Twig/tree/v3.23.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-23T21:00:41+00:00"
+ },
+ {
+ "name": "willdurand/negotiation",
+ "version": "3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/willdurand/Negotiation.git",
+ "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/68e9ea0553ef6e2ee8db5c1d98829f111e623ec2",
+ "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.0"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Negotiation\\": "src/Negotiation"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "William Durand",
+ "email": "will+git@drnd.me"
+ }
+ ],
+ "description": "Content Negotiation tools for PHP provided as a standalone library.",
+ "homepage": "http://williamdurand.fr/Negotiation/",
+ "keywords": [
+ "accept",
+ "content",
+ "format",
+ "header",
+ "negotiation"
+ ],
+ "support": {
+ "issues": "https://github.com/willdurand/Negotiation/issues",
+ "source": "https://github.com/willdurand/Negotiation/tree/3.1.0"
+ },
+ "time": "2022-01-30T20:08:53+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "clue/ndjson-react",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/clue/reactphp-ndjson.git",
+ "reference": "392dc165fce93b5bb5c637b67e59619223c931b0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0",
+ "reference": "392dc165fce93b5bb5c637b67e59619223c931b0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3",
+ "react/stream": "^1.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35",
+ "react/event-loop": "^1.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Clue\\React\\NDJson\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering"
+ }
+ ],
+ "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.",
+ "homepage": "https://github.com/clue/reactphp-ndjson",
+ "keywords": [
+ "NDJSON",
+ "json",
+ "jsonlines",
+ "newline",
+ "reactphp",
+ "streaming"
+ ],
+ "support": {
+ "issues": "https://github.com/clue/reactphp-ndjson/issues",
+ "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://clue.engineering/support",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/clue",
+ "type": "github"
+ }
+ ],
+ "time": "2022-12-23T10:58:28+00:00"
+ },
+ {
+ "name": "composer/pcre",
+ "version": "3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<1.11.10"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-strict-rules": "^1 || ^2",
+ "phpunit/phpunit": "^8 || ^9"
+ },
+ "type": "library",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-12T16:29:46+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "3.4.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.11",
+ "symfony/phpunit-bridge": "^3 || ^7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/semver/issues",
+ "source": "https://github.com/composer/semver/tree/3.4.4"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ }
+ ],
+ "time": "2025-08-20T19:15:30+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "3.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1 || ^2 || ^3",
+ "php": "^7.2.5 || ^8.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/3.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-06T16:37:16+00:00"
+ },
+ {
+ "name": "doctrine/data-fixtures",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/data-fixtures.git",
+ "reference": "7a615ba135e45d67674bb623d90f34f6c7b6bd97"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/7a615ba135e45d67674bb623d90f34f6c7b6bd97",
+ "reference": "7a615ba135e45d67674bb623d90f34f6c7b6bd97",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/persistence": "^3.1 || ^4.0",
+ "php": "^8.1",
+ "psr/log": "^1.1 || ^2 || ^3"
+ },
+ "conflict": {
+ "doctrine/dbal": "<3.5 || >=5",
+ "doctrine/orm": "<2.14 || >=4",
+ "doctrine/phpcr-odm": "<1.3.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "doctrine/dbal": "^3.5 || ^4",
+ "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0",
+ "doctrine/orm": "^2.14 || ^3",
+ "ext-sqlite3": "*",
+ "fig/log-test": "^1",
+ "phpstan/phpstan": "2.1.31",
+ "phpunit/phpunit": "10.5.45 || 12.4.0",
+ "symfony/cache": "^6.4 || ^7",
+ "symfony/var-exporter": "^6.4 || ^7"
+ },
+ "suggest": {
+ "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)",
+ "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures",
+ "doctrine/orm": "For loading ORM fixtures",
+ "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\DataFixtures\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ }
+ ],
+ "description": "Data Fixtures for all Doctrine Object Managers",
+ "homepage": "https://www.doctrine-project.org",
+ "keywords": [
+ "database"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/data-fixtures/issues",
+ "source": "https://github.com/doctrine/data-fixtures/tree/2.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-10-17T20:06:20+00:00"
+ },
+ {
+ "name": "doctrine/doctrine-fixtures-bundle",
+ "version": "4.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/DoctrineFixturesBundle.git",
+ "reference": "9e013ed10d49bf7746b07204d336384a7d9b5a4d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/9e013ed10d49bf7746b07204d336384a7d9b5a4d",
+ "reference": "9e013ed10d49bf7746b07204d336384a7d9b5a4d",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/data-fixtures": "^2.2",
+ "doctrine/doctrine-bundle": "^2.2 || ^3.0",
+ "doctrine/orm": "^2.14.0 || ^3.0",
+ "doctrine/persistence": "^2.4 || ^3.0 || ^4.0",
+ "php": "^8.1",
+ "psr/log": "^2 || ^3",
+ "symfony/config": "^6.4 || ^7.0 || ^8.0",
+ "symfony/console": "^6.4 || ^7.0 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0",
+ "symfony/deprecation-contracts": "^2.1 || ^3",
+ "symfony/doctrine-bridge": "^6.4.16 || ^7.1.9 || ^8.0",
+ "symfony/http-kernel": "^6.4 || ^7.0 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/dbal": "< 3"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "14.0.0",
+ "phpstan/phpstan": "2.1.11",
+ "phpunit/phpunit": "^10.5.38 || 11.4.14"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Bundle\\FixturesBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Doctrine Project",
+ "homepage": "https://www.doctrine-project.org"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony DoctrineFixturesBundle",
+ "homepage": "https://www.doctrine-project.org",
+ "keywords": [
+ "Fixture",
+ "persistence"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues",
+ "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/4.3.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%2Fdoctrine-fixtures-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-03T16:05:42+00:00"
+ },
+ {
+ "name": "evenement/evenement",
+ "version": "v3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/igorw/evenement.git",
+ "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc",
+ "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9 || ^6"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Evenement\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Igor Wiedler",
+ "email": "igor@wiedler.ch"
+ }
+ ],
+ "description": "Événement is a very simple event dispatching library for PHP",
+ "keywords": [
+ "event-dispatcher",
+ "event-emitter"
+ ],
+ "support": {
+ "issues": "https://github.com/igorw/evenement/issues",
+ "source": "https://github.com/igorw/evenement/tree/v3.0.2"
+ },
+ "time": "2023-08-08T05:53:35+00:00"
+ },
+ {
+ "name": "fidry/cpu-core-counter",
+ "version": "1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theofidry/cpu-core-counter.git",
+ "reference": "db9508f7b1474469d9d3c53b86f817e344732678"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678",
+ "reference": "db9508f7b1474469d9d3c53b86f817e344732678",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "fidry/makefile": "^0.2.0",
+ "fidry/php-cs-fixer-config": "^1.1.2",
+ "phpstan/extension-installer": "^1.2.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-deprecation-rules": "^2.0.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^8.5.31 || ^9.5.26",
+ "webmozarts/strict-phpunit": "^7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Fidry\\CpuCoreCounter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Théo FIDRY",
+ "email": "theo.fidry@gmail.com"
+ }
+ ],
+ "description": "Tiny utility to get the number of CPU cores.",
+ "keywords": [
+ "CPU",
+ "core"
+ ],
+ "support": {
+ "issues": "https://github.com/theofidry/cpu-core-counter/issues",
+ "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theofidry",
+ "type": "github"
+ }
+ ],
+ "time": "2025-08-14T07:29:31+00:00"
+ },
+ {
+ "name": "friendsofphp/php-cs-fixer",
+ "version": "v3.93.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
+ "reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b3546ab487c0762c39f308dc1ec0ea2c461fc21a",
+ "reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a",
+ "shasum": ""
+ },
+ "require": {
+ "clue/ndjson-react": "^1.3",
+ "composer/semver": "^3.4",
+ "composer/xdebug-handler": "^3.0.5",
+ "ext-filter": "*",
+ "ext-hash": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "fidry/cpu-core-counter": "^1.3",
+ "php": "^7.4 || ^8.0",
+ "react/child-process": "^0.6.6",
+ "react/event-loop": "^1.5",
+ "react/socket": "^1.16",
+ "react/stream": "^1.4",
+ "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0",
+ "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0",
+ "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
+ "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
+ "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
+ "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0",
+ "symfony/polyfill-mbstring": "^1.33",
+ "symfony/polyfill-php80": "^1.33",
+ "symfony/polyfill-php81": "^1.33",
+ "symfony/polyfill-php84": "^1.33",
+ "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0",
+ "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "facile-it/paraunit": "^1.3.1 || ^2.7",
+ "infection/infection": "^0.32",
+ "justinrainbow/json-schema": "^6.6",
+ "keradus/cli-executor": "^2.3",
+ "mikey179/vfsstream": "^1.6.12",
+ "php-coveralls/php-coveralls": "^2.9",
+ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
+ "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
+ "phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.48",
+ "symfony/polyfill-php85": "^1.33",
+ "symfony/var-dumper": "^5.4.48 || ^6.4.26 || ^7.4.0 || ^8.0",
+ "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0"
+ },
+ "suggest": {
+ "ext-dom": "For handling output formats in XML",
+ "ext-mbstring": "For handling non-UTF8 characters."
+ },
+ "bin": [
+ "php-cs-fixer"
+ ],
+ "type": "application",
+ "autoload": {
+ "psr-4": {
+ "PhpCsFixer\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "src/**/Internal/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Dariusz Rumiński",
+ "email": "dariusz.ruminski@gmail.com"
+ }
+ ],
+ "description": "A tool to automatically fix PHP code style",
+ "keywords": [
+ "Static code analysis",
+ "fixer",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
+ "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/keradus",
+ "type": "github"
+ }
+ ],
+ "time": "2026-01-28T23:50:50+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": "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": "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": "phpat/phpat",
+ "version": "0.12.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/carlosas/phpat.git",
+ "reference": "fe9caef4f8633a57c1d19643d37b58050b11806c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/carlosas/phpat/zipball/fe9caef4f8633a57c1d19643d37b58050b11806c",
+ "reference": "fe9caef4f8633a57c1d19643d37b58050b11806c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1",
+ "phpstan/phpstan": "^2.1"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.87",
+ "kubawerlos/php-cs-fixer-custom-fixers": "3.32.*",
+ "phpunit/phpunit": "^10.5",
+ "vimeo/psalm": "^6.13"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "files": [
+ "helpers.php"
+ ],
+ "psr-4": {
+ "PHPat\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Carlos Alandete Sastre",
+ "email": "carlos.alandete@gmail.com"
+ }
+ ],
+ "description": "PHP Architecture Tester",
+ "support": {
+ "issues": "https://github.com/carlosas/phpat/issues",
+ "source": "https://github.com/carlosas/phpat/tree/0.12.2"
+ },
+ "time": "2026-01-27T11:41:37+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "2.1.37",
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/28cd424c5ea984128c95cfa7ea658808e8954e49",
+ "reference": "28cd424c5ea984128c95cfa7ea658808e8954e49",
+ "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": "2026-01-24T08:21:55+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-doctrine",
+ "version": "2.0.14",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-doctrine.git",
+ "reference": "70cd3e82fef49171163ff682a89cfe793d88581c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/70cd3e82fef49171163ff682a89cfe793d88581c",
+ "reference": "70cd3e82fef49171163ff682a89cfe793d88581c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpstan": "^2.1.34"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.0",
+ "doctrine/common": "<2.7",
+ "doctrine/mongodb-odm": "<1.2",
+ "doctrine/orm": "<2.5",
+ "doctrine/persistence": "<1.3"
+ },
+ "require-dev": {
+ "cache/array-adapter": "^1.1",
+ "composer/semver": "^3.3.2",
+ "cweagans/composer-patches": "^1.7.3",
+ "doctrine/annotations": "^2.0",
+ "doctrine/collections": "^1.6 || ^2.1",
+ "doctrine/common": "^2.7 || ^3.0",
+ "doctrine/dbal": "^3.3.8",
+ "doctrine/lexer": "^2.0 || ^3.0",
+ "doctrine/mongodb-odm": "^2.4.3",
+ "doctrine/orm": "^2.16.0",
+ "doctrine/persistence": "^2.2.1 || ^3.2",
+ "gedmo/doctrine-extensions": "^3.8",
+ "nesbot/carbon": "^2.49",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-deprecation-rules": "^2.0.2",
+ "phpstan/phpstan-phpunit": "^2.0.8",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6.20",
+ "ramsey/uuid": "^4.2",
+ "symfony/cache": "^5.4",
+ "symfony/uid": "^5.4 || ^6.4 || ^7.3"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon",
+ "rules.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Doctrine extensions for PHPStan",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-doctrine/issues",
+ "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.14"
+ },
+ "time": "2026-01-25T14:56:09+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-symfony",
+ "version": "2.0.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-symfony.git",
+ "reference": "a46dd92eaf15146cd932d897a272e59cd4108ce2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/a46dd92eaf15146cd932d897a272e59cd4108ce2",
+ "reference": "a46dd92eaf15146cd932d897a272e59cd4108ce2",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpstan": "^2.1.13"
+ },
+ "conflict": {
+ "symfony/framework-bundle": "<3.0"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-phpunit": "^2.0.8",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "psr/container": "1.1.2",
+ "symfony/config": "^5.4 || ^6.1",
+ "symfony/console": "^5.4 || ^6.1",
+ "symfony/dependency-injection": "^5.4 || ^6.1",
+ "symfony/form": "^5.4 || ^6.1",
+ "symfony/framework-bundle": "^5.4 || ^6.1",
+ "symfony/http-foundation": "^5.4 || ^6.1",
+ "symfony/messenger": "^5.4",
+ "symfony/polyfill-php80": "^1.24",
+ "symfony/serializer": "^5.4",
+ "symfony/service-contracts": "^2.2.0"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon",
+ "rules.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Lukáš Unger",
+ "email": "looky.msc@gmail.com",
+ "homepage": "https://lookyman.net"
+ }
+ ],
+ "description": "Symfony Framework extensions and rules for PHPStan",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-symfony/issues",
+ "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.12"
+ },
+ "time": "2026-01-23T09:04:33+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "11.0.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56",
+ "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^5.7.0",
+ "php": ">=8.2",
+ "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-text-template": "^4.0.1",
+ "sebastian/code-unit-reverse-lookup": "^4.0.1",
+ "sebastian/complexity": "^4.0.1",
+ "sebastian/environment": "^7.2.1",
+ "sebastian/lines-of-code": "^3.0.1",
+ "sebastian/version": "^5.0.2",
+ "theseer/tokenizer": "^1.3.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5.46"
+ },
+ "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": "11.0.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/11.0.12"
+ },
+ "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/phpunit/php-code-coverage",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-24T07:01:01+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "5.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6",
+ "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.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",
+ "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/5.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-27T05:02:59+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "5.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2",
+ "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^11.0"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "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",
+ "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",
+ "security": "https://github.com/sebastianbergmann/php-invoker/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:07:44+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964",
+ "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.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": "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/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:08:43+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "7.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3",
+ "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.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",
+ "security": "https://github.com/sebastianbergmann/php-timer/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:09:35+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "11.5.50",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3",
+ "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3",
+ "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.2",
+ "phpunit/php-code-coverage": "^11.0.12",
+ "phpunit/php-file-iterator": "^5.1.0",
+ "phpunit/php-invoker": "^5.0.1",
+ "phpunit/php-text-template": "^4.0.1",
+ "phpunit/php-timer": "^7.0.1",
+ "sebastian/cli-parser": "^3.0.2",
+ "sebastian/code-unit": "^3.0.3",
+ "sebastian/comparator": "^6.3.3",
+ "sebastian/diff": "^6.0.2",
+ "sebastian/environment": "^7.2.1",
+ "sebastian/exporter": "^6.3.2",
+ "sebastian/global-state": "^7.0.2",
+ "sebastian/object-enumerator": "^6.0.1",
+ "sebastian/type": "^5.1.3",
+ "sebastian/version": "^5.0.2",
+ "staabm/side-effects-detector": "^1.0.5"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "11.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/11.5.50"
+ },
+ "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": "2026-01-27T05:59:18+00:00"
+ },
+ {
+ "name": "react/cache",
+ "version": "v1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/cache.git",
+ "reference": "d47c472b64aa5608225f47965a484b75c7817d5b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b",
+ "reference": "d47c472b64aa5608225f47965a484b75c7817d5b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "react/promise": "^3.0 || ^2.0 || ^1.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Async, Promise-based cache interface for ReactPHP",
+ "keywords": [
+ "cache",
+ "caching",
+ "promise",
+ "reactphp"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/cache/issues",
+ "source": "https://github.com/reactphp/cache/tree/v1.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2022-11-30T15:59:55+00:00"
+ },
+ {
+ "name": "react/child-process",
+ "version": "v0.6.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/child-process.git",
+ "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3",
+ "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3",
+ "shasum": ""
+ },
+ "require": {
+ "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
+ "php": ">=5.3.0",
+ "react/event-loop": "^1.2",
+ "react/stream": "^1.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
+ "react/socket": "^1.16",
+ "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\ChildProcess\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Event-driven library for executing child processes with ReactPHP.",
+ "keywords": [
+ "event-driven",
+ "process",
+ "reactphp"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/child-process/issues",
+ "source": "https://github.com/reactphp/child-process/tree/v0.6.7"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2025-12-23T15:25:20+00:00"
+ },
+ {
+ "name": "react/dns",
+ "version": "v1.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/dns.git",
+ "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3",
+ "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "react/cache": "^1.0 || ^0.6 || ^0.5",
+ "react/event-loop": "^1.2",
+ "react/promise": "^3.2 || ^2.7 || ^1.2.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
+ "react/async": "^4.3 || ^3 || ^2",
+ "react/promise-timer": "^1.11"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Dns\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Async DNS resolver for ReactPHP",
+ "keywords": [
+ "async",
+ "dns",
+ "dns-resolver",
+ "reactphp"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/dns/issues",
+ "source": "https://github.com/reactphp/dns/tree/v1.14.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2025-11-18T19:34:28+00:00"
+ },
+ {
+ "name": "react/event-loop",
+ "version": "v1.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/event-loop.git",
+ "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a",
+ "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
+ },
+ "suggest": {
+ "ext-pcntl": "For signal handling support when using the StreamSelectLoop"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\EventLoop\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
+ "keywords": [
+ "asynchronous",
+ "event-loop"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/event-loop/issues",
+ "source": "https://github.com/reactphp/event-loop/tree/v1.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2025-11-17T20:46:25+00:00"
+ },
+ {
+ "name": "react/promise",
+ "version": "v3.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/promise.git",
+ "reference": "23444f53a813a3296c1368bb104793ce8d88f04a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a",
+ "reference": "23444f53a813a3296c1368bb104793ce8d88f04a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "1.12.28 || 1.4.10",
+ "phpunit/phpunit": "^9.6 || ^7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "React\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "A lightweight implementation of CommonJS Promises/A for PHP",
+ "keywords": [
+ "promise",
+ "promises"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/promise/issues",
+ "source": "https://github.com/reactphp/promise/tree/v3.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2025-08-19T18:57:03+00:00"
+ },
+ {
+ "name": "react/socket",
+ "version": "v1.17.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/socket.git",
+ "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08",
+ "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08",
+ "shasum": ""
+ },
+ "require": {
+ "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
+ "php": ">=5.3.0",
+ "react/dns": "^1.13",
+ "react/event-loop": "^1.2",
+ "react/promise": "^3.2 || ^2.6 || ^1.2.1",
+ "react/stream": "^1.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
+ "react/async": "^4.3 || ^3.3 || ^2",
+ "react/promise-stream": "^1.4",
+ "react/promise-timer": "^1.11"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Socket\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
+ "keywords": [
+ "Connection",
+ "Socket",
+ "async",
+ "reactphp",
+ "stream"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/socket/issues",
+ "source": "https://github.com/reactphp/socket/tree/v1.17.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2025-11-19T20:47:34+00:00"
+ },
+ {
+ "name": "react/stream",
+ "version": "v1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/stream.git",
+ "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d",
+ "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d",
+ "shasum": ""
+ },
+ "require": {
+ "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
+ "php": ">=5.3.8",
+ "react/event-loop": "^1.2"
+ },
+ "require-dev": {
+ "clue/stream-filter": "~1.2",
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Stream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP",
+ "keywords": [
+ "event-driven",
+ "io",
+ "non-blocking",
+ "pipe",
+ "reactphp",
+ "readable",
+ "stream",
+ "writable"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/stream/issues",
+ "source": "https://github.com/reactphp/stream/tree/v1.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-06-11T12:45:25+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180",
+ "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.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": "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/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:41:36+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64",
+ "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5"
+ },
+ "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": "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",
+ "security": "https://github.com/sebastianbergmann/code-unit/security/policy",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-03-19T07:56:08+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "183a9b2632194febd219bb9246eee421dad8d45e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e",
+ "reference": "183a9b2632194febd219bb9246eee421dad8d45e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.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"
+ }
+ ],
+ "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",
+ "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:45:54+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "6.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9",
+ "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "php": ">=8.2",
+ "sebastian/diff": "^6.0",
+ "sebastian/exporter": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.4"
+ },
+ "suggest": {
+ "ext-bcmath": "For comparing BcMath\\Number objects"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.3-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/6.3.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-24T09:26:40+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "ee41d384ab1906c68852636b6de493846e13e5a0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0",
+ "reference": "ee41d384ab1906c68852636b6de493846e13e5a0",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.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": "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/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:49:50+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "6.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544",
+ "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "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"
+ },
+ {
+ "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/6.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:53:05+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "7.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4",
+ "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.2-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/7.2.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/environment",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-05-21T11:55:47+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "6.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "70a298763b40b213ec087c51c739efcaa90bcd74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74",
+ "reference": "70a298763b40b213ec087c51c739efcaa90bcd74",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=8.2",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.3-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/6.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-09-24T06:12:51+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "7.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "3be331570a721f9a4b5917f4209773de17f747d7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7",
+ "reference": "3be331570a721f9a4b5917f4209773de17f747d7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "sebastian/object-reflector": "^4.0",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.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/7.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:57:36+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a",
+ "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.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": "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/3.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T04:58:38+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "6.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "f5b498e631a74204185071eb41f33f38d64608aa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa",
+ "reference": "f5b498e631a74204185071eb41f33f38d64608aa",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "sebastian/object-reflector": "^4.0",
+ "sebastian/recursion-context": "^6.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.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": "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",
+ "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:00:13+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9",
+ "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.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"
+ }
+ ],
+ "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",
+ "security": "https://github.com/sebastianbergmann/object-reflector/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-03T05:01:32+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "6.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc",
+ "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "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"
+ },
+ {
+ "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/6.0.3"
+ },
+ "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-13T04:42:22+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "5.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
+ "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.3"
+ },
+ "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",
+ "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",
+ "security": "https://github.com/sebastianbergmann/type/security/policy",
+ "source": "https://github.com/sebastianbergmann/type/tree/5.1.3"
+ },
+ "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/type",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-09T06:55:48+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "5.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874",
+ "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "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",
+ "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",
+ "security": "https://github.com/sebastianbergmann/version/security/policy",
+ "source": "https://github.com/sebastianbergmann/version/tree/5.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-09T05:16:32+00:00"
+ },
+ {
+ "name": "staabm/side-effects-detector",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/staabm/side-effects-detector.git",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^1.12.6",
+ "phpunit/phpunit": "^9.6.21",
+ "symfony/var-dumper": "^5.4.43",
+ "tomasvotruba/type-coverage": "1.0.0",
+ "tomasvotruba/unused-public": "1.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "lib/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A static analysis tool to detect side effects in PHP code",
+ "keywords": [
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/staabm/side-effects-detector/issues",
+ "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/staabm",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-20T05:08:20+00:00"
+ },
+ {
+ "name": "symfony/browser-kit",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/browser-kit.git",
+ "reference": "0d998c101e1920fc68572209d1316fec0db728ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/browser-kit/zipball/0d998c101e1920fc68572209d1316fec0db728ef",
+ "reference": "0d998c101e1920fc68572209d1316fec0db728ef",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/dom-crawler": "^7.4|^8.0"
+ },
+ "require-dev": {
+ "symfony/css-selector": "^7.4|^8.0",
+ "symfony/http-client": "^7.4|^8.0",
+ "symfony/mime": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\BrowserKit\\": ""
+ },
+ "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": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/browser-kit/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-13T13:06:50+00:00"
+ },
+ {
+ "name": "symfony/css-selector",
+ "version": "v8.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/css-selector.git",
+ "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/6225bd458c53ecdee056214cb4a2ffaf58bd592b",
+ "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\CssSelector\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Jean-François Simon",
+ "email": "jeanfrancois.simon@sensiolabs.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Converts CSS selectors to XPath expressions",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/css-selector/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-10-30T14:17:19+00:00"
+ },
+ {
+ "name": "symfony/debug-bundle",
+ "version": "v8.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/debug-bundle.git",
+ "reference": "02665efd40733372a4b4bdd78722426a6f95849b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/02665efd40733372a4b4bdd78722426a6f95849b",
+ "reference": "02665efd40733372a4b4bdd78722426a6f95849b",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "ext-xml": "*",
+ "php": ">=8.4",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/twig-bridge": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^8.0"
+ },
+ "require-dev": {
+ "symfony/web-profiler-bundle": "^7.4|^8.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\DebugBundle\\": ""
+ },
+ "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 tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/debug-bundle/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-10-24T14:30:40+00:00"
+ },
+ {
+ "name": "symfony/dom-crawler",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dom-crawler.git",
+ "reference": "fd78228fa362b41729173183493f46b1df49485f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/fd78228fa362b41729173183493f46b1df49485f",
+ "reference": "fd78228fa362b41729173183493f46b1df49485f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "require-dev": {
+ "symfony/css-selector": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DomCrawler\\": ""
+ },
+ "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 DOM navigation for HTML and XML documents",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dom-crawler/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-05T09:27:50+00:00"
+ },
+ {
+ "name": "symfony/maker-bundle",
+ "version": "v1.65.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/maker-bundle.git",
+ "reference": "eba30452d212769c9a5bcf0716959fd8ba1e54e3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/eba30452d212769c9a5bcf0716959fd8ba1e54e3",
+ "reference": "eba30452d212769c9a5bcf0716959fd8ba1e54e3",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/inflector": "^2.0",
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.1",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/deprecation-contracts": "^2.2|^3",
+ "symfony/filesystem": "^6.4|^7.0|^8.0",
+ "symfony/finder": "^6.4|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0"
+ },
+ "conflict": {
+ "doctrine/doctrine-bundle": "<2.10",
+ "doctrine/orm": "<2.15"
+ },
+ "require-dev": {
+ "composer/semver": "^3.0",
+ "doctrine/doctrine-bundle": "^2.5.0|^3.0.0",
+ "doctrine/orm": "^2.15|^3",
+ "doctrine/persistence": "^3.1|^4.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/phpunit-bridge": "^6.4.1|^7.0|^8.0",
+ "symfony/security-core": "^6.4|^7.0|^8.0",
+ "symfony/security-http": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0",
+ "twig/twig": "^3.0|^4.x-dev"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\MakerBundle\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.",
+ "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html",
+ "keywords": [
+ "code generator",
+ "dev",
+ "generator",
+ "scaffold",
+ "scaffolding"
+ ],
+ "support": {
+ "issues": "https://github.com/symfony/maker-bundle/issues",
+ "source": "https://github.com/symfony/maker-bundle/tree/v1.65.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-02T07:14:37+00:00"
+ },
+ {
+ "name": "symfony/options-resolver",
+ "version": "v8.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/options-resolver.git",
+ "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
+ "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4",
+ "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/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-12T15:55:31+00:00"
+ },
+ {
+ "name": "symfony/phpunit-bridge",
+ "version": "v8.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/phpunit-bridge.git",
+ "reference": "3c59b07980df5d4ae4a2620287016209802261ea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/3c59b07980df5d4ae4a2620287016209802261ea",
+ "reference": "3c59b07980df5d4ae4a2620287016209802261ea",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1.0"
+ },
+ "require-dev": {
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/error-handler": "^6.4.3|^7.0.3|^8.0"
+ },
+ "bin": [
+ "bin/simple-phpunit"
+ ],
+ "type": "symfony-bridge",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/sebastianbergmann/phpunit",
+ "name": "phpunit/phpunit"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Bridge\\PhpUnit\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/",
+ "/bin/"
+ ]
+ },
+ "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 utilities for PHPUnit, especially user deprecation notices management",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "testing"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/phpunit-bridge/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-10T13:10:54+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v8.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
+ "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Executes commands in sub-processes",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/process/tree/v8.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-26T15:08:38+00:00"
+ },
+ {
+ "name": "symfony/web-profiler-bundle",
+ "version": "v8.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/web-profiler-bundle.git",
+ "reference": "0d0df8b3601f80b455d0bf40402d104c02d8b6fa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/0d0df8b3601f80b455d0bf40402d104c02d8b6fa",
+ "reference": "0d0df8b3601f80b455d0bf40402d104c02d8b6fa",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "php": ">=8.4",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/framework-bundle": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/routing": "^7.4|^8.0",
+ "symfony/twig-bundle": "^7.4|^8.0"
+ },
+ "conflict": {
+ "symfony/serializer": "<7.4",
+ "symfony/workflow": "<7.4"
+ },
+ "require-dev": {
+ "symfony/browser-kit": "^7.4|^8.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/css-selector": "^7.4|^8.0",
+ "symfony/runtime": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\WebProfilerBundle\\": ""
+ },
+ "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 development tool that gives detailed information about the execution of any request",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "dev"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/web-profiler-bundle/tree/v8.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-07T12:23:22+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": "dev",
+ "stability-flags": {
+ "doctrine/doctrine-bundle": 20
+ },
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=8.5",
+ "ext-ctype": "*",
+ "ext-iconv": "*",
+ "ext-intl": "*"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.9.0"
+}
diff --git a/backend/config/bundles.php b/backend/config/bundles.php
new file mode 100644
index 0000000..de7982b
--- /dev/null
+++ b/backend/config/bundles.php
@@ -0,0 +1,18 @@
+ ['all' => true],
+ Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
+ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
+ Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
+ Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
+ Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
+ ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
+ Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
+ Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
+ Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
+ Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
+ Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
+];
diff --git a/backend/config/packages/debug.yaml b/backend/config/packages/debug.yaml
new file mode 100644
index 0000000..ad874af
--- /dev/null
+++ b/backend/config/packages/debug.yaml
@@ -0,0 +1,5 @@
+when@dev:
+ debug:
+ # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
+ # See the "server:dump" command to start a new server.
+ dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"
diff --git a/backend/config/packages/dev/tenant.yaml b/backend/config/packages/dev/tenant.yaml
new file mode 100644
index 0000000..2534ac5
--- /dev/null
+++ b/backend/config/packages/dev/tenant.yaml
@@ -0,0 +1,18 @@
+# Tenants de développement
+# Ces tenants sont automatiquement chargés en environnement dev
+
+parameters:
+ tenant.dev_configs:
+ - tenantId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
+ subdomain: 'ecole-alpha'
+ databaseUrl: '%env(DATABASE_URL)%'
+ - tenantId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
+ subdomain: 'ecole-beta'
+ databaseUrl: '%env(DATABASE_URL)%'
+
+services:
+ App\Shared\Infrastructure\Tenant\TenantRegistry:
+ class: App\Shared\Infrastructure\Tenant\InMemoryTenantRegistry
+ factory: ['@App\Shared\Infrastructure\Tenant\TenantRegistryFactory', 'createFromConfig']
+ arguments:
+ $configs: '%tenant.dev_configs%'
diff --git a/backend/config/packages/prod/tenant.yaml b/backend/config/packages/prod/tenant.yaml
new file mode 100644
index 0000000..dbff0af
--- /dev/null
+++ b/backend/config/packages/prod/tenant.yaml
@@ -0,0 +1,19 @@
+# Configuration des tenants en production
+#
+# En production, les tenants peuvent être configurés de deux façons :
+# 1. Via la variable d'environnement TENANT_CONFIGS (JSON)
+# 2. Via une implémentation DatabaseTenantRegistry (à implémenter)
+#
+# Pour l'instant, on utilise InMemoryTenantRegistry avec configuration env.
+# Si aucun tenant n'est configuré, toutes les requêtes retourneront 404.
+
+parameters:
+ # Format JSON attendu: [{"tenantId":"uuid","subdomain":"ecole","databaseUrl":"postgres://..."}]
+ tenant.prod_configs_json: '%env(default::TENANT_CONFIGS)%'
+
+services:
+ App\Shared\Infrastructure\Tenant\TenantRegistry:
+ class: App\Shared\Infrastructure\Tenant\InMemoryTenantRegistry
+ factory: ['@App\Shared\Infrastructure\Tenant\TenantRegistryFactory', 'createFromEnv']
+ arguments:
+ $configsJson: '%tenant.prod_configs_json%'
diff --git a/backend/config/packages/property_info.yaml b/backend/config/packages/property_info.yaml
new file mode 100644
index 0000000..dd31b9d
--- /dev/null
+++ b/backend/config/packages/property_info.yaml
@@ -0,0 +1,3 @@
+framework:
+ property_info:
+ with_constructor_extractor: true
diff --git a/backend/config/packages/routing.yaml b/backend/config/packages/routing.yaml
new file mode 100644
index 0000000..0f34f87
--- /dev/null
+++ b/backend/config/packages/routing.yaml
@@ -0,0 +1,10 @@
+framework:
+ router:
+ # Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
+ # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
+ default_uri: '%env(DEFAULT_URI)%'
+
+when@prod:
+ framework:
+ router:
+ strict_requirements: null
diff --git a/backend/config/packages/tenant.yaml b/backend/config/packages/tenant.yaml
new file mode 100644
index 0000000..1be7e3f
--- /dev/null
+++ b/backend/config/packages/tenant.yaml
@@ -0,0 +1,9 @@
+services:
+ # Tenant infrastructure event subscribers
+ App\Shared\Infrastructure\Tenant\TenantMiddleware:
+ tags:
+ - { name: kernel.event_subscriber }
+
+ App\Shared\Infrastructure\Security\TenantAccessDeniedHandler:
+ tags:
+ - { name: kernel.event_subscriber }
diff --git a/backend/config/packages/test/tenant.yaml b/backend/config/packages/test/tenant.yaml
new file mode 100644
index 0000000..fd4c5c7
--- /dev/null
+++ b/backend/config/packages/test/tenant.yaml
@@ -0,0 +1,18 @@
+# Tenants pour les tests
+# Utilise les mêmes tenants que dev pour les tests d'intégration
+
+parameters:
+ tenant.test_configs:
+ - tenantId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
+ subdomain: 'ecole-alpha'
+ databaseUrl: '%env(DATABASE_URL)%'
+ - tenantId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
+ subdomain: 'ecole-beta'
+ databaseUrl: '%env(DATABASE_URL)%'
+
+services:
+ App\Shared\Infrastructure\Tenant\TenantRegistry:
+ class: App\Shared\Infrastructure\Tenant\InMemoryTenantRegistry
+ factory: ['@App\Shared\Infrastructure\Tenant\TenantRegistryFactory', 'createFromConfig']
+ arguments:
+ $configs: '%tenant.test_configs%'
diff --git a/backend/config/packages/web_profiler.yaml b/backend/config/packages/web_profiler.yaml
new file mode 100644
index 0000000..0eac3c9
--- /dev/null
+++ b/backend/config/packages/web_profiler.yaml
@@ -0,0 +1,13 @@
+when@dev:
+ web_profiler:
+ toolbar: true
+
+ framework:
+ profiler:
+ collect_serializer_data: true
+
+when@test:
+ framework:
+ profiler:
+ collect: false
+ collect_serializer_data: true
diff --git a/backend/config/routes/framework.yaml b/backend/config/routes/framework.yaml
new file mode 100644
index 0000000..bc1feac
--- /dev/null
+++ b/backend/config/routes/framework.yaml
@@ -0,0 +1,4 @@
+when@dev:
+ _errors:
+ resource: '@FrameworkBundle/Resources/config/routing/errors.php'
+ prefix: /_error
diff --git a/backend/config/routes/security.yaml b/backend/config/routes/security.yaml
new file mode 100644
index 0000000..f853be1
--- /dev/null
+++ b/backend/config/routes/security.yaml
@@ -0,0 +1,3 @@
+_security_logout:
+ resource: security.route_loader.logout
+ type: service
diff --git a/backend/config/routes/web_profiler.yaml b/backend/config/routes/web_profiler.yaml
new file mode 100644
index 0000000..b3b7b4b
--- /dev/null
+++ b/backend/config/routes/web_profiler.yaml
@@ -0,0 +1,8 @@
+when@dev:
+ web_profiler_wdt:
+ resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
+ prefix: /_wdt
+
+ web_profiler_profiler:
+ resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
+ prefix: /_profiler
diff --git a/backend/config/services.yaml b/backend/config/services.yaml
index 733a8da..783d7f2 100644
--- a/backend/config/services.yaml
+++ b/backend/config/services.yaml
@@ -4,6 +4,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
+ tenant.base_domain: '%env(TENANT_BASE_DOMAIN)%'
services:
# default configuration for services in this file
@@ -25,3 +26,20 @@ services:
# Domain services need to be registered explicitly to avoid framework coupling
# Example: App\Administration\Application\Command\:
# resource: '../src/Administration/Application/Command/'
+
+ # Tenant services
+ App\Shared\Infrastructure\Tenant\TenantResolver:
+ arguments:
+ $baseDomain: '%tenant.base_domain%'
+
+ # TenantRegistry est configuré par environnement :
+ # - dev: config/packages/dev/tenant.yaml (tenants de test)
+ # - prod: à configurer via admin ou env vars
+
+ App\Shared\Infrastructure\Tenant\Command\CreateTenantDatabaseCommand:
+ arguments:
+ $masterDatabaseUrl: '%env(DATABASE_URL)%'
+
+ App\Shared\Infrastructure\Tenant\Command\TenantMigrateCommand:
+ arguments:
+ $projectDir: '%kernel.project_dir%'
diff --git a/backend/src/Shared/Infrastructure/Tenant/.gitkeep b/backend/migrations/.gitignore
similarity index 100%
rename from backend/src/Shared/Infrastructure/Tenant/.gitkeep
rename to backend/migrations/.gitignore
diff --git a/backend/phpstan.neon b/backend/phpstan.neon
index 3c6469d..b7005fc 100644
--- a/backend/phpstan.neon
+++ b/backend/phpstan.neon
@@ -2,6 +2,7 @@ parameters:
level: 9
paths:
- src
+ - tests/Architecture
excludePaths:
- src/Kernel.php
treatPhpDocTypesAsCertain: false
@@ -10,3 +11,4 @@ parameters:
includes:
- vendor/phpstan/phpstan-doctrine/extension.neon
- vendor/phpstan/phpstan-symfony/extension.neon
+ - vendor/phpat/phpat/extension.neon
diff --git a/backend/phpunit.dist.xml b/backend/phpunit.dist.xml
new file mode 100644
index 0000000..c034c41
--- /dev/null
+++ b/backend/phpunit.dist.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests
+ tests/Architecture
+
+
+
+
+
+ src
+
+
+
+ Doctrine\Deprecations\Deprecation::trigger
+ Doctrine\Deprecations\Deprecation::delegateTriggerToBackend
+ trigger_deprecation
+
+
+
+
+
+
+
+
+
+
diff --git a/backend/src/ApiResource/.gitignore b/backend/src/ApiResource/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/backend/src/Controller/.gitignore b/backend/src/Controller/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/backend/src/Entity/.gitignore b/backend/src/Entity/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/backend/src/Repository/.gitignore b/backend/src/Repository/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/backend/src/Shared/Infrastructure/Security/TenantAccessDeniedHandler.php b/backend/src/Shared/Infrastructure/Security/TenantAccessDeniedHandler.php
new file mode 100644
index 0000000..0517f4f
--- /dev/null
+++ b/backend/src/Shared/Infrastructure/Security/TenantAccessDeniedHandler.php
@@ -0,0 +1,56 @@
+ ['onKernelException', 2], // Higher priority than default
+ ];
+ }
+
+ public function onKernelException(ExceptionEvent $event): void
+ {
+ $exception = $event->getThrowable();
+
+ if (!$exception instanceof AccessDeniedException) {
+ return;
+ }
+
+ // Check if this is a TenantAware resource denial
+ // The subject is stored in the exception's attributes
+ $subject = $exception->getSubject();
+
+ if ($subject instanceof TenantAwareInterface) {
+ // Convert to 404 to hide resource existence
+ $response = new JsonResponse(
+ [
+ 'status' => Response::HTTP_NOT_FOUND,
+ 'message' => 'Resource not found',
+ 'type' => 'https://classeo.fr/errors/resource-not-found',
+ ],
+ Response::HTTP_NOT_FOUND
+ );
+
+ $event->setResponse($response);
+ }
+ }
+}
diff --git a/backend/src/Shared/Infrastructure/Security/TenantAwareInterface.php b/backend/src/Shared/Infrastructure/Security/TenantAwareInterface.php
new file mode 100644
index 0000000..7a5baf5
--- /dev/null
+++ b/backend/src/Shared/Infrastructure/Security/TenantAwareInterface.php
@@ -0,0 +1,16 @@
+
+ */
+final class TenantVoter extends Voter
+{
+ public const string ATTRIBUTE = 'TENANT_ACCESS';
+
+ public function __construct(
+ private readonly TenantContext $tenantContext,
+ ) {
+ }
+
+ #[Override]
+ protected function supports(string $attribute, mixed $subject): bool
+ {
+ // Only vote on TENANT_ACCESS attribute to avoid bypassing other voters
+ return $attribute === self::ATTRIBUTE && $subject instanceof TenantAwareInterface;
+ }
+
+ #[Override]
+ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
+ {
+ $user = $token->getUser();
+
+ // User must be authenticated
+ if (!$user instanceof UserInterface) {
+ return false;
+ }
+
+ // Subject must be tenant-aware (should always be true due to supports())
+ if (!$subject instanceof TenantAwareInterface) {
+ return false;
+ }
+
+ // Tenant context must be set
+ try {
+ $currentTenantId = $this->tenantContext->getCurrentTenantId();
+ } catch (TenantNotSetException) {
+ // No tenant context - deny access
+ return false;
+ }
+
+ // CRITICAL: Verify resource belongs to current tenant
+ // If resource belongs to different tenant, return false (-> 404)
+ return $subject->getTenantId()->equals($currentTenantId);
+ }
+}
diff --git a/backend/src/Shared/Infrastructure/Tenant/Command/CreateTenantDatabaseCommand.php b/backend/src/Shared/Infrastructure/Tenant/Command/CreateTenantDatabaseCommand.php
new file mode 100644
index 0000000..c77b159
--- /dev/null
+++ b/backend/src/Shared/Infrastructure/Tenant/Command/CreateTenantDatabaseCommand.php
@@ -0,0 +1,102 @@
+addArgument('database_name', InputArgument::REQUIRED, 'The name of the database to create (e.g., classeo_tenant_alpha)')
+ ->addArgument('database_user', InputArgument::OPTIONAL, 'The database user to grant access to', 'classeo')
+ ;
+ }
+
+ #[Override]
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ /** @var string $databaseName */
+ $databaseName = $input->getArgument('database_name');
+ /** @var string $databaseUser */
+ $databaseUser = $input->getArgument('database_user');
+
+ // Validate database name format
+ if (!preg_match('/^classeo_tenant_[a-z0-9_]+$/', $databaseName)) {
+ $io->error('Database name must follow pattern: classeo_tenant_');
+
+ return Command::FAILURE;
+ }
+
+ $io->title("Creating database: {$databaseName}");
+
+ try {
+ // Connect to master database (postgres) to create new database
+ $connection = DriverManager::getConnection(['url' => $this->masterDatabaseUrl]);
+
+ // Check if database already exists
+ $existsQuery = $connection->executeQuery(
+ 'SELECT 1 FROM pg_database WHERE datname = :name',
+ ['name' => $databaseName]
+ );
+
+ if ($existsQuery->fetchOne() !== false) {
+ $io->warning("Database '{$databaseName}' already exists.");
+
+ return Command::SUCCESS;
+ }
+
+ // Create database
+ // Note: Database names cannot be parameterized in SQL, so we use a validated name
+ $connection->executeStatement(sprintf(
+ 'CREATE DATABASE %s WITH OWNER = %s ENCODING = \'UTF8\' LC_COLLATE = \'en_US.utf8\' LC_CTYPE = \'en_US.utf8\'',
+ $this->quoteIdentifier($databaseName),
+ $this->quoteIdentifier($databaseUser)
+ ));
+
+ $io->success("Database '{$databaseName}' created successfully.");
+
+ return Command::SUCCESS;
+ } catch (Throwable $e) {
+ $io->error([
+ 'Failed to create database',
+ $e->getMessage(),
+ ]);
+
+ return Command::FAILURE;
+ }
+ }
+
+ private function quoteIdentifier(string $identifier): string
+ {
+ // Simple identifier quoting for PostgreSQL
+ return '"' . str_replace('"', '""', $identifier) . '"';
+ }
+}
diff --git a/backend/src/Shared/Infrastructure/Tenant/Command/TenantMigrateCommand.php b/backend/src/Shared/Infrastructure/Tenant/Command/TenantMigrateCommand.php
new file mode 100644
index 0000000..eb032e7
--- /dev/null
+++ b/backend/src/Shared/Infrastructure/Tenant/Command/TenantMigrateCommand.php
@@ -0,0 +1,149 @@
+addArgument('subdomain', InputArgument::OPTIONAL, 'The subdomain of the tenant to migrate (or "all" for all tenants)')
+ ->addOption('all', 'a', InputOption::VALUE_NONE, 'Run migrations for all tenants')
+ ;
+ }
+
+ #[Override]
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ /** @var string|null $subdomain */
+ $subdomain = $input->getArgument('subdomain');
+ $all = $input->getOption('all');
+
+ if ($all || $subdomain === 'all') {
+ return $this->migrateAllTenants($io);
+ }
+
+ if ($subdomain === null) {
+ $io->error('Please provide a subdomain or use --all to migrate all tenants.');
+
+ return Command::FAILURE;
+ }
+
+ return $this->migrateTenant($subdomain, $io);
+ }
+
+ private function migrateTenant(string $subdomain, SymfonyStyle $io): int
+ {
+ try {
+ $config = $this->registry->getBySubdomain($subdomain);
+
+ return $this->runMigrationForConfig($config, $io);
+ } catch (Throwable $e) {
+ $io->error([
+ "Failed to migrate tenant '{$subdomain}'",
+ $e->getMessage(),
+ ]);
+
+ return Command::FAILURE;
+ }
+ }
+
+ private function migrateAllTenants(SymfonyStyle $io): int
+ {
+ $io->title('Running migrations for all tenants');
+
+ $configs = $this->registry->getAllConfigs();
+
+ if ($configs === []) {
+ $io->warning('No tenants found in registry.');
+
+ return Command::SUCCESS;
+ }
+
+ $io->info(sprintf('Found %d tenant(s) to migrate.', count($configs)));
+
+ $failed = 0;
+ foreach ($configs as $config) {
+ $result = $this->runMigrationForConfig($config, $io);
+ if ($result !== Command::SUCCESS) {
+ ++$failed;
+ }
+ }
+
+ if ($failed > 0) {
+ $io->error(sprintf('%d tenant(s) failed to migrate.', $failed));
+
+ return Command::FAILURE;
+ }
+
+ $io->success('All tenants migrated successfully.');
+
+ return Command::SUCCESS;
+ }
+
+ private function runMigrationForConfig(TenantConfig $config, SymfonyStyle $io): int
+ {
+ $io->section("Migrating tenant: {$config->subdomain}");
+
+ // Spawn a new process with DATABASE_URL set BEFORE the kernel boots.
+ // This ensures Doctrine uses the tenant's database connection.
+ $process = new Process(
+ command: ['php', 'bin/console', 'doctrine:migrations:migrate', '--no-interaction'],
+ cwd: $this->projectDir,
+ env: [
+ ...getenv(),
+ 'DATABASE_URL' => $config->databaseUrl,
+ ],
+ timeout: 300,
+ );
+
+ $process->run(static function (string $type, string $buffer) use ($io): void {
+ $io->write($buffer);
+ });
+
+ if ($process->isSuccessful()) {
+ $io->success("Tenant '{$config->subdomain}' migrated successfully.");
+
+ return Command::SUCCESS;
+ }
+
+ $io->error("Migration failed for tenant '{$config->subdomain}'");
+
+ return Command::FAILURE;
+ }
+}
diff --git a/backend/src/Shared/Infrastructure/Tenant/InMemoryTenantRegistry.php b/backend/src/Shared/Infrastructure/Tenant/InMemoryTenantRegistry.php
new file mode 100644
index 0000000..cbfecfa
--- /dev/null
+++ b/backend/src/Shared/Infrastructure/Tenant/InMemoryTenantRegistry.php
@@ -0,0 +1,65 @@
+ Indexed by tenant ID */
+ private array $byId = [];
+
+ /** @var array Indexed by subdomain */
+ private array $bySubdomain = [];
+
+ /**
+ * @param TenantConfig[] $configs
+ */
+ public function __construct(array $configs)
+ {
+ foreach ($configs as $config) {
+ $this->byId[(string) $config->tenantId] = $config;
+ $this->bySubdomain[$config->subdomain] = $config;
+ }
+ }
+
+ #[Override]
+ public function getConfig(TenantId $tenantId): TenantConfig
+ {
+ $key = (string) $tenantId;
+
+ if (!isset($this->byId[$key])) {
+ throw TenantNotFoundException::withId($tenantId);
+ }
+
+ return $this->byId[$key];
+ }
+
+ #[Override]
+ public function getBySubdomain(string $subdomain): TenantConfig
+ {
+ if (!isset($this->bySubdomain[$subdomain])) {
+ throw TenantNotFoundException::withSubdomain($subdomain);
+ }
+
+ return $this->bySubdomain[$subdomain];
+ }
+
+ #[Override]
+ public function exists(string $subdomain): bool
+ {
+ return isset($this->bySubdomain[$subdomain]);
+ }
+
+ #[Override]
+ public function getAllConfigs(): array
+ {
+ return array_values($this->byId);
+ }
+}
diff --git a/backend/src/Shared/Infrastructure/Tenant/TenantConfig.php b/backend/src/Shared/Infrastructure/Tenant/TenantConfig.php
new file mode 100644
index 0000000..9d87685
--- /dev/null
+++ b/backend/src/Shared/Infrastructure/Tenant/TenantConfig.php
@@ -0,0 +1,15 @@
+currentTenant = $config;
+ }
+
+ public function getCurrentTenantId(): TenantId
+ {
+ if ($this->currentTenant === null) {
+ throw new TenantNotSetException('No tenant is set in the current context.');
+ }
+
+ return $this->currentTenant->tenantId;
+ }
+
+ public function getCurrentTenantConfig(): TenantConfig
+ {
+ if ($this->currentTenant === null) {
+ throw new TenantNotSetException('No tenant is set in the current context.');
+ }
+
+ return $this->currentTenant;
+ }
+
+ public function hasTenant(): bool
+ {
+ return $this->currentTenant !== null;
+ }
+
+ public function clear(): void
+ {
+ $this->currentTenant = null;
+ }
+}
diff --git a/backend/src/Shared/Infrastructure/Tenant/TenantEntityManagerFactory.php b/backend/src/Shared/Infrastructure/Tenant/TenantEntityManagerFactory.php
new file mode 100644
index 0000000..2e55db8
--- /dev/null
+++ b/backend/src/Shared/Infrastructure/Tenant/TenantEntityManagerFactory.php
@@ -0,0 +1,159 @@
+ */
+ private array $managers = [];
+
+ public function __construct(
+ private readonly TenantRegistry $registry,
+ private readonly Clock $clock,
+ private readonly Configuration $ormConfiguration,
+ ) {
+ }
+
+ public function getForTenant(TenantId $tenantId): EntityManagerInterface
+ {
+ $key = (string) $tenantId;
+
+ // Evict idle connections first
+ $this->evictIdleConnections();
+
+ // Evict LRU if pool is full and we need a new manager
+ if (!isset($this->managers[$key]) && count($this->managers) >= self::MAX_MANAGERS) {
+ $this->evictLeastRecentlyUsed();
+ }
+
+ if (!isset($this->managers[$key])) {
+ $this->managers[$key] = [
+ 'manager' => $this->createManagerForTenant($tenantId),
+ 'lastUsed' => $this->clock->now(),
+ ];
+ } else {
+ // Health check before returning cached manager (AC4 requirement)
+ $manager = $this->managers[$key]['manager'];
+ if (!$manager->isOpen() || !$manager->getConnection()->isConnected()) {
+ // Connection is dead, recreate the manager
+ $this->closeAndRemove($key);
+ $this->managers[$key] = [
+ 'manager' => $this->createManagerForTenant($tenantId),
+ 'lastUsed' => $this->clock->now(),
+ ];
+ } else {
+ $this->managers[$key]['lastUsed'] = $this->clock->now();
+ }
+ }
+
+ return $this->managers[$key]['manager'];
+ }
+
+ public function getPoolSize(): int
+ {
+ return count($this->managers);
+ }
+
+ private function evictLeastRecentlyUsed(): void
+ {
+ if (empty($this->managers)) {
+ return;
+ }
+
+ $oldestKey = null;
+ $oldestTime = null;
+
+ foreach ($this->managers as $key => $data) {
+ if ($oldestTime === null || $data['lastUsed'] < $oldestTime) {
+ $oldestKey = $key;
+ $oldestTime = $data['lastUsed'];
+ }
+ }
+
+ if ($oldestKey !== null) {
+ $this->closeAndRemove($oldestKey);
+ }
+ }
+
+ private function evictIdleConnections(): void
+ {
+ $now = $this->clock->now();
+ $keysToRemove = [];
+
+ foreach ($this->managers as $key => $data) {
+ $idleSeconds = $now->getTimestamp() - $data['lastUsed']->getTimestamp();
+ if ($idleSeconds > self::IDLE_TIMEOUT_SECONDS) {
+ $keysToRemove[] = $key;
+ }
+ }
+
+ foreach ($keysToRemove as $key) {
+ $this->closeAndRemove($key);
+ }
+ }
+
+ private function closeAndRemove(string $key): void
+ {
+ if (isset($this->managers[$key])) {
+ $manager = $this->managers[$key]['manager'];
+ if ($manager->isOpen()) {
+ $manager->close();
+ }
+ unset($this->managers[$key]);
+ }
+ }
+
+ private function createManagerForTenant(TenantId $tenantId): EntityManagerInterface
+ {
+ $config = $this->registry->getConfig($tenantId);
+
+ // Parse database URL and create connection parameters
+ $connectionParams = $this->parseConnectionParams($config->databaseUrl);
+
+ // Health check before creation
+ /** @phpstan-ignore argument.type (Doctrine accepts both URL and explicit params) */
+ $connection = DriverManager::getConnection($connectionParams);
+ $this->healthCheck($connection);
+
+ return new EntityManager($connection, $this->ormConfiguration);
+ }
+
+ /**
+ * @return array
+ */
+ private function parseConnectionParams(string $databaseUrl): array
+ {
+ // Handle SQLite in-memory specially
+ if (str_starts_with($databaseUrl, 'sqlite:///:memory:')) {
+ return [
+ 'driver' => 'pdo_sqlite',
+ 'memory' => true,
+ ];
+ }
+
+ // For other databases, use URL parameter
+ return ['url' => $databaseUrl];
+ }
+
+ private function healthCheck(\Doctrine\DBAL\Connection $connection): void
+ {
+ // Verify the database is accessible by executing a simple query
+ // This implicitly connects and validates the connection
+ $connection->executeQuery('SELECT 1');
+ }
+}
diff --git a/backend/src/Shared/Infrastructure/Tenant/TenantId.php b/backend/src/Shared/Infrastructure/Tenant/TenantId.php
new file mode 100644
index 0000000..cdb07b3
--- /dev/null
+++ b/backend/src/Shared/Infrastructure/Tenant/TenantId.php
@@ -0,0 +1,11 @@
+ ['onKernelRequest', 256], // High priority - run early
+ KernelEvents::TERMINATE => 'onKernelTerminate',
+ ];
+ }
+
+ private const array PUBLIC_PATHS = [
+ '/api/docs',
+ '/api/docs.json',
+ '/api/docs.jsonld',
+ '/api/contexts',
+ '/_profiler',
+ '/_wdt',
+ '/_error',
+ ];
+
+ public function onKernelRequest(RequestEvent $event): void
+ {
+ if (!$event->isMainRequest()) {
+ return;
+ }
+
+ $request = $event->getRequest();
+ $path = $request->getPathInfo();
+
+ // Skip tenant resolution for public paths (docs, profiler, etc.)
+ foreach (self::PUBLIC_PATHS as $publicPath) {
+ if (str_starts_with($path, $publicPath)) {
+ return;
+ }
+ }
+
+ $host = $request->getHost();
+
+ try {
+ $config = $this->resolver->resolve($host);
+ $this->context->setCurrentTenant($config);
+
+ // Store tenant config in request for easy access
+ $request->attributes->set('_tenant', $config);
+ } catch (TenantNotFoundException) {
+ // Return 404 with generic message - DO NOT reveal tenant existence
+ $response = new JsonResponse(
+ [
+ 'status' => Response::HTTP_NOT_FOUND,
+ 'message' => 'Resource not found',
+ 'type' => 'https://classeo.fr/errors/resource-not-found',
+ ],
+ Response::HTTP_NOT_FOUND
+ );
+
+ $event->setResponse($response);
+ }
+ }
+
+ public function onKernelTerminate(): void
+ {
+ $this->context->clear();
+ }
+}
diff --git a/backend/src/Shared/Infrastructure/Tenant/TenantNotFoundException.php b/backend/src/Shared/Infrastructure/Tenant/TenantNotFoundException.php
new file mode 100644
index 0000000..3387034
--- /dev/null
+++ b/backend/src/Shared/Infrastructure/Tenant/TenantNotFoundException.php
@@ -0,0 +1,22 @@
+ $configs
+ */
+ public function createFromConfig(array $configs): TenantRegistry
+ {
+ return new InMemoryTenantRegistry($this->parseConfigs($configs));
+ }
+
+ /**
+ * Creates registry from JSON environment variable (prod).
+ *
+ * Expected format: [{"tenantId":"uuid","subdomain":"ecole","databaseUrl":"postgres://..."}]
+ */
+ public function createFromEnv(string $configsJson): TenantRegistry
+ {
+ if ($configsJson === '') {
+ return new InMemoryTenantRegistry([]);
+ }
+
+ $decoded = json_decode($configsJson, true, 512, JSON_THROW_ON_ERROR);
+
+ if (!is_array($decoded)) {
+ throw new InvalidArgumentException('TENANT_CONFIGS must be a JSON array');
+ }
+
+ /** @var array $configs */
+ $configs = $decoded;
+
+ return new InMemoryTenantRegistry($this->parseConfigs($configs));
+ }
+
+ /**
+ * @param array $configs
+ *
+ * @return TenantConfig[]
+ */
+ private function parseConfigs(array $configs): array
+ {
+ $tenantConfigs = [];
+
+ foreach ($configs as $config) {
+ $tenantConfigs[] = new TenantConfig(
+ tenantId: TenantId::fromString($config['tenantId']),
+ subdomain: $config['subdomain'],
+ databaseUrl: $config['databaseUrl'],
+ );
+ }
+
+ return $tenantConfigs;
+ }
+}
diff --git a/backend/src/Shared/Infrastructure/Tenant/TenantResolver.php b/backend/src/Shared/Infrastructure/Tenant/TenantResolver.php
new file mode 100644
index 0000000..6f6c041
--- /dev/null
+++ b/backend/src/Shared/Infrastructure/Tenant/TenantResolver.php
@@ -0,0 +1,66 @@
+extractSubdomain($host);
+
+ if ($subdomain === null) {
+ throw TenantNotFoundException::withSubdomain('');
+ }
+
+ return $this->registry->getBySubdomain($subdomain);
+ }
+
+ /**
+ * Extracts the subdomain from a host header.
+ * Returns null if no tenant subdomain is present (main domain or reserved subdomain).
+ */
+ public function extractSubdomain(string $host): ?string
+ {
+ // Remove port if present
+ $host = explode(':', $host)[0];
+
+ // Check if host ends with base domain
+ $baseDomain = '.' . $this->baseDomain;
+ if (!str_ends_with($host, $baseDomain)) {
+ // Host doesn't match our domain - could be the base domain itself
+ if ($host === $this->baseDomain) {
+ return null;
+ }
+
+ return null;
+ }
+
+ // Extract subdomain
+ $subdomain = substr($host, 0, -strlen($baseDomain));
+
+ // Empty subdomain or reserved
+ if ($subdomain === '' || in_array($subdomain, self::RESERVED_SUBDOMAINS, true)) {
+ return null;
+ }
+
+ return $subdomain;
+ }
+}
diff --git a/backend/symfony.lock b/backend/symfony.lock
new file mode 100644
index 0000000..dbc51ba
--- /dev/null
+++ b/backend/symfony.lock
@@ -0,0 +1,296 @@
+{
+ "api-platform/core": {
+ "version": "4.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "4.0",
+ "ref": "cb9e6b8ceb9b62f32d41fc8ad72a25d5bd674c6d"
+ },
+ "files": [
+ "config/packages/api_platform.yaml",
+ "config/routes/api_platform.yaml",
+ "src/ApiResource/.gitignore"
+ ]
+ },
+ "doctrine/deprecations": {
+ "version": "1.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "87424683adc81d7dc305eefec1fced883084aab9"
+ }
+ },
+ "doctrine/doctrine-bundle": {
+ "version": "3.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.0",
+ "ref": "18ee08e513ba0303fd09a01fc1c934870af06ffa"
+ },
+ "files": [
+ "config/packages/doctrine.yaml",
+ "src/Entity/.gitignore",
+ "src/Repository/.gitignore"
+ ]
+ },
+ "doctrine/doctrine-fixtures-bundle": {
+ "version": "4.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.0",
+ "ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
+ },
+ "files": [
+ "src/DataFixtures/AppFixtures.php"
+ ]
+ },
+ "doctrine/doctrine-migrations-bundle": {
+ "version": "3.7",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.1",
+ "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
+ },
+ "files": [
+ "config/packages/doctrine_migrations.yaml",
+ "migrations/.gitignore"
+ ]
+ },
+ "friendsofphp/php-cs-fixer": {
+ "version": "3.93",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.0",
+ "ref": "be2103eb4a20942e28a6dd87736669b757132435"
+ },
+ "files": [
+ ".php-cs-fixer.dist.php"
+ ]
+ },
+ "lexik/jwt-authentication-bundle": {
+ "version": "3.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "2.5",
+ "ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7"
+ },
+ "files": [
+ "config/packages/lexik_jwt_authentication.yaml"
+ ]
+ },
+ "phpstan/phpstan": {
+ "version": "2.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes-contrib",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767"
+ }
+ },
+ "phpunit/phpunit": {
+ "version": "11.5",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "11.1",
+ "ref": "1117deb12541f35793eec9fff7494d7aa12283fc"
+ },
+ "files": [
+ ".env.test",
+ "phpunit.dist.xml",
+ "tests/bootstrap.php",
+ "bin/phpunit"
+ ]
+ },
+ "symfony/console": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
+ },
+ "files": [
+ "bin/console"
+ ]
+ },
+ "symfony/debug-bundle": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "5aa8aa48234c8eb6dbdd7b3cd5d791485d2cec4b"
+ },
+ "files": [
+ "config/packages/debug.yaml"
+ ]
+ },
+ "symfony/flex": {
+ "version": "2.10",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "2.4",
+ "ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
+ },
+ "files": [
+ ".env",
+ ".env.dev"
+ ]
+ },
+ "symfony/framework-bundle": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "7.4",
+ "ref": "09f6e081c763a206802674ce0cb34a022f0ffc6d"
+ },
+ "files": [
+ "config/packages/cache.yaml",
+ "config/packages/framework.yaml",
+ "config/preload.php",
+ "config/routes/framework.yaml",
+ "config/services.yaml",
+ "public/index.php",
+ "src/Controller/.gitignore",
+ "src/Kernel.php",
+ ".editorconfig"
+ ]
+ },
+ "symfony/maker-bundle": {
+ "version": "1.65",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
+ }
+ },
+ "symfony/messenger": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.0",
+ "ref": "d8936e2e2230637ef97e5eecc0eea074eecae58b"
+ },
+ "files": [
+ "config/packages/messenger.yaml"
+ ]
+ },
+ "symfony/monolog-bundle": {
+ "version": "4.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.7",
+ "ref": "1b9efb10c54cb51c713a9391c9300ff8bceda459"
+ },
+ "files": [
+ "config/packages/monolog.yaml"
+ ]
+ },
+ "symfony/phpunit-bridge": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "7.3",
+ "ref": "dc13fec96bd527bd399c3c01f0aab915c67fd544"
+ }
+ },
+ "symfony/property-info": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "7.3",
+ "ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
+ },
+ "files": [
+ "config/packages/property_info.yaml"
+ ]
+ },
+ "symfony/routing": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "7.4",
+ "ref": "bc94c4fd86f393f3ab3947c18b830ea343e51ded"
+ },
+ "files": [
+ "config/packages/routing.yaml",
+ "config/routes.yaml"
+ ]
+ },
+ "symfony/security-bundle": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "7.4",
+ "ref": "c42fee7802181cdd50f61b8622715829f5d2335c"
+ },
+ "files": [
+ "config/packages/security.yaml",
+ "config/routes/security.yaml"
+ ]
+ },
+ "symfony/twig-bundle": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.4",
+ "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877"
+ },
+ "files": [
+ "config/packages/twig.yaml",
+ "templates/base.html.twig"
+ ]
+ },
+ "symfony/uid": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "7.0",
+ "ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
+ }
+ },
+ "symfony/validator": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "7.0",
+ "ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
+ },
+ "files": [
+ "config/packages/validator.yaml"
+ ]
+ },
+ "symfony/web-profiler-bundle": {
+ "version": "8.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "7.3",
+ "ref": "a363460c1b0b4a4d0242f2ce1a843ca0f6ac9026"
+ },
+ "files": [
+ "config/packages/web_profiler.yaml",
+ "config/routes/web_profiler.yaml"
+ ]
+ }
+}
diff --git a/backend/templates/base.html.twig b/backend/templates/base.html.twig
new file mode 100644
index 0000000..1069c14
--- /dev/null
+++ b/backend/templates/base.html.twig
@@ -0,0 +1,16 @@
+
+
+
+
+ {% block title %}Welcome!{% endblock %}
+
+ {% block stylesheets %}
+ {% endblock %}
+
+ {% block javascripts %}
+ {% endblock %}
+
+
+ {% block body %}{% endblock %}
+
+
diff --git a/backend/tests/Architecture/BoundedContextIsolationTest.php b/backend/tests/Architecture/BoundedContextIsolationTest.php
new file mode 100644
index 0000000..7132ec3
--- /dev/null
+++ b/backend/tests/Architecture/BoundedContextIsolationTest.php
@@ -0,0 +1,126 @@
+classes(Selector::inNamespace('App\Scolarite'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\VieScolaire'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_scolarite_should_not_depend_on_communication(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Scolarite'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Communication'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_scolarite_should_not_depend_on_administration(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Scolarite'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Administration'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_vie_scolaire_should_not_depend_on_scolarite(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\VieScolaire'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Scolarite'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_vie_scolaire_should_not_depend_on_communication(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\VieScolaire'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Communication'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_vie_scolaire_should_not_depend_on_administration(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\VieScolaire'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Administration'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_communication_should_not_depend_on_scolarite(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Communication'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Scolarite'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_communication_should_not_depend_on_vie_scolaire(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Communication'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\VieScolaire'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_communication_should_not_depend_on_administration(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Communication'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Administration'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_administration_should_not_depend_on_scolarite(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Administration'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Scolarite'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_administration_should_not_depend_on_vie_scolaire(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\VieScolaire'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Administration'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+
+ public function test_administration_should_not_depend_on_communication(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Administration'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Communication'))
+ ->because('Bounded Contexts must communicate through events, not direct calls');
+ }
+}
diff --git a/backend/tests/Architecture/DomainPurityTest.php b/backend/tests/Architecture/DomainPurityTest.php
new file mode 100644
index 0000000..fbf8cb2
--- /dev/null
+++ b/backend/tests/Architecture/DomainPurityTest.php
@@ -0,0 +1,129 @@
+classes(Selector::inNamespace('App\Scolarite\Domain'))
+ ->shouldNotDependOn()
+ ->classes(
+ Selector::inNamespace('App\Scolarite\Infrastructure'),
+ Selector::inNamespace('App\Shared\Infrastructure'),
+ Selector::inNamespace('Symfony'),
+ Selector::inNamespace('Doctrine'),
+ Selector::inNamespace('ApiPlatform'),
+ )
+ ->because('Domain must be pure PHP without Infrastructure/framework dependencies');
+ }
+
+ public function test_vie_scolaire_domain_should_not_depend_on_infrastructure(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\VieScolaire\Domain'))
+ ->shouldNotDependOn()
+ ->classes(
+ Selector::inNamespace('App\VieScolaire\Infrastructure'),
+ Selector::inNamespace('App\Shared\Infrastructure'),
+ Selector::inNamespace('Symfony'),
+ Selector::inNamespace('Doctrine'),
+ Selector::inNamespace('ApiPlatform'),
+ )
+ ->because('Domain must be pure PHP without Infrastructure/framework dependencies');
+ }
+
+ public function test_communication_domain_should_not_depend_on_infrastructure(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Communication\Domain'))
+ ->shouldNotDependOn()
+ ->classes(
+ Selector::inNamespace('App\Communication\Infrastructure'),
+ Selector::inNamespace('App\Shared\Infrastructure'),
+ Selector::inNamespace('Symfony'),
+ Selector::inNamespace('Doctrine'),
+ Selector::inNamespace('ApiPlatform'),
+ )
+ ->because('Domain must be pure PHP without Infrastructure/framework dependencies');
+ }
+
+ public function test_administration_domain_should_not_depend_on_infrastructure(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Administration\Domain'))
+ ->shouldNotDependOn()
+ ->classes(
+ Selector::inNamespace('App\Administration\Infrastructure'),
+ Selector::inNamespace('App\Shared\Infrastructure'),
+ Selector::inNamespace('Symfony'),
+ Selector::inNamespace('Doctrine'),
+ Selector::inNamespace('ApiPlatform'),
+ )
+ ->because('Domain must be pure PHP without Infrastructure/framework dependencies');
+ }
+
+ public function test_scolarite_domain_should_not_depend_on_application(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Scolarite\Domain'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Scolarite\Application'))
+ ->because('Domain must not know about Application layer');
+ }
+
+ public function test_vie_scolaire_domain_should_not_depend_on_application(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\VieScolaire\Domain'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\VieScolaire\Application'))
+ ->because('Domain must not know about Application layer');
+ }
+
+ public function test_communication_domain_should_not_depend_on_application(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Communication\Domain'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Communication\Application'))
+ ->because('Domain must not know about Application layer');
+ }
+
+ public function test_administration_domain_should_not_depend_on_application(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Administration\Domain'))
+ ->shouldNotDependOn()
+ ->classes(Selector::inNamespace('App\Administration\Application'))
+ ->because('Domain must not know about Application layer');
+ }
+
+ public function test_shared_domain_should_be_pure(): BuildStep
+ {
+ return PHPat::rule()
+ ->classes(Selector::inNamespace('App\Shared\Domain'))
+ ->shouldNotDependOn()
+ ->classes(
+ Selector::inNamespace('Symfony'),
+ Selector::inNamespace('Doctrine'),
+ Selector::inNamespace('ApiPlatform'),
+ Selector::inNamespace('App\Shared\Infrastructure'),
+ Selector::inNamespace('App\Shared\Application'),
+ )
+ ->because('Shared Domain must be pure PHP');
+ }
+}
diff --git a/backend/tests/Integration/Shared/Infrastructure/Tenant/CrossTenantIsolationTest.php b/backend/tests/Integration/Shared/Infrastructure/Tenant/CrossTenantIsolationTest.php
new file mode 100644
index 0000000..54cdb7b
--- /dev/null
+++ b/backend/tests/Integration/Shared/Infrastructure/Tenant/CrossTenantIsolationTest.php
@@ -0,0 +1,293 @@
+tenantIdAlpha = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $this->tenantIdBeta = TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901');
+
+ $this->configAlpha = new TenantConfig(
+ tenantId: $this->tenantIdAlpha,
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
+ );
+ $this->configBeta = new TenantConfig(
+ tenantId: $this->tenantIdBeta,
+ subdomain: 'ecole-beta',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_beta',
+ );
+
+ $this->registry = new InMemoryTenantRegistry([$this->configAlpha, $this->configBeta]);
+ $this->context = new TenantContext();
+ $this->resolver = new TenantResolver($this->registry, self::BASE_DOMAIN);
+ $this->middleware = new TenantMiddleware($this->resolver, $this->context);
+ $this->voter = new TenantVoter($this->context);
+ }
+
+ #[Test]
+ public function userFromTenantACannotAccessResourceFromTenantB(): void
+ {
+ // User authenticates on tenant Alpha
+ $this->context->setCurrentTenant($this->configAlpha);
+
+ // Create a resource belonging to tenant Beta
+ $resourceFromBeta = $this->createTenantAwareResource($this->tenantIdBeta);
+
+ // Create authenticated user token
+ $user = $this->createMock(UserInterface::class);
+ $token = $this->createMock(TokenInterface::class);
+ $token->method('getUser')->willReturn($user);
+
+ // Vote should DENY access
+ $result = $this->voter->vote($token, $resourceFromBeta, [TenantVoter::ATTRIBUTE]);
+
+ self::assertSame(VoterInterface::ACCESS_DENIED, $result);
+ }
+
+ #[Test]
+ public function userFromTenantACanAccessOwnResources(): void
+ {
+ // User authenticates on tenant Alpha
+ $this->context->setCurrentTenant($this->configAlpha);
+
+ // Create a resource belonging to tenant Alpha
+ $resourceFromAlpha = $this->createTenantAwareResource($this->tenantIdAlpha);
+
+ // Create authenticated user token
+ $user = $this->createMock(UserInterface::class);
+ $token = $this->createMock(TokenInterface::class);
+ $token->method('getUser')->willReturn($user);
+
+ // Vote should GRANT access
+ $result = $this->voter->vote($token, $resourceFromAlpha, [TenantVoter::ATTRIBUTE]);
+
+ self::assertSame(VoterInterface::ACCESS_GRANTED, $result);
+ }
+
+ #[Test]
+ public function unknownSubdomainReturns404(): void
+ {
+ $request = Request::create('https://ecole-inexistant.classeo.local/api/dashboard');
+ $event = $this->createRequestEvent($request);
+
+ $this->middleware->onKernelRequest($event);
+
+ self::assertTrue($event->hasResponse());
+ self::assertSame(Response::HTTP_NOT_FOUND, $event->getResponse()?->getStatusCode());
+
+ // Verify error message is generic (no information leakage)
+ $content = json_decode((string) $event->getResponse()->getContent(), true);
+ self::assertSame('Resource not found', $content['message']);
+ self::assertArrayNotHasKey('subdomain', $content);
+ self::assertArrayNotHasKey('tenant', $content);
+ }
+
+ #[Test]
+ public function validSubdomainSetsContext(): void
+ {
+ $request = Request::create('https://ecole-alpha.classeo.local/api/dashboard');
+ $event = $this->createRequestEvent($request);
+
+ $this->middleware->onKernelRequest($event);
+
+ self::assertFalse($event->hasResponse()); // No error response
+ self::assertTrue($this->context->hasTenant());
+ self::assertTrue($this->tenantIdAlpha->equals($this->context->getCurrentTenantId()));
+ }
+
+ #[Test]
+ public function accessToResourceFromOtherTenantReturns404NotRevealingExistence(): void
+ {
+ // This test verifies the critical security requirement:
+ // When denied access to a resource from another tenant,
+ // the response MUST be 404 (not 403) to prevent enumeration attacks.
+
+ // User is authenticated on tenant Alpha
+ $this->context->setCurrentTenant($this->configAlpha);
+
+ // Resource exists in tenant Beta
+ $resourceFromBeta = $this->createTenantAwareResource($this->tenantIdBeta);
+
+ // Create authenticated user token
+ $user = $this->createMock(UserInterface::class);
+ $token = $this->createMock(TokenInterface::class);
+ $token->method('getUser')->willReturn($user);
+
+ // Vote should DENY (which will be converted to 404 by TenantAccessDeniedHandler)
+ $result = $this->voter->vote($token, $resourceFromBeta, [TenantVoter::ATTRIBUTE]);
+
+ // The voter returns DENIED, not ABSTAIN
+ // This ensures the access is actively denied rather than just not supported
+ self::assertSame(VoterInterface::ACCESS_DENIED, $result);
+
+ // The AccessDeniedHandler will convert this to 404
+ // (tested separately in TenantAccessDeniedHandler tests)
+ }
+
+ #[Test]
+ public function tenantContextIsClearedOnRequestTermination(): void
+ {
+ // Set a tenant
+ $this->context->setCurrentTenant($this->configAlpha);
+ self::assertTrue($this->context->hasTenant());
+
+ // Terminate request
+ $this->middleware->onKernelTerminate();
+
+ // Context should be cleared
+ self::assertFalse($this->context->hasTenant());
+ }
+
+ #[Test]
+ public function subdomainMismatchWithAuthenticatedUserFromDifferentTenant(): void
+ {
+ // User tries to access ecole-beta.classeo.local
+ $request = Request::create('https://ecole-beta.classeo.local/api/dashboard');
+ $event = $this->createRequestEvent($request);
+
+ $this->middleware->onKernelRequest($event);
+
+ // Context should be set to Beta (the subdomain in the request)
+ self::assertTrue($this->context->hasTenant());
+ self::assertTrue($this->tenantIdBeta->equals($this->context->getCurrentTenantId()));
+
+ // If a user's JWT was issued by Alpha but they're accessing Beta,
+ // the authentication layer should reject the token (not tested here,
+ // that's handled by JWT validation which checks tenant claims)
+ }
+
+ #[Test]
+ public function reservedSubdomainsAreRejected(): void
+ {
+ $reservedSubdomains = ['www', 'api', 'admin', 'static', 'cdn', 'mail'];
+
+ foreach ($reservedSubdomains as $subdomain) {
+ $this->context->clear();
+
+ $request = Request::create("https://{$subdomain}.classeo.local/api/test");
+ $event = $this->createRequestEvent($request);
+
+ $this->middleware->onKernelRequest($event);
+
+ self::assertTrue(
+ $event->hasResponse(),
+ "Expected 404 response for reserved subdomain: {$subdomain}"
+ );
+ self::assertSame(
+ Response::HTTP_NOT_FOUND,
+ $event->getResponse()?->getStatusCode(),
+ "Expected 404 for reserved subdomain: {$subdomain}"
+ );
+ }
+ }
+
+ #[Test]
+ public function testWithTwoTenantsMinimum(): void
+ {
+ // This test ensures we always test with at least 2 tenants
+ // as per the project's critical rules
+
+ // Verify both tenants are set up
+ self::assertTrue($this->registry->exists('ecole-alpha'));
+ self::assertTrue($this->registry->exists('ecole-beta'));
+
+ // Test Alpha isolation
+ $this->context->setCurrentTenant($this->configAlpha);
+ $resourceAlpha = $this->createTenantAwareResource($this->tenantIdAlpha);
+ $resourceBeta = $this->createTenantAwareResource($this->tenantIdBeta);
+
+ $user = $this->createMock(UserInterface::class);
+ $token = $this->createMock(TokenInterface::class);
+ $token->method('getUser')->willReturn($user);
+
+ self::assertSame(
+ VoterInterface::ACCESS_GRANTED,
+ $this->voter->vote($token, $resourceAlpha, [TenantVoter::ATTRIBUTE]),
+ 'Alpha user should access Alpha resource'
+ );
+ self::assertSame(
+ VoterInterface::ACCESS_DENIED,
+ $this->voter->vote($token, $resourceBeta, [TenantVoter::ATTRIBUTE]),
+ 'Alpha user should NOT access Beta resource'
+ );
+
+ // Test Beta isolation
+ $this->context->setCurrentTenant($this->configBeta);
+
+ self::assertSame(
+ VoterInterface::ACCESS_DENIED,
+ $this->voter->vote($token, $resourceAlpha, [TenantVoter::ATTRIBUTE]),
+ 'Beta user should NOT access Alpha resource'
+ );
+ self::assertSame(
+ VoterInterface::ACCESS_GRANTED,
+ $this->voter->vote($token, $resourceBeta, [TenantVoter::ATTRIBUTE]),
+ 'Beta user should access Beta resource'
+ );
+ }
+
+ private function createRequestEvent(Request $request): RequestEvent
+ {
+ $kernel = $this->createMock(HttpKernelInterface::class);
+
+ return new RequestEvent(
+ $kernel,
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ );
+ }
+
+ private function createTenantAwareResource(TenantId $tenantId): TenantAwareInterface
+ {
+ $resource = $this->createMock(TenantAwareInterface::class);
+ $resource->method('getTenantId')->willReturn($tenantId);
+
+ return $resource;
+ }
+}
diff --git a/backend/tests/Integration/Shared/Infrastructure/Tenant/TenantDatabaseCreationTest.php b/backend/tests/Integration/Shared/Infrastructure/Tenant/TenantDatabaseCreationTest.php
new file mode 100644
index 0000000..956cc04
--- /dev/null
+++ b/backend/tests/Integration/Shared/Infrastructure/Tenant/TenantDatabaseCreationTest.php
@@ -0,0 +1,117 @@
+createMock(Clock::class);
+ $clock->method('now')->willReturn(new DateTimeImmutable());
+
+ $ormConfig = new Configuration();
+ $ormConfig->setProxyDir(sys_get_temp_dir());
+ $ormConfig->setProxyNamespace('DoctrineProxies');
+ $ormConfig->setAutoGenerateProxyClasses(true);
+ $ormConfig->setMetadataDriverImpl(new AttributeDriver([]));
+ $ormConfig->enableNativeLazyObjects(true);
+
+ $factory = new TenantEntityManagerFactory($registry, $clock, $ormConfig);
+
+ $em = $factory->getForTenant($tenantId);
+
+ // Verify connection is working
+ $connection = $em->getConnection();
+ self::assertTrue($connection->isConnected() || $connection->connect());
+
+ // Verify we can execute queries
+ $result = $connection->executeQuery('SELECT 1 as test');
+ self::assertEquals(1, $result->fetchOne());
+ }
+
+ #[Test]
+ public function itMaintainsIsolationBetweenTenants(): void
+ {
+ $tenantIdAlpha = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $tenantIdBeta = TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901');
+
+ $configAlpha = new TenantConfig(
+ tenantId: $tenantIdAlpha,
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'sqlite:///:memory:',
+ );
+ $configBeta = new TenantConfig(
+ tenantId: $tenantIdBeta,
+ subdomain: 'ecole-beta',
+ databaseUrl: 'sqlite:///:memory:',
+ );
+
+ $registry = new InMemoryTenantRegistry([$configAlpha, $configBeta]);
+ $clock = $this->createMock(Clock::class);
+ $clock->method('now')->willReturn(new DateTimeImmutable());
+
+ $ormConfig = new Configuration();
+ $ormConfig->setProxyDir(sys_get_temp_dir());
+ $ormConfig->setProxyNamespace('DoctrineProxies');
+ $ormConfig->setAutoGenerateProxyClasses(true);
+ $ormConfig->setMetadataDriverImpl(new AttributeDriver([]));
+ $ormConfig->enableNativeLazyObjects(true);
+
+ $factory = new TenantEntityManagerFactory($registry, $clock, $ormConfig);
+
+ $emAlpha = $factory->getForTenant($tenantIdAlpha);
+ $emBeta = $factory->getForTenant($tenantIdBeta);
+
+ // Create table in Alpha
+ $emAlpha->getConnection()->executeStatement(
+ 'CREATE TABLE test_data (id INTEGER PRIMARY KEY, value TEXT)'
+ );
+ $emAlpha->getConnection()->executeStatement(
+ "INSERT INTO test_data (id, value) VALUES (1, 'alpha_data')"
+ );
+
+ // Create different table in Beta
+ $emBeta->getConnection()->executeStatement(
+ 'CREATE TABLE test_data (id INTEGER PRIMARY KEY, value TEXT)'
+ );
+ $emBeta->getConnection()->executeStatement(
+ "INSERT INTO test_data (id, value) VALUES (1, 'beta_data')"
+ );
+
+ // Verify isolation - each tenant sees only their data
+ $alphaValue = $emAlpha->getConnection()
+ ->executeQuery('SELECT value FROM test_data WHERE id = 1')
+ ->fetchOne();
+ $betaValue = $emBeta->getConnection()
+ ->executeQuery('SELECT value FROM test_data WHERE id = 1')
+ ->fetchOne();
+
+ self::assertSame('alpha_data', $alphaValue);
+ self::assertSame('beta_data', $betaValue);
+ self::assertNotSame($alphaValue, $betaValue);
+ }
+}
diff --git a/backend/tests/Unit/Shared/Infrastructure/Security/TenantAccessDeniedHandlerTest.php b/backend/tests/Unit/Shared/Infrastructure/Security/TenantAccessDeniedHandlerTest.php
new file mode 100644
index 0000000..d840730
--- /dev/null
+++ b/backend/tests/Unit/Shared/Infrastructure/Security/TenantAccessDeniedHandlerTest.php
@@ -0,0 +1,142 @@
+handler = new TenantAccessDeniedHandler();
+ }
+
+ #[Test]
+ public function itConvertsAccessDeniedExceptionForTenantAwareResourceTo404(): void
+ {
+ // Create a TenantAware resource
+ $resource = $this->createMock(TenantAwareInterface::class);
+ $resource->method('getTenantId')
+ ->willReturn(TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'));
+
+ // Create AccessDeniedException with TenantAware subject
+ $exception = new AccessDeniedException('Access Denied', null);
+ // Note: Symfony's AccessDeniedException doesn't have a public setSubject method,
+ // but it stores the subject internally. For this test, we'll use reflection.
+ $reflection = new ReflectionClass($exception);
+ $property = $reflection->getProperty('subject');
+ $property->setAccessible(true);
+ $property->setValue($exception, $resource);
+
+ $event = $this->createExceptionEvent($exception);
+
+ $this->handler->onKernelException($event);
+
+ // CRITICAL: Must return 404, not 403
+ self::assertTrue($event->hasResponse());
+ self::assertSame(Response::HTTP_NOT_FOUND, $event->getResponse()?->getStatusCode());
+
+ // Verify error message is generic (no information leakage)
+ $content = json_decode((string) $event->getResponse()->getContent(), true);
+ self::assertSame('Resource not found', $content['message']);
+ self::assertSame(Response::HTTP_NOT_FOUND, $content['status']);
+ self::assertSame('https://classeo.fr/errors/resource-not-found', $content['type']);
+
+ // Must NOT reveal tenant information
+ self::assertArrayNotHasKey('tenant', $content);
+ self::assertArrayNotHasKey('resource', $content);
+ }
+
+ #[Test]
+ public function itDoesNotModifyAccessDeniedExceptionForNonTenantAwareResources(): void
+ {
+ // Regular access denied (not tenant-related)
+ $exception = new AccessDeniedException('Access Denied');
+
+ $event = $this->createExceptionEvent($exception);
+
+ $this->handler->onKernelException($event);
+
+ // Should NOT set a response - let other handlers deal with it
+ self::assertFalse($event->hasResponse());
+ }
+
+ #[Test]
+ public function itIgnoresOtherExceptions(): void
+ {
+ // Non-AccessDeniedException
+ $exception = new RuntimeException('Some error');
+
+ $event = $this->createExceptionEvent($exception);
+
+ $this->handler->onKernelException($event);
+
+ // Should NOT set a response
+ self::assertFalse($event->hasResponse());
+ }
+
+ #[Test]
+ public function itReturns404NotRevealingResourceExistence(): void
+ {
+ // This test specifically validates the security requirement:
+ // 403 reveals resource exists, 404 hides it
+
+ $resource = $this->createMock(TenantAwareInterface::class);
+ $resource->method('getTenantId')
+ ->willReturn(TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901'));
+
+ $exception = new AccessDeniedException('You cannot access this resource', null);
+ $reflection = new ReflectionClass($exception);
+ $property = $reflection->getProperty('subject');
+ $property->setAccessible(true);
+ $property->setValue($exception, $resource);
+
+ $event = $this->createExceptionEvent($exception);
+
+ $this->handler->onKernelException($event);
+
+ $response = $event->getResponse();
+ self::assertNotNull($response);
+
+ // MUST be 404 (not 403) to prevent enumeration
+ self::assertSame(404, $response->getStatusCode());
+ self::assertNotSame(403, $response->getStatusCode(), 'SECURITY: Must be 404, not 403');
+ }
+
+ private function createExceptionEvent(Throwable $exception): ExceptionEvent
+ {
+ $kernel = $this->createMock(HttpKernelInterface::class);
+ $request = Request::create('/api/resource/123');
+
+ return new ExceptionEvent(
+ $kernel,
+ $request,
+ HttpKernelInterface::MAIN_REQUEST,
+ $exception
+ );
+ }
+}
diff --git a/backend/tests/Unit/Shared/Infrastructure/Security/TenantVoterTest.php b/backend/tests/Unit/Shared/Infrastructure/Security/TenantVoterTest.php
new file mode 100644
index 0000000..19b6cdb
--- /dev/null
+++ b/backend/tests/Unit/Shared/Infrastructure/Security/TenantVoterTest.php
@@ -0,0 +1,158 @@
+tenantContext = new TenantContext();
+ }
+
+ #[Test]
+ public function itAbstainsForNonTenantAwareSubjects(): void
+ {
+ $voter = new TenantVoter($this->tenantContext);
+
+ $token = $this->createMock(TokenInterface::class);
+ $subject = new stdClass();
+
+ $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]);
+
+ self::assertSame(VoterInterface::ACCESS_ABSTAIN, $result);
+ }
+
+ #[Test]
+ public function itAbstainsForNonTenantAccessAttributes(): void
+ {
+ $tenantIdString = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
+ $this->setCurrentTenant($tenantIdString, 'ecole-alpha');
+
+ $voter = new TenantVoter($this->tenantContext);
+
+ $user = $this->createMock(UserInterface::class);
+ $token = $this->createMock(TokenInterface::class);
+ $token->method('getUser')->willReturn($user);
+
+ $subject = $this->createTenantAwareSubject($tenantIdString);
+
+ // Voter should abstain for other attributes to not bypass other voters
+ foreach (['VIEW', 'EDIT', 'DELETE', 'ROLE_ADMIN'] as $attribute) {
+ $result = $voter->vote($token, $subject, [$attribute]);
+ self::assertSame(VoterInterface::ACCESS_ABSTAIN, $result, "Should abstain for: {$attribute}");
+ }
+ }
+
+ #[Test]
+ public function itDeniesAccessWhenUserNotAuthenticated(): void
+ {
+ $this->setCurrentTenant('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'ecole-alpha');
+
+ $voter = new TenantVoter($this->tenantContext);
+
+ $token = $this->createMock(TokenInterface::class);
+ $token->method('getUser')->willReturn(null);
+
+ $subject = $this->createTenantAwareSubject('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+
+ $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]);
+
+ self::assertSame(VoterInterface::ACCESS_DENIED, $result);
+ }
+
+ #[Test]
+ public function itGrantsAccessWhenSubjectBelongsToCurrentTenant(): void
+ {
+ $tenantIdString = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
+ $this->setCurrentTenant($tenantIdString, 'ecole-alpha');
+
+ $voter = new TenantVoter($this->tenantContext);
+
+ $user = $this->createMock(UserInterface::class);
+ $token = $this->createMock(TokenInterface::class);
+ $token->method('getUser')->willReturn($user);
+
+ $subject = $this->createTenantAwareSubject($tenantIdString);
+
+ $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]);
+
+ self::assertSame(VoterInterface::ACCESS_GRANTED, $result);
+ }
+
+ #[Test]
+ public function itDeniesAccessWhenSubjectBelongsToDifferentTenant(): void
+ {
+ // Current tenant is alpha
+ $this->setCurrentTenant('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'ecole-alpha');
+
+ $voter = new TenantVoter($this->tenantContext);
+
+ $user = $this->createMock(UserInterface::class);
+ $token = $this->createMock(TokenInterface::class);
+ $token->method('getUser')->willReturn($user);
+
+ // Subject belongs to beta tenant
+ $subject = $this->createTenantAwareSubject('b2c3d4e5-f6a7-8901-bcde-f12345678901');
+
+ $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]);
+
+ // Should be DENIED (will be converted to 404 by access denied handler)
+ self::assertSame(VoterInterface::ACCESS_DENIED, $result);
+ }
+
+ #[Test]
+ public function itDeniesAccessWhenNoTenantContextSet(): void
+ {
+ // Don't set any tenant context
+ $voter = new TenantVoter($this->tenantContext);
+
+ $user = $this->createMock(UserInterface::class);
+ $token = $this->createMock(TokenInterface::class);
+ $token->method('getUser')->willReturn($user);
+
+ $subject = $this->createTenantAwareSubject('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+
+ $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]);
+
+ self::assertSame(VoterInterface::ACCESS_DENIED, $result);
+ }
+
+ private function setCurrentTenant(string $tenantIdString, string $subdomain): void
+ {
+ $tenantId = TenantId::fromString($tenantIdString);
+ $config = new TenantConfig(
+ tenantId: $tenantId,
+ subdomain: $subdomain,
+ databaseUrl: "postgresql://user:pass@localhost:5432/classeo_{$subdomain}",
+ );
+ $this->tenantContext->setCurrentTenant($config);
+ }
+
+ private function createTenantAwareSubject(string $tenantIdString): TenantAwareInterface
+ {
+ $tenantId = TenantId::fromString($tenantIdString);
+
+ $subject = $this->createMock(TenantAwareInterface::class);
+ $subject->method('getTenantId')->willReturn($tenantId);
+
+ return $subject;
+ }
+}
diff --git a/backend/tests/Unit/Shared/Infrastructure/Tenant/InMemoryTenantRegistryTest.php b/backend/tests/Unit/Shared/Infrastructure/Tenant/InMemoryTenantRegistryTest.php
new file mode 100644
index 0000000..5aee414
--- /dev/null
+++ b/backend/tests/Unit/Shared/Infrastructure/Tenant/InMemoryTenantRegistryTest.php
@@ -0,0 +1,106 @@
+getConfig($tenantId);
+
+ self::assertSame($config, $result);
+ }
+
+ #[Test]
+ public function itReturnsConfigBySubdomain(): void
+ {
+ $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $config = new TenantConfig(
+ tenantId: $tenantId,
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
+ );
+
+ $registry = new InMemoryTenantRegistry([$config]);
+
+ $result = $registry->getBySubdomain('ecole-alpha');
+
+ self::assertSame($config, $result);
+ }
+
+ #[Test]
+ public function itThrowsExceptionForUnknownTenantId(): void
+ {
+ $registry = new InMemoryTenantRegistry([]);
+
+ $this->expectException(TenantNotFoundException::class);
+
+ $registry->getConfig(TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'));
+ }
+
+ #[Test]
+ public function itThrowsExceptionForUnknownSubdomain(): void
+ {
+ $registry = new InMemoryTenantRegistry([]);
+
+ $this->expectException(TenantNotFoundException::class);
+
+ $registry->getBySubdomain('ecole-inexistant');
+ }
+
+ #[Test]
+ public function itChecksIfTenantExists(): void
+ {
+ $config = new TenantConfig(
+ tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'),
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
+ );
+
+ $registry = new InMemoryTenantRegistry([$config]);
+
+ self::assertTrue($registry->exists('ecole-alpha'));
+ self::assertFalse($registry->exists('ecole-inexistant'));
+ }
+
+ #[Test]
+ public function itSupportsMultipleTenants(): void
+ {
+ $configAlpha = new TenantConfig(
+ tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'),
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
+ );
+ $configBeta = new TenantConfig(
+ tenantId: TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901'),
+ subdomain: 'ecole-beta',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_beta',
+ );
+
+ $registry = new InMemoryTenantRegistry([$configAlpha, $configBeta]);
+
+ self::assertSame($configAlpha, $registry->getBySubdomain('ecole-alpha'));
+ self::assertSame($configBeta, $registry->getBySubdomain('ecole-beta'));
+ }
+}
diff --git a/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantConfigTest.php b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantConfigTest.php
new file mode 100644
index 0000000..7403771
--- /dev/null
+++ b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantConfigTest.php
@@ -0,0 +1,48 @@
+equals($config->tenantId));
+ self::assertSame($subdomain, $config->subdomain);
+ self::assertSame($databaseUrl, $config->databaseUrl);
+ }
+
+ #[Test]
+ public function itIsImmutable(): void
+ {
+ $config = new TenantConfig(
+ tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'),
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
+ );
+
+ $reflection = new ReflectionClass($config);
+
+ self::assertTrue($reflection->isReadonly());
+ }
+}
diff --git a/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantContextTest.php b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantContextTest.php
new file mode 100644
index 0000000..2979f51
--- /dev/null
+++ b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantContextTest.php
@@ -0,0 +1,90 @@
+setCurrentTenant($config);
+
+ self::assertTrue($tenantId->equals($context->getCurrentTenantId()));
+ self::assertSame($config, $context->getCurrentTenantConfig());
+ }
+
+ #[Test]
+ public function itThrowsExceptionWhenNoTenantIsSet(): void
+ {
+ $context = new TenantContext();
+
+ $this->expectException(TenantNotSetException::class);
+
+ $context->getCurrentTenantId();
+ }
+
+ #[Test]
+ public function itThrowsExceptionWhenGettingConfigWithNoTenantSet(): void
+ {
+ $context = new TenantContext();
+
+ $this->expectException(TenantNotSetException::class);
+
+ $context->getCurrentTenantConfig();
+ }
+
+ #[Test]
+ public function itCanCheckIfTenantIsSet(): void
+ {
+ $context = new TenantContext();
+
+ self::assertFalse($context->hasTenant());
+
+ $config = new TenantConfig(
+ tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'),
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
+ );
+ $context->setCurrentTenant($config);
+
+ self::assertTrue($context->hasTenant());
+ }
+
+ #[Test]
+ public function itCanClearTenant(): void
+ {
+ $config = new TenantConfig(
+ tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'),
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
+ );
+
+ $context = new TenantContext();
+ $context->setCurrentTenant($config);
+
+ self::assertTrue($context->hasTenant());
+
+ $context->clear();
+
+ self::assertFalse($context->hasTenant());
+ }
+}
diff --git a/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantEntityManagerFactoryTest.php b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantEntityManagerFactoryTest.php
new file mode 100644
index 0000000..139695e
--- /dev/null
+++ b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantEntityManagerFactoryTest.php
@@ -0,0 +1,213 @@
+registry = $this->createMock(TenantRegistry::class);
+ $this->clock = $this->createMock(Clock::class);
+ $this->ormConfiguration = $this->createOrmConfiguration();
+ }
+
+ #[Test]
+ public function itReturnsEntityManagerForTenant(): void
+ {
+ $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $config = new TenantConfig(
+ tenantId: $tenantId,
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'sqlite:///:memory:',
+ );
+
+ $this->registry->method('getConfig')->with($tenantId)->willReturn($config);
+ $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00'));
+
+ $factory = new TenantEntityManagerFactory(
+ $this->registry,
+ $this->clock,
+ $this->ormConfiguration,
+ );
+
+ $entityManager = $factory->getForTenant($tenantId);
+
+ self::assertInstanceOf(EntityManagerInterface::class, $entityManager);
+ }
+
+ #[Test]
+ public function itReturnsSameEntityManagerForSameTenant(): void
+ {
+ $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $config = new TenantConfig(
+ tenantId: $tenantId,
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'sqlite:///:memory:',
+ );
+
+ $this->registry->method('getConfig')->with($tenantId)->willReturn($config);
+ $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00'));
+
+ $factory = new TenantEntityManagerFactory(
+ $this->registry,
+ $this->clock,
+ $this->ormConfiguration,
+ );
+
+ $em1 = $factory->getForTenant($tenantId);
+ $em2 = $factory->getForTenant($tenantId);
+
+ self::assertSame($em1, $em2);
+ }
+
+ #[Test]
+ public function itReturnsDifferentEntityManagersForDifferentTenants(): void
+ {
+ $tenantIdAlpha = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $tenantIdBeta = TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901');
+
+ $configAlpha = new TenantConfig(
+ tenantId: $tenantIdAlpha,
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'sqlite:///:memory:',
+ );
+ $configBeta = new TenantConfig(
+ tenantId: $tenantIdBeta,
+ subdomain: 'ecole-beta',
+ databaseUrl: 'sqlite:///:memory:',
+ );
+
+ $this->registry->method('getConfig')->willReturnMap([
+ [$tenantIdAlpha, $configAlpha],
+ [$tenantIdBeta, $configBeta],
+ ]);
+ $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00'));
+
+ $factory = new TenantEntityManagerFactory(
+ $this->registry,
+ $this->clock,
+ $this->ormConfiguration,
+ );
+
+ $emAlpha = $factory->getForTenant($tenantIdAlpha);
+ $emBeta = $factory->getForTenant($tenantIdBeta);
+
+ self::assertNotSame($emAlpha, $emBeta);
+ }
+
+ #[Test]
+ public function itReturnsCorrectPoolSize(): void
+ {
+ $tenantId1 = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $tenantId2 = TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901');
+
+ $config1 = new TenantConfig(
+ tenantId: $tenantId1,
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'sqlite:///:memory:',
+ );
+ $config2 = new TenantConfig(
+ tenantId: $tenantId2,
+ subdomain: 'ecole-beta',
+ databaseUrl: 'sqlite:///:memory:',
+ );
+
+ $this->registry->method('getConfig')->willReturnMap([
+ [$tenantId1, $config1],
+ [$tenantId2, $config2],
+ ]);
+ $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00'));
+
+ $factory = new TenantEntityManagerFactory(
+ $this->registry,
+ $this->clock,
+ $this->ormConfiguration,
+ );
+
+ self::assertSame(0, $factory->getPoolSize());
+
+ $factory->getForTenant($tenantId1);
+ self::assertSame(1, $factory->getPoolSize());
+
+ $factory->getForTenant($tenantId2);
+ self::assertSame(2, $factory->getPoolSize());
+
+ // Accessing same tenant shouldn't increase pool size
+ $factory->getForTenant($tenantId1);
+ self::assertSame(2, $factory->getPoolSize());
+ }
+
+ #[Test]
+ public function itEvictsIdleConnectionsAfterTimeout(): void
+ {
+ $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $config = new TenantConfig(
+ tenantId: $tenantId,
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'sqlite:///:memory:',
+ );
+
+ $this->registry->method('getConfig')->with($tenantId)->willReturn($config);
+
+ $initialTime = new DateTimeImmutable('2026-01-30 10:00:00');
+ $afterTimeout = new DateTimeImmutable('2026-01-30 10:06:00'); // 6 minutes later
+
+ $this->clock->method('now')->willReturnOnConsecutiveCalls(
+ $initialTime, // First call - eviction check
+ $initialTime, // Store lastUsed
+ $afterTimeout, // Second call - eviction check (finds idle)
+ $afterTimeout, // Store lastUsed for new manager
+ );
+
+ $factory = new TenantEntityManagerFactory(
+ $this->registry,
+ $this->clock,
+ $this->ormConfiguration,
+ );
+
+ $em1 = $factory->getForTenant($tenantId);
+ $em2 = $factory->getForTenant($tenantId);
+
+ // Due to idle eviction, we should have a new entity manager
+ self::assertNotSame($em1, $em2);
+ }
+
+ private function createOrmConfiguration(): Configuration
+ {
+ $config = new Configuration();
+ $config->setProxyDir(sys_get_temp_dir() . '/doctrine_proxies_' . uniqid());
+ $config->setProxyNamespace('DoctrineProxies');
+ $config->setAutoGenerateProxyClasses(true);
+ $config->setMetadataDriverImpl(new AttributeDriver([]));
+ $config->enableNativeLazyObjects(true);
+
+ return $config;
+ }
+}
diff --git a/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantIdTest.php b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantIdTest.php
new file mode 100644
index 0000000..be01592
--- /dev/null
+++ b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantIdTest.php
@@ -0,0 +1,62 @@
+equals($tenantId2));
+ }
+
+ #[Test]
+ public function twoTenantIdsWithDifferentValuesAreNotEqual(): void
+ {
+ $tenantId1 = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $tenantId2 = TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901');
+
+ self::assertFalse($tenantId1->equals($tenantId2));
+ }
+
+ #[Test]
+ public function itCanBeConvertedToString(): void
+ {
+ $uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
+ $tenantId = TenantId::fromString($uuid);
+
+ self::assertSame($uuid, (string) $tenantId);
+ }
+}
diff --git a/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantMiddlewareTest.php b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantMiddlewareTest.php
new file mode 100644
index 0000000..e72b2f5
--- /dev/null
+++ b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantMiddlewareTest.php
@@ -0,0 +1,141 @@
+resolver = $this->createMock(TenantResolver::class);
+ $this->context = new TenantContext();
+ }
+
+ #[Test]
+ public function itSetsTenantContextForValidTenant(): void
+ {
+ $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $config = new TenantConfig(
+ tenantId: $tenantId,
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
+ );
+
+ $this->resolver->method('resolve')
+ ->with('ecole-alpha.classeo.local')
+ ->willReturn($config);
+
+ $middleware = new TenantMiddleware($this->resolver, $this->context);
+
+ $request = Request::create('https://ecole-alpha.classeo.local/api/test');
+ $event = $this->createRequestEvent($request);
+
+ $middleware->onKernelRequest($event);
+
+ self::assertTrue($this->context->hasTenant());
+ self::assertTrue($tenantId->equals($this->context->getCurrentTenantId()));
+ }
+
+ #[Test]
+ public function itReturns404ForNonExistentTenant(): void
+ {
+ $this->resolver->method('resolve')
+ ->with('ecole-inexistant.classeo.local')
+ ->willThrowException(TenantNotFoundException::withSubdomain('ecole-inexistant'));
+
+ $middleware = new TenantMiddleware($this->resolver, $this->context);
+
+ $request = Request::create('https://ecole-inexistant.classeo.local/api/test');
+ $event = $this->createRequestEvent($request);
+
+ $middleware->onKernelRequest($event);
+
+ self::assertTrue($event->hasResponse());
+ self::assertSame(Response::HTTP_NOT_FOUND, $event->getResponse()?->getStatusCode());
+ }
+
+ #[Test]
+ public function itReturnsGenericErrorMessageFor404(): void
+ {
+ $this->resolver->method('resolve')
+ ->willThrowException(TenantNotFoundException::withSubdomain('test'));
+
+ $middleware = new TenantMiddleware($this->resolver, $this->context);
+
+ $request = Request::create('https://test.classeo.local/api/test');
+ $event = $this->createRequestEvent($request);
+
+ $middleware->onKernelRequest($event);
+
+ $response = $event->getResponse();
+ self::assertNotNull($response);
+
+ $content = json_decode((string) $response->getContent(), true);
+ self::assertSame('Resource not found', $content['message']);
+ self::assertArrayNotHasKey('subdomain', $content);
+ }
+
+ #[Test]
+ public function itClearsTenantContextOnTerminate(): void
+ {
+ $config = new TenantConfig(
+ tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'),
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
+ );
+
+ $this->context->setCurrentTenant($config);
+ self::assertTrue($this->context->hasTenant());
+
+ $middleware = new TenantMiddleware($this->resolver, $this->context);
+ $middleware->onKernelTerminate();
+
+ self::assertFalse($this->context->hasTenant());
+ }
+
+ #[Test]
+ public function itRegistersCorrectEvents(): void
+ {
+ $events = TenantMiddleware::getSubscribedEvents();
+
+ self::assertArrayHasKey(KernelEvents::REQUEST, $events);
+ self::assertArrayHasKey(KernelEvents::TERMINATE, $events);
+
+ // Request listener should have high priority to run early
+ $requestConfig = $events[KernelEvents::REQUEST];
+ self::assertIsArray($requestConfig);
+ self::assertSame('onKernelRequest', $requestConfig[0]);
+ self::assertGreaterThan(0, $requestConfig[1]); // High priority
+ }
+
+ private function createRequestEvent(Request $request): RequestEvent
+ {
+ $kernel = $this->createMock(HttpKernelInterface::class);
+
+ return new RequestEvent(
+ $kernel,
+ $request,
+ HttpKernelInterface::MAIN_REQUEST
+ );
+ }
+}
diff --git a/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantResolverTest.php b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantResolverTest.php
new file mode 100644
index 0000000..f147081
--- /dev/null
+++ b/backend/tests/Unit/Shared/Infrastructure/Tenant/TenantResolverTest.php
@@ -0,0 +1,145 @@
+registry = $this->createMock(TenantRegistry::class);
+ }
+
+ #[Test]
+ public function itExtractsSubdomainFromHost(): void
+ {
+ $resolver = new TenantResolver($this->registry, 'classeo.local');
+
+ $subdomain = $resolver->extractSubdomain('ecole-alpha.classeo.local');
+
+ self::assertSame('ecole-alpha', $subdomain);
+ }
+
+ #[Test]
+ public function itExtractsSubdomainFromHostWithPort(): void
+ {
+ $resolver = new TenantResolver($this->registry, 'classeo.local');
+
+ $subdomain = $resolver->extractSubdomain('ecole-alpha.classeo.local:8080');
+
+ self::assertSame('ecole-alpha', $subdomain);
+ }
+
+ #[Test]
+ public function itReturnsNullForMainDomainWithoutSubdomain(): void
+ {
+ $resolver = new TenantResolver($this->registry, 'classeo.local');
+
+ $subdomain = $resolver->extractSubdomain('classeo.local');
+
+ self::assertNull($subdomain);
+ }
+
+ #[Test]
+ public function itReturnsNullForWwwSubdomain(): void
+ {
+ $resolver = new TenantResolver($this->registry, 'classeo.local');
+
+ $subdomain = $resolver->extractSubdomain('www.classeo.local');
+
+ self::assertNull($subdomain);
+ }
+
+ #[Test]
+ public function itReturnsNullForApiSubdomain(): void
+ {
+ $resolver = new TenantResolver($this->registry, 'classeo.local');
+
+ $subdomain = $resolver->extractSubdomain('api.classeo.local');
+
+ self::assertNull($subdomain);
+ }
+
+ #[Test]
+ public function itResolvesValidTenantFromHost(): void
+ {
+ $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
+ $config = new TenantConfig(
+ tenantId: $tenantId,
+ subdomain: 'ecole-alpha',
+ databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
+ );
+
+ $this->registry->method('getBySubdomain')
+ ->with('ecole-alpha')
+ ->willReturn($config);
+
+ $resolver = new TenantResolver($this->registry, 'classeo.local');
+
+ $resolved = $resolver->resolve('ecole-alpha.classeo.local');
+
+ self::assertTrue($tenantId->equals($resolved->tenantId));
+ }
+
+ #[Test]
+ public function itThrowsExceptionForNonExistentTenant(): void
+ {
+ $this->registry->method('getBySubdomain')
+ ->with('ecole-inexistant')
+ ->willThrowException(TenantNotFoundException::withSubdomain('ecole-inexistant'));
+
+ $resolver = new TenantResolver($this->registry, 'classeo.local');
+
+ $this->expectException(TenantNotFoundException::class);
+
+ $resolver->resolve('ecole-inexistant.classeo.local');
+ }
+
+ #[Test]
+ public function itThrowsExceptionWhenNoSubdomainInHost(): void
+ {
+ $resolver = new TenantResolver($this->registry, 'classeo.local');
+
+ $this->expectException(TenantNotFoundException::class);
+
+ $resolver->resolve('classeo.local');
+ }
+
+ #[Test]
+ #[DataProvider('reservedSubdomainsProvider')]
+ public function itRejectsReservedSubdomains(string $subdomain): void
+ {
+ $resolver = new TenantResolver($this->registry, 'classeo.local');
+
+ $this->expectException(TenantNotFoundException::class);
+
+ $resolver->resolve("{$subdomain}.classeo.local");
+ }
+
+ /**
+ * @return iterable
+ */
+ public static function reservedSubdomainsProvider(): iterable
+ {
+ yield 'www' => ['www'];
+ yield 'api' => ['api'];
+ yield 'admin' => ['admin'];
+ yield 'static' => ['static'];
+ yield 'cdn' => ['cdn'];
+ yield 'mail' => ['mail'];
+ }
+}
diff --git a/compose.yaml b/compose.yaml
index 023a96a..7f0962c 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -8,22 +8,28 @@ services:
dockerfile: Dockerfile
target: dev
container_name: classeo_php
+ # FrankenPHP charge les variables d'environnement système AVANT que Symfony
+ # ne parse le fichier .env. Sans env_file, les variables du .env ne seraient
+ # pas disponibles au démarrage de FrankenPHP.
+ # Avantage : une seule source de vérité (.env), pas de duplication.
+ # Note : les variables dans 'environment:' ci-dessous écrasent celles du .env
+ env_file:
+ - ./backend/.env
environment:
- APP_ENV: dev
- APP_DEBUG: 1
+ # Overrides pour Docker : les hostnames des services utilisent les noms
+ # des containers (db, redis, rabbitmq...) au lieu de localhost
DATABASE_URL: postgresql://classeo:classeo@db:5432/classeo_master?serverVersion=18&charset=utf8
REDIS_URL: redis://redis:6379
MESSENGER_TRANSPORT_DSN: amqp://guest:guest@rabbitmq:5672/%2f/messages
MERCURE_URL: http://mercure/.well-known/mercure
- MERCURE_PUBLIC_URL: http://localhost:3000/.well-known/mercure
- MERCURE_JWT_SECRET: mercure_publisher_secret_change_me_in_production
MEILISEARCH_URL: http://meilisearch:7700
- MEILISEARCH_API_KEY: masterKey
MAILER_DSN: smtp://mailpit:1025
ports:
- "18000:8000" # Port externe 18000 pour eviter conflit
volumes:
- ./backend:/app:cached
+ - caddy_data:/data
+ - caddy_config:/config
depends_on:
db:
condition: service_healthy
@@ -49,7 +55,10 @@ services:
target: dev
container_name: classeo_frontend
environment:
+ # URL de fallback, sera remplacée dynamiquement par le hostname en multi-tenant
PUBLIC_API_URL: http://localhost:18000/api
+ PUBLIC_API_PORT: "18000"
+ PUBLIC_BASE_DOMAIN: classeo.local
PUBLIC_MERCURE_URL: http://localhost:3000/.well-known/mercure
ports:
- "5174:5173" # Port externe 5174 pour eviter conflit
@@ -197,3 +206,5 @@ volumes:
rabbitmq_data:
meilisearch_data:
frontend_node_modules:
+ caddy_data:
+ caddy_config:
diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md
new file mode 100644
index 0000000..fa95e85
--- /dev/null
+++ b/docs/DEPLOYMENT.md
@@ -0,0 +1,182 @@
+# Déploiement en Production
+
+## Architecture Multi-tenant
+
+Classeo utilise une architecture multi-tenant où chaque école a son propre sous-domaine :
+- `ecole-alpha.classeo.fr`
+- `ecole-beta.classeo.fr`
+
+## Différences Dev vs Prod
+
+| Aspect | Dev | Prod |
+|--------|-----|------|
+| Domaine | `classeo.local` | `classeo.fr` |
+| Frontend | `:5174` | même domaine |
+| API | `:18000/api` | `/api` (même domaine) |
+| HTTPS | Non | Oui (obligatoire) |
+| Reverse proxy | Non | Oui |
+| Base de données | Une seule (SQLite/PostgreSQL) | Une par tenant |
+
+## Configuration Reverse Proxy
+
+En production, un reverse proxy route les requêtes sur le même domaine :
+- `ecole-alpha.classeo.fr/` → Frontend (SvelteKit)
+- `ecole-alpha.classeo.fr/api` → Backend (FrankenPHP)
+
+### Option recommandée : Caddy (intégré à FrankenPHP)
+
+```caddyfile
+# Caddyfile pour production
+*.classeo.fr {
+ # Certificats SSL automatiques via Let's Encrypt
+ tls {
+ dns cloudflare {env.CLOUDFLARE_API_TOKEN}
+ }
+
+ # API routes
+ handle /api/* {
+ reverse_proxy php:8000
+ }
+
+ # Frontend
+ handle {
+ reverse_proxy frontend:3000
+ }
+}
+```
+
+### Alternative : nginx
+
+```nginx
+server {
+ listen 443 ssl http2;
+ server_name ~^(?.+)\.classeo\.fr$;
+
+ ssl_certificate /etc/letsencrypt/live/classeo.fr/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/classeo.fr/privkey.pem;
+
+ location /api {
+ proxy_pass http://php:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location / {
+ proxy_pass http://frontend:3000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+}
+```
+
+## Variables d'environnement Production
+
+### Backend (.env.prod)
+
+```env
+APP_ENV=prod
+APP_DEBUG=0
+APP_SECRET=
+
+TRUSTED_HOSTS=^(.+\.)?classeo\.fr$
+TRUSTED_PROXIES=REMOTE_ADDR
+
+TENANT_BASE_DOMAIN=classeo.fr
+
+DATABASE_URL=postgresql://user:password@db-host:5432/classeo_master
+
+REDIS_URL=redis://redis-host:6379
+MESSENGER_TRANSPORT_DSN=amqp://user:password@rabbitmq-host:5672/%2f/messages
+
+# JWT
+JWT_PASSPHRASE=
+```
+
+### Frontend
+
+```env
+PUBLIC_API_URL=https://classeo.fr/api
+PUBLIC_BASE_DOMAIN=classeo.fr
+PUBLIC_MERCURE_URL=https://classeo.fr/.well-known/mercure
+```
+
+## Certificats SSL
+
+### Wildcard avec Let's Encrypt + Cloudflare DNS
+
+Pour les sous-domaines dynamiques, un certificat wildcard est nécessaire :
+
+```bash
+# Avec certbot et Cloudflare DNS
+certbot certonly \
+ --dns-cloudflare \
+ --dns-cloudflare-credentials /etc/cloudflare.ini \
+ -d classeo.fr \
+ -d "*.classeo.fr"
+```
+
+### Avec Caddy (automatique)
+
+Caddy gère automatiquement les certificats wildcard si vous configurez un provider DNS.
+
+## Base de données par tenant
+
+Chaque tenant a sa propre base de données PostgreSQL :
+- `classeo_tenant_alpha`
+- `classeo_tenant_beta`
+
+### Création d'un nouveau tenant
+
+```bash
+# 1. Créer la base de données
+php bin/console tenant:database:create classeo_tenant_
+
+# 2. Exécuter les migrations
+php bin/console tenant:migrate
+
+# 3. Ajouter le tenant au registry (ou en base master)
+```
+
+## Options de déploiement
+
+### 1. VPS simple (petit volume)
+
+- Un serveur avec Docker Compose
+- Convient pour < 50 écoles
+- Coût : ~20-50€/mois
+
+### 2. Docker Swarm (moyen volume)
+
+- Plusieurs serveurs avec orchestration
+- Scaling horizontal
+- Convient pour 50-500 écoles
+
+### 3. Kubernetes (grand volume)
+
+- Orchestration avancée
+- Auto-scaling
+- Convient pour 500+ écoles
+- Coût plus élevé, complexité accrue
+
+## Checklist de mise en production
+
+- [ ] Configurer le domaine DNS (wildcard `*.classeo.fr`)
+- [ ] Obtenir certificat SSL wildcard
+- [ ] Configurer le reverse proxy (Caddy ou nginx)
+- [ ] Configurer les variables d'environnement prod
+- [ ] Générer les clés JWT de production
+- [ ] Configurer la base de données master
+- [ ] Créer les bases de données tenant
+- [ ] Configurer les backups automatiques
+- [ ] Configurer le monitoring (logs, métriques)
+- [ ] Tester le déploiement sur un environnement staging
+- [ ] Configurer CI/CD pour les déploiements automatiques
+
+## TODO
+
+- [ ] Créer un `compose.prod.yaml` pour la production
+- [ ] Script de création automatique de tenant
+- [ ] Interface admin pour gérer les tenants
+- [ ] Monitoring et alerting
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index f48d5d7..4742fa6 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -9,6 +9,10 @@ FROM node:22-alpine AS base
# Install pnpm
RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
+# Configure pnpm to use a directory inside the project (works with volume mounts)
+ENV PNPM_HOME=/app/.pnpm-store
+ENV PATH="$PNPM_HOME:$PATH"
+
# Set working directory
WORKDIR /app
@@ -17,15 +21,57 @@ WORKDIR /app
# =============================================================================
FROM base AS dev
-# Create entrypoint script for dev (installs deps if needed)
-RUN echo '#!/bin/sh' > /usr/local/bin/docker-entrypoint.sh && \
- echo 'set -e' >> /usr/local/bin/docker-entrypoint.sh && \
- echo 'if [ ! -d /app/node_modules ] || [ ! -f /app/node_modules/.pnpm/lock.yaml ]; then' >> /usr/local/bin/docker-entrypoint.sh && \
- echo ' echo "Installing pnpm dependencies..."' >> /usr/local/bin/docker-entrypoint.sh && \
- echo ' pnpm install' >> /usr/local/bin/docker-entrypoint.sh && \
- echo 'fi' >> /usr/local/bin/docker-entrypoint.sh && \
- echo 'exec "$@"' >> /usr/local/bin/docker-entrypoint.sh && \
- chmod +x /usr/local/bin/docker-entrypoint.sh
+# Install gosu for proper user switching
+ENV GOSU_VERSION=1.17
+RUN set -eux; \
+ apk add --no-cache --virtual .gosu-deps dpkg gnupg; \
+ dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
+ wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
+ chmod +x /usr/local/bin/gosu; \
+ gosu --version; \
+ gosu nobody true; \
+ apk del --no-network .gosu-deps
+
+# Entrypoint: detect host UID/GID and run as matching user
+# Uses gosu with UID:GID directly (no need to create user in Dockerfile)
+COPY --chmod=755 <<'EOF' /usr/local/bin/docker-entrypoint.sh
+#!/bin/sh
+set -e
+
+# Detect UID/GID from mounted /app directory
+HOST_UID=$(stat -c %u /app)
+HOST_GID=$(stat -c %g /app)
+
+# If root owns /app, run as root (CI environment or volume not mounted)
+if [ "$HOST_UID" = "0" ]; then
+ # Install dependencies if not present
+ if [ ! -d /app/node_modules ] || [ ! -f /app/node_modules/.pnpm/lock.yaml ]; then
+ echo "Installing pnpm dependencies..."
+ pnpm install
+ fi
+ exec "$@"
+fi
+
+# Fix node_modules volume ownership (Docker creates volumes as root)
+# This only takes time on first run when the volume is empty
+if [ -d /app/node_modules ] && [ "$(stat -c %u /app/node_modules)" = "0" ]; then
+ echo "Fixing node_modules ownership..."
+ chown -R "$HOST_UID:$HOST_GID" /app/node_modules
+fi
+
+# Ensure pnpm store directory exists and is writable
+mkdir -p /app/.pnpm-store
+chown "$HOST_UID:$HOST_GID" /app/.pnpm-store
+
+# Install pnpm dependencies if not present (as host user)
+if [ ! -d /app/node_modules/.pnpm ]; then
+ echo "Installing pnpm dependencies..."
+ gosu "$HOST_UID:$HOST_GID" pnpm install
+fi
+
+# Run command as host user via gosu (using UID:GID directly)
+exec gosu "$HOST_UID:$HOST_GID" "$@"
+EOF
EXPOSE 5173
diff --git a/frontend/src/lib/api/config.ts b/frontend/src/lib/api/config.ts
new file mode 100644
index 0000000..4a3fbbe
--- /dev/null
+++ b/frontend/src/lib/api/config.ts
@@ -0,0 +1,62 @@
+import { browser } from '$app/environment';
+import { env } from '$env/dynamic/public';
+
+/**
+ * Construit l'URL de base de l'API en fonction du hostname actuel.
+ *
+ * En multi-tenant, l'API utilise le même sous-domaine que le frontend
+ * pour garantir l'isolation des données par établissement.
+ *
+ * Exemples :
+ * - Dev: ecole-alpha.classeo.local:5174 -> ecole-alpha.classeo.local:18000/api
+ * - Prod: ecole-alpha.classeo.fr -> ecole-alpha.classeo.fr/api (relative)
+ */
+export function getApiBaseUrl(): string {
+ // Côté browser : toujours utiliser le hostname actuel pour préserver le tenant
+ if (browser) {
+ const { hostname, protocol } = window.location;
+
+ // En prod (pas de port API séparé), utiliser une URL relative
+ // Cela préserve automatiquement le sous-domaine du tenant
+ if (!env['PUBLIC_API_PORT']) {
+ return '/api';
+ }
+
+ // En dev, construire l'URL avec le hostname actuel et le port API
+ const apiPort = env['PUBLIC_API_PORT'];
+ return `${protocol}//${hostname}:${apiPort}/api`;
+ }
+
+ // SSR : utiliser PUBLIC_API_URL si défini, sinon fallback interne
+ if (env['PUBLIC_API_URL']) {
+ return env['PUBLIC_API_URL'];
+ }
+
+ // SSR fallback : communication interne Docker
+ return 'http://php:8000/api';
+}
+
+/**
+ * Extrait le sous-domaine (tenant) du hostname actuel.
+ *
+ * Exemple : ecole-alpha.classeo.local -> ecole-alpha
+ */
+export function getCurrentTenant(): string | null {
+ if (!browser) return null;
+
+ const hostname = window.location.hostname;
+ const baseDomain = env['PUBLIC_BASE_DOMAIN'] || 'classeo.local';
+
+ if (!hostname.endsWith(baseDomain)) {
+ return null;
+ }
+
+ const subdomain = hostname.replace(`.${baseDomain}`, '');
+
+ // Pas de sous-domaine ou sous-domaine réservé
+ if (!subdomain || subdomain === hostname || subdomain === 'www') {
+ return null;
+ }
+
+ return subdomain;
+}
diff --git a/frontend/src/lib/api/index.ts b/frontend/src/lib/api/index.ts
new file mode 100644
index 0000000..8690eca
--- /dev/null
+++ b/frontend/src/lib/api/index.ts
@@ -0,0 +1 @@
+export { getApiBaseUrl, getCurrentTenant } from './config';
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 1e9f39a..c98678e 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -69,6 +69,8 @@ export default defineConfig({
server: {
host: '0.0.0.0',
port: 5173,
- strictPort: true
+ strictPort: true,
+ // Autorise les sous-domaines pour le multi-tenant (dev + prod)
+ allowedHosts: ['.classeo.local', '.classeo.fr', 'localhost']
}
});
diff --git a/scripts/check-tenants.sh b/scripts/check-tenants.sh
new file mode 100755
index 0000000..1fa79c2
--- /dev/null
+++ b/scripts/check-tenants.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+
+# =============================================================================
+# Vérifie que les tenants de dev répondent correctement
+# Usage: ./scripts/check-tenants.sh
+# =============================================================================
+
+set -e
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+TENANTS=("ecole-alpha" "ecole-beta")
+BASE_DOMAIN="classeo.local"
+API_PORT="18000"
+FRONTEND_PORT="5174"
+
+echo "🔍 Vérification des tenants de développement..."
+echo ""
+
+# Vérifier /etc/hosts
+echo "📋 Vérification de /etc/hosts..."
+MISSING_HOSTS=()
+for tenant in "${TENANTS[@]}"; do
+ if ! grep -q "${tenant}.${BASE_DOMAIN}" /etc/hosts 2>/dev/null; then
+ MISSING_HOSTS+=("${tenant}.${BASE_DOMAIN}")
+ fi
+done
+
+if [ ${#MISSING_HOSTS[@]} -ne 0 ]; then
+ echo -e "${YELLOW}⚠️ Entrées manquantes dans /etc/hosts:${NC}"
+ echo ""
+ echo " Ajoutez cette ligne à /etc/hosts :"
+ echo -e "${YELLOW} 127.0.0.1 classeo.local ${MISSING_HOSTS[*]}${NC}"
+ echo ""
+ echo " Commande :"
+ echo " sudo sh -c 'echo \"127.0.0.1 classeo.local ${MISSING_HOSTS[*]}\" >> /etc/hosts'"
+ echo ""
+ exit 1
+fi
+echo -e "${GREEN}✓ /etc/hosts configuré correctement${NC}"
+echo ""
+
+# Vérifier que les containers tournent
+echo "🐳 Vérification des containers..."
+if ! docker compose ps --status running | grep -q "classeo_php"; then
+ echo -e "${RED}✗ Container PHP non démarré. Lancez 'make up'${NC}"
+ exit 1
+fi
+if ! docker compose ps --status running | grep -q "classeo_frontend"; then
+ echo -e "${RED}✗ Container Frontend non démarré. Lancez 'make up'${NC}"
+ exit 1
+fi
+echo -e "${GREEN}✓ Containers démarrés${NC}"
+echo ""
+
+# Vérifier les endpoints
+echo "🌐 Vérification des endpoints..."
+ERRORS=0
+
+for tenant in "${TENANTS[@]}"; do
+ # API
+ API_URL="http://${tenant}.${BASE_DOMAIN}:${API_PORT}/api/docs"
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$API_URL" 2>/dev/null || echo "000")
+
+ if [ "$HTTP_CODE" = "200" ]; then
+ echo -e "${GREEN}✓ API ${tenant}: ${API_URL}${NC}"
+ else
+ echo -e "${RED}✗ API ${tenant}: ${API_URL} (HTTP ${HTTP_CODE})${NC}"
+ ERRORS=$((ERRORS + 1))
+ fi
+
+ # Frontend
+ FRONTEND_URL="http://${tenant}.${BASE_DOMAIN}:${FRONTEND_PORT}/"
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$FRONTEND_URL" 2>/dev/null || echo "000")
+
+ if [ "$HTTP_CODE" = "200" ]; then
+ echo -e "${GREEN}✓ Front ${tenant}: ${FRONTEND_URL}${NC}"
+ else
+ echo -e "${RED}✗ Front ${tenant}: ${FRONTEND_URL} (HTTP ${HTTP_CODE})${NC}"
+ ERRORS=$((ERRORS + 1))
+ fi
+done
+
+echo ""
+
+if [ $ERRORS -ne 0 ]; then
+ echo -e "${RED}❌ ${ERRORS} erreur(s) détectée(s)${NC}"
+ exit 1
+fi
+
+echo -e "${GREEN}✅ Tous les tenants répondent correctement !${NC}"