diff --git a/.docker/Dockerfile b/.docker/Dockerfile deleted file mode 100755 index b6e0318..0000000 --- a/.docker/Dockerfile +++ /dev/null @@ -1,145 +0,0 @@ -#---------------------------------------------------------------------- -# Download vendor from composer.json in dedicated layer -#---------------------------------------------------------------------- -ARG registry=docker.io -FROM ${registry}/library/composer:latest AS vendor - -RUN mkdir -p /opt/validator-api -WORKDIR /opt/validator-api -COPY composer.json . -RUN composer install --no-scripts --prefer-dist --ignore-platform-req=ext-pcntl - -#---------------------------------------------------------------------- -# Create application container -#---------------------------------------------------------------------- -FROM ${registry}/library/ubuntu:24.04 - -ENV DEBIAN_FRONTEND=noninteractive -ENV LANG=fr_FR.UTF-8 -ENV VALIDATOR_PATH=/opt/ign-validator/validator-cli.jar - -#---------------------------------------------------------------------- -# Configure locale to fr_FR.UTF-8 -# see also https://stackoverflow.com/a/41797247 -#---------------------------------------------------------------------- -RUN apt-get update && apt-get install --no-install-recommends -y locales \ - && sed -i -e 's/# en_US.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' /etc/locale.gen \ - && dpkg-reconfigure locales \ - && update-locale LANG=fr_FR.UTF-8 \ - && rm -rf /var/lib/apt/lists/* - -#---------------------------------------------------------------------- -# Install common tools -#---------------------------------------------------------------------- -RUN apt-get update \ - && apt-get install --no-install-recommends -y \ - unzip zip \ - curl wget \ - file \ - postgresql-client \ - && rm -rf /var/lib/apt/lists/* - - -#------------------------------------------------------------------------ -# Configure https://packages.sury.org/php/ to get latests PHP versions -#------------------------------------------------------------------------ -RUN apt-get update \ - && apt-get install --no-install-recommends -y gnupg2 software-properties-common \ - && add-apt-repository -y ppa:ondrej/php \ - && apt-get remove -y software-properties-common \ - && rm -rf /var/lib/apt/lists/* - -#---------------------------------------------------------------------- -# Install Apache, PHP and its extensions -#---------------------------------------------------------------------- -RUN apt-get update \ - && apt-get install --no-install-recommends -y \ - apache2 php8.4 libapache2-mod-php8.4 \ - php8.4-opcache php8.4-xml \ - php8.4-pdo php8.4-pgsql php8.4-zip \ - php8.4-curl \ - && rm -rf /var/lib/apt/lists/* - -#------------------------------------------------------------------------ -# Add helper script to start apache -# (see https://github.com/docker-library/php) -#------------------------------------------------------------------------ -COPY .docker/apache2-foreground /usr/local/bin/apache2-foreground -RUN chmod +x /usr/local/bin/apache2-foreground - -#------------------------------------------------------------------------ -# Create apache2 repository -# (see https://github.com/docker-library/php) -#------------------------------------------------------------------------ -RUN mkdir -p /var/run/apache2 && chown -R www-data:www-data /var/run/apache2 \ - && mkdir -p /var/lock/apache2 && chown -R www-data:www-data /var/lock/apache2 \ - && mkdir -p /var/log/apache2 && chown -R www-data:www-data /var/log/apache2 - -#------------------------------------------------------------------------ -# Redirects logs to stdout / stderr -# (see https://github.com/docker-library/php) -#------------------------------------------------------------------------ -RUN ln -sfT /dev/stderr "/var/log/apache2/error.log" \ - && ln -sfT /dev/stdout "/var/log/apache2/access.log" \ - && ln -sfT /dev/stdout "/var/log/apache2/other_vhosts_access.log" \ - && chown www-data:www-data /var/log/apache2/*.log - -#---------------------------------------------------------------------- -# Configure PHP -#---------------------------------------------------------------------- -COPY .docker/php.ini /etc/php/8.4/apache2/conf.d/99-app.ini -COPY .docker/php.ini /etc/php/8.4/cli/conf.d/99-app.ini - -#---------------------------------------------------------------------- -# Configure apache -#---------------------------------------------------------------------- -COPY .docker/apache-ports.conf /etc/apache2/ports.conf -COPY .docker/apache-security.conf /etc/apache2/conf-enabled/security.conf -COPY .docker/apache-vhost.conf /etc/apache2/sites-available/000-default.conf - -RUN a2enmod rewrite remoteip - -#---------------------------------------------------------------------- -# Setup validator-cli.jar dependencies (java & ogr2ogr) -#---------------------------------------------------------------------- -RUN apt-get update -qq \ - # see https://github.com/debuerreotype/docker-debian-artifacts/issues/24 - && mkdir -p /usr/share/man/man1 \ - && apt-get install --no-install-recommends -y openjdk-17-jdk-headless gdal-bin \ - && java -version \ - && ogrinfo --version \ - && rm -rf /var/lib/apt/lists/* - -#---------------------------------------------------------------------- -# Setup /opt/ign-validator/validator-cli.jar -#---------------------------------------------------------------------- -ARG validator_version=4.4.18 -RUN mkdir -p /opt/ign-validator \ - && wget --quiet -O ${VALIDATOR_PATH} https://github.com/IGNF/validator/releases/download/v${validator_version}/validator-cli.jar \ - && echo "validator-cli.jar version : $(java -jar /opt/ign-validator/validator-cli.jar version)" - -#---------------------------------------------------------------------- -# Install validator-api -#---------------------------------------------------------------------- -COPY . /opt/validator-api -WORKDIR /opt/validator-api -COPY --from=vendor /opt/validator-api/vendor vendor - -#---------------------------------------------------------------------- -# Prepare data storage -# (Note that /opt/validator-api/var/data is shared between containers) -#---------------------------------------------------------------------- -RUN mkdir -p /opt/validator-api/var/data/validations \ - && chown -R www-data:www-data /opt/validator-api/var - -# ensure ogr2ogr can write in $HOME/.gdal ... -ENV HOME=/opt/validator-api/var - -VOLUME /opt/validator-api/var/data - -USER www-data - -ENV APP_ENV=prod - -EXPOSE 8000 -CMD ["/opt/validator-api/.docker/application.sh"] diff --git a/.docker/apache-vhost.conf b/.docker/apache-vhost.conf index a926426..636930a 100755 --- a/.docker/apache-vhost.conf +++ b/.docker/apache-vhost.conf @@ -9,6 +9,14 @@ Require all granted + # Client IP read from X-Forwarded-For when the request comes from a reverse proxy + # (ingress, traefik) on a private network, same ranges as TRUSTED_PROXIES=PRIVATE_SUBNETS. + RemoteIPHeader X-Forwarded-For + RemoteIPInternalProxy 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 ::1 fc00::/7 fe80::/10 + + # "combined" with the client IP (%a, set by mod_remoteip) instead of the proxy IP (%h) + LogFormat "%a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined_client_ip + ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined + CustomLog ${APACHE_LOG_DIR}/access.log combined_client_ip diff --git a/.dockerignore b/.dockerignore index 3bdf2f5..45d170a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,10 @@ var /node_modules/ /.env.* + +# front assets are built in the Dockerfile +/public/build/ +/public/vendor/ +/public/css/ +/public/font/ +/public/img/ diff --git a/.env b/.env index dea79a9..cee4e2d 100755 --- a/.env +++ b/.env @@ -19,6 +19,15 @@ VALIDATOR_PATH=%kernel.project_dir%/bin/validator-cli.jar VALIDATOR_JAVA_OPTS='-Xms256m -Xmx2g' DATA_DIR=%kernel.project_dir%/var/data +# Allow download of source and normalized data (/api/validations/{uid}/files/*), disabled by default for security reasons +DATA_DOWNLOAD_ENABLED=0 +# Hosts (comma separated, subdomains included) allowed for the "model" argument (https only) +VALIDATOR_MODEL_ALLOWED_HOSTS=geoportail-urbanisme.gouv.fr,ignf.github.io +# Max number of validations created or updated per hour and per client IP +VALIDATION_RATE_LIMIT=100 +# Reverse proxies (ex : PRIVATE_SUBNETS, REMOTE_ADDR, 10.0.0.0/8) whose X-Forwarded-For is trusted to get the client IP +# (required behind an ingress / traefik, otherwise all the clients share the rate limit of the proxy IP) +TRUSTED_PROXIES=PRIVATE_SUBNETS #--------------------------------------------- # docker defaults diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 0873e04..97a69ee 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -61,7 +61,7 @@ jobs: uses: docker/build-push-action@v3 with: context: . - file: .docker/Dockerfile + file: Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/php-ci.yml b/.github/workflows/php-ci.yml index 25fa74b..0dd29d9 100644 --- a/.github/workflows/php-ci.yml +++ b/.github/workflows/php-ci.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - php-version: [8.3] + php-version: [8.5] runs-on: ubuntu-latest @@ -65,7 +65,7 @@ jobs: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/validator_api?serverVersion=14&charset=utf8 - name: Upload coverage results to coveralls.io - if: github.ref == 'refs/heads/master' && matrix.php-version == '8.3' + if: github.ref == 'refs/heads/master' && matrix.php-version == '8.5' run: | vendor/bin/php-coveralls --coverage_clover=var/data/output/coverage.xml --json_path=var/data/output/coveralls.json -v env: diff --git a/.gitignore b/.gitignore index e6365e3..b1d4c8a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,16 +18,12 @@ validator-debug.log /nbproject/ /tests/TempData/* -/*.lock -/package-lock.json /.idea /.vscode validator-cli.jar -.scannerwork -sonar-scanner node_modules /output/ @@ -45,3 +41,10 @@ node_modules /phpunit.xml .phpunit.result.cache ###< phpunit/phpunit ### + +# front assets built by webpack (npm ci && npm run build), see webpack.config.js +/public/build/ +/public/vendor/ +/public/css/ +/public/font/ +/public/img/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8f9d052 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,171 @@ +# Changelog + +Les changements notables de ce projet sont documentés dans ce fichier. + +Le format s'inspire de [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/). + +## [Non publié] + +Branche `upgrade/php85-symfony74` : montée de version PHP 8.5 / Symfony 7.4 et durcissement de la sécurité. + +### ⚠️ Points d'attention au déploiement + +- **PHP 8.5 est désormais requis.** +- **Le téléchargement des données sources et normalisées est désactivé par défaut.** `GET /api/validations/{uid}/files/source` et `/files/normalized` répondent `403 Data download is disabled`. Ils sont aussi retirés de la spécification OpenAPI. Pour les réactiver sur une instance : `DATA_DOWNLOAD_ENABLED=1`. +- **Les archives zip sont désormais contrôlées avant et pendant l'extraction.** Une archive auparavant acceptée peut être rejetée, avec une validation en `error` (voir *Sécurité*). Les extensions autorisées sont listées dans `ZipArchiveValidator::ALLOWED_EXTENSIONS`. +- **Le nom du fichier uploadé est contrôlé.** Sans l'extension `.zip`, il doit respecter `^[A-Za-z0-9_][A-Za-z0-9_.-]{0,99}$` : pas d'espace, d'accent ni de `/`, et pas de `.` ou de `-` en premier caractère. Sinon, l'upload répond `400`. +- **L'argument `model` doit être une URL `https://` sur un hôte autorisé** (`VALIDATOR_MODEL_ALLOWED_HOSTS`, par défaut `geoportail-urbanisme.gouv.fr,ignf.github.io`, sous-domaines compris). +- **`PATCH` et `DELETE` répondent `409`** pendant le traitement d'une validation. +- **Limite de débit** : au plus `VALIDATION_RATE_LIMIT` (100 par défaut) créations et mises à jour de validations par heure et par adresse IP, puis `429`. Derrière un reverse proxy, l'adresse du client est lue dans `X-Forwarded-For` pour les proxys de `TRUSTED_PROXIES` (par défaut `PRIVATE_SUBNETS`). Si l'ingress n'a pas une IP privée, il faut adapter cette variable, sinon tous les clients partagent la même limite. +- **Le front n'est plus commité** (`public/build`, `public/vendor`, `public/css`, `public/font`, `public/img`). Il est construit par le Dockerfile (stage `assets`) ou en local avec `npm ci && npm run build`. +- **`/api` redirige vers la documentation de la démo** (`/#/api`, swagger-ui 5). L'ancienne page swagger-ui 3 chargée depuis unpkg est supprimée. +- **`composer.lock`, `symfony.lock` et `package-lock.json` sont versionnés.** +- **La structure de la base est gérée par les migrations Doctrine.** `DB_UPGRADE=1` lance `doctrine:migrations:migrate` à la place de `doctrine:schema:update`. La migration initiale aligne aussi les bases existantes : elle ajoute les colonnes manquantes, passe `status` et `delete_data` en `NOT NULL` et ajoute la contrainte `CHECK` sur les statuts. +- **Adresse IP des clients dans les logs** : derrière un reverse proxy, c'est l'IP du client (`X-Forwarded-For`) qui est enregistrée, et non plus celle du proxy. C'est le cas dans les logs applicatifs (`extra.ip`, à partir de `TRUSTED_PROXIES`) comme dans le log d'accès Apache (`mod_remoteip`, proxys des réseaux privés). +- **Tests** : la base `validator_api_test` est désormais utilisée. Auparavant ils tournaient, par erreur, sur la base `validator_api` et la purgeaient. `make test` crée la base si besoin. + +### Sécurité + +- Contrôle des archives zip avant extraction : seul le répertoire central est lu, sans décompression. + - Protection contre les zip bombs : au plus 10 000 entrées, 5 Gio décompressés et un taux de compression de 1000. Code d'erreur `ARCHIVE_TOO_LARGE`. + - Rejet des chemins absolus ou contenant `..`, des liens symboliques et des fichiers chiffrés. Code d'erreur `BAD_ARCHIVE_ENTRY`. + - Liste blanche d'extensions (formats lus par validator-cli et fichiers annexes). Code d'erreur `FILE_EXTENSION_NOT_ALLOWED`. + - Les entrées ajoutées par les systèmes d'exploitation (`__MACOSX/`, `.DS_Store`, `Thumbs.db`, `desktop.ini`) sont ignorées. +- Extraction contrôlée (`ZipArchiveExtractor`) en remplacement de `ZipArchive::extractTo` : + - les exécutables, scripts et archives imbriquées (ELF, PE, Mach-O, `#!`, zip, gzip…) sont détectés par leur signature, quelle que soit l'extension ; + - la signature doit correspondre à l'extension pour pdf, shp/shx, dbf, gpkg, xml/gml, json et les images. Code d'erreur `FILE_CONTENT_NOT_ALLOWED` ; + - la taille réelle de chaque fichier est limitée à la taille déclarée dans l'archive ; + - les fichiers sont créés comme fichiers ordinaires non exécutables ; + - en cas d'erreur, le répertoire extrait est supprimé. +- Le nom du dataset (issu du nom du fichier uploadé) est contrôlé à l'upload et avant le traitement. Un fichier `...zip` donnait auparavant le nom `..` et permettait de vider le répertoire de travail de toutes les validations. +- Protection contre la SSRF via l'argument `model` : `https://` uniquement, sans userinfo ni port, et sur un hôte autorisé (`VALIDATOR_MODEL_ALLOWED_HOSTS`). Les URL `file://`, `http://`, les IP internes et les autres hôtes sont refusés. +- Le message d'erreur d'une validation (champ `message`, public) n'expose plus la ligne de commande, les chemins ni la sortie de validator-cli. Il vaut par exemple `Validation failed (exit code 1)`, et le détail reste dans les logs serveur. +- `/health/db` ne renvoie plus le message brut de la base de données (hôte, port, utilisateur). +- `/logs` est servi en `text/plain` avec `X-Content-Type-Options: nosniff`, pour éviter que le navigateur interprète du HTML issu des données. +- Téléchargement des données sources et normalisées désactivable (`DATA_DOWNLOAD_ENABLED`, désactivé par défaut). Le contrôle a lieu avant la recherche de la validation, pour ne pas révéler l'existence d'un uid. +- validator-cli.jar est téléchargé par `bin/install-validator.sh`, avec vérification de l'empreinte sha256. Ce script est la seule source de la version, partagée entre composer et le Dockerfile. +- Le rapport PDF n'est plus généré côté serveur par wkhtmltopdf, qui n'est plus maintenu et a des CVE connues (dont une SSRF) : l'API sert un rapport HTML imprimable, que le navigateur enregistre en PDF. +- En-têtes de sécurité sur toutes les réponses : `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY` et `Referrer-Policy`. Les pages HTML reçoivent aussi une `Content-Security-Policy` (hors mode debug) : tout est servi par l'API, sans CDN. +- Limite de débit sur `POST` et `PATCH /api/validations` (`VALIDATION_RATE_LIMIT`, `429 Too Many Requests`). +- Export CSV : les valeurs issues du dataset qui commencent par `=`, `+`, `-` ou `@` sont préfixées par `'`, pour que les tableurs ne les interprètent pas comme des formules. Les nombres sont conservés tels quels. +- Démo : plus aucune ressource chargée depuis un CDN (Bootstrap, jQuery, Popper, select2, swagger-ui@3 sur unpkg sans version fixée ni SRI). +- docker-compose : PostgreSQL n'est plus exposé sur l'hôte (seulement `127.0.0.1` via `compose.override.yaml`), et l'environnement `prod` de l'image n'est plus remplacé par le `APP_ENV=dev` du `.env`. +- Image Docker : `curl` et `wget` ne sont plus présents dans l'image finale, et Pebble (embarqué dans l'image Ubuntu, inutilisé et vulnérable) est supprimé. + +### Ajouté + +- Le fichier `document-info.json` produit par le validator (option `normalize`) est exposé dans le champ `document_info` des validations. +- `GET /api/validations/{uid}/report` : rapport de validation imprimable (HTML), à enregistrer en PDF avec le navigateur. Avec `?print`, la boîte de dialogue d'impression s'ouvre directement. +- Logs applicatifs : IP du client, URL, méthode HTTP et user-agent ajoutés à chaque log (`WebProcessor`). +- Canal de log `audit`, toujours écrit en prod (y compris sans erreur) : création, mise à jour des arguments et suppression des validations, avec l'uid, le nom du dataset et l'IP du client. +- Variables d'environnement `DATA_DOWNLOAD_ENABLED`, `VALIDATOR_MODEL_ALLOWED_HOSTS`, `VALIDATION_RATE_LIMIT` et `TRUSTED_PROXIES`. +- `ign-validator:validations:cleanup --processing-timeout` (par défaut `PT1H`) : les validations encore en `processing` au-delà de ce délai (worker tué, manque de mémoire…) passent en `error` avec le message `Validation failed (processing interrupted)`. +- Documentation OpenAPI : route `/logs`, réponses `403`, `404` et `429`, schémas `Error` et `Validation` (`results`, `delete_data`) conformes aux réponses réelles. +- Réponse `409 Conflict` sur `PATCH` et `DELETE` d'une validation en cours de traitement. +- Fichier `CHANGELOG.md`. + +### Modifié + +- Montée de version : + - PHP 8.5 et Symfony 7.4 ; + - Doctrine ORM 3 et DBAL 4 ; + - doctrine-bundle 3, doctrine-migrations-bundle 4, doctrine-fixtures-bundle 4 ; + - JMS Serializer 5, monolog-bundle 4, justinrainbow/json-schema 6 ; + - PHPUnit 13, liip/test-fixtures-bundle 3, phpstan 2. +- Le code utilise désormais les attributs PHP (routes, mapping Doctrine, commandes) et l'`EntityManagerInterface` injecté. +- Configuration Doctrine : les options de proxy supprimées par DoctrineBundle 3 sont retirées, car les objets lazy natifs sont désormais toujours actifs. +- Base de test configurée par `dbname_suffix` au lieu de `dbname`. +- Les rapports de couverture sont générés par `make test` et ne sont plus définis dans `phpunit.xml.dist`. PHPUnit 13 refuse sinon de s'exécuter sans driver de couverture. +- Dockerfile déplacé à la racine et scripts regroupés dans `bin/` : `application.sh`, `archive.sh`, `loop-validate.sh`, `install-validator.sh`. +- Le JRE remplace le JDK dans l'image Docker. +- Refactorings : + - les routes de fichiers (logs, results.csv/pdf, files/*) passent dans `ValidationFilesController` ; + - la gestion des répertoires de travail et du stockage passe dans `ValidationWorkspace`. +- Front de démo : `@ignf/validator-client` passe en v0.5.9, avec chargement des chunks `runtime` et `vendors`. La dépendance pointe vers l'archive du tag GitHub (et non `git+ssh`) : `npm ci` n'a besoin ni de git ni d'une clé SSH. +- webpack-cli 4 → 7, et `webpack-copy-plugin` (inutilisé) est supprimé. Node 20.9 minimum (`engines`). +- Build webpack : + - les dossiers générés de `public/` sont vidés à chaque build (plus de chunks périmés après une mise à jour du client) ; + - les bundles du client sont copiés tels quels, sans être reminifiés (build ~25 fois plus rapide) ; + - les polices `eot` et `otf`, non référencées par la CSS, ne sont plus copiées (6 Mo de moins dans l'image). +- phpstan passe du niveau 2 au niveau 5, sans règle d'exclusion. +- Routes : `/`, `/api/validator-api.yml`, `/api/schema/*` et `/health/*` n'acceptent plus que `GET`. Les imports de routes du framework et du profiler passent au format PHP (format XML déprécié en Symfony 7.4). +- Entité `Validation` : propriétés et accesseurs typés. `delete_data` n'est plus nullable (`false` par défaut), et le `columnDefinition` de `status` est supprimé : il provoquait une différence permanente entre le mapping et la base. +- Les tests n'écrivent plus dans `var/data` : le stockage de test est `var/data-test`, supprimé après chaque test. + +### Corrigé + +- `results.pdf` renvoyait toujours une erreur 500 : le binaire `wkhtmltopdf` utilisé par knp-snappy n'était installé ni dans l'image ni dans la CI. +- Rapport : les messages longs sans espace (chemins, identifiants) ne débordent plus de la page. Ils passent à la ligne, et les en-têtes de colonnes sont répétés sur chaque page imprimée. +- Une validation avec `normalize: false` finissait toujours en `error` : la sauvegarde attendait des données normalisées. +- En cas d'échec d'une validation : + - le log `validator-debug.log` est désormais sauvegardé, donc `/logs` est disponible ; + - le répertoire de travail local est supprimé ; + - l'option `delete-data` est respectée. Les fichiers sont supprimés, mais le statut `error` est conservé. +- `DELETE` supprime désormais les fichiers avant la ligne en base, ainsi que le schéma `validation` et le répertoire local. +- L'archivage (`ign-validator:validations:cleanup`) : + - ignore les validations en cours de traitement ; + - continue en cas d'erreur sur une validation ; + - renvoie un code d'erreur s'il y a eu des échecs. +- **Perte de données en cours de validation** : `doctrine:schema:update`, lancé à chaque démarrage de l'API (`DB_UPGRADE=1`), supprimait les tables des schémas `validation` créés par validator-cli. Doctrine ignore désormais ces schémas (`schema_filter`). +- `STORAGE_TYPE=S3` défini dans un fichier `.env` était ignoré : la variable était lue avec `getenv()`. Elle est désormais injectée par la configuration. +- Rollback de la transaction (et donc libération du verrou sur la table) en cas d'erreur dans `popNextPending`. +- `/logs` : + - répond `200` au lieu de `201` ; + - répond `404` quand le log n'existe pas, au lieu d'une erreur 500. +- `results.csv` : + - `403` pour une validation non exécutée ; + - `404` quand la validation n'a pas de résultats, au lieu d'un CSV vide ou d'une erreur 500 ; + - le CSV est servi en `text/csv` ; + - le PDF gère les erreurs de pré-validation du zip, qui n'ont pas de niveau. +- Téléchargements : + - en-tête `Content-Transfer-Encoding` malformé supprimé ; + - `404` au lieu de `403` quand le fichier est absent ; + - nom de fichier échappé dans `Content-Disposition`. +- Arrêt du worker (SIGTERM) : validator-cli.jar est désormais arrêté et le répertoire local supprimé, avant que la validation repasse en `pending`. +- `bin/application.sh archive` et `bin/archive.sh` pointaient vers des chemins inexistants : l'archivage ne pouvait pas tourner. +- docker-compose : + - le service `database` était défini deux fois (la recette Doctrine remplaçait PostGIS par `postgres:16`) ; + - le worker lançait `.docker/application.sh`, qui n'existe plus ; + - le `serverVersion` différait entre l'API et le worker. +- La CI de publication Docker pointait vers `.docker/Dockerfile`, qui n'existe plus. +- validator-cli : les options valant `0` (ex : `max-errors`, `dgpr-tolerance`) étaient ignorées, et un `VALIDATOR_JAVA_OPTS` vide produisait un argument vide. +- Tests réactivés et fiabilisés : traitement complet par validator-cli (le test était désactivé, et instable car il dépendait de l'ordre de traitement des validations), téléchargements réussis. +- La démo restait bloquée sur « Chargement… » avec le bundle validator-client découpé en chunks. +- Les tests purgeaient la base de dev : l'option `dbname` de test était ignorée en présence de `url`. +- Build Docker : + - `VALIDATOR_VERSION` était utilisé sans être déclaré, ce qui donnait une URL de téléchargement invalide ; + - le paquet `php8.5-opcache` n'existe pas (OPcache est intégré à PHP 8.5). +- `bin/application.sh test` utilisait une base `validator_api_test_test` et l'option obsolète `--complete`. +- Corrections phpmd sur l'ensemble du code : + - imports manquants et code mort supprimé ; + - `exit()` remplacé par un code de retour ; + - ordre des groupes du rapport PDF réellement appliqué. + +### Supprimé + +- Dépendances inutilisées : + - `composer/package-versions-deprecated` (abandonné) ; + - `league/flysystem-aws-s3-v3` et `aws/aws-sdk-php` (seul l'adaptateur async-aws est utilisé) ; + - `symfony/validator` ; + - `symfony/requirements-checker` ; + - `knplabs/knp-snappy-bundle` (wkhtmltopdf), remplacé par l'impression du navigateur. +- Les polyfills PHP 5.6 à 7.1 et `paragonie/random_compat` sont remplacés par les polyfills PHP 7.2 à 8.5, fournis par PHP 8.5. +- Option composer `secure-http: false`. +- Configuration (`config/`) : + - `services_test.yaml`, qui dupliquait `services.yaml` : seul le dossier de travail diffère en test (paramètre `validations_dir` dans un bloc `when@test`) ; + - sous-dossiers `packages/dev`, `packages/prod` et `packages/test`, intégrés dans des blocs `when@` ; + - `routes.yaml` (entièrement commenté) ; + - déclaration en double du listener d'exceptions (remplacée par l'attribut `#[AsEventListener]`) ; + - bloc `App\Controller\` et exclusions de dossiers inexistants ; + - sessions, inutilisées par l'API ; + - vieille branche `srcApp_` de `preload.php`. +- Fichiers de configuration en double avec les blocs `when@` (`web_profiler` de dev et test, `routes/dev/`), ainsi que `prod/deprecations.yaml`, entièrement commenté. +- Script `download-validator.sh`, remplacé par `bin/install-validator.sh`. +- Script `sql/validator-api.0.1.sql` et anciennes migrations de 2020 (jamais exécutées), remplacés par une migration initiale unique. +- Fichiers inutilisés : + - `src/DataFixtures/AppFixtures.php` (fixture vide de la recette) ; + - `src/.preload.php` (préchargement d'un conteneur `srcApp_` d'avant Symfony 5) ; + - `bin/phpunit` (wrapper `simple-phpunit`, les tests utilisent `vendor/bin/phpunit`). +- `make clean` ne supprime plus `composer.lock`, `symfony.lock` ni `package-lock.json`, désormais versionnés. +- `templates/swagger.html.twig` (swagger-ui 3 chargé depuis unpkg). +- Fichiers générés par le build front, qui n'étaient plus synchronisés avec la version de validator-api-client. diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..22e7c78 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,195 @@ +ARG REGISTRY=docker.io +# images versions +ARG UBUNTU_IMAGE_VERSION=24.04 +ARG COMPOSER_IMAGE_VERSION=2.10 +ARG NODE_IMAGE_VERSION=22-alpine +# packages versions +ARG OPENJDK_VERSION=17 +ARG PHP_VERSION=8.5 +# composer no dev +ARG COMPOSER_NO_DEV=1 + +#---------------------------------------------------------------------- +# Composer stage - PHP dependencies ------------------------------------- +#---------------------------------------------------------------------- +FROM ${REGISTRY}/library/composer:${COMPOSER_IMAGE_VERSION} AS composer + +ARG COMPOSER_NO_DEV + +WORKDIR /opt/validator-api +COPY composer.json composer.lock ./ + +# add --no-dev if COMPOSER_NO_DEV is 1 (for production) +RUN composer install $(if [ "${COMPOSER_NO_DEV}" = "1" ]; then echo --no-dev; fi) \ + --no-scripts \ + --prefer-dist \ + --optimize-autoloader \ + --no-interaction \ + --ignore-platform-req=ext-pcntl \ + && composer clear-cache + +#---------------------------------------------------------------------- +# Assets stage - front (demo) built with webpack -------------------------- +#---------------------------------------------------------------------- +FROM ${REGISTRY}/library/node:${NODE_IMAGE_VERSION} AS assets + +WORKDIR /opt/validator-api +COPY package.json package-lock.json webpack.config.js ./ +COPY assets ./assets +# produces public/build, public/vendor/validator-api-client, public/css, public/font and public/img +RUN npm ci --no-audit --no-fund \ + && npm run build \ + && rm -rf node_modules + +#---------------------------------------------------------------------- +# Production application container --------------------------------------- +#---------------------------------------------------------------------- +FROM ${REGISTRY}/library/ubuntu:${UBUNTU_IMAGE_VERSION} + +# Redeclare build args for this stage +ARG PHP_VERSION +ARG OPENJDK_VERSION + +# Metadata labels +LABEL description="validator-api - APIsation of Validator, a tool that allows to validate and normalize datasets according to a file mapping and a FeatureCatalog." \ + org.opencontainers.image.source="https://github.com/IGNF/validator-api" + +# Environment variables +ENV DEBIAN_FRONTEND=noninteractive \ + LANG=fr_FR.UTF-8 LC_ALL=fr_FR.UTF-8 LANGUAGE=fr_FR.UTF-8 \ + TZ=Europe/Paris \ + VALIDATOR_PATH=/opt/ign-validator/validator-cli.jar + +# System setup: packages, locale (https://stackoverflow.com/a/41797247), trust store +RUN apt-get update -qq \ + && apt-get install --no-install-recommends -y \ + locales \ + ca-certificates \ + && echo "fr_FR.UTF-8 UTF-8" > /etc/locale.gen \ + && locale-gen fr_FR.UTF-8 \ + && update-locale LANG=fr_FR.UTF-8 LC_ALL=fr_FR.UTF-8 \ + && update-ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +#------------------------------------------------------------------------ +# Configure https://packages.sury.org/php/ to get latest PHP versions +#------------------------------------------------------------------------ +RUN apt-get update -qq \ + && apt-get install --no-install-recommends -y gnupg2 software-properties-common \ + && add-apt-repository -y ppa:ondrej/php \ + && apt-get remove -y software-properties-common \ + && rm -rf /var/lib/apt/lists/* + +# Install required packages +RUN apt-get update -qq \ + # see https://github.com/debuerreotype/docker-debian-artifacts/issues/24 + # (openjdk install fails on minimal images without this directory) + && mkdir -p /usr/share/man/man1 \ + && apt-get install --no-install-recommends -y \ + # System utilities + unzip zip file \ + # Database client + postgresql-client \ + # Apache2 + apache2 libapache2-mod-php${PHP_VERSION} \ + # PHP & extensions + php${PHP_VERSION} \ + php${PHP_VERSION}-intl \ + php${PHP_VERSION}-mbstring \ + # (no php-opcache package: OPcache is built into PHP since 8.5) + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-pdo \ + php${PHP_VERSION}-pgsql \ + php${PHP_VERSION}-zip \ + php${PHP_VERSION}-curl \ + # java & ogr2ogr, required by validator-cli.jar + openjdk-${OPENJDK_VERSION}-jre-headless gdal-bin \ + && java -version \ + && ogrinfo --version \ + && apt-get upgrade -y --no-install-recommends \ + && apt-get autoremove -y \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +#---------------------------------------------------------------------- +# Apache and PHP configuration ------------------------------------------- +#---------------------------------------------------------------------- + +# Add helper script to start apache (https://github.com/docker-library/php) +COPY .docker/apache2-foreground /usr/local/bin/apache2-foreground +# Copy php configuration files +COPY .docker/php.ini /etc/php/${PHP_VERSION}/apache2/conf.d/99-app.ini +COPY .docker/php.ini /etc/php/${PHP_VERSION}/cli/conf.d/99-app.ini +# Copy apache configuration files +COPY .docker/apache-ports.conf /etc/apache2/ports.conf +COPY .docker/apache-security.conf /etc/apache2/conf-available/security.conf +COPY .docker/apache-vhost.conf /etc/apache2/sites-available/000-default.conf +# Set permissions for helper script and apache configuration files +RUN chmod +x /usr/local/bin/apache2-foreground \ + # Create apache2 repository (https://github.com/docker-library/php) + && mkdir -p /var/run/apache2 /var/lock/apache2 /var/log/apache2 \ + && chown -R www-data:www-data /var/run/apache2 /var/lock/apache2 /var/log/apache2 \ + # Redirects logs to stdout / stderr (https://github.com/docker-library/php) + && ln -sfT /dev/stderr /var/log/apache2/error.log \ + && ln -sfT /dev/stdout /var/log/apache2/access.log \ + && ln -sfT /dev/stdout /var/log/apache2/other_vhosts_access.log \ + # Enable Apache modules and configurations + && a2enconf security \ + && a2enmod rewrite remoteip + +#---------------------------------------------------------------------- +# Setup /opt/ign-validator/validator-cli.jar +# (version and sha256 are defined in bin/install-validator.sh) +#---------------------------------------------------------------------- +COPY bin/install-validator.sh /tmp/install-validator.sh +RUN apt-get update -qq \ + && apt-get install --no-install-recommends -y curl \ + && mkdir -p /opt/ign-validator \ + && sh /tmp/install-validator.sh ${VALIDATOR_PATH} \ + && echo "validator-cli.jar version : $(java -jar ${VALIDATOR_PATH} version)" \ + && rm -rf /tmp/install-validator.sh /tmp/hsperfdata_root \ + && apt-get purge -y curl \ + && apt-get autoremove -y \ + # Echec du build si curl reste present dans l'image + && ! [ -e /usr/bin/curl ] \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + # Remove unused Canonical Pebble bundled in the Ubuntu base image + # (vulnerable Go stdlib, not used: container runs via bin/application.sh) + && rm -rf /usr/bin/pebble /var/lib/pebble + +#---------------------------------------------------------------------- +# Application setup +#---------------------------------------------------------------------- +COPY . /opt/validator-api +WORKDIR /opt/validator-api +COPY --from=composer --chown=www-data:www-data /opt/validator-api/vendor ./vendor +COPY --from=assets /opt/validator-api/public ./public + +#---------------------------------------------------------------------- +# Prepare data storage +# (Note that /opt/validator-api/var/data is shared between containers) +#---------------------------------------------------------------------- +RUN mkdir -p var/data/validations \ + && chown -R www-data:www-data var \ + && chmod -R +x bin + +# ensure ogr2ogr can write in $HOME/.gdal ... +ENV HOME=/opt/validator-api/var + +# Define volume for persistent data +VOLUME /opt/validator-api/var/data + +#---------------------------------------------------------------------- +# Security and final configuration --------------------------------------- +#---------------------------------------------------------------------- + +# Security: Run as non-root user +USER www-data + +ENV APP_ENV=prod + +EXPOSE 8000 + +# Use exec form for better signal handling +CMD ["/opt/validator-api/bin/application.sh"] diff --git a/Makefile b/Makefile index 10fc511..01dad80 100755 --- a/Makefile +++ b/Makefile @@ -1,32 +1,26 @@ -PHP_CS_RULES=@Symfony -PHP_MD_RULES=./phpmd.xml - -test: vendor bin/validator-cli.jar +test: vendor # see SYMFONY_DEPRECATIONS_HELPER in phpunit.xml.dist rm -rf var/log/test.deprecations.log - APP_ENV=test XDEBUG_MODE=coverage vendor/bin/phpunit - -bin/validator-cli.jar: - bash download-validator.sh + # test database (validator_api_test, see when@test in config/packages/doctrine.yaml) + APP_ENV=test php bin/console doctrine:database:create --if-not-exists + APP_ENV=test XDEBUG_MODE=coverage vendor/bin/phpunit \ + --coverage-clover var/data/output/coverage.xml \ + --coverage-html var/data/output/coverage/ .PHONY: check-rules -check-rules: vendor - @echo "-- Checking coding rules using phpmd (see @SuppressWarning to bypass control)" - vendor/bin/phpmd src text $(PHP_MD_RULES) +check-rules: @echo "-- Checking coding rules using phpstan" vendor/bin/phpstan analyse -c phpstan.neon --error-format=raw .PHONY: fix-style -fix-style: vendor +fix-style: @echo "-- Fixing coding style using php-cs-fixer..." - vendor/bin/php-cs-fixer fix src --rules $(PHP_CS_RULES) --using-cache=no - vendor/bin/php-cs-fixer fix tests --rules $(PHP_CS_RULES) --using-cache=no + vendor/bin/php-cs-fixer fix .PHONY: check-style -check-style: vendor +check-style: @echo "-- Checking coding style using php-cs-fixer (run 'make fix-style' if it fails)" - vendor/bin/php-cs-fixer fix src --rules $(PHP_CS_RULES) -v --dry-run --diff --using-cache=no - vendor/bin/php-cs-fixer fix tests --rules $(PHP_CS_RULES) -v --dry-run --diff --using-cache=no + vendor/bin/php-cs-fixer fix -v --dry-run --diff .PHONY: vendor vendor: @@ -34,14 +28,10 @@ vendor: .PHONY: clean clean: + # note : composer.lock, symfony.lock and package-lock.json are versioned (not removed) rm -rf vendor rm -rf var - rm -rf output - rm -f *.log - rm -f *.lock - rm -f package-lock.json - rm -f .php_cs.cache - rm -rf output rm -rf node_modules - rm -rf .scannerwork - rm -rf sonar-scanner + rm -rf public/build public/vendor public/css public/font public/img + rm -f *.log + rm -f .php-cs-fixer.cache .phpunit.result.cache diff --git a/.docker/application.sh b/bin/application.sh similarity index 79% rename from .docker/application.sh rename to bin/application.sh index bbe8dae..8ff8325 100755 --- a/.docker/application.sh +++ b/bin/application.sh @@ -2,12 +2,12 @@ set -e #--------------------------------------------------------------------------- -# env vars specific to .docker/application.sh +# env vars specific to bin/application.sh #--------------------------------------------------------------------------- # allows to enable / disable automatic database creation (doctrine:database:create) DB_CREATE=${DB_CREATE:-0} -# allows to enable / disable automatic schema upgrade (doctrine:schema:update) +# allows to enable / disable automatic schema upgrade (doctrine:migrations:migrate) DB_UPGRADE=${DB_UPGRADE:-1} # run (apache2) / backend / test @@ -23,7 +23,7 @@ run(){ fi if [ "$DB_UPGRADE" = "1" ]; then - bin/console doctrine:schema:update --force --complete + bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration fi #--------------------------------------------------------------------------- @@ -33,18 +33,18 @@ run(){ } backend(){ - exec bash loop-validate.sh + exec bash bin/loop-validate.sh } archive(){ - exec bash archive.sh + exec bash bin/archive.sh } test(){ export APP_ENV=test - export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:5432/validator_api_test?serverVersion=15&charset=utf8" + export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:5432/validator_api?serverVersion=15&charset=utf8" bin/console --env=test doctrine:database:create --if-not-exists - bin/console --env=test doctrine:schema:update --complete --force + bin/console --env=test doctrine:migrations:migrate --no-interaction --allow-no-migration XDEBUG_MODE=coverage vendor/bin/phpunit } diff --git a/archive.sh b/bin/archive.sh similarity index 91% rename from archive.sh rename to bin/archive.sh index 28d09cd..86b7338 100644 --- a/archive.sh +++ b/bin/archive.sh @@ -23,7 +23,7 @@ echo "$BASH_SOURCE - started with PID=$$" if [ $RUNNING -eq 1 ] then - php "${SCRIPT_DIR}/bin/console" ign-validator:validations:cleanup -vvv & + php "${SCRIPT_DIR}/console" ign-validator:validations:cleanup -vvv & child_pid=$! wait $child_pid diff --git a/bin/install-validator.sh b/bin/install-validator.sh new file mode 100755 index 0000000..90615e5 --- /dev/null +++ b/bin/install-validator.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Telecharge validator-cli.jar et verifie son empreinte. +# Source unique de la version : appele par composer (post-install-cmd) et par le Dockerfile. +# Usage : bin/install-validator.sh [fichier cible, defaut bin/validator-cli.jar] +set -eu + +VALIDATOR_VERSION=4.5.6 +# sha256 de validator-cli.jar (a mettre a jour avec VALIDATOR_VERSION) +VALIDATOR_SHA256=2c773342afd4a9107a1cc9e6176933dbb246cf473cd8f92811d296b4e28805fa + +TARGET="${1:-bin/validator-cli.jar}" + +curl -fsSL -o "$TARGET" "https://github.com/IGNF/validator/releases/download/v${VALIDATOR_VERSION}/validator-cli.jar" +echo "${VALIDATOR_SHA256} ${TARGET}" | sha256sum -c - diff --git a/loop-validate.sh b/bin/loop-validate.sh similarity index 91% rename from loop-validate.sh rename to bin/loop-validate.sh index af60bf7..fe8e7ab 100755 --- a/loop-validate.sh +++ b/bin/loop-validate.sh @@ -23,7 +23,7 @@ echo "$BASH_SOURCE - started with PID=$$" while [ $RUNNING -eq 1 ] do - php "${SCRIPT_DIR}/bin/console" ign-validator:validations:process-one -vvv & + php "${SCRIPT_DIR}/console" ign-validator:validations:process-one -vvv & child_pid=$! wait $child_pid diff --git a/bin/phpunit b/bin/phpunit deleted file mode 100755 index 4d1ed05..0000000 --- a/bin/phpunit +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env php -=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "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.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "doctrine/doctrine-bundle", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "e6cd6aca912a5701fdc9bf4f0ad8f18f3f1448ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/e6cd6aca912a5701fdc9bf4f0ad8f18f3f1448ed", + "reference": "e6cd6aca912a5701fdc9bf4f0ad8f18f3f1448ed", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^4.0", + "doctrine/deprecations": "^1.0", + "doctrine/persistence": "^4", + "doctrine/sql-formatter": "^1.0.1", + "ext-mbstring": "*", + "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 || >=5" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "doctrine/orm": "^3.4.4", + "phpstan/phpstan": "^2.1.13", + "phpstan/phpstan-phpunit": "2.0.3", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-symfony": "^2.0.9", + "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/http-kernel": "^6.4 || ^7.0 || ^8.0", + "symfony/messenger": "^6.4 || ^7.0 || ^8.0", + "symfony/property-info": "^6.4 || ^7.0 || ^8.0", + "symfony/runtime": "^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 || ^4" + }, + "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.3.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": "2026-09-09T11:58:04+00:00" + }, + { + "name": "doctrine/doctrine-migrations-bundle", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "43e9212c7441178e3748580e53f88c08f4e70e5f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/43e9212c7441178e3748580e53f88c08f4e70e5f", + "reference": "43e9212c7441178e3748580e53f88c08f4e70e5f", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^4", + "doctrine/doctrine-bundle": "^3", + "doctrine/migrations": "^3.2", + "php": "^8.4", + "psr/log": "^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": "^3", + "symfony/framework-bundle": "^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/service-contracts": "^3.0" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/coding-standard": "^14", + "doctrine/orm": "^3", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-symfony": "^2", + "phpunit/phpunit": "^12.5", + "symfony/var-exporter": "^6.4 || ^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/4.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%2Fdoctrine-migrations-bundle", + "type": "tidelift" + } + ], + "time": "2026-08-26T05:54:54+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.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/71d305eaa6480d3d3460c7d053d1da7749fa9cca", + "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10.5.58 || ^12.5.4" + }, + "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.3" + }, + "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": "2026-10-01T06:59:29+00:00" + }, + { + "name": "doctrine/migrations", + "version": "3.9.7", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", + "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.7" + }, + "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": "2026-04-23T19:33:20+00:00" + }, + { + "name": "doctrine/orm", + "version": "3.7.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "2637802f3bb695a88f54af1a6f0c2e76a4b523fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/2637802f3bb695a88f54af1a6f0c2e76a4b523fb", + "reference": "2637802f3bb695a88f54af1a6f0c2e76a4b523fb", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/collections": "^2.2 || ^3.1", + "doctrine/dbal": "^3.8.2 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^2.1.1", + "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/polyfill-php86": "^1.37", + "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.2.14", + "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-deepclone": "Improves performance when not using native lazy objects (Symfony 8.1+)", + "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.7.3" + }, + "time": "2026-09-26T21:45:25+00:00" + }, + { + "name": "doctrine/persistence", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", + "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "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 || ^8.0", + "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "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 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.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%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2026-04-26T12:12:52+00:00" + }, + { + "name": "doctrine/sql-formatter", + "version": "1.5.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/sql-formatter.git", + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/9563949f5cd3bd12a17d12fb980528bc141c5806", + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806", + "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.4" + }, + "time": "2026-02-08T16:21:46+00:00" + }, + { + "name": "jms/metadata", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/metadata.git", + "reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/554319d2e5f0c5d8ccaeffe755eac924e14da330", + "reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "require-dev": { + "doctrine/cache": "^1.0|^2.0", + "doctrine/coding-standard": "^8.0", + "mikey179/vfsstream": "^1.6.7", + "phpunit/phpunit": "^8.5.42|^9.6.23", + "psr/container": "^1.0|^2.0", + "symfony/cache": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0", + "symfony/dependency-injection": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Metadata\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "Class/method/property metadata management in PHP", + "keywords": [ + "annotations", + "metadata", + "xml", + "yaml" + ], + "support": { + "issues": "https://github.com/schmittjoh/metadata/issues", + "source": "https://github.com/schmittjoh/metadata/tree/2.9.0" + }, + "time": "2025-11-30T20:12:26+00:00" + }, + { + "name": "jms/serializer", + "version": "3.32.9", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/serializer.git", + "reference": "25cf987fa31d5f412feaae29745a5421cb803773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/25cf987fa31d5f412feaae29745a5421cb803773", + "reference": "25cf987fa31d5f412feaae29745a5421cb803773", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2.0", + "doctrine/lexer": "^2.0 || ^3.0", + "jms/metadata": "^2.6", + "php": "^7.4 || ^8.0", + "phpstan/phpdoc-parser": "^1.20 || ^2.0" + }, + "require-dev": { + "doctrine/annotations": "^1.14 || ^2.0", + "doctrine/coding-standard": "^12.0", + "doctrine/orm": "^2.14 || ^3.0", + "doctrine/persistence": "^2.5.2 || ^3.0", + "doctrine/phpcr-odm": "^1.5.2 || ^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.3", + "ocramius/proxy-manager": "^1.0 || ^2.0", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^9.0 || ^10.0 || ^11.0", + "psr/container": "^1.0 || ^2.0", + "rector/rector": "^1.0.0 || ^2.0", + "symfony/clock": "^6.4 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/expression-language": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/form": "^5.4.45 || ^6.4.27 || ^7.0 || ^8.0", + "symfony/translation": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/uid": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/validator": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "twig/twig": "^1.34 || ^2.4 || ^3.0" + }, + "suggest": { + "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", + "symfony/cache": "Required if you like to use cache functionality.", + "symfony/uid": "Required if you'd like to serialize UID objects.", + "symfony/yaml": "Required if you'd like to use the YAML metadata format." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "JMS\\Serializer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "Library for (de-)serializing data of any complexity; supports XML, and JSON.", + "homepage": "http://jmsyst.com/libs/serializer", + "keywords": [ + "deserialization", + "jaxb", + "json", + "serialization", + "xml" + ], + "support": { + "issues": "https://github.com/schmittjoh/serializer/issues", + "source": "https://github.com/schmittjoh/serializer/tree/3.32.9" + }, + "funding": [ + { + "url": "https://github.com/goetas", + "type": "github" + }, + { + "url": "https://github.com/scyzoryck", + "type": "github" + } + ], + "time": "2026-09-04T14:41:03+00:00" + }, + { + "name": "jms/serializer-bundle", + "version": "5.5.2", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/JMSSerializerBundle.git", + "reference": "34d01be85521e99ca29079438002672af35ff9b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/JMSSerializerBundle/zipball/34d01be85521e99ca29079438002672af35ff9b0", + "reference": "34d01be85521e99ca29079438002672af35ff9b0", + "shasum": "" + }, + "require": { + "jms/metadata": "^2.6", + "jms/serializer": "^3.31", + "php": "^7.4 || ^8.0", + "symfony/config": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^1.14 || ^2.0", + "doctrine/coding-standard": "^12.0", + "doctrine/orm": "^2.14 || ^3.0", + "phpunit/phpunit": "^8.0 || ^9.0", + "symfony/expression-language": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/form": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/templating": "^5.4 || ^6.0", + "symfony/twig-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/uid": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/validator": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "suggest": { + "symfony/expression-language": "Required for opcache preloading ^5.4 || ^6.0 || ^7.0", + "symfony/finder": "Required for cache warmup, supported versions ^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "JMS\\SerializerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "Allows you to easily serialize, and deserialize data of any complexity", + "homepage": "http://jmsyst.com/bundles/JMSSerializerBundle", + "keywords": [ + "deserialization", + "json", + "serialization", + "xml" + ], + "support": { + "issues": "https://github.com/schmittjoh/JMSSerializerBundle/issues", + "source": "https://github.com/schmittjoh/JMSSerializerBundle/tree/5.5.2" + }, + "funding": [ + { + "url": "https://github.com/goetas", + "type": "github" + } + ], + "time": "2025-11-25T21:41:31+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "6.13.1", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "e54aacc4c0657b3a4af4f628428d2702146828c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/e54aacc4c0657b3a4af4f628428d2702146828c6", + "reference": "e54aacc4c0657b3a4af4f628428d2702146828c6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-json": "*", + "marc-mabe/php-enum": "^4.4", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "dev-main", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Danny van der Sluijs", + "email": "danny.vandersluijs@icloud.com", + "role": "Maintainer" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.13.1" + }, + "time": "2026-09-30T07:42:28+00:00" + }, + { + "name": "league/flysystem", + "version": "3.36.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f7fb152932f30072d573510cbd4dd657d6475b25", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.36.0" + }, + "time": "2026-09-02T08:00:27+00:00" + }, + { + "name": "league/flysystem-async-aws-s3", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-async-aws-s3.git", + "reference": "6928568d3fe24afc4d7d583fc1844d0c1d07651b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-async-aws-s3/zipball/6928568d3fe24afc4d7d583fc1844d0c1d07651b", + "reference": "6928568d3fe24afc4d7d583fc1844d0c1d07651b", + "shasum": "" + }, + "require": { + "async-aws/s3": "^1.5 || ^2.0 || ^3.0", + "league/flysystem": "^3.10.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/simple-s3": "^2.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\AsyncAwsS3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "AsyncAws S3 filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "async-aws", + "aws", + "file", + "files", + "filesystem", + "s3", + "storage" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-async-aws-s3/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/flysystem-bundle", + "version": "3.7.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-bundle.git", + "reference": "9cccb6862c9fcd16e6d8a0fbd62254abb8017c99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-bundle/zipball/9cccb6862c9fcd16e6d8a0fbd62254abb8017c99", + "reference": "9cccb6862c9fcd16e6d8a0fbd62254abb8017c99", + "shasum": "" + }, + "require": { + "league/flysystem": "^3.0", + "php": ">=8.2", + "symfony/config": "^6.0 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^6.0 || ^7.0 || ^8.0", + "symfony/deprecation-contracts": "^2.1 || ^3", + "symfony/http-kernel": "^6.0 || ^7.0 || ^8.0", + "symfony/options-resolver": "^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "doctrine/mongodb-odm": "^2.0", + "league/flysystem-async-aws-s3": "^3.1", + "league/flysystem-aws-s3-v3": "^3.1", + "league/flysystem-azure-blob-storage": "^3.1", + "league/flysystem-ftp": "^3.1", + "league/flysystem-google-cloud-storage": "^3.1", + "league/flysystem-gridfs": "^3.28", + "league/flysystem-memory": "^3.1", + "league/flysystem-read-only": "^3.15", + "league/flysystem-sftp-v3": "^3.1", + "league/flysystem-webdav": "^3.29", + "platformcommunity/flysystem-bunnycdn": "^3.3", + "symfony/dotenv": "^6.0 || ^7.0 || ^8.0", + "symfony/framework-bundle": "^6.0 || ^7.0 || ^8.0", + "symfony/phpunit-bridge": "^6.0 || ^7.0 || ^8.0", + "symfony/var-dumper": "^6.0 || ^7.0 || ^8.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "League\\FlysystemBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Titouan Galopin", + "email": "galopintitouan@gmail.com" + } + ], + "description": "Symfony bundle integrating Flysystem into Symfony applications", + "keywords": [ + "Flysystem", + "bundle", + "filesystem", + "symfony" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem-bundle/issues", + "source": "https://github.com/thephpleague/flysystem-bundle/tree/3.7.1" + }, + "time": "2026-08-10T15:55:07+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.35.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/a099b24dce160f3b2239043d13d47c4a1a214ea4", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.35.3" + }, + "time": "2026-08-12T13:29:21+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.17.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2026-07-09T11:49:27+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.12.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/d7059e4c691d05ea1222b5124bed0f961be46e15", + "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15", + "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", + "psr/clock": "^1.0", + "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", + "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records", + "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.12.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-09-29T19:21:12+00:00" + }, + { + "name": "nelmio/cors-bundle", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c", + "reference": "3d80dbcd5d1eb5f8b20ed5199e1778d44c2e4d1c", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.5", + "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-symfony": "^1.4.4", + "phpunit/phpunit": "^8" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.6.1" + }, + "time": "2026-01-12T15:59:08+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "1427af4647235b4a73b13940b1f1258d3584f36e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1427af4647235b4a73b13940b1f1258d3584f36e", + "reference": "1427af4647235b4a73b13940b1f1258d3584f36e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.6" + }, + "time": "2026-09-27T20:07:45+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/apache-pack", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/apache-pack.git", + "reference": "3aa5818d73ad2551281fc58a75afd9ca82622e6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/apache-pack/zipball/3aa5818d73ad2551281fc58a75afd9ca82622e6c", + "reference": "3aa5818d73ad2551281fc58a75afd9ca82622e6c", + "shasum": "" + }, + "type": "symfony-pack", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A pack for Apache support in Symfony", + "support": { + "issues": "https://github.com/symfony/apache-pack/issues", + "source": "https://github.com/symfony/apache-pack/tree/master" + }, + "time": "2017-12-12T01:46:35+00:00" + }, + { + "name": "symfony/asset", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", + "reference": "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/http-foundation": "<6.4" + }, + "require-dev": { + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^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/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "e7e604eab58639503495a46f5c194cc69a9c80a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/e7e604eab58639503495a46f5c194cc69a9c80a8", + "reference": "e7e604eab58639503495a46f5c194cc69a9c80a8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "ext-relay": "<0.12.1", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "^1.0.3", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-29T12:35:16+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/9789738bc19af1106dc54d6afba9a0b467516cf2", + "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2", + "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.7-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.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/config", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "696e12da8eea497a1a3808d714b43ff67a7df43e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/696e12da8eea497a1a3808d714b43ff67a7df43e", + "reference": "696e12da8eea497a1a3808d714b43ff67a7df43e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1|^8.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^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/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-20T09:55:18+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", + "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-18T15:55:00+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "a7faee783a87b55f27460fc571f8c639bf9d93a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a7faee783a87b55f27460fc571f8c639bf9d93a2", + "reference": "a7faee783a87b55f27460fc571f8c639bf9d93a2", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^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/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-29T12:35:16+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/doctrine-bridge", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "03d285cfe0f633a856e7acb68cecf5964f8494a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/03d285cfe0f633a856e7acb68cecf5964f8494a7", + "reference": "03d285cfe0f633a856e7acb68cecf5964f8494a7", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^2", + "doctrine/persistence": "^3.1|^4", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/collections": "<1.8", + "doctrine/dbal": "<3.6", + "doctrine/lexer": "<1.1", + "doctrine/orm": "<2.15", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/form": "<6.4.6|>=7,<7.0.6", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/lock": "<6.4", + "symfony/messenger": "<6.4", + "symfony/property-info": "<6.4", + "symfony/security-bundle": "<6.4", + "symfony/security-core": "<6.4", + "symfony/validator": "<7.4.17|>=8.0,<8.1.5" + }, + "require-dev": { + "doctrine/collections": "^1.8|^2.0", + "doctrine/data-fixtures": "^1.1|^2", + "doctrine/dbal": "^3.6|^4", + "doctrine/orm": "^2.15|^3", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/doctrine-messenger": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.2|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^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/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-28T19:09:50+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "99b5b14953237c89ed24f1af5ba34225caf598a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/99b5b14953237c89ed24f1af5ba34225caf598a4", + "reference": "99b5b14953237c89ed24f1af5ba34225caf598a4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/process": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^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/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-26T10:22:28+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0", + "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^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/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-22T18:56:40+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d269974ee93c61d03620ffee358355bfdb471d66", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "90d412aa5277c6819db39e7605aa46b1019e3232" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/90d412aa5277c6819db39e7605aa46b1019e3232", + "reference": "90d412aa5277c6819db39e7605aa46b1019e3232", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^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/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-23T10:03:40+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-26T06:31:10+00:00" + }, + { + "name": "symfony/flex", + "version": "v2.11.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "4a6d98eea3ebc7f68d82810cb682eedca2649e99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/4a6d98eea3ebc7f68d82810cb682eedca2649e99", + "reference": "4a6d98eea3ebc7f68d82810cb682eedca2649e99", + "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", + "phpunit/phpunit": "^12.4", + "symfony/dotenv": "^6.4.41|^7.4.13|^8.0.13", + "symfony/filesystem": "^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.11.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T17:25:22+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "99786f9338c24770ce4de6a95d049675aa75b129" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/99786f9338c24770ce4de6a95d049675aa75b129", + "reference": "99786f9338c24770ce4de6a95d049675aa75b129", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/cache": "^6.4.12|^7.0|^8.0", + "symfony/config": "^7.4.4|^8.0.4", + "symfony/dependency-injection": "^7.4.15|~8.0.15|^8.1.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.3|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^7.1|^8.0", + "symfony/finder": "^6.4|^7.0|^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|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/asset": "<6.4", + "symfony/asset-mapper": "<6.4", + "symfony/clock": "<6.4", + "symfony/console": "<6.4.43|>=7.0,<7.4.15|>=8.0,<8.0.15", + "symfony/dom-crawler": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/form": "<7.4", + "symfony/http-client": "<6.4", + "symfony/lock": "<6.4", + "symfony/mailer": "<6.4.44|>=7.0,<7.4.17|>=8.0,<8.1.5", + "symfony/messenger": "<7.4", + "symfony/mime": "<6.4.37|>=7.0,<7.4.9|>=8.0,<8.0.9", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", + "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", + "symfony/security-core": "<6.4", + "symfony/security-csrf": "<7.2", + "symfony/serializer": "<7.2.5", + "symfony/stopwatch": "<6.4", + "symfony/translation": "<7.3", + "symfony/twig-bridge": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4", + "symfony/web-profiler-bundle": "<6.4", + "symfony/webhook": "<7.4", + "symfony/workflow": "<7.4" + }, + "require-dev": { + "dragonmantank/cron-expression": "^3.1", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "seld/jsonlint": "^1.10", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4.43|^7.4.15|^8.0.15", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/json-streamer": "^7.3|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^6.4.37|^7.4.9|^8.0.9", + "symfony/notifier": "^6.4|^7.0|^8.0", + "symfony/object-mapper": "^7.3|^8.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/scheduler": "^6.4.4|^7.0.4|^8.0", + "symfony/security-bundle": "^6.4|^7.0|^8.0", + "symfony/semaphore": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.2.5|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^7.3|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/webhook": "^7.4|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.3|^8.0", + "twig/twig": "^3.12|^4.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/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-28T19:09:50+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "750a420b8dd0af3d6e1c6c1412802abf2cdd2b6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/750a420b8dd0af3d6e1c6c1412802abf2cdd2b6c", + "reference": "750a420b8dd0af3d6e1c6c1412802abf2cdd2b6c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "amphp/socket": "<1.1", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T11:02:13+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "35be0019e2c2c9fba80f9dc033290a5240f7b44f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/35be0019e2c2c9fba80f9dc033290a5240f7b44f", + "reference": "35be0019e2c2c9fba80f9dc033290a5240f7b44f", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-04T08:41:16+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "9d0761c0da6bd5b801b401666834901a9a1ffc33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9d0761c0da6bd5b801b401666834901a9a1ffc33", + "reference": "9d0761c0da6bd5b801b401666834901a9a1ffc33", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^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/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T07:12:44+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "392784b5d7b959b8d872542a2530c50c540a788f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/392784b5d7b959b8d872542a2530c50c540a788f", + "reference": "392784b5d7b959b8d872542a2530c50c540a788f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^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.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "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/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-29T14:05:16+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.44|^7.4.17|^8.1.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-04T10:45:44+00:00" + }, + { + "name": "symfony/monolog-bridge", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "b72ab77337167f2a545c35e624141df68c274ad8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/b72ab77337167f2a545c35e624141df68c274ad8", + "reference": "b72ab77337167f2a545c35e624141df68c274ad8", + "shasum": "" + }, + "require": { + "monolog/monolog": "^3", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/notifier": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^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/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-30T00:47:26+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "a22de65db405738364af77310761fdd0dce7a40e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/a22de65db405738364af77310761fdd0dce7a40e", + "reference": "a22de65db405738364af77310761fdd0dce7a40e", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "monolog/monolog": "^3.12", + "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.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-10T19:32:07+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/533d68cf99c8f8ee2e8893c59ed59192e92cfd74", + "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T14:40:04+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T22:02:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T15:19:04+00:00" + }, + { + "name": "symfony/polyfill-php86", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "8840df45b24f33a49dc419fa18dc0057231ae1d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/8840df45b24f33a49dc419fa18dc0057231ae1d8", + "reference": "8840df45b24f33a49dc419fa18dc0057231ae1d8", + "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\\Php86\\": "" + }, + "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.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php86/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "ed0ae095b86994d370d5791612e55984f15aa30e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/ed0ae095b86994d370d5791612e55984f15aa30e", + "reference": "ed0ae095b86994d370d5791612e55984f15aa30e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-02T12:38:34+00:00" + }, + { + "name": "symfony/rate-limiter", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/rate-limiter.git", + "reference": "6703d040ab83401b27f3c7b29ff4c8c8106fedba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/6703d040ab83401b27f3c7b29ff4c8c8106fedba", + "reference": "6703d040ab83401b27f3c7b29ff4c8c8106fedba", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/options-resolver": "^7.3|^8.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/lock": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\RateLimiter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Wouter de Jong", + "email": "wouter@wouterj.nl" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a Token Bucket implementation to rate limit input and output in your application", + "homepage": "https://symfony.com", + "keywords": [ + "limiter", + "rate-limiter" + ], + "support": { + "source": "https://github.com/symfony/rate-limiter/tree/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-07T15:34:54+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "2bacb72be5eadeaf9874d5accb75bfb33d21c74b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/2bacb72be5eadeaf9874d5accb75bfb33d21c74b", + "reference": "2bacb72be5eadeaf9874d5accb75bfb33d21c74b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^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/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-28T19:09:50+00:00" + }, + { + "name": "symfony/runtime", + "version": "v7.4.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/runtime.git", + "reference": "15497f743dd714f3167cb6a56509b9d42e6417b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/runtime/zipball/15497f743dd714f3167cb6a56509b9d42e6417b3", + "reference": "15497f743dd714f3167cb6a56509b9d42e6417b3", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": ">=8.2" + }, + "conflict": { + "symfony/dotenv": "<6.4", + "symfony/http-foundation": "<6.4" + }, + "require-dev": { + "composer/composer": "^2.6", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^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/v7.4.14" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:22:21+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-27T15:39:01+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/70a852d72fec4d51efb1f48dcd968efcaf5ccb89", + "reference": "70a852d72fec4d51efb1f48dcd968efcaf5ccb89", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "02731ce2bd26514bec9efc0baef2cd0b0cf67f46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/02731ce2bd26514bec9efc0baef2cd0b0cf67f46", + "reference": "02731ce2bd26514bec9efc0baef2cd0b0cf67f46", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "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.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^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/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-11T14:46:58+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "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.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/twig-bridge", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "10d369dd3845bb650a9450e643d6d9027e8a86af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/10d369dd3845bb650a9450e643d6d9027e8a86af", + "reference": "10d369dd3845bb650a9450e643d6d9027e8a86af", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/translation-contracts": "^2.5|^3", + "twig/twig": "^3.21|^4.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/console": "<6.4", + "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/mime": "<6.4.37|>7,<7.4.9|>8.0,<8.0.9", + "symfony/serializer": "<6.4", + "symfony/translation": "<6.4", + "symfony/workflow": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4.32|~7.3.10|^7.4.4|^8.0.4", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.3|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4.37|^7.4.9|^8.0.9", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.44|^7.4.17|^8.1.5", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/workflow": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^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/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-14T12:19:06+00:00" + }, + { + "name": "symfony/twig-bundle", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "231c0b37dada053458d47281261a3af0270a634d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/231c0b37dada053458d47281261a3af0270a634d", + "reference": "231c0b37dada053458d47281261a3af0270a634d", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.2", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/twig-bridge": "^7.3|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "conflict": { + "symfony/framework-bundle": "<6.4", + "symfony/translation": "<6.4" + }, + "require-dev": { + "symfony/asset": "^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.13|^7.1.6|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^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/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-11T14:46:58+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "e088da50b813f32473a76871616cbb8fa54653a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e088da50b813f32473a76871616cbb8fa54653a8", + "reference": "e088da50b813f32473a76871616cbb8fa54653a8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "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/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-30T20:10:52+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "d6a87acbe48cbc707b9aba7f1a6c2215aab02603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/d6a87acbe48cbc707b9aba7f1a6c2215aab02603", + "reference": "d6a87acbe48cbc707b9aba7f1a6c2215aab02603", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^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/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-23T10:03:40+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "0222bf4dfaa1e3ceb8fff579635d655f2950d5a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0222bf4dfaa1e3ceb8fff579635d655f2950d5a7", + "reference": "0222bf4dfaa1e3ceb8fff579635d655f2950d5a7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-26T06:31:10+00:00" + }, + { + "name": "twig/twig", + "version": "v3.30.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/8c737079b726af72ff8ef3c595be9f6a810ea1ef", + "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.0@stable", + "phpstan/phpstan": "^2.0@stable", + "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.30.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-09-25T13:20:01+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.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, + { + "name": "composer/semver", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5", + "reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Version comparison library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.5.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-09-24T14:38:51+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "doctrine/data-fixtures", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "bf7ac3a050b54b261cedfb3d0a44733819062275" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/bf7ac3a050b54b261cedfb3d0a44733819062275", + "reference": "bf7ac3a050b54b261cedfb3d0a44733819062275", + "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", + "doctrine/phpcr-odm": "^1.8 || ^2.0", + "ext-sqlite3": "*", + "fig/log-test": "^1", + "jackalope/jackalope-fs": "*", + "phpstan/phpstan": "2.1.46", + "phpunit/phpunit": "10.5.63 || 12.5.12", + "symfony/cache": "^6.4 || ^7 || ^8", + "symfony/var-exporter": "^6.4 || ^7 || ^8" + }, + "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.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%2Fdata-fixtures", + "type": "tidelift" + } + ], + "time": "2026-04-01T13:56:01+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": "ergebnis/agent-detector", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/agent-detector.git", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.51.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "infection/infection": "^0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a detector for detecting the presence of an agent.", + "homepage": "https://github.com/ergebnis/agent-detector", + "support": { + "issues": "https://github.com/ergebnis/agent-detector/issues", + "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/agent-detector" + }, + "time": "2026-05-07T08:19:07+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "d384a7d0d6e67aa497327260ca95023e92dc2032" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/d384a7d0d6e67aa497327260ca95023e92dc2032", + "reference": "d384a7d0d6e67aa497327260ca95023e92dc2032", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0 || ^1.1.2", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "symfony/polyfill-mbstring": "^1.33", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.4.1" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2026-09-29T22:11:39+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.95.27", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "1d0c68ee5f37446d8ad5970b82384cd1bb0a3317" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/1d0c68ee5f37446d8ad5970b82384cd1bb0a3317", + "reference": "1d0c68ee5f37446d8ad5970b82384cd1bb0a3317", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.3", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.5", + "ergebnis/agent-detector": "^1.2", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.3", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0 || ^9.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.37", + "symfony/polyfill-php80": "^1.37", + "symfony/polyfill-php81": "^1.37", + "symfony/polyfill-php84": "^1.37", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.11.0", + "infection/infection": "^0.32.7", + "justinrainbow/json-schema": "^6.12.0", + "keradus/cli-executor": "^2.3", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", + "phpunit/phpunit": "^9.6.36 || ^10.5.64 || ^11.5.56 || ^12.5.35 || ^13.3.3", + "symfony/polyfill-php85": "^1.41", + "symfony/var-dumper": "^5.4.48 || ^6.4.45 || ^7.4.18 || ^8.1.7", + "symfony/yaml": "^5.4.53 || ^6.4.45 || ^7.4.18 || ^8.1.6" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/**/Internal/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.27" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2026-09-22T09:54:55+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.15.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee80339fd9177ba44c49cdb653ff02a4d1106b9a", + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.5.3", + "guzzlehttp/psr7": "^2.13.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.7", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.15.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:21:06+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.5.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/cde49999552d185d64715fe9c1f77a2aadd2f9f1", + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.5.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:11:28+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.13.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "95e7828100de18b4e269fb1703be530082d5166d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/95e7828100de18b4e269fb1703be530082d5166d", + "reference": "95e7828100de18b4e269fb1703be530082d5166d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.13.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:13:11+00:00" + }, + { + "name": "liip/functional-test-bundle", + "version": "4.16.0", + "source": { + "type": "git", + "url": "https://github.com/liip/LiipFunctionalTestBundle.git", + "reference": "f78774663b08b8b014b0fb93d5d86c92c384b695" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/liip/LiipFunctionalTestBundle/zipball/f78774663b08b8b014b0fb93d5d86c92c384b695", + "reference": "f78774663b08b8b014b0fb93d5d86c92c384b695", + "shasum": "" + }, + "require": { + "php": "^8.0", + "phpunit/phpunit": "^9.6 || ^10.0 || ^11.0 || ^12.0 || ^13.0", + "symfony/browser-kit": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/framework-bundle": "^5.4 || ^6.4 || ^7.0 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^1.3 || ^2.0", + "doctrine/doctrine-bundle": "^2.11 || ^3.1.0", + "doctrine/orm": "^2.7 || ^3.0", + "ext-json": "*", + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "symfony/css-selector": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/doctrine-bridge": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/form": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/http-kernel": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/monolog-bundle": "^3.4 || ^4.0", + "symfony/security-bundle": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/twig-bundle": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/validator": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "twig/twig": "^2.0 || ^3.8" + }, + "suggest": { + "doctrine/annotations": "Required to use the @QueryCount(…) annotation", + "liip/test-fixtures-bundle": "Efficient loading of Doctrine fixtures in functional test-cases for Symfony applications" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Liip\\FunctionalTestBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Liip AG", + "homepage": "https://www.liip.ch/" + }, + { + "name": "Community contributions", + "homepage": "https://github.com/liip/LiipFunctionalTestBundle/contributors" + } + ], + "description": "This bundles provides additional functional test-cases for Symfony applications", + "keywords": [ + "symfony", + "testing" + ], + "support": { + "issues": "https://github.com/liip/LiipFunctionalTestBundle/issues", + "source": "https://github.com/liip/LiipFunctionalTestBundle/tree/4.16.0" + }, + "time": "2026-04-23T11:03:44+00:00" + }, + { + "name": "liip/test-fixtures-bundle", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/liip/LiipTestFixturesBundle.git", + "reference": "dfb7f32ecbbee95af0e086f2b90bfb08c04f28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/liip/LiipTestFixturesBundle/zipball/dfb7f32ecbbee95af0e086f2b90bfb08c04f28cb", + "reference": "dfb7f32ecbbee95af0e086f2b90bfb08c04f28cb", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^1.3.3 || ^2.0 || ^3.0 || ^4.0", + "php": "^8.1", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/event-dispatcher": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/event-dispatcher-contracts": "^1 || ^2 || ^3", + "symfony/framework-bundle": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.3 || ^7.0 || ^8.0" + }, + "conflict": { + "doctrine/annotations": "<1.13.1 || >=3.0", + "doctrine/dbal": "<2.13.1 || ~3.0.0 || >=5.0", + "doctrine/mongodb-odm": "<2.2 || >=3.0", + "doctrine/orm": "<2.14 || >=4.0" + }, + "require-dev": { + "doctrine/data-fixtures": "^1.7 || ^2.0.1", + "doctrine/doctrine-bundle": "^2.11 || ^3.1.0", + "doctrine/doctrine-fixtures-bundle": "^3.5.1 || ^4.0", + "doctrine/mongodb-odm": "^2.5", + "doctrine/mongodb-odm-bundle": "^4.4 || ^5.0", + "doctrine/orm": "^2.14 || ^3.1.0", + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "phpunit/phpunit": "^10.5.11 || ^11.0.4 || ^12.0.0", + "symfony/doctrine-bridge": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/monolog-bridge": "^5.4 || ^6.3 || ^7.0 || ^8.0", + "symfony/monolog-bundle": "^3.2", + "theofidry/alice-data-fixtures": "^1.5.2" + }, + "suggest": { + "doctrine/dbal": "Required when using the fixture loading functionality with an ORM and SQLite", + "doctrine/doctrine-fixtures-bundle": "Required when using the fixture loading functionality", + "doctrine/orm": "Required when using the fixture loading functionality with an ORM and SQLite", + "hautelook/alice-bundle": "Required when using loadFixtureFiles functionality with custom providers", + "theofidry/alice-data-fixtures": "Required when using loadFixtureFiles functionality" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Liip\\TestFixturesBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Liip AG", + "homepage": "http://www.liip.ch/" + }, + { + "name": "Community contributions", + "homepage": "https://github.com/liip/LiipTestFixturesBundle/contributors" + } + ], + "description": "This bundles enables efficient loading of Doctrine fixtures in functional test-cases for Symfony applications", + "keywords": [ + "fixtures", + "symfony", + "testing" + ], + "support": { + "issues": "https://github.com/liip/LiipTestFixturesBundle/issues", + "source": "https://github.com/liip/LiipTestFixturesBundle/tree/3.9.0" + }, + "time": "2026-03-26T18:01:52+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.11.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7", + "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=7.4" + }, + "require-dev": { + "phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.11.0" + }, + "time": "2026-08-18T06:18:41+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, + { + "name": "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": "php-coveralls/php-coveralls", + "version": "v2.9.1", + "source": { + "type": "git", + "url": "https://github.com/php-coveralls/php-coveralls.git", + "reference": "916bdb118597f61ce6715fb738ab8f234b89a2cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-coveralls/php-coveralls/zipball/916bdb118597f61ce6715fb738ab8f234b89a2cb", + "reference": "916bdb118597f61ce6715fb738ab8f234b89a2cb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "php": "^7.4 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/config": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.4 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpspec/prophecy-phpunit": "^2.4", + "phpunit/phpunit": "^9.6.29 || ^10.5.58 || ^11.5.43" + }, + "suggest": { + "symfony/http-kernel": "Allows Symfony integration" + }, + "bin": [ + "bin/php-coveralls" + ], + "type": "library", + "autoload": { + "psr-4": { + "PhpCoveralls\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kitamura Satoshi", + "email": "with.no.parachute@gmail.com", + "homepage": "https://www.facebook.com/satooshi.jp", + "role": "Original creator" + }, + { + "name": "Takashi Matsuo", + "email": "tmatsuo@google.com" + }, + { + "name": "Google Inc" + }, + { + "name": "Dariusz Ruminski", + "email": "dariusz.ruminski@gmail.com", + "homepage": "https://github.com/keradus" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-coveralls/php-coveralls/graphs/contributors" + } + ], + "description": "PHP client library for Coveralls API", + "homepage": "https://github.com/php-coveralls/php-coveralls", + "keywords": [ + "ci", + "coverage", + "github", + "test" + ], + "support": { + "issues": "https://github.com/php-coveralls/php-coveralls/issues", + "source": "https://github.com/php-coveralls/php-coveralls/tree/v2.9.1" + }, + "time": "2025-12-18T13:08:37+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.2.16", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92", + "reference": "46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-09-25T09:31:51+00:00" + }, + { + "name": "phpstan/phpstan-phpunit", + "version": "2.0.21", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-phpunit.git", + "reference": "8dbdc3a3e2d7c5dfea655d20ecca5bc9b1b1e441" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/8dbdc3a3e2d7c5dfea655d20ecca5bc9b1b1e441", + "reference": "8dbdc3a3e2d7c5dfea655d20ecca5bc9b1b1e441", + "shasum": "" + }, + "require": { + "phar-io/version": "^3.2", + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.2.6" + }, + "conflict": { + "phpunit/phpunit": "<7.0" + }, + "require-dev": { + "nikic/php-parser": "^5", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "shipmonk/name-collision-detector": "^2.1" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPUnit extensions and rules for PHPStan", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.21" + }, + "time": "2026-10-01T11:59:29+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "14.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "9be27409ad7bedddd34ed3bc27561d47c6faea06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9be27409ad7bedddd34ed3bc27561d47c6faea06", + "reference": "9be27409ad7bedddd34ed3bc27561d47c6faea06", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.9.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.3.2", + "sebastian/git-state": "^1.0", + "sebastian/lines-of-code": "^5.0.2", + "sebastian/version": "^7.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.6" + }, + "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": "14.4.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/14.4.0" + }, + "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": "2026-09-30T12:46:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/9bb4e6c58b62c1e043be995c66abec7c97307aae", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1" + }, + "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": "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/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-08-25T14:47:43+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "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": "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/7.0.0" + }, + "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-invoker", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:34:47+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "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/6.0.0" + }, + "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-text-template", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:36:37+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "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/9.0.0" + }, + "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-timer", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:37:53+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "13.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "ef714df238ae4d050e74efaa23f7c428bd01034e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ef714df238ae4d050e74efaa23f7c428bd01034e", + "reference": "ef714df238ae4d050e74efaa23f7c428bd01034e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.4", + "phpunit/php-file-iterator": "^7.0.2", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.1", + "sebastian/comparator": "^8.4", + "sebastian/diff": "^9.0.1", + "sebastian/environment": "^9.3.3", + "sebastian/exporter": "^8.2.1", + "sebastian/file-filter": "^1.0", + "sebastian/git-state": "^1.0", + "sebastian/global-state": "^9.0.1", + "sebastian/object-enumerator": "^8.1.0", + "sebastian/recursion-context": "^8.0.1", + "sebastian/type": "^7.0.2", + "sebastian/version": "^7.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "13.4-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/13.4.0" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-10-02T03:20:44+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+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": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/eeb759ad3146b7096fb59c3195d39e071cd409e3", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" + }, + "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 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/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-08-01T04:27:14+00:00" + }, + { + "name": "sebastian/comparator", + "version": "8.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/diff": "^9.0", + "sebastian/exporter": "^8.2" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.4-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/8.4.0" + }, + "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-08-07T07:23:13+00:00" + }, + { + "name": "sebastian/complexity", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "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/6.0.0" + }, + "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/complexity", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:41:32+00:00" + }, + { + "name": "sebastian/diff", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2df6626c1baf31d5a88674882a3072f151b5a26", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1", + "symfony/process": "^7.4.17" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-08-25T15:38:55+00:00" + }, + { + "name": "sebastian/environment", + "version": "9.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "d4dd10267a11055066a2a78ae04583d8ddd4f595" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d4dd10267a11055066a2a78ae04583d8ddd4f595", + "reference": "d4dd10267a11055066a2a78ae04583d8ddd4f595", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.6" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.3-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/9.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/environment", + "type": "tidelift" + } + ], + "time": "2026-10-01T12:37:56+00:00" + }, + { + "name": "sebastian/exporter", + "version": "8.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.2-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/8.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/exporter", + "type": "tidelift" + } + ], + "time": "2026-08-07T07:22:06+00:00" + }, + { + "name": "sebastian/file-filter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/file-filter.git", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/file-filter/zipball/33a26f394330f6faa7684bb9cc73afb7727aae93", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.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 filtering files", + "homepage": "https://github.com/sebastianbergmann/file-filter", + "support": { + "issues": "https://github.com/sebastianbergmann/file-filter/issues", + "security": "https://github.com/sebastianbergmann/file-filter/security/policy", + "source": "https://github.com/sebastianbergmann/file-filter/tree/1.0.0" + }, + "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/file-filter", + "type": "tidelift" + } + ], + "time": "2026-04-22T07:20:04+00:00" + }, + { + "name": "sebastian/git-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/792a952e0eba55b6960a48aeceb9f371aad1f76b", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.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 describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", + "support": { + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.0.0" + }, + "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/git-state", + "type": "tidelift" + } + ], + "time": "2026-03-21T12:54:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^13.1.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "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/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:11:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.8.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.4" + }, + "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 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/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-07-09T08:42:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "8.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/511064ecde82bd747e2ba2fab3dda8d977b59576", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-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/8.1.0" + }, + "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/object-enumerator", + "type": "tidelift" + } + ], + "time": "2026-08-13T07:05:05+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f71bbcdc4f95456b4622810bec64eb06372e25b2", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.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": "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/6.1.0" + }, + "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/object-reflector", + "type": "tidelift" + } + ], + "time": "2026-08-13T06:34:36+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "8.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.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/8.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2026-08-03T05:58:12+00:00" + }, + { + "name": "sebastian/type", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "bd1df467864cb95140414059a535b2d906173fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/bd1df467864cb95140414059a535b2d906173fcf", + "reference": "bd1df467864cb95140414059a535b2d906173fcf", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.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": "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/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-08-10T08:00:57+00:00" + }, + { + "name": "sebastian/version", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "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": "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/7.0.0" + }, + "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/version", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:52:52+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": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "7aff5c4b17a7b04ea6a8390773a93148d15a40b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/7aff5c4b17a7b04ea6a8390773a93148d15a40b7", + "reference": "7aff5c4b17a7b04ea6a8390773a93148d15a40b7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/dom-crawler": "^6.4|^7.0|^8.0" + }, + "require-dev": { + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^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/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "fecf40067fc8d8880ea87b8ac227600b9aadd1d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/fecf40067fc8d8880ea87b8ac227600b9aadd1d0", + "reference": "fecf40067fc8d8880ea87b8ac227600b9aadd1d0", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "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/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-23T10:03:40+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "6b037d62595666cf65de867073c9a1f00184be0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/6b037d62595666cf65de867073c9a1f00184be0c", + "reference": "6b037d62595666cf65de867073c9a1f00184be0c", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^6.4|^7.0|^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/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "1.68.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "1b0fb459594fdeb1d183065451c5e954babeb16c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/1b0fb459594fdeb1d183065451c5e954babeb16c", + "reference": "1b0fb459594fdeb1d183065451c5e954babeb16c", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1", + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^5.0", + "php": ">=8.2", + "symfony/config": "^7.4|^8.1", + "symfony/console": "^7.4|^8.1", + "symfony/dependency-injection": "^7.4|^8.1", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^7.4|^8.1", + "symfony/finder": "^7.4|^8.1", + "symfony/framework-bundle": "^7.4|^8.1", + "symfony/http-kernel": "^7.4|^8.1", + "symfony/process": "^7.4|^8.1" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.10|^3.0", + "doctrine/orm": "^2.15|^3", + "doctrine/persistence": "^3.1|^4.0", + "symfony/doctrine-bridge": "^7.4|^8.1", + "symfony/http-client": "^7.4|^8.1", + "symfony/phpunit-bridge": "^7.4|^8.1", + "symfony/security-core": "^7.4|^8.1", + "symfony/security-http": "^7.4|^8.1", + "symfony/yaml": "^7.4|^8.1", + "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/1.68.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-12T21:13:48+00:00" + }, + { + "name": "symfony/phpunit-bridge", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/phpunit-bridge.git", + "reference": "ff3a78aceb731b24b256a2845a8ad6e6851f942b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/ff3a78aceb731b24b256a2845a8ad6e6851f942b", + "reference": "ff3a78aceb731b24b256a2845a8ad6e6851f942b", + "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/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-30T18:28:44+00:00" + }, + { + "name": "symfony/web-profiler-bundle", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "916c2a4edf51270a5173a57d8d3936c02a4543a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/916c2a4edf51270a5173a57d8d3936c02a4543a1", + "reference": "916c2a4edf51270a5173a57d8d3936c02a4543a1", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.2", + "symfony/config": "^7.3|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "twig/twig": "^3.15|^4.0" + }, + "conflict": { + "symfony/form": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/serializer": "<7.2", + "symfony/workflow": "<7.3" + }, + "require-dev": { + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^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/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-28T19:09:50+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "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/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.5", + "ext-ctype": "*", + "ext-iconv": "*", + "ext-zip": "*", + "ext-pcntl": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/config/bundles.php b/config/bundles.php index 2b74fe6..0d2f765 100755 --- a/config/bundles.php +++ b/config/bundles.php @@ -14,5 +14,4 @@ Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], League\FlysystemBundle\FlysystemBundle::class => ['all' => true], - Knp\Bundle\SnappyBundle\KnpSnappyBundle::class => ['all' => true], ]; diff --git a/config/packages/dev/jms_serializer.yaml b/config/packages/dev/jms_serializer.yaml deleted file mode 100644 index f946041..0000000 --- a/config/packages/dev/jms_serializer.yaml +++ /dev/null @@ -1,7 +0,0 @@ -jms_serializer: - visitors: - json_serialization: - options: - - JSON_PRETTY_PRINT - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION diff --git a/config/packages/dev/web_profiler.yaml b/config/packages/dev/web_profiler.yaml deleted file mode 100644 index e92166a..0000000 --- a/config/packages/dev/web_profiler.yaml +++ /dev/null @@ -1,6 +0,0 @@ -web_profiler: - toolbar: true - intercept_redirects: false - -framework: - profiler: { only_exceptions: false } diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index ad30556..7ce98c7 100755 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -1,17 +1,44 @@ doctrine: dbal: + # server version is defined in DATABASE_URL (serverVersion) url: '%env(resolve:DATABASE_URL)%' - # IMPORTANT: You MUST configure your server version, - # either here or in the DATABASE_URL env var (see .env file) - #server_version: '5.7' + # ignore the "validation" schemas created by validator-cli.jar (otherwise schema:update and + # migrations:diff would drop the tables of the validations) + schema_filter: '~^(?!validation[a-z0-9]{24}\.)~' orm: - auto_generate_proxy_classes: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true mappings: App: + type: attribute is_bundle: false - type: annotation dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App + +when@test: + doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + +when@prod: + doctrine: + orm: + metadata_cache_driver: + type: pool + pool: doctrine.system_cache_pool + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/config/packages/flysystem.yaml b/config/packages/flysystem.yaml index 3d2ea0b..80e80e9 100644 --- a/config/packages/flysystem.yaml +++ b/config/packages/flysystem.yaml @@ -14,11 +14,16 @@ services: flysystem: storages: default.storage: - adapter: 'local' - options: + local: directory: '%kernel.project_dir%/var/data' data.storage: - adapter: 'asyncaws' - options: + asyncaws: client: 's3_client' - bucket: '%env(S3_BUCKET)%' \ No newline at end of file + bucket: '%env(S3_BUCKET)%' +when@test: + flysystem: + storages: + # removed after each test (see App\Tests\WebTestCase::tearDown) + default.storage: + local: + directory: '%kernel.project_dir%/var/data-test' diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index cad7f78..25d3478 100755 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -1,17 +1,19 @@ # see https://symfony.com/doc/current/reference/configuration/framework.html framework: secret: '%env(APP_SECRET)%' - #csrf_protection: true - #http_method_override: true + http_method_override: false + handle_all_throwables: true - # Enables session support. Note that the session will ONLY be started if you read or write from it. - # Remove or comment this section to explicitly disable session support. - session: - handler_id: null - cookie_secure: auto - cookie_samesite: lax + # stateless API (no session, no cookie) + session: false - #esi: true - #fragments: true php_errors: log: true + + # reverse proxies (TRUSTED_PROXIES, ex : PRIVATE_SUBNETS) whose X-Forwarded-For is trusted to get the client IP (rate limiter, logs) + trusted_proxies: '%env(default::TRUSTED_PROXIES)%' + trusted_headers: ['x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-host'] + +when@test: + framework: + test: true diff --git a/config/packages/jms_serializer.yaml b/config/packages/jms_serializer.yaml index bc55c64..af81473 100644 --- a/config/packages/jms_serializer.yaml +++ b/config/packages/jms_serializer.yaml @@ -3,3 +3,17 @@ jms_serializer: serialization: serialize_null: true enable_max_depth_checks: true + visitors: + json_serialization: + options: + - JSON_UNESCAPED_SLASHES + - JSON_PRESERVE_ZERO_FRACTION + +when@dev: + jms_serializer: + visitors: + json_serialization: + options: + - JSON_PRETTY_PRINT + - JSON_UNESCAPED_SLASHES + - JSON_PRESERVE_ZERO_FRACTION diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index 9db7d8a..ae4081a 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -1,6 +1,7 @@ monolog: channels: - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + - audit # creation, update and deletion of validations (with the client IP, see WebProcessor) when@dev: monolog: @@ -46,6 +47,14 @@ when@prod: handler: nested excluded_http_codes: [404, 405] buffer_size: 50 # How many messages should be saved? Prevent memory leaks + channels: ["!audit"] + # always logged (not only when an error occurs) + audit: + type: stream + channels: [audit] + path: php://stderr + level: info + formatter: monolog.formatter.json nested: type: stream path: php://stderr diff --git a/config/packages/prod/deprecations.yaml b/config/packages/prod/deprecations.yaml deleted file mode 100644 index 920a061..0000000 --- a/config/packages/prod/deprecations.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists -#monolog: -# channels: [deprecation] -# handlers: -# deprecation: -# type: stream -# channels: [deprecation] -# path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log" diff --git a/config/packages/prod/doctrine.yaml b/config/packages/prod/doctrine.yaml deleted file mode 100755 index 084f59a..0000000 --- a/config/packages/prod/doctrine.yaml +++ /dev/null @@ -1,20 +0,0 @@ -doctrine: - orm: - auto_generate_proxy_classes: false - metadata_cache_driver: - type: pool - pool: doctrine.system_cache_pool - query_cache_driver: - type: pool - pool: doctrine.system_cache_pool - result_cache_driver: - type: pool - pool: doctrine.result_cache_pool - -framework: - cache: - pools: - doctrine.result_cache_pool: - adapter: cache.app - doctrine.system_cache_pool: - adapter: cache.system diff --git a/config/packages/prod/jms_serializer.yaml b/config/packages/prod/jms_serializer.yaml deleted file mode 100644 index 89c86c8..0000000 --- a/config/packages/prod/jms_serializer.yaml +++ /dev/null @@ -1,6 +0,0 @@ -jms_serializer: - visitors: - json_serialization: - options: - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION diff --git a/config/packages/prod/routing.yaml b/config/packages/prod/routing.yaml deleted file mode 100755 index b3e6a0a..0000000 --- a/config/packages/prod/routing.yaml +++ /dev/null @@ -1,3 +0,0 @@ -framework: - router: - strict_requirements: null diff --git a/config/packages/rate_limiter.yaml b/config/packages/rate_limiter.yaml new file mode 100644 index 0000000..d48c165 --- /dev/null +++ b/config/packages/rate_limiter.yaml @@ -0,0 +1,8 @@ +framework: + rate_limiter: + # creation (POST) and update (PATCH) of validations, per client IP + # (behind a reverse proxy, see TRUSTED_PROXIES to get the client IP) + validation: + policy: sliding_window + limit: '%env(int:VALIDATION_RATE_LIMIT)%' + interval: '1 hour' diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml index 7e97762..60a7991 100755 --- a/config/packages/routing.yaml +++ b/config/packages/routing.yaml @@ -1,3 +1,8 @@ framework: router: utf8: true + +when@prod: + framework: + router: + strict_requirements: null diff --git a/config/packages/test/doctrine.yaml b/config/packages/test/doctrine.yaml deleted file mode 100644 index 169d4f1..0000000 --- a/config/packages/test/doctrine.yaml +++ /dev/null @@ -1,4 +0,0 @@ -doctrine: - dbal: - # "TEST_TOKEN" is typically set by ParaTest - dbname: 'validator_api_test%env(default::TEST_TOKEN)%' diff --git a/config/packages/test/framework.yaml b/config/packages/test/framework.yaml deleted file mode 100755 index 3c43569..0000000 --- a/config/packages/test/framework.yaml +++ /dev/null @@ -1,5 +0,0 @@ -framework: - test: true - session: - storage_factory_id: session.storage.factory.mock_file - diff --git a/config/packages/test/twig.yaml b/config/packages/test/twig.yaml deleted file mode 100755 index 8c6e0b4..0000000 --- a/config/packages/test/twig.yaml +++ /dev/null @@ -1,2 +0,0 @@ -twig: - strict_variables: true diff --git a/config/packages/test/validator.yaml b/config/packages/test/validator.yaml deleted file mode 100755 index 1e5ab78..0000000 --- a/config/packages/test/validator.yaml +++ /dev/null @@ -1,3 +0,0 @@ -framework: - validation: - not_compromised_password: false diff --git a/config/packages/test/web_profiler.yaml b/config/packages/test/web_profiler.yaml deleted file mode 100644 index 03752de..0000000 --- a/config/packages/test/web_profiler.yaml +++ /dev/null @@ -1,6 +0,0 @@ -web_profiler: - toolbar: false - intercept_redirects: false - -framework: - profiler: { collect: false } diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 6403e6a..c194f93 100755 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -2,4 +2,7 @@ twig: default_path: '%kernel.project_dir%/templates' debug: '%kernel.debug%' strict_variables: '%kernel.debug%' - exception_controller: null + +when@test: + twig: + strict_variables: true diff --git a/config/packages/validator.yaml b/config/packages/validator.yaml deleted file mode 100755 index 350786a..0000000 --- a/config/packages/validator.yaml +++ /dev/null @@ -1,8 +0,0 @@ -framework: - validation: - email_validation_mode: html5 - - # Enables validator auto-mapping support. - # For instance, basic validation constraints will be inferred from Doctrine's metadata. - #auto_mapping: - # App\Entity\: [] diff --git a/config/packages/web_profiler.yaml b/config/packages/web_profiler.yaml index 17893da..7945080 100644 --- a/config/packages/web_profiler.yaml +++ b/config/packages/web_profiler.yaml @@ -4,7 +4,7 @@ when@dev: intercept_redirects: false framework: - profiler: { only_exceptions: false } + profiler: { only_exceptions: false, collect_serializer_data: true } when@test: web_profiler: @@ -12,4 +12,4 @@ when@test: intercept_redirects: false framework: - profiler: { collect: false } + profiler: { collect: false, collect_serializer_data: true } diff --git a/config/preload.php b/config/preload.php index 064bdcd..5ebcdb2 100755 --- a/config/preload.php +++ b/config/preload.php @@ -1,9 +1,5 @@ [ + * 'App\\' => [ + * 'resource' => '../src/', + * ], + * ], + * ]); + * ``` + * + * @psalm-type ImportsConfig = list + * @psalm-type ParametersConfig = array|Param|null>|Param|null> + * @psalm-type ArgumentsType = list|array + * @psalm-type CallType = array|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool} + * @psalm-type TagsType = list>> // arrays inside the list must have only one element, with the tag name as the key + * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator + * @psalm-type DeprecationType = array{package: string, version: string, message?: string} + * @psalm-type DefaultsType = array{ + * public?: bool, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * } + * @psalm-type InstanceofType = array{ + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type DefinitionType = array{ + * class?: string, + * file?: string, + * parent?: string, + * shared?: bool, + * synthetic?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * configurator?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * decorates?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * from_callable?: CallbackType, + * } + * @psalm-type AliasType = string|array{ + * alias: string, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type PrototypeType = array{ + * resource: string, + * namespace?: string, + * exclude?: string|list, + * parent?: string, + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type StackType = array{ + * stack: list>, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type ServicesConfig = array{ + * _defaults?: DefaultsType, + * _instanceof?: array, + * ... + * } + * @psalm-type ExtensionType = array + * @psalm-type FrameworkConfig = array{ + * secret?: scalar|Param|null, + * http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false + * allowed_http_method_override?: null|list, + * trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%" + * ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%" + * test?: bool|Param, + * default_locale?: scalar|Param|null, // Default: "en" + * set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false + * set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false + * enabled_locales?: list, + * trusted_hosts?: Param|string|list, + * trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"] + * trusted_headers?: Param|string|list, + * error_controller?: scalar|Param|null, // Default: "error_controller" + * handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true + * csrf_protection?: bool|array{ + * enabled?: scalar|Param|null, // Default: null + * stateless_token_ids?: list, + * check_header?: scalar|Param|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false + * cookie_name?: scalar|Param|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token" + * }, + * form?: bool|array{ // Form configuration + * enabled?: bool|Param, // Default: false + * csrf_protection?: bool|array{ + * enabled?: scalar|Param|null, // Default: null + * token_id?: scalar|Param|null, // Default: null + * field_name?: scalar|Param|null, // Default: "_token" + * field_attr?: array, + * }, + * }, + * http_cache?: bool|array{ // HTTP cache configuration + * enabled?: bool|Param, // Default: false + * debug?: bool|Param, // Default: "%kernel.debug%" + * trace_level?: "none"|"short"|"full"|Param, + * trace_header?: scalar|Param|null, + * default_ttl?: int|Param, + * private_headers?: list, + * skip_response_headers?: list, + * allow_reload?: bool|Param, + * allow_revalidate?: bool|Param, + * stale_while_revalidate?: int|Param, + * stale_if_error?: int|Param, + * terminate_on_cache_hit?: bool|Param, + * }, + * esi?: bool|array{ // ESI configuration + * enabled?: bool|Param, // Default: false + * }, + * ssi?: bool|array{ // SSI configuration + * enabled?: bool|Param, // Default: false + * }, + * fragments?: bool|array{ // Fragments configuration + * enabled?: bool|Param, // Default: false + * hinclude_default_template?: scalar|Param|null, // Default: null + * path?: scalar|Param|null, // Default: "/_fragment" + * }, + * profiler?: bool|array{ // Profiler configuration + * enabled?: bool|Param, // Default: false + * collect?: bool|Param, // Default: true + * collect_parameter?: scalar|Param|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null + * only_exceptions?: bool|Param, // Default: false + * only_main_requests?: bool|Param, // Default: false + * dsn?: scalar|Param|null, // Default: "file:%kernel.cache_dir%/profiler" + * collect_serializer_data?: bool|Param, // Enables the serializer data collector and profiler panel. // Default: false + * }, + * workflows?: bool|array{ + * enabled?: bool|Param, // Default: false + * workflows?: array, + * definition_validators?: list, + * support_strategy?: scalar|Param|null, + * initial_marking?: \BackedEnum|Param|string|list, + * events_to_dispatch?: null|list, + * places?: Param|string|list, + * }>, + * transitions?: list, + * to?: \BackedEnum|Param|string|list, + * weight?: int|Param, // Default: 1 + * metadata?: array, + * }>, + * metadata?: array, + * }>, + * }, + * router?: bool|array{ // Router configuration + * enabled?: bool|Param, // Default: false + * resource?: scalar|Param|null, + * type?: scalar|Param|null, + * cache_dir?: scalar|Param|null, // Deprecated: Setting the "framework.router.cache_dir.cache_dir" configuration option is deprecated. It will be removed in version 8.0. // Default: "%kernel.build_dir%" + * default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null + * http_port?: scalar|Param|null, // Default: 80 + * https_port?: scalar|Param|null, // Default: 443 + * strict_requirements?: scalar|Param|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true + * utf8?: bool|Param, // Default: true + * }, + * session?: bool|array{ // Session configuration + * enabled?: bool|Param, // Default: false + * storage_factory_id?: scalar|Param|null, // Default: "session.storage.factory.native" + * handler_id?: scalar|Param|null, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null. + * name?: scalar|Param|null, + * cookie_lifetime?: scalar|Param|null, + * cookie_path?: scalar|Param|null, + * cookie_domain?: scalar|Param|null, + * cookie_secure?: true|false|"auto"|Param, // Default: "auto" + * cookie_httponly?: bool|Param, // Default: true + * cookie_samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" + * use_cookies?: bool|Param, + * gc_divisor?: scalar|Param|null, + * gc_probability?: scalar|Param|null, + * gc_maxlifetime?: scalar|Param|null, + * save_path?: scalar|Param|null, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null. + * metadata_update_threshold?: int|Param, // Seconds to wait between 2 session metadata updates. // Default: 0 + * sid_length?: int|Param, // Deprecated: Setting the "framework.session.sid_length.sid_length" configuration option is deprecated. It will be removed in version 8.0. No alternative is provided as PHP 8.4 has deprecated the related option. + * sid_bits_per_character?: int|Param, // Deprecated: Setting the "framework.session.sid_bits_per_character.sid_bits_per_character" configuration option is deprecated. It will be removed in version 8.0. No alternative is provided as PHP 8.4 has deprecated the related option. + * }, + * request?: bool|array{ // Request configuration + * enabled?: bool|Param, // Default: false + * formats?: array>, + * }, + * assets?: bool|array{ // Assets configuration + * enabled?: bool|Param, // Default: true + * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false + * version_strategy?: scalar|Param|null, // Default: null + * version?: scalar|Param|null, // Default: null + * version_format?: scalar|Param|null, // Default: "%%s?%%s" + * json_manifest_path?: scalar|Param|null, // Default: null + * base_path?: scalar|Param|null, // Default: "" + * base_urls?: Param|string|list, + * packages?: array, + * }>, + * }, + * asset_mapper?: bool|array{ // Asset Mapper configuration + * enabled?: bool|Param, // Default: false + * paths?: Param|string|array, + * excluded_patterns?: list, + * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true + * server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true + * public_prefix?: scalar|Param|null, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/" + * missing_import_mode?: "strict"|"warn"|"ignore"|Param, // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn" + * extensions?: array, + * importmap_path?: scalar|Param|null, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php" + * importmap_polyfill?: scalar|Param|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims" + * importmap_script_attributes?: array, + * vendor_dir?: scalar|Param|null, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor" + * precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip. + * enabled?: bool|Param, // Default: false + * formats?: list, + * extensions?: list, + * }, + * }, + * translator?: bool|array{ // Translator configuration + * enabled?: bool|Param, // Default: false + * fallbacks?: Param|string|list, + * logging?: bool|Param, // Default: false + * formatter?: scalar|Param|null, // Default: "translator.formatter.default" + * cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations" + * default_path?: scalar|Param|null, // The default path used to load translations. // Default: "%kernel.project_dir%/translations" + * paths?: list, + * pseudo_localization?: bool|array{ + * enabled?: bool|Param, // Default: false + * accents?: bool|Param, // Default: true + * expansion_factor?: float|Param, // Default: 1.0 + * brackets?: bool|Param, // Default: true + * parse_html?: bool|Param, // Default: false + * localizable_html_attributes?: list, + * }, + * providers?: array, + * locales?: list, + * }>, + * globals?: array, + * domain?: string|Param, + * }>, + * }, + * validation?: bool|array{ // Validation configuration + * enabled?: bool|Param, // Default: false + * cache?: scalar|Param|null, // Deprecated: Setting the "framework.validation.cache.cache" configuration option is deprecated. It will be removed in version 8.0. + * enable_attributes?: bool|Param, // Default: true + * static_method?: Param|string|list, + * translation_domain?: scalar|Param|null, // Default: "validators" + * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|"loose"|Param, // Default: "html5" + * mapping?: array{ + * paths?: list, + * }, + * not_compromised_password?: bool|array{ + * enabled?: bool|Param, // When disabled, compromised passwords will be accepted as valid. // Default: true + * endpoint?: scalar|Param|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null + * }, + * disable_translation?: bool|Param, // Default: false + * auto_mapping?: array, + * }>, + * }, + * annotations?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * serializer?: bool|array{ // Serializer configuration + * enabled?: bool|Param, // Default: false + * enable_attributes?: bool|Param, // Default: true + * name_converter?: scalar|Param|null, + * circular_reference_handler?: scalar|Param|null, + * max_depth_handler?: scalar|Param|null, + * mapping?: array{ + * paths?: list, + * }, + * default_context?: array, + * named_serializers?: array, + * include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true + * include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true + * }>, + * }, + * property_access?: bool|array{ // Property access configuration + * enabled?: bool|Param, // Default: false + * magic_call?: bool|Param, // Default: false + * magic_get?: bool|Param, // Default: true + * magic_set?: bool|Param, // Default: true + * throw_exception_on_invalid_index?: bool|Param, // Default: false + * throw_exception_on_invalid_property_path?: bool|Param, // Default: true + * }, + * type_info?: bool|array{ // Type info configuration + * enabled?: bool|Param, // Default: false + * aliases?: array, + * }, + * property_info?: bool|array{ // Property info configuration + * enabled?: bool|Param, // Default: false + * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. + * }, + * cache?: array{ // Cache configuration + * prefix_seed?: scalar|Param|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%" + * app?: scalar|Param|null, // App related cache pools configuration. // Default: "cache.adapter.filesystem" + * system?: scalar|Param|null, // System related cache pools configuration. // Default: "cache.adapter.system" + * directory?: scalar|Param|null, // Default: "%kernel.share_dir%/pools/app" + * default_psr6_provider?: scalar|Param|null, + * default_redis_provider?: scalar|Param|null, // Default: "redis://localhost" + * default_valkey_provider?: scalar|Param|null, // Default: "valkey://localhost" + * default_memcached_provider?: scalar|Param|null, // Default: "memcached://localhost" + * default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection" + * default_pdo_provider?: scalar|Param|null, // Default: null + * pools?: array, + * tags?: scalar|Param|null, // Default: null + * public?: bool|Param, // Default: false + * default_lifetime?: scalar|Param|null, // Default lifetime of the pool. + * provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter. + * early_expiration_message_bus?: scalar|Param|null, + * clearer?: scalar|Param|null, + * }>, + * }, + * php_errors?: array{ // PHP errors handling configuration + * log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true + * throw?: bool|Param, // Throw PHP errors as \ErrorException instances. // Default: true + * }, + * exceptions?: array, + * web_link?: bool|array{ // Web links configuration + * enabled?: bool|Param, // Default: false + * }, + * lock?: Param|bool|string|array{ // Lock configuration + * enabled?: bool|Param, // Default: false + * resources?: Param|string|array>, + * }, + * semaphore?: Param|bool|string|array{ // Semaphore configuration + * enabled?: bool|Param, // Default: false + * resources?: Param|string|array, + * }, + * messenger?: bool|array{ // Messenger configuration + * enabled?: bool|Param, // Default: false + * routing?: array, + * }>, + * serializer?: array{ + * default_serializer?: scalar|Param|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer" + * symfony_serializer?: array{ + * format?: scalar|Param|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json" + * context?: array, + * }, + * }, + * transports?: array, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * retry_strategy?: Param|string|array{ + * service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1 + * }, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null + * }>, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * stop_worker_on_signals?: Param|int|string|list, + * default_bus?: scalar|Param|null, // Default: null + * buses?: array, + * }>, + * }>, + * }, + * scheduler?: bool|array{ // Scheduler configuration + * enabled?: bool|Param, // Default: false + * }, + * disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true + * http_client?: bool|array{ // HTTP Client configuration + * enabled?: bool|Param, // Default: true + * max_host_connections?: int|Param, // The maximum number of connections to a single host. + * default_options?: array{ + * headers?: array, + * vars?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...) + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: Param|int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }, + * mock_response_factory?: scalar|Param|null, // The id of the service that should generate mock responses. It should be either an invokable or an iterable. + * scoped_clients?: array, + * headers?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...). + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: Param|int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }>, + * }, + * mailer?: bool|array{ // Mailer configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * dsn?: scalar|Param|null, // Default: null + * transports?: array, + * envelope?: array{ // Mailer Envelope configuration + * sender?: scalar|Param|null, + * recipients?: Param|string|list, + * allowed_recipients?: Param|string|list, + * }, + * headers?: array, + * dkim_signer?: bool|array{ // DKIM signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|Param|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: "" + * domain?: scalar|Param|null, // Default: "" + * select?: scalar|Param|null, // Default: "" + * passphrase?: scalar|Param|null, // The private key passphrase // Default: "" + * options?: array, + * }, + * smime_signer?: bool|array{ // S/MIME signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|Param|null, // Path to key (in PEM format) // Default: "" + * certificate?: scalar|Param|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: "" + * passphrase?: scalar|Param|null, // The private key passphrase // Default: null + * extra_certificates?: scalar|Param|null, // Default: null + * sign_options?: int|Param, // Default: null + * }, + * smime_encrypter?: bool|array{ // S/MIME encrypter configuration + * enabled?: bool|Param, // Default: false + * repository?: scalar|Param|null, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: "" + * cipher?: int|Param, // A set of algorithms used to encrypt the message // Default: null + * }, + * }, + * secrets?: bool|array{ + * enabled?: bool|Param, // Default: true + * vault_directory?: scalar|Param|null, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%" + * local_dotenv_file?: scalar|Param|null, // Default: "%kernel.project_dir%/.env.%kernel.environment%.local" + * decryption_env_var?: scalar|Param|null, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET" + * }, + * notifier?: bool|array{ // Notifier configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * chatter_transports?: array, + * texter_transports?: array, + * notification_on_failed_messages?: bool|Param, // Default: false + * channel_policy?: array>, + * admin_recipients?: list, + * }, + * rate_limiter?: bool|array{ // Rate limiter configuration + * enabled?: bool|Param, // Default: true + * limiters?: array, + * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst. + * interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket". + * interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * amount?: int|Param, // Amount of tokens to add each interval. // Default: 1 + * }, + * }>, + * }, + * uid?: bool|array{ // Uid configuration + * enabled?: bool|Param, // Default: false + * default_uuid_version?: 7|6|4|1|Param, // Default: 7 + * name_based_uuid_version?: 5|3|Param, // Default: 5 + * name_based_uuid_namespace?: scalar|Param|null, + * time_based_uuid_version?: 7|6|1|Param, // Default: 7 + * time_based_uuid_node?: scalar|Param|null, + * }, + * html_sanitizer?: bool|array{ // HtmlSanitizer configuration + * enabled?: bool|Param, // Default: false + * sanitizers?: array, + * block_elements?: Param|string|list, + * drop_elements?: Param|string|list, + * allow_attributes?: array, + * drop_attributes?: array, + * force_attributes?: array>, + * force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false + * allowed_link_schemes?: Param|string|list, + * allowed_link_hosts?: Param|null|string|list, + * allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false + * allowed_media_schemes?: Param|string|list, + * allowed_media_hosts?: Param|null|string|list, + * allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false + * with_attribute_sanitizers?: Param|string|list, + * without_attribute_sanitizers?: Param|string|list, + * max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0 + * }>, + * }, + * webhook?: bool|array{ // Webhook configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus" + * routing?: array, + * }, + * remote_event?: bool|array{ // RemoteEvent configuration + * enabled?: bool|Param, // Default: false + * }, + * json_streamer?: bool|array{ // JSON streamer configuration + * enabled?: bool|Param, // Default: false + * }, + * } + * @psalm-type TwigConfig = array{ + * form_themes?: list, + * globals?: array + * }>, + * autoescape_service?: scalar|Param|null, // Default: null + * autoescape_service_method?: scalar|Param|null, // Default: null + * base_template_class?: scalar|Param|null, // Deprecated: The child node "base_template_class" at path "twig.base_template_class" is deprecated. + * cache?: scalar|Param|null, // Default: true + * charset?: scalar|Param|null, // Default: "%kernel.charset%" + * debug?: bool|Param, // Default: "%kernel.debug%" + * strict_variables?: bool|Param, // Default: "%kernel.debug%" + * auto_reload?: scalar|Param|null, + * optimizations?: int|Param, + * default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" + * file_name_pattern?: Param|string|list, + * paths?: array, + * date?: array{ // The default format options used by the date filter. + * format?: scalar|Param|null, // Default: "F j, Y H:i" + * interval_format?: scalar|Param|null, // Default: "%d days" + * timezone?: scalar|Param|null, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null + * }, + * number_format?: array{ // The default format options for the number_format filter. + * decimals?: int|Param, // Default: 0 + * decimal_point?: scalar|Param|null, // Default: "." + * thousands_separator?: scalar|Param|null, // Default: "," + * }, + * mailer?: array{ + * html_to_text_converter?: scalar|Param|null, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null + * }, + * } + * @psalm-type MakerConfig = array{ + * root_namespace?: scalar|Param|null, // Default: "App" + * generate_final_classes?: bool|Param, // Default: true + * generate_final_entities?: bool|Param, // Default: false + * } + * @psalm-type DoctrineConfig = array{ + * dbal?: array{ + * default_connection?: scalar|Param|null, + * types?: array, + * driver_schemes?: array, + * connections?: array, + * mapping_types?: array, + * default_table_options?: array, + * schema_manager_factory?: scalar|Param|null, // Default: "doctrine.dbal.default_schema_manager_factory" + * result_cache?: scalar|Param|null, + * replicas?: array + * }>, + * ... + * }>, + * ... + * }, + * orm?: array{ + * default_entity_manager?: scalar|Param|null, + * enable_native_lazy_objects?: bool|Param, // Deprecated: The "enable_native_lazy_objects" option is deprecated and will be removed in DoctrineBundle 4.0, as native lazy objects are now always enabled. // Default: true + * controller_resolver?: bool|array{ + * enabled?: bool|Param, // Default: true + * auto_mapping?: bool|Param, // Deprecated: The "doctrine.orm.controller_resolver.auto_mapping.auto_mapping" option is deprecated and will be removed in DoctrineBundle 4.0, as it only accepts `false` since 3.0. // Set to true to enable using route placeholders as lookup criteria when the primary key doesn't match the argument name // Default: false + * evict_cache?: bool|Param, // Set to true to fetch the entity from the database instead of using the cache, if any // Default: false + * }, + * entity_managers?: array, + * }>, + * }>, + * ... + * }, + * connection?: scalar|Param|null, + * class_metadata_factory_name?: scalar|Param|null, // Default: "Doctrine\\ORM\\Mapping\\ClassMetadataFactory" + * default_repository_class?: scalar|Param|null, // Default: "Doctrine\\ORM\\EntityRepository" + * auto_mapping?: scalar|Param|null, // Default: false + * naming_strategy?: scalar|Param|null, // Default: "doctrine.orm.naming_strategy.default" + * quote_strategy?: scalar|Param|null, // Default: "doctrine.orm.quote_strategy.default" + * typed_field_mapper?: scalar|Param|null, // Default: "doctrine.orm.typed_field_mapper.default" + * entity_listener_resolver?: scalar|Param|null, // Default: null + * fetch_mode_subselect_batch_size?: scalar|Param|null, + * repository_factory?: scalar|Param|null, // Default: "doctrine.orm.container_repository_factory" + * schema_ignore_classes?: list, + * validate_xml_mapping?: bool|Param, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.14 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/6728. // Default: false + * second_level_cache?: array{ + * region_cache_driver?: Param|string|array{ + * type?: scalar|Param|null, // Default: null + * id?: scalar|Param|null, + * pool?: scalar|Param|null, + * }, + * region_lock_lifetime?: scalar|Param|null, // Default: 60 + * log_enabled?: bool|Param, // Default: true + * region_lifetime?: scalar|Param|null, // Default: 3600 + * enabled?: bool|Param, // Default: true + * factory?: scalar|Param|null, + * regions?: array, + * loggers?: array, + * }, + * hydrators?: array, + * mappings?: array, + * dql?: array{ + * string_functions?: array, + * numeric_functions?: array, + * datetime_functions?: array, + * }, + * filters?: array, + * ... + * }>, + * identity_generation_preferences?: array, + * }>, + * resolve_target_entities?: array, + * ... + * }, + * } + * @psalm-type DoctrineMigrationsConfig = array{ + * enable_service_migrations?: bool|Param, // Whether to enable fetching migrations from the service container. // Default: false + * migrations_paths?: array, + * services?: array, + * factories?: array, + * storage?: array{ // Storage to use for migration status metadata. + * table_storage?: array{ // The default metadata storage, implemented as a table in the database. + * table_name?: scalar|Param|null, // Default: null + * version_column_name?: scalar|Param|null, // Default: null + * version_column_length?: scalar|Param|null, // Default: null + * executed_at_column_name?: scalar|Param|null, // Default: null + * execution_time_column_name?: scalar|Param|null, // Default: null + * }, + * }, + * migrations?: list, + * connection?: scalar|Param|null, // Connection name to use for the migrations database. // Default: null + * em?: scalar|Param|null, // Entity manager name to use for the migrations database (available when doctrine/orm is installed). // Default: null + * all_or_nothing?: scalar|Param|null, // Run all migrations in a transaction. // Default: false + * check_database_platform?: scalar|Param|null, // Adds an extra check in the generated migrations to allow execution only on the same platform as they were initially generated on. // Default: true + * custom_template?: scalar|Param|null, // Custom template path for generated migration classes. // Default: null + * organize_migrations?: scalar|Param|null, // Organize migrations mode. Possible values are: "BY_YEAR", "BY_YEAR_AND_MONTH", false // Default: false + * enable_profiler?: bool|Param, // Whether or not to enable the profiler collector to calculate and visualize migration status. This adds some queries overhead. // Default: false + * transactional?: bool|Param, // Whether or not to wrap migrations in a single transaction. // Default: true + * } + * @psalm-type JmsSerializerConfig = array{ + * twig_enabled?: scalar|Param|null, // Default: "default" + * profiler?: scalar|Param|null, // Default: true + * enum_support?: scalar|Param|null, // Default: false + * default_value_property_reader_support?: scalar|Param|null, // Default: false + * handlers?: array{ + * datetime?: array{ + * default_format?: scalar|Param|null, // Default: "Y-m-d\\TH:i:sP" + * default_deserialization_formats?: list, + * default_timezone?: scalar|Param|null, // Default: "UTC" + * cdata?: scalar|Param|null, // Default: true + * }, + * array_collection?: array{ + * initialize_excluded?: bool|Param, // Default: false + * }, + * symfony_uid?: array{ + * default_format?: scalar|Param|null, // Default: "canonical" + * cdata?: scalar|Param|null, // Default: true + * }, + * }, + * subscribers?: array{ + * doctrine_proxy?: array{ + * initialize_excluded?: bool|Param, // Default: false + * initialize_virtual_types?: bool|Param, // Default: false + * }, + * }, + * object_constructors?: array{ + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: true + * fallback_strategy?: "null"|"exception"|"fallback"|Param, // Default: "null" + * }, + * }, + * property_naming?: Param|string|array{ + * id?: scalar|Param|null, + * separator?: scalar|Param|null, // Default: "_" + * lower_case?: bool|Param, // Default: true + * }, + * expression_evaluator?: Param|string|array{ + * id?: scalar|Param|null, // Default: null + * }, + * metadata?: array{ + * warmup?: array{ + * paths?: array{ + * included?: list, + * excluded?: list, + * }, + * }, + * cache?: scalar|Param|null, // Default: "file" + * debug?: bool|Param, // Default: true + * file_cache?: array{ + * dir?: scalar|Param|null, // Default: null + * }, + * include_interfaces?: bool|Param, // Default: false + * auto_detection?: bool|Param, // Default: true + * infer_types_from_doc_block?: bool|Param, // Default: false + * infer_types_from_doctrine_metadata?: bool|Param, // Infers type information from Doctrine metadata if no explicit type has been defined for a property. // Default: true + * directories?: array, + * }, + * visitors?: array{ + * json_serialization?: array{ + * depth?: scalar|Param|null, + * options?: scalar|Param|null, // Default: 1024 + * }, + * json_deserialization?: array{ + * options?: scalar|Param|null, // Default: 0 + * strict?: bool|Param, // Default: false + * }, + * xml_serialization?: array{ + * version?: scalar|Param|null, + * encoding?: scalar|Param|null, + * format_output?: bool|Param, // Default: false + * default_root_name?: scalar|Param|null, + * default_root_ns?: scalar|Param|null, // Default: "" + * }, + * xml_deserialization?: array{ + * doctype_whitelist?: list, + * external_entities?: bool|Param, // Default: false + * options?: scalar|Param|null, // Default: 0 + * }, + * }, + * default_context?: array{ + * serialization?: Param|string|array{ + * id?: scalar|Param|null, + * serialize_null?: scalar|Param|null, // Flag if null values should be serialized + * enable_max_depth_checks?: scalar|Param|null, // Flag to enable the max-depth exclusion strategy + * attributes?: array, + * groups?: list, + * version?: scalar|Param|null, // Application version to use in exclusion strategies + * }, + * deserialization?: Param|string|array{ + * id?: scalar|Param|null, + * serialize_null?: scalar|Param|null, // Flag if null values should be serialized + * enable_max_depth_checks?: scalar|Param|null, // Flag to enable the max-depth exclusion strategy + * attributes?: array, + * groups?: list, + * version?: scalar|Param|null, // Application version to use in exclusion strategies + * }, + * }, + * instances?: array, + * default_timezone?: scalar|Param|null, // Default: "UTC" + * cdata?: scalar|Param|null, // Default: true + * }, + * array_collection?: array{ + * initialize_excluded?: bool|Param, // Default: false + * }, + * symfony_uid?: array{ + * default_format?: scalar|Param|null, // Default: "canonical" + * cdata?: scalar|Param|null, // Default: true + * }, + * }, + * subscribers?: array{ + * doctrine_proxy?: array{ + * initialize_excluded?: bool|Param, // Default: false + * initialize_virtual_types?: bool|Param, // Default: false + * }, + * }, + * object_constructors?: array{ + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: true + * fallback_strategy?: "null"|"exception"|"fallback"|Param, // Default: "null" + * }, + * }, + * property_naming?: Param|string|array{ + * id?: scalar|Param|null, + * separator?: scalar|Param|null, // Default: "_" + * lower_case?: bool|Param, // Default: true + * }, + * expression_evaluator?: Param|string|array{ + * id?: scalar|Param|null, // Default: null + * }, + * metadata?: array{ + * warmup?: array{ + * paths?: array{ + * included?: list, + * excluded?: list, + * }, + * }, + * cache?: scalar|Param|null, // Default: "file" + * debug?: bool|Param, // Default: true + * file_cache?: array{ + * dir?: scalar|Param|null, // Default: null + * }, + * include_interfaces?: bool|Param, // Default: false + * auto_detection?: bool|Param, // Default: true + * infer_types_from_doc_block?: bool|Param, // Default: false + * infer_types_from_doctrine_metadata?: bool|Param, // Infers type information from Doctrine metadata if no explicit type has been defined for a property. // Default: true + * directories?: array, + * }, + * visitors?: array{ + * json_serialization?: array{ + * depth?: scalar|Param|null, + * options?: scalar|Param|null, // Default: 1024 + * }, + * json_deserialization?: array{ + * options?: scalar|Param|null, // Default: 0 + * strict?: bool|Param, // Default: false + * }, + * xml_serialization?: array{ + * version?: scalar|Param|null, + * encoding?: scalar|Param|null, + * format_output?: bool|Param, // Default: false + * default_root_name?: scalar|Param|null, + * default_root_ns?: scalar|Param|null, // Default: "" + * }, + * xml_deserialization?: array{ + * doctype_whitelist?: list, + * external_entities?: bool|Param, // Default: false + * options?: scalar|Param|null, // Default: 0 + * }, + * }, + * default_context?: array{ + * serialization?: Param|string|array{ + * id?: scalar|Param|null, + * serialize_null?: scalar|Param|null, // Flag if null values should be serialized + * enable_max_depth_checks?: scalar|Param|null, // Flag to enable the max-depth exclusion strategy + * attributes?: array, + * groups?: list, + * version?: scalar|Param|null, // Application version to use in exclusion strategies + * }, + * deserialization?: Param|string|array{ + * id?: scalar|Param|null, + * serialize_null?: scalar|Param|null, // Flag if null values should be serialized + * enable_max_depth_checks?: scalar|Param|null, // Flag to enable the max-depth exclusion strategy + * attributes?: array, + * groups?: list, + * version?: scalar|Param|null, // Application version to use in exclusion strategies + * }, + * }, + * }>, + * } + * @psalm-type MonologConfig = array{ + * use_microseconds?: scalar|Param|null, // Default: true + * timezone?: string|Param, // The timezone used for the timestamp of every log record (e.g. "UTC" or "Europe/Paris"). Defaults to the PHP default timezone. // Default: null + * channels?: list, + * handlers?: array + * }, + * path?: scalar|Param|null, // Default: "%kernel.logs_dir%/%kernel.environment%.log" + * file_permission?: scalar|Param|null, // Default: null + * use_locking?: bool|Param, // Default: false + * filename_format?: scalar|Param|null, // Default: "{filename}-{date}" + * date_format?: scalar|Param|null, // Default: "Y-m-d" + * ident?: scalar|Param|null, // Default: "php" + * logopts?: scalar|Param|null, // Default: 1 + * facility?: scalar|Param|null, // Default: "user" + * max_files?: scalar|Param|null, // Default: 0 + * action_level?: scalar|Param|null, // Default: "WARNING" + * activation_strategy?: scalar|Param|null, // Default: null + * stop_buffering?: bool|Param, // Default: true + * passthru_level?: scalar|Param|null, // Default: null + * excluded_http_codes?: list, + * }>, + * accepted_levels?: list, + * min_level?: scalar|Param|null, // Default: "DEBUG" + * max_level?: scalar|Param|null, // Default: "EMERGENCY" + * buffer_size?: scalar|Param|null, // Default: 0 + * flush_on_overflow?: bool|Param, // Default: false + * handler?: scalar|Param|null, + * url?: scalar|Param|null, + * exchange?: scalar|Param|null, + * exchange_name?: scalar|Param|null, // Default: "log" + * channel?: scalar|Param|null, // Default: null + * bot_name?: scalar|Param|null, // Default: "Monolog" + * use_attachment?: scalar|Param|null, // Default: true + * use_short_attachment?: scalar|Param|null, // Default: false + * include_extra?: scalar|Param|null, // Default: false + * icon_emoji?: scalar|Param|null, // Default: null + * webhook_url?: scalar|Param|null, + * exclude_fields?: list, + * token?: scalar|Param|null, + * region?: scalar|Param|null, + * source?: scalar|Param|null, + * use_ssl?: bool|Param, // Default: true + * user?: mixed, + * title?: scalar|Param|null, // Default: null + * host?: scalar|Param|null, // Default: null + * port?: scalar|Param|null, // Default: 514 + * rfc?: scalar|Param|null, // Default: 1 + * config?: list, + * members?: list, + * connection_string?: scalar|Param|null, + * timeout?: scalar|Param|null, + * time?: scalar|Param|null, // Default: 60 + * deduplication_level?: scalar|Param|null, // Default: 400 + * store?: scalar|Param|null, // Default: null + * connection_timeout?: scalar|Param|null, + * persistent?: bool|Param, + * message_type?: scalar|Param|null, // Default: 0 + * expand_newlines?: bool|Param, // Default: false + * parse_mode?: scalar|Param|null, // Default: null + * disable_webpage_preview?: bool|Param|null, // Default: null + * disable_notification?: bool|Param|null, // Default: null + * split_long_messages?: bool|Param, // Default: false + * delay_between_messages?: bool|Param, // Default: false + * topic?: int|Param, // Default: null + * factor?: int|Param, // Default: 1 + * tags?: Param|string|list, + * console_formatter_options?: mixed, // Default: [] + * formatter?: scalar|Param|null, + * nested?: bool|Param, // Default: false + * publisher?: Param|string|array{ + * id?: scalar|Param|null, + * hostname?: scalar|Param|null, + * port?: scalar|Param|null, // Default: 12201 + * chunk_size?: scalar|Param|null, // Default: 1420 + * encoder?: "json"|"compressed_json"|Param, + * }, + * mongodb?: Param|string|array{ + * id?: scalar|Param|null, // ID of a MongoDB\Client service + * uri?: scalar|Param|null, + * username?: scalar|Param|null, + * password?: scalar|Param|null, + * database?: scalar|Param|null, // Default: "monolog" + * collection?: scalar|Param|null, // Default: "logs" + * }, + * elasticsearch?: Param|string|array{ + * id?: scalar|Param|null, + * hosts?: list, + * host?: scalar|Param|null, + * port?: scalar|Param|null, // Default: 9200 + * transport?: scalar|Param|null, // Default: "Http" + * user?: scalar|Param|null, // Default: null + * password?: scalar|Param|null, // Default: null + * }, + * index?: scalar|Param|null, // Default: "monolog" + * document_type?: scalar|Param|null, // Default: "logs" + * ignore_error?: scalar|Param|null, // Default: false + * redis?: Param|string|array{ + * id?: scalar|Param|null, + * host?: scalar|Param|null, + * password?: scalar|Param|null, // Default: null + * port?: scalar|Param|null, // Default: 6379 + * database?: scalar|Param|null, // Default: 0 + * key_name?: scalar|Param|null, // Default: "monolog_redis" + * }, + * predis?: Param|string|array{ // Deprecated: The "predis" option is deprecated and ignored, use the "redis" option to configure the Predis client. + * id?: scalar|Param|null, + * host?: scalar|Param|null, + * }, + * from_email?: scalar|Param|null, + * to_email?: Param|string|list, + * subject?: scalar|Param|null, + * content_type?: scalar|Param|null, // Default: null + * headers?: list, + * parameters?: list, + * mailer?: scalar|Param|null, // Default: null + * email_prototype?: Param|string|array{ + * id?: scalar|Param|null, + * method?: scalar|Param|null, // Default: null + * }, + * verbosity_levels?: array{ + * VERBOSITY_QUIET?: scalar|Param|null, // Default: "ERROR" + * VERBOSITY_NORMAL?: scalar|Param|null, // Default: "WARNING" + * VERBOSITY_VERBOSE?: scalar|Param|null, // Default: "NOTICE" + * VERBOSITY_VERY_VERBOSE?: scalar|Param|null, // Default: "INFO" + * VERBOSITY_DEBUG?: scalar|Param|null, // Default: "DEBUG" + * }, + * channels?: Param|string|array{ + * type?: scalar|Param|null, + * elements?: list, + * ... + * }, + * }>, + * } + * @psalm-type WebProfilerConfig = array{ + * toolbar?: bool|array{ // Profiler toolbar configuration + * enabled?: bool|Param, // Default: false + * ajax_replace?: bool|Param, // Replace toolbar on AJAX requests // Default: false + * }, + * intercept_redirects?: bool|Param, // Default: false + * excluded_ajax_paths?: scalar|Param|null, // Default: "^/((index|app(_[\\w]+)?)\\.php/)?_wdt" + * } + * @psalm-type LiipFunctionalTestConfig = array{ + * command_verbosity?: scalar|Param|null, // Default: "normal" + * command_decoration?: bool|Param, // Default: true + * query?: array{ + * max_query_count?: scalar|Param|null, // Default: null + * }, + * authentication?: array{ + * username?: scalar|Param|null, // Default: "" + * password?: scalar|Param|null, // Default: "" + * }, + * } + * @psalm-type LiipTestFixturesConfig = array{ + * cache_db?: array{ + * sqlite?: scalar|Param|null, // Default: null + * ... + * }, + * keep_database_and_schema?: bool|Param, // Default: false + * cache_metadata?: bool|Param, // Default: true + * } + * @psalm-type NelmioCorsConfig = array{ + * defaults?: array{ + * allow_credentials?: bool|Param, // Default: false + * allow_origin?: list, + * allow_headers?: list, + * allow_methods?: list, + * allow_private_network?: bool|Param, // Default: false + * expose_headers?: list, + * max_age?: scalar|Param|null, // Default: 0 + * hosts?: list, + * origin_regex?: bool|Param, // Default: false + * forced_allow_origin_value?: scalar|Param|null, // Default: null + * skip_same_as_origin?: bool|Param, // Default: true + * }, + * paths?: array, + * allow_headers?: list, + * allow_methods?: list, + * allow_private_network?: bool|Param, + * expose_headers?: list, + * max_age?: scalar|Param|null, // Default: 0 + * hosts?: list, + * origin_regex?: bool|Param, + * forced_allow_origin_value?: scalar|Param|null, // Default: null + * skip_same_as_origin?: bool|Param, + * }>, + * } + * @psalm-type FlysystemConfig = array{ + * storages?: array, + * asyncaws?: array{ + * client?: scalar|Param|null, // The AsyncAws S3 client service name + * bucket?: scalar|Param|null, // The name of the AWS S3 bucket + * prefix?: scalar|Param|null, // Optional path prefix to prepend to all object keys // Default: "" + * }, + * aws?: array{ + * client?: scalar|Param|null, // The AWS S3 client service name + * bucket?: scalar|Param|null, // The name of the AWS S3 bucket + * prefix?: scalar|Param|null, // Optional path prefix to prepend to all object keys // Default: "" + * options?: list, + * streamReads?: bool|Param, // Whether to use streaming for file reads // Default: true + * }, + * azure?: array{ + * client?: scalar|Param|null, // The Azure Blob Storage client service name + * container?: scalar|Param|null, // The name of the Azure Blob Storage container + * prefix?: scalar|Param|null, // Optional path prefix to prepend to all blob names // Default: "" + * }, + * ftp?: array{ + * host?: scalar|Param|null, // FTP host + * username?: scalar|Param|null, // FTP username + * password?: scalar|Param|null, // FTP password + * port?: int|Param, // FTP port number // Default: 21 + * root?: scalar|Param|null, // FTP root directory // Default: "" + * passive?: bool|Param, // Use passive mode // Default: true + * ssl?: bool|Param, // Use SSL/TLS encryption // Default: false + * timeout?: int|Param, // Connection timeout in seconds // Default: 90 + * ignore_passive_address?: scalar|Param|null, // Ignore passive address // Default: null + * utf8?: bool|Param, // Enable UTF8 mode // Default: false + * transfer_mode?: scalar|Param|null, // Transfer mode (FTP_ASCII or FTP_BINARY constant on ftp extension) // Default: null + * system_type?: null|"windows"|"unix"|Param, // FTP system type // Default: null + * timestamps_on_unix_listings_enabled?: bool|Param, // Enable timestamps on Unix listings // Default: false + * recurse_manually?: bool|Param, // Recurse directories manually // Default: true + * use_raw_list_options?: bool|Param|null, // Use raw list options // Default: null + * connectivityChecker?: scalar|Param|null, // Connectivity checker service name // Default: null + * permissions?: array{ // Unix permissions configuration for files and directories + * file?: array{ // File permissions + * public?: int|Param, // Public file permissions // Default: 420 + * private?: int|Param, // Private file permissions // Default: 384 + * }, + * dir?: array{ // Directory permissions + * public?: int|Param, // Public directory permissions // Default: 493 + * private?: int|Param, // Private directory permissions // Default: 448 + * }, + * }, + * }, + * gcloud?: array{ + * client?: scalar|Param|null, // The Google Cloud Storage client service name + * bucket?: scalar|Param|null, // The name of the Google Cloud Storage bucket + * prefix?: scalar|Param|null, // Optional path prefix to prepend to all object keys // Default: "" + * visibility_handler?: scalar|Param|null, // Optional visibility handler service name // Default: null + * streamReads?: bool|Param, // Whether to use streaming for file reads // Default: false + * }, + * gridfs?: array{ + * bucket?: scalar|Param|null, // GridFS bucket service name (if using an existing bucket service) // Default: null + * prefix?: scalar|Param|null, // Optional path prefix to prepend to all file names // Default: "" + * database?: scalar|Param|null, // MongoDB database name // Default: null + * doctrine_connection?: scalar|Param|null, // Doctrine MongoDB connection name (mutually exclusive with mongodb_uri) + * mongodb_uri?: scalar|Param|null, // MongoDB connection URI (mutually exclusive with doctrine_connection) + * mongodb_uri_options?: list, + * mongodb_driver_options?: list, + * }, + * lazy?: array{ // Lazy adapter for runtime storage selection + * source?: scalar|Param|null, // The service name of the storage to use at runtime + * }, + * local?: array{ + * directory?: scalar|Param|null, // Directory path for local storage + * lock?: int|Param, // Lock flags for file operations // Default: 0 + * skip_links?: bool|Param, // Whether to skip symbolic links // Default: false + * lazy_root_creation?: bool|Param, // Whether to create the root directory lazily // Default: false + * permissions?: array{ // Unix permissions configuration for files and directories + * file?: array{ // File permissions + * public?: int|Param, // Public file permissions // Default: 420 + * private?: int|Param, // Private file permissions // Default: 384 + * }, + * dir?: array{ // Directory permissions + * public?: int|Param, // Public directory permissions // Default: 493 + * private?: int|Param, // Private directory permissions // Default: 448 + * }, + * }, + * }, + * memory?: array, + * sftp?: array{ + * host?: scalar|Param|null, // SFTP host + * username?: scalar|Param|null, // SFTP username + * password?: scalar|Param|null, // SFTP password (optional if using private key) // Default: null + * privateKey?: scalar|Param|null, // Path to private key file or private key content // Default: null + * passphrase?: scalar|Param|null, // Private key passphrase // Default: null + * port?: int|Param, // SFTP port number // Default: 22 + * timeout?: int|Param, // Connection timeout in seconds // Default: 90 + * hostFingerprint?: scalar|Param|null, // Host fingerprint for verification // Default: null + * connectivityChecker?: scalar|Param|null, // Connectivity checker service name // Default: null + * preferredAlgorithms?: list, + * root?: scalar|Param|null, // SFTP root directory // Default: "" + * permissions?: array{ // Unix permissions configuration for files and directories + * file?: array{ // File permissions + * public?: int|Param, // Public file permissions // Default: 420 + * private?: int|Param, // Private file permissions // Default: 384 + * }, + * dir?: array{ // Directory permissions + * public?: int|Param, // Public directory permissions // Default: 493 + * private?: int|Param, // Private directory permissions // Default: 448 + * }, + * }, + * }, + * webdav?: array{ + * client?: scalar|Param|null, // The WebDAV client service name + * prefix?: scalar|Param|null, // Optional path prefix to prepend to all paths // Default: "" + * visibility_handling?: "throw"|"ignore"|Param, // How to handle visibility operations // Default: "throw" + * manual_copy?: bool|Param, // Whether to handle copy operations manually // Default: false + * manual_move?: bool|Param, // Whether to handle move operations manually // Default: false + * }, + * bunnycdn?: array{ + * client?: scalar|Param|null, // The BunnyCDN client service name + * pull_zone?: scalar|Param|null, // The BunnyCDN pull zone name // Default: "" + * }, + * service?: scalar|Param|null, // Reference to a custom adapter service (alternative to registered adapter types) + * visibility?: scalar|Param|null, // Default visibility for files // Default: null + * directory_visibility?: scalar|Param|null, // Default visibility for directories // Default: null + * retain_visibility?: scalar|Param|null, // Keeps the original file visibility (public/private) when copying or moving. // Default: null + * case_sensitive?: bool|Param, // Deprecated: The "case_sensitive" option is deprecated and will be removed in 4.0. // Default: true + * disable_asserts?: bool|Param, // Deprecated: The "disable_asserts" option is deprecated and will be removed in 4.0. // Default: false + * public_url?: list, + * path_normalizer?: scalar|Param|null, // Path normalizer service name (should implement League\Flysystem\PathNormalizer) // Default: null + * public_url_generator?: scalar|Param|null, // For adapter that do not provide public URLs or override adapter capabilities and public_url option, a public URL generator service name can be configured in the main Filesystem configuration (should implement League\Flysystem\UrlGeneration\PublicUrlGenerator) // Default: null + * temporary_url_generator?: scalar|Param|null, // For adapter that do not provide public URLs or override adapter capabilities, a temporary URL generator service name can be configured in the main Filesystem configuration (should implement League\Flysystem\UrlGeneration\TemporaryUrlGenerator) // Default: null + * read_only?: bool|Param, // Converts a file system to read-only // Default: false + * }>, + * } + * @psalm-type ConfigType = array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * jms_serializer?: JmsSerializerConfig, + * monolog?: MonologConfig, + * nelmio_cors?: NelmioCorsConfig, + * flysystem?: FlysystemConfig, + * "when@dev"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * maker?: MakerConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * jms_serializer?: JmsSerializerConfig, + * monolog?: MonologConfig, + * web_profiler?: WebProfilerConfig, + * liip_functional_test?: LiipFunctionalTestConfig, + * liip_test_fixtures?: LiipTestFixturesConfig, + * nelmio_cors?: NelmioCorsConfig, + * flysystem?: FlysystemConfig, + * }, + * "when@prod"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * jms_serializer?: JmsSerializerConfig, + * monolog?: MonologConfig, + * nelmio_cors?: NelmioCorsConfig, + * flysystem?: FlysystemConfig, + * }, + * "when@test"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * jms_serializer?: JmsSerializerConfig, + * monolog?: MonologConfig, + * web_profiler?: WebProfilerConfig, + * liip_functional_test?: LiipFunctionalTestConfig, + * liip_test_fixtures?: LiipTestFixturesConfig, + * nelmio_cors?: NelmioCorsConfig, + * flysystem?: FlysystemConfig, + * }, + * ..., + * }> + * } + */ +final class App +{ + /** + * @param ConfigType $config + * + * @psalm-return ConfigType + */ + public static function config(array $config): array + { + /** @var ConfigType $config */ + $config = AppReference::config($config); + + return $config; + } +} + +namespace Symfony\Component\Routing\Loader\Configurator; + +/** + * This class provides array-shapes for configuring the routes of an application. + * + * Example: + * + * ```php + * // config/routes.php + * namespace Symfony\Component\Routing\Loader\Configurator; + * + * return Routes::config([ + * 'controllers' => [ + * 'resource' => 'routing.controllers', + * ], + * ]); + * ``` + * + * @psalm-type RouteConfig = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type ImportConfig = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type AliasConfig = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type RoutesConfig = array{ + * "when@dev"?: array, + * "when@prod"?: array, + * "when@test"?: array, + * ... + * } + */ +final class Routes +{ + /** + * @param RoutesConfig $config + * + * @psalm-return RoutesConfig + */ + public static function config(array $config): array + { + return $config; + } +} diff --git a/config/routes.yaml b/config/routes.yaml deleted file mode 100755 index c3283aa..0000000 --- a/config/routes.yaml +++ /dev/null @@ -1,3 +0,0 @@ -#index: -# path: / -# controller: App\Controller\DefaultController::index diff --git a/config/routes/annotations.yaml b/config/routes/annotations.yaml deleted file mode 100755 index e92efc5..0000000 --- a/config/routes/annotations.yaml +++ /dev/null @@ -1,7 +0,0 @@ -controllers: - resource: ../../src/Controller/ - type: annotation - -kernel: - resource: ../../src/Kernel.php - type: annotation diff --git a/config/routes/attributes.yaml b/config/routes/attributes.yaml new file mode 100644 index 0000000..cb9cf42 --- /dev/null +++ b/config/routes/attributes.yaml @@ -0,0 +1,3 @@ +controllers: + resource: ../../src/Controller/ + type: attribute diff --git a/config/routes/dev/framework.yaml b/config/routes/dev/framework.yaml deleted file mode 100755 index bcbbf13..0000000 --- a/config/routes/dev/framework.yaml +++ /dev/null @@ -1,3 +0,0 @@ -_errors: - resource: '@FrameworkBundle/Resources/config/routing/errors.xml' - prefix: /_error diff --git a/config/routes/dev/web_profiler.yaml b/config/routes/dev/web_profiler.yaml deleted file mode 100644 index c82beff..0000000 --- a/config/routes/dev/web_profiler.yaml +++ /dev/null @@ -1,7 +0,0 @@ -web_profiler_wdt: - resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' - prefix: /_wdt - -web_profiler_profiler: - resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' - prefix: /_profiler diff --git a/config/routes/framework.yaml b/config/routes/framework.yaml index 0fc74bb..bc1feac 100644 --- a/config/routes/framework.yaml +++ b/config/routes/framework.yaml @@ -1,4 +1,4 @@ when@dev: _errors: - resource: '@FrameworkBundle/Resources/config/routing/errors.xml' + resource: '@FrameworkBundle/Resources/config/routing/errors.php' prefix: /_error diff --git a/config/routes/web_profiler.yaml b/config/routes/web_profiler.yaml index 8d85319..b3b7b4b 100644 --- a/config/routes/web_profiler.yaml +++ b/config/routes/web_profiler.yaml @@ -1,8 +1,8 @@ when@dev: web_profiler_wdt: - resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' + resource: '@WebProfilerBundle/Resources/config/routing/wdt.php' prefix: /_wdt web_profiler_profiler: - resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' + resource: '@WebProfilerBundle/Resources/config/routing/profiler.php' prefix: /_profiler diff --git a/config/services.yaml b/config/services.yaml index beeb23b..a3f3540 100755 --- a/config/services.yaml +++ b/config/services.yaml @@ -7,6 +7,10 @@ parameters: ignf_validator.path: '%env(resolve:VALIDATOR_PATH)%' # GML/PCRS validation - see config/gdal/README.md and https://github.com/IGNF/validator-api/issues/15 env(GMLAS_CONFIG): '%kernel.project_dir%/config/gdal/gmlasconf-validator.xml' + # local working directory of the validations + validations_dir: '%env(resolve:DATA_DIR)%/validations' + # storage of the uploaded and produced files : "local" (var/data) or "S3" + env(STORAGE_TYPE): 'local' services: # default configuration for services in *this* file @@ -14,35 +18,31 @@ services: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. bind: - $validationsDir: "%env(resolve:DATA_DIR)%/validations" + $validationsDir: "%validations_dir%" $projectDir: "%kernel.project_dir%" $validatorPath: "%ignf_validator.path%" $validatorJavaOpts: "%env(resolve:VALIDATOR_JAVA_OPTS)%" $gmlasConfigPath: "%env(resolve:GMLAS_CONFIG)%" + $dataDownloadEnabled: "%env(bool:DATA_DOWNLOAD_ENABLED)%" + $modelAllowedHosts: "%env(VALIDATOR_MODEL_ALLOWED_HOSTS)%" + $storageType: "%env(STORAGE_TYPE)%" # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App\: resource: "../src/" exclude: - - "../src/DependencyInjection/" - "../src/Entity/" - "../src/Kernel.php" - - "../src/Tests/" - # controllers are imported separately to make sure services can be injected - # as action arguments even if you don't extend any base controller class - App\Controller\: - resource: "../src/Controller/" - tags: ["controller.service_arguments"] - # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones - exception.listener: - class: App\EventListener\ExceptionListener - tags: - - { - name: kernel.event_listener, - event: kernel.exception, - method: onKernelException, - } + # adds the client IP (X-Forwarded-For from TRUSTED_PROXIES), url, method and user agent to the logs + Symfony\Bridge\Monolog\Processor\WebProcessor: + arguments: + $extraFields: { url: REQUEST_URI, ip: REMOTE_ADDR, http_method: REQUEST_METHOD, user_agent: HTTP_USER_AGENT } + tags: [monolog.processor] + +when@test: + parameters: + validations_dir: '%env(resolve:DATA_DIR)%/validations-test' diff --git a/config/services_test.yaml b/config/services_test.yaml deleted file mode 100644 index b100ece..0000000 --- a/config/services_test.yaml +++ /dev/null @@ -1,20 +0,0 @@ -services: - _defaults: - autowire: true - autoconfigure: true - bind: - $validationsDir: "%env(resolve:DATA_DIR)%/validations-test" - $projectDir: "%kernel.project_dir%" - $validatorPath: "%ignf_validator.path%" - $validatorJavaOpts: "%env(resolve:VALIDATOR_JAVA_OPTS)%" - $gmlasConfigPath: "%env(resolve:GMLAS_CONFIG)%" - - App\: - resource: "../src/" - exclude: - - "../src/DependencyInjection/" - - "../src/Entity/" - - "../src/Kernel.php" - - App\Storage\ValidationsStorage: - public: true diff --git a/docker-compose.yml b/docker-compose.yml index f229ac7..df3962f 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,6 @@ services: image: ignf/validator-api:master-dev build: context: . - dockerfile: .docker/Dockerfile args: - http_proxy - https_proxy @@ -15,7 +14,7 @@ services: # important for ogr2ogr... - http_proxy=${HTTP_PROXY} - https_proxy=${HTTPS_PROXY} - - APP_ENV=${APP_ENV} + # APP_ENV=prod is defined in the image (override it in compose.override.yaml for development) - DB_CREATE=${DB_CREATE:-0} - DB_UPGRADE=${DB_UPGRADE:-0} - POSTGRES_USER=${POSTGRES_USER} @@ -33,7 +32,8 @@ services: # local exposure when traefik is not available - "8000:8000" depends_on: - - database + database: + condition: service_healthy labels: - "traefik.enable=true" - "traefik.http.routers.validator-api.rule=Host(`validator-api-v1.${HOST_HOSTNAME}`)" @@ -45,10 +45,9 @@ services: environment: - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - ports: - - 5432:5432 + # not exposed on the host (see compose.override.yaml for local development) healthcheck: - test: ["CMD-SHELL", "pg_isready -U validator"] + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"] interval: 10s timeout: 5s retries: 5 @@ -63,17 +62,16 @@ services: #----------------------------------------------------------------------- worker: image: ignf/validator-api:master-dev - command: [".docker/application.sh","backend"] + command: ["bin/application.sh","backend"] environment: - HTTP_PROXY=${HTTP_PROXY} - HTTPS_PROXY=${HTTPS_PROXY} # important for ogr2ogr... - http_proxy=${HTTP_PROXY} - https_proxy=${HTTPS_PROXY} - - APP_ENV=${APP_ENV} - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:5432/validator_api?serverVersion=15&charset=utf8 + - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:5432/validator_api?serverVersion=13&charset=utf8 - S3_ENDPOINT=${S3_ENDPOINT} - S3_ACCESS_KEY=${S3_ACCESS_KEY} - S3_SECRET_KEY=${S3_SECRET_KEY} diff --git a/docs/developer-guide/docker.md b/docs/developer-guide/docker.md index bc312a0..a7211cf 100644 --- a/docs/developer-guide/docker.md +++ b/docs/developer-guide/docker.md @@ -7,26 +7,34 @@ ## Principes -* L'image est définie par le fichier [.docker/Dockerfile](../../.docker/Dockerfile) avec deux "targets" : - * `prod` construite avec GitHub actions et stockée sur GitHub container registry - * `dev` (incluant xdebug) construite et utilisée par [docker-compose.yml](../../docker-compose.yml) -* Un script utilitaire [.docker/application.sh](../../.docker/application.sh) est intégré à l'image pour simplifier l'initialisation de la base et l'exécution des tests en environnement de développement. +* L'image est définie par le fichier [Dockerfile](../../Dockerfile), en plusieurs étapes : + * `composer` : dépendances PHP (sans les dépendances de dev, sauf `--build-arg COMPOSER_NO_DEV=0`) ; + * `assets` : front de démonstration construit avec webpack (voir [front.md](front.md)) ; + * image finale (Ubuntu, Apache, PHP, Java, GDAL et `validator-cli.jar` installé par [bin/install-validator.sh](../../bin/install-validator.sh)). +* L'image est construite par GitHub actions et publiée sur GitHub container registry. +* Le script [bin/application.sh](../../bin/application.sh) est le point d'entrée de l'image : + * `run` (défaut) : API (apache2) ; + * `backend` : traitement des validations en boucle ([bin/loop-validate.sh](../../bin/loop-validate.sh)) ; + * `archive` : archivage des validations expirées ([bin/archive.sh](../../bin/archive.sh)) ; + * `test` : exécution des tests (image construite avec `COMPOSER_NO_DEV=0`). +* [docker-compose.yml](../../docker-compose.yml) définit les services `api`, `worker` et `database` (PostGIS). +* [compose.override.yaml](../../compose.override.yaml), chargé automatiquement par `docker compose`, contient les réglages de développement : `APP_ENV=dev` et exposition de PostgreSQL sur `127.0.0.1:5432`. ## Paramètrage Le paramétrage de l'application est réalisé via des variables d'environnements. Voir [.env](../../.env) servant de modèle. -Le script [.docker/application.sh](../../.docker/application.sh) comporte des options spécifiques au démarrage de l'API : +Le script [bin/application.sh](../../bin/application.sh) comporte des options spécifiques au démarrage de l'API : * `DB_CREATE` à définir à 0 ou 1 pour créer automatiquement la base de données -* `DB_UPGRADE` à définir à 0 ou 1 pour mettre à jour automatiquement la structure +* `DB_UPGRADE` à définir à 0 ou 1 pour mettre à jour automatiquement la structure (`doctrine:migrations:migrate`, défaut 1) ## Construction et démarrage de l'application ```bash git clone https://github.com/IGNF/validator-api.git cd validator-api -# Construction de l'image docker +# Construction de l'image docker (derrière un proxy, les variables http_proxy/https_proxy sont transmises) docker compose build # Démarrage de la stack de développement docker compose up -d @@ -36,8 +44,11 @@ docker compose up -d ## Exécution des tests ```bash +# image avec les dépendances de dev +docker compose build --build-arg COMPOSER_NO_DEV=0 +docker compose up -d # Exécution des tests dans l'image docker -docker compose exec api .docker/application.sh test +docker compose exec api bin/application.sh test ``` Pour tester via l'interface : @@ -49,10 +60,8 @@ Pour tester via l'interface : ## Quelques commandes utiles pour le debug -* Visualiser les logs du backend : `docker compose logs -f backend` +* Visualiser les logs du worker : `docker compose logs -f worker` * Ouvrir un terminal dans le conteneur : `docker compose exec api /bin/bash` * Lister les fichiers : `docker compose exec api find var/data/validations` -* Suivre un traitement particulier : `docker compose exec api tail -f var/data/validations/${VALIDATION_ID}/validator-debug.log` - - - +* Suivre un traitement particulier : `docker compose exec worker tail -f var/data/validations/${VALIDATION_ID}/validator-debug.log` +* Archiver les validations expirées : `docker compose exec worker bin/application.sh archive` diff --git a/docs/developer-guide/front.md b/docs/developer-guide/front.md index ccb0e54..77f75b6 100644 --- a/docs/developer-guide/front.md +++ b/docs/developer-guide/front.md @@ -1,17 +1,29 @@ - # Documentation développeur pour le développement du front (JavaScript) ## Principe -* Les dépendances JS sont commitées dans le dossier `public/vendor` -* En cas de modification des dépendances JS (package.json), il convient de reconstruire et commit +* Le démonstrateur est fourni par [IGNF/validator-api-client](https://github.com/IGNF/validator-api-client) (`@ignf/validator-client`, version définie dans `package.json`). +* `npm run build` (webpack) produit `public/build`, `public/vendor/validator-api-client`, `public/css`, `public/font` et `public/img` (voir `webpack.config.js`). +* Ces dossiers sont vidés à chaque build (les fichiers d'une ancienne version du client ne restent pas), les autres fichiers de `public/` (`index.php`, `js/`...) sont conservés. +* Si validator-api-client change le nom de ses fichiers JS (ex : `runtime.` et `vendors.`), adapter les balises ` - - - {% endblock %} + {% block javascripts %}{% endblock %} diff --git a/templates/demo.html.twig b/templates/demo.html.twig index c20d430..783a632 100755 --- a/templates/demo.html.twig +++ b/templates/demo.html.twig @@ -4,8 +4,6 @@ {{ parent() }} {% endblock %} -{% block title %}{{ "Démo - " ~ parent() }}{% endblock %} - {% block body %}


diff --git a/templates/pdfModel.html.twig b/templates/pdfModel.html.twig deleted file mode 100644 index f8b9afa..0000000 --- a/templates/pdfModel.html.twig +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - - {# ── Top banner ── #} - {% if hasErrors %} -

- {% else %} - - {% endif %} - - {# ── One table per level ── #} - {% for level, rows in groupedEntries %} -

{{ level|upper }}

- - - - - - - - - - - {% for entry in rows %} - - - - - - {% endfor %} - -
CodeLevelMessage
{{ entry.code }} - {{ entry.level }} - {{ entry.message }}
- {% endfor %} - - - \ No newline at end of file diff --git a/templates/report.html.twig b/templates/report.html.twig new file mode 100644 index 0000000..a050615 --- /dev/null +++ b/templates/report.html.twig @@ -0,0 +1,248 @@ + + + + + + {# the title is used by browsers as default name of the PDF file #} + Rapport de validation - {{ validation.datasetName }} + + + + +
+ +
+ +

Rapport de validation

+ + + + + + + {% if validation.arguments.model is defined %} + + + + + {% endif %} + {% if validation.dateFinish %} + + + + + {% endif %} + + + + +
Jeu de données{{ validation.datasetName }}
Modèle{{ validation.arguments.model }}
Date{{ validation.dateFinish|date('d/m/Y H:i') }}
Identifiant{{ validation.uid }}
+ + {# ── Top banner ── #} + {% if hasErrors %} + + {% else %} + + {% endif %} + + {# ── One table per level ── #} + {% for level, rows in groupedEntries %} +

{{ level|upper }}

+ + + + + + + + + + + {% for entry in rows %} + + + + + + {% endfor %} + +
CodeLevelMessage
{{ entry.code }} + {{ entry.level|default(level) }} + + {{ entry.message }} + {% if entry.file is defined and entry.file %} +
{{ entry.file }}
+ {% endif %} +
+ {% endfor %} + + + + diff --git a/templates/swagger.html.twig b/templates/swagger.html.twig deleted file mode 100755 index 0af7bd5..0000000 --- a/templates/swagger.html.twig +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block stylesheets %} - {{ parent() }} - -{% endblock %} - -{% block title %}{{ "Documentation - " ~ parent() }}{% endblock %} - -{% block body %} -
Chargement...
-{% endblock %} - - -{% block javascripts %} - {{ parent() }} - - -{% endblock %} \ No newline at end of file diff --git a/tests/Command/Validations/CleanupCommandTest.php b/tests/Command/Validations/CleanupCommandTest.php index c970cbf..b52442a 100644 --- a/tests/Command/Validations/CleanupCommandTest.php +++ b/tests/Command/Validations/CleanupCommandTest.php @@ -78,4 +78,49 @@ public function testCleanupOneSecond() $this->assertFalse(file_exists($validationDirectory)); } } + + /** + * Validations being processed by a worker are not archived. + */ + public function testCleanupIgnoresProcessing() + { + $validation = $this->em->getRepository(Validation::class)->findOneBy(['status' => Validation::STATUS_PENDING]); + $validation->setStatus(Validation::STATUS_PROCESSING); + $this->em->flush(); + $uid = $validation->getUid(); + + static::ensureKernelShutdown(); + sleep(2); + + $application = new Application(static::createKernel()); + $commandTester = new CommandTester($application->find('ign-validator:validations:cleanup')); + $this->assertEquals(0, $commandTester->execute(['--max-age' => 'PT1S'])); + + $this->em->clear(); + $validation = $this->em->getRepository(Validation::class)->findOneByUid($uid); + $this->assertEquals(Validation::STATUS_PROCESSING, $validation->getStatus()); + } + + /** + * Validations still processing after processing-timeout (worker killed...) are marked as failed. + */ + public function testCleanupMarksInterruptedValidations() + { + $validation = $this->em->getRepository(Validation::class)->findOneBy(['status' => Validation::STATUS_PENDING]); + $validation->setStatus(Validation::STATUS_PROCESSING); + $validation->setDateStart(new \DateTime('-2 hours')); + $this->em->flush(); + $uid = $validation->getUid(); + + static::ensureKernelShutdown(); + $application = new Application(static::createKernel()); + $commandTester = new CommandTester($application->find('ign-validator:validations:cleanup')); + $this->assertEquals(0, $commandTester->execute(['--processing-timeout' => 'PT1H'])); + $this->assertStringContainsString('1 interrupted validation(s) marked as failed.', $commandTester->getDisplay()); + + $this->em->clear(); + $validation = $this->em->getRepository(Validation::class)->findOneByUid($uid); + $this->assertEquals(Validation::STATUS_ERROR, $validation->getStatus()); + $this->assertEquals('Validation failed (processing interrupted)', $validation->getMessage()); + } } diff --git a/tests/Command/Validations/ProcessOneCommandTest.php b/tests/Command/Validations/ProcessOneCommandTest.php index 07c24e1..4bf7a0e 100644 --- a/tests/Command/Validations/ProcessOneCommandTest.php +++ b/tests/Command/Validations/ProcessOneCommandTest.php @@ -50,70 +50,89 @@ public function tearDown(): void } /** - * Testing execution of the command. + * Valid dataset : validator-cli.jar is executed and the results are saved. */ - public function testExecute() + public function testExecuteValid() { - $this->markTestSkipped('TODO : fix test (use local directory for dev and test?)'); + $validation = $this->processOnly(ValidationsFixtures::VALIDATION_WITH_ARGS); - $repo = $this->em->getRepository(Validation::class); - - // this validation will run without any errors - static::ensureKernelShutdown(); - $kernel = static::createKernel(); - $application = new Application($kernel); - $command = $application->find('ign-validator:validations:process-one'); - $commandTester = new CommandTester($command); - $statusCode = $commandTester->execute([]); - - $this->assertEquals(0, $statusCode); - - $valWithArgs = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS); - $validation = $repo->findOneByUid($valWithArgs->getUid()); - - $this->assertEquals('', $validation->getMessage()); + $this->assertNull($validation->getMessage()); $this->assertEquals(Validation::STATUS_FINISHED, $validation->getStatus()); $this->assertNotNull($validation->getDateStart()); $this->assertNotNull($validation->getDateFinish()); $this->assertNotNull($validation->getResults()); + // logs are saved to the storage + $storage = $this->getValidationsStorage(); + $this->assertTrue($storage->getStorage()->fileExists($storage->getOutputDirectory($validation).'validator-debug.log')); + } - // this one will fail, the model_url argument is wrong and will raise a Java runtime exception which will mark the Symfony process as failed - $command = $application->find('ign-validator:validations:process-one'); - $commandTester = new CommandTester($command); - $statusCode = $commandTester->execute([]); - - $this->assertEquals(0, $statusCode); - - $valWithBadArgs = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_BAD_ARGS); - $validation = $repo->findOneByUid($valWithBadArgs->getUid()); + /** + * Wrong model url : validator-cli.jar fails, the raw error is not exposed. + */ + public function testExecuteValidatorFailure() + { + $validation = $this->processOnly(ValidationsFixtures::VALIDATION_WITH_BAD_ARGS); - $this->assertNotEquals('', $validation->getMessage()); + $this->assertStringStartsWith('Validation failed (exit code ', $validation->getMessage()); $this->assertEquals(Validation::STATUS_ERROR, $validation->getStatus()); $this->assertNotNull($validation->getDateStart()); $this->assertNotNull($validation->getDateFinish()); - $this->assertNull($validation->getResults()); // TODO fails intermittently ¯\_(ツ)_/¯ - - // this one will fail because the zip archive is invalid - $command = $application->find('ign-validator:validations:process-one'); - $commandTester = new CommandTester($command); - $statusCode = $commandTester->execute([]); - - $this->assertEquals(0, $statusCode); + $this->assertNull($validation->getResults()); + } - $valInvalidRegex = $this->getValidationFixture(ValidationsFixtures::VALIDATION_INVALID_REGEX); - $validation = $repo->findOneByUid($valInvalidRegex->getUid()); + /** + * Invalid file names in the zip : the zip pre-validation fails. + */ + public function testExecuteInvalidZip() + { + $validation = $this->processOnly(ValidationsFixtures::VALIDATION_INVALID_REGEX); $this->assertEquals('Zip archive pre-validation failed', $validation->getMessage()); $this->assertEquals(Validation::STATUS_ERROR, $validation->getStatus()); - $this->assertNotNull($validation->getDateStart()); $this->assertNotNull($validation->getDateFinish()); $this->assertEquals(2, count($validation->getResults())); + } + + /** + * No validation pending : the command exits right away. + */ + public function testExecuteNothingPending() + { + foreach ($this->em->getRepository(Validation::class)->findBy(['status' => Validation::STATUS_PENDING]) as $validation) { + $validation->setStatus(Validation::STATUS_WAITING_ARGS); + } + $this->em->flush(); - // no validation pending, the command should exit right away - $command = $application->find('ign-validator:validations:process-one'); - $commandTester = new CommandTester($command); - $statusCode = $commandTester->execute([]); + $this->assertEquals(0, $this->executeCommand()); + } + + /** + * Processes the given fixture only (the other pending validations are put on hold + * as popNextPending order is undefined for validations created in the same second). + */ + private function processOnly(string $fixture): Validation + { + $uid = $this->getValidationFixture($fixture)->getUid(); + foreach ($this->em->getRepository(Validation::class)->findBy(['status' => Validation::STATUS_PENDING]) as $validation) { + if ($validation->getUid() !== $uid) { + $validation->setStatus(Validation::STATUS_WAITING_ARGS); + } + } + $this->em->flush(); + + $this->assertEquals(0, $this->executeCommand()); + + $this->em->clear(); + + return $this->em->getRepository(Validation::class)->findOneByUid($uid); + } + + private function executeCommand(): int + { + static::ensureKernelShutdown(); + $application = new Application(static::createKernel()); + $commandTester = new CommandTester($application->find('ign-validator:validations:process-one')); - $this->assertEquals(0, $statusCode); + return $commandTester->execute([]); } } diff --git a/tests/Controller/Api/DataDownloadDisabledTest.php b/tests/Controller/Api/DataDownloadDisabledTest.php new file mode 100644 index 0000000..3da1f52 --- /dev/null +++ b/tests/Controller/Api/DataDownloadDisabledTest.php @@ -0,0 +1,101 @@ +client = static::createClient(); + + $databaseTool = static::getContainer()->get(DatabaseToolCollection::class)->get(); + $this->fixtures = $databaseTool->loadFixtures([ + ValidationsFixtures::class, + ]); + } + + public function tearDown(): void + { + $_ENV['DATA_DOWNLOAD_ENABLED'] = $_SERVER['DATA_DOWNLOAD_ENABLED'] = '1'; + + parent::tearDown(); + } + + public static function filesProvider(): array + { + return [ + 'source' => ['source'], + 'normalized' => ['normalized'], + ]; + } + + #[DataProvider('filesProvider')] + public function testDownloadIsDenied(string $files) + { + $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS); + + $this->client->request('GET', '/api/validations/'.$validation->getUid().'/files/'.$files); + + $json = \json_decode($this->client->getResponse()->getContent(), true); + $this->assertStatusCode(403, $this->client); + $this->assertEquals('Data download is disabled', $json['message']); + } + + /** + * Unknown uids get the same answer, so the endpoint cannot be used to probe uids. + */ + #[DataProvider('filesProvider')] + public function testDownloadIsDeniedForUnknownUid(string $files) + { + $this->client->request('GET', '/api/validations/uid-validation-doesnt-exist/files/'.$files); + + $this->assertStatusCode(403, $this->client); + } + + /** + * Download endpoints are flagged as disabled in the OpenAPI specification. + */ + public function testSwaggerFlagsDownloadPathsAsDisabled() + { + $this->client->request('GET', '/api/validator-api.yml'); + + $this->assertResponseIsSuccessful(); + $specs = Yaml::parse($this->client->getResponse()->getContent()); + $this->assertArrayNotHasKey('x-disabled', $specs['paths']['/api/validations/{uid}/results.csv']['get']); + + $expected = Yaml::parseFile(static::getContainer()->getParameter('kernel.project_dir').'/docs/specs/validator-api.yml'); + foreach (['source', 'normalized'] as $files) { + $path = "/api/validations/{uid}/files/$files"; + $operation = $specs['paths'][$path]['get']; + $this->assertTrue($operation['deprecated']); + $this->assertTrue($operation['x-disabled']); + $this->assertStringStartsWith('[Désactivé] ', $operation['summary']); + $this->assertStringContainsString('désactivée temporairement pour des raisons de sécurité', $operation['description']); + + // the documentation of the route is kept + $this->assertEquals($expected['paths'][$path]['get']['responses'], $operation['responses']); + unset($specs['paths'][$path], $expected['paths'][$path]); + } + + // the rest of the specification is unchanged + $this->assertEquals($expected, $specs); + } +} diff --git a/tests/Controller/Api/DocumentationControllerTest.php b/tests/Controller/Api/DocumentationControllerTest.php index 0a6e574..e8b4065 100644 --- a/tests/Controller/Api/DocumentationControllerTest.php +++ b/tests/Controller/Api/DocumentationControllerTest.php @@ -4,12 +4,27 @@ use App\Tests\WebTestCase; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Yaml\Yaml; /** * Tests for ValidatorController class. */ class DocumentationControllerTest extends WebTestCase { + /** + * /api redirects to the swagger displayed by the demo client. + */ + public function testApiRootRedirectsToSwagger() + { + self::ensureKernelShutdown(); + $client = static::createClient(); + $client->request('GET', '/api'); + + $this->assertResponseRedirects('/#/api'); + } + /** * Test openapi specification. */ @@ -33,6 +48,32 @@ public function testSwagger() ); } + /** + * Every operation documented in the specification matches an existing route + * (prevents documenting routes that have been removed). + */ + public function testSwaggerOperationsMatchRoutes() + { + self::ensureKernelShutdown(); + static::bootKernel(); + $router = static::getContainer()->get(RouterInterface::class); + $specs = Yaml::parseFile(static::getContainer()->getParameter('kernel.project_dir').'/docs/specs/validator-api.yml'); + + foreach ($specs['paths'] as $path => $operations) { + // replace path parameters ({uid}, {schemaName}...) by a sample value + $url = preg_replace('/\{[^}]+\}/', 'sample', $path); + foreach (array_keys($operations) as $method) { + $router->getContext()->setMethod(strtoupper($method)); + try { + $router->match($url); + } catch (ResourceNotFoundException $e) { + $this->fail(strtoupper($method)." $path is documented but no route matches it"); + } + } + } + $this->addToAssertionCount(1); + } + /** * Get specification. */ diff --git a/tests/Controller/Api/RateLimitTest.php b/tests/Controller/Api/RateLimitTest.php new file mode 100644 index 0000000..85f6540 --- /dev/null +++ b/tests/Controller/Api/RateLimitTest.php @@ -0,0 +1,71 @@ +client = static::createClient(); + // counters of previous tests + static::getContainer()->get('limiter.validation')->create('127.0.0.1')->reset(); + + $databaseTool = static::getContainer()->get(DatabaseToolCollection::class)->get(); + $this->fixtures = $databaseTool->loadFixtures([ + ValidationsFixtures::class, + ]); + } + + public function tearDown(): void + { + $_ENV['VALIDATION_RATE_LIMIT'] = $_SERVER['VALIDATION_RATE_LIMIT'] = '1000'; + + parent::tearDown(); + + (new Filesystem())->remove($this->getValidationsStorage()->getPath()); + } + + public function testTooManyRequests() + { + $uid = $this->getValidationFixture(ValidationsFixtures::VALIDATION_NO_ARGS)->getUid(); + $patch = fn () => $this->client->request( + 'PATCH', + '/api/validations/'.$uid, + [], + [], + ['CONTENT_TYPE' => 'application/json'], + json_encode(['srs' => 'EPSG:2154', 'model' => 'https://www.geoportail-urbanisme.gouv.fr/standard/cnig_SUP_PM3_2016.json']) + ); + + $patch(); + $this->assertStatusCode(200, $this->client); + $patch(); + $this->assertStatusCode(200, $this->client); + + $patch(); + $this->assertStatusCode(429, $this->client); + $json = \json_decode($this->client->getResponse()->getContent(), true); + $this->assertStringStartsWith('Too many requests, retry after ', $json['message']); + + // uploads share the same limit + $this->client->request('POST', '/api/validations/', [], ['dataset' => $this->createFakeUpload(ValidationsFixtures::FILENAME_SUP_PM3)]); + $this->assertStatusCode(429, $this->client); + } +} diff --git a/tests/Controller/Api/ValidationControllerTest.php b/tests/Controller/Api/ValidationControllerTest.php index c07e913..cda4cc3 100644 --- a/tests/Controller/Api/ValidationControllerTest.php +++ b/tests/Controller/Api/ValidationControllerTest.php @@ -8,8 +8,10 @@ use App\Tests\WebTestCase; use Doctrine\ORM\EntityManagerInterface; use Liip\TestFixturesBundle\Services\DatabaseToolCollection; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpFoundation\File\UploadedFile; /** * Tests for ValidatorController class. @@ -70,7 +72,7 @@ public function testGetValidation() $this->client->request( 'GET', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), ); $response = $this->client->getResponse(); @@ -203,6 +205,54 @@ public function testUploadDatasetWrongFileType() $this->assertEquals('Dataset must be in a compressed [.zip] file', $json['message']); } + public static function invalidDatasetNameProvider(): array + { + return [ + 'parent directory' => ['...zip'], + 'current directory' => ['..zip'], + 'empty name' => ['.zip'], + 'leading dash' => ['-r.zip'], + 'space' => ['my dataset.zip'], + 'accent' => ['données.zip'], + 'too long' => [str_repeat('a', 101).'.zip'], + ]; + } + + /** + * Uploading a zip whose name is not safe to be used in file paths. + */ + #[DataProvider('invalidDatasetNameProvider')] + public function testUploadDatasetInvalidName(string $clientName) + { + $sample = $this->createFakeUpload(ValidationsFixtures::FILENAME_SUP_PM3); + $dataset = new UploadedFile($sample->getPathname(), $clientName, 'application/zip', null, true); + + $this->client->request('POST', '/api/validations/', [], ['dataset' => $dataset]); + + $json = \json_decode($this->client->getResponse()->getContent(), true); + $this->assertStatusCode(400, $this->client); + $this->assertStringStartsWith('Dataset filename is not valid', $json['message']); + $this->assertEquals(0, $this->em->getRepository(Validation::class)->count([ + 'status' => Validation::STATUS_WAITING_ARGS, + 'datasetName' => preg_replace('/\.zip$/', '', $clientName), + ])); + } + + /** + * The .zip extension is removed whatever its case. + */ + public function testUploadDatasetUpperCaseExtension() + { + $sample = $this->createFakeUpload(ValidationsFixtures::FILENAME_SUP_PM3); + $dataset = new UploadedFile($sample->getPathname(), 'PLU_2026.v2.ZIP', 'application/zip', null, true); + + $this->client->request('POST', '/api/validations/', [], ['dataset' => $dataset]); + + $json = \json_decode($this->client->getResponse()->getContent(), true); + $this->assertStatusCode(201, $this->client); + $this->assertEquals('PLU_2026.v2', $json['dataset_name']); + } + /** * Uploading no file at all. */ @@ -231,7 +281,7 @@ public function testDeleteValidation() $this->client->request( 'DELETE', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), ); $response = $this->client->getResponse(); @@ -243,7 +293,7 @@ public function testDeleteValidation() // trying to delete a validation that does not exist $this->client->request( 'DELETE', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), ); $response = $this->client->getResponse(); @@ -252,6 +302,52 @@ public function testDeleteValidation() $this->assertStatusCode(404, $this->client); } + /** + * Deleting a validation removes its persisted files. + */ + public function testDeleteValidationRemovesFiles() + { + $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS); + $storage = $this->getValidationsStorage(); + $uploadDirectory = $storage->getUploadDirectory($validation); + $this->assertTrue($storage->getStorage()->directoryExists($uploadDirectory)); + + $this->client->request('DELETE', '/api/validations/'.$validation->getUid()); + + $this->assertStatusCode(204, $this->client); + $this->assertFalse($storage->getStorage()->directoryExists($uploadDirectory)); + } + + /** + * A validation being processed by a worker can't be updated or deleted. + */ + public function testUpdateOrDeleteValidationBeingProcessed() + { + $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS); + $validation = $this->em->getRepository(Validation::class)->findOneByUid($validation->getUid()); + $validation->setStatus(Validation::STATUS_PROCESSING); + $this->em->flush(); + + $this->client->request( + 'PATCH', + '/api/validations/'.$validation->getUid(), + [], + [], + ['CONTENT_TYPE' => 'application/json'], + json_encode(['srs' => 'EPSG:2154', 'model' => 'https://www.geoportail-urbanisme.gouv.fr/standard/cnig_SUP_PM3_2016.json']) + ); + $this->assertStatusCode(409, $this->client); + $json = \json_decode($this->client->getResponse()->getContent(), true); + $this->assertEquals('Validation is being processed, retry later', $json['message']); + + $this->client->request('DELETE', '/api/validations/'.$validation->getUid()); + $this->assertStatusCode(409, $this->client); + + $this->em->clear(); + $validation = $this->em->getRepository(Validation::class)->findOneByUid($validation->getUid()); + $this->assertEquals(Validation::STATUS_PROCESSING, $validation->getStatus()); + } + /** * Updating arguments with correct parameters. */ @@ -267,7 +363,7 @@ public function testUpdateArguments() $this->client->request( 'PATCH', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), [], [], ['CONTENT_TYPE' => 'application/json'], @@ -330,7 +426,7 @@ public function testUpdateArgumentsInvalidJson() $this->client->request( 'PATCH', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), [], [], ['CONTENT_TYPE' => 'application/json'], @@ -354,7 +450,7 @@ public function testUpdateArgumentsNoArguments() $this->client->request( 'PATCH', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), [], [], ['CONTENT_TYPE' => 'application/json'], @@ -380,7 +476,7 @@ public function testUpdateArgumentsMissingReqArguments() $this->client->request( 'PATCH', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), [], [], ['CONTENT_TYPE' => 'application/json'], @@ -412,7 +508,7 @@ public function testUpdateArgumentsUnknownParameters() $this->client->request( 'PATCH', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), [], [], ['CONTENT_TYPE' => 'application/json'], @@ -441,7 +537,7 @@ public function testUpdateArgumentsInvalidBoolean() $this->client->request( 'PATCH', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), [], [], ['CONTENT_TYPE' => 'application/json'], @@ -469,7 +565,7 @@ public function testUpdateArgumentsUnknownProjection() $this->client->request( 'PATCH', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), [], [], ['CONTENT_TYPE' => 'application/json'], @@ -497,7 +593,7 @@ public function testUpdateArgumentsInvalidModelUrl() $this->client->request( 'PATCH', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), [], [], ['CONTENT_TYPE' => 'application/json'], @@ -511,6 +607,67 @@ public function testUpdateArgumentsInvalidModelUrl() $this->assertEquals('Invalid arguments, check details', $json['message']); } + public static function forbiddenModelUrlProvider(): array + { + return [ + 'http' => ['http://www.geoportail-urbanisme.gouv.fr/standard/cnig_PLU_2017.json'], + 'local file' => ['file:///etc/passwd'], + 'cloud metadata' => ['https://169.254.169.254/latest/meta-data/'], + 'internal host' => ['https://localhost/model.json'], + 'userinfo trick' => ['https://ignf.github.io@evil.example/model.json'], + 'backslash trick' => ['https://evil.example\\@ignf.github.io/model.json'], + 'port' => ['https://ignf.github.io:8443/model.json'], + 'suffix trick' => ['https://evilignf.github.io/model.json'], + 'allowed host as subdomain' => ['https://ignf.github.io.evil.example/model.json'], + ]; + } + + /** + * The model url must be https on an allowed host (SSRF protection). + */ + #[DataProvider('forbiddenModelUrlProvider')] + public function testUpdateArgumentsForbiddenModelUrl(string $model) + { + $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_NO_ARGS); + + $this->client->request( + 'PATCH', + '/api/validations/'.$validation->getUid(), + [], + [], + ['CONTENT_TYPE' => 'application/json'], + json_encode(['srs' => 'EPSG:2154', 'model' => $model]) + ); + + $json = \json_decode($this->client->getResponse()->getContent(), true); + $this->assertStatusCode(400, $this->client); + $this->assertEquals('Invalid arguments, check details', $json['message']); + $this->assertEquals('model', $json['details'][0]['name']); + } + + /** + * Subdomains of the allowed hosts are allowed. + */ + public function testUpdateArgumentsAllowedModelUrl() + { + $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_NO_ARGS); + + foreach ([ + 'https://www.geoportail-urbanisme.gouv.fr/standard/cnig_PLU_2017.json', + 'https://IGNF.github.io/validator/validator-plugin-cnig/src/test/resources/config/cnig_SUP_PM3_2016.json', + ] as $model) { + $this->client->request( + 'PATCH', + '/api/validations/'.$validation->getUid(), + [], + [], + ['CONTENT_TYPE' => 'application/json'], + json_encode(['srs' => 'EPSG:2154', 'model' => $model]) + ); + $this->assertStatusCode(200, $this->client); + } + } + /** * Updating arguments but validation already archived. */ @@ -525,7 +682,7 @@ public function testUpdateArgumentsValArchived() $this->client->request( 'PATCH', - '/api/validations/' . $validation->getUid(), + '/api/validations/'.$validation->getUid(), [], [], ['CONTENT_TYPE' => 'application/json'], diff --git a/tests/Controller/Api/ValidationFilesControllerTest.php b/tests/Controller/Api/ValidationFilesControllerTest.php new file mode 100644 index 0000000..672039d --- /dev/null +++ b/tests/Controller/Api/ValidationFilesControllerTest.php @@ -0,0 +1,179 @@ +client = static::createClient(); + $this->em = $this->getContainer()->get('doctrine')->getManager(); + + $databaseTool = static::getContainer()->get(DatabaseToolCollection::class)->get(); + $this->fixtures = $databaseTool->loadFixtures([ + ValidationsFixtures::class, + ]); + } + + public function tearDown(): void + { + parent::tearDown(); + + $fs = new Filesystem(); + $fs->remove($this->getValidationsStorage()->getPath()); + } + + public function testLogsNotFound() + { + $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS); + + $this->client->request('GET', '/api/validations/'.$validation->getUid().'/logs'); + + $this->assertStatusCode(404, $this->client); + $json = \json_decode($this->client->getResponse()->getContent(), true); + $this->assertEquals('No logs found for this validation', $json['message']); + } + + /** + * Logs are returned as plain text (never rendered as HTML by browsers). + */ + public function testLogs() + { + $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS); + $storage = $this->getValidationsStorage(); + $storage->getStorage()->write( + $storage->getOutputDirectory($validation).'validator-debug.log', + '' + ); + + $this->client->request('GET', '/api/validations/'.$validation->getUid().'/logs'); + + $this->assertStatusCode(200, $this->client); + $response = $this->client->getResponse(); + $this->assertEquals('text/plain; charset=utf-8', $response->headers->get('Content-Type')); + $this->assertEquals('nosniff', $response->headers->get('X-Content-Type-Options')); + $this->assertEquals('', $response->getContent()); + } + + public function testCsvNotExecuted() + { + $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS); + + $this->client->request('GET', '/api/validations/'.$validation->getUid().'/results.csv'); + + $this->assertStatusCode(403, $this->client); + $json = \json_decode($this->client->getResponse()->getContent(), true); + $this->assertEquals("Validation hasn't been executed yet", $json['message']); + } + + public function testCsvNoResults() + { + $validation = $this->updateValidation(ValidationsFixtures::VALIDATION_WITH_ARGS, Validation::STATUS_ERROR, null); + + $this->client->request('GET', '/api/validations/'.$validation->getUid().'/results.csv'); + + $this->assertStatusCode(404, $this->client); + } + + public function testCsv() + { + $validation = $this->updateValidation(ValidationsFixtures::VALIDATION_WITH_ARGS, Validation::STATUS_FINISHED, [ + ['level' => 'ERROR', 'code' => 'FILE_EMPTY', 'message' => 'fichier vide', 'file' => 'a.csv'], + ]); + + $this->client->request('GET', '/api/validations/'.$validation->getUid().'/results.csv'); + + $this->assertStatusCode(200, $this->client); + $response = $this->client->getResponse(); + $this->assertEquals('text/csv; charset=utf-8', $response->headers->get('Content-Type')); + $this->assertEquals( + 'attachment; filename='.$validation->getUid().'-results.csv', + $response->headers->get('Content-Disposition') + ); + } + + public function testReportNotExecuted() + { + $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS); + + $this->client->request('GET', '/api/validations/'.$validation->getUid().'/report'); + + $this->assertStatusCode(403, $this->client); + } + + /** + * Printable HTML report, printed to PDF by the browser. + */ + public function testReport() + { + $longMessage = 'chemin '.str_repeat('Donnees_geographiques/', 20).'PM3_ACTE_SUP.dbf'; + $validation = $this->updateValidation(ValidationsFixtures::VALIDATION_WITH_ARGS, Validation::STATUS_FINISHED, [ + ['level' => 'ERROR', 'code' => 'FILE_EMPTY', 'message' => 'fichier vide é', 'file' => 'a.csv'], + ['level' => 'WARNING', 'code' => 'ATTRIBUTE_UNEXPECTED', 'message' => $longMessage], + ['level' => 'INFO', 'code' => 'TABLE_LOADED', 'message' => 'table chargée'], + ]); + + $crawler = $this->client->request('GET', '/api/validations/'.$validation->getUid().'/report'); + + $this->assertStatusCode(200, $this->client); + $response = $this->client->getResponse(); + $this->assertEquals('text/html; charset=utf-8', $response->headers->get('Content-Type')); + $this->assertSelectorTextContains('title', 'Rapport de validation - '.$validation->getDatasetName()); + $this->assertSelectorTextContains('.banner', 'Document Invalide'); + $this->assertEquals(['ERROR', 'WARNING', 'INFO'], $crawler->filter('h2.group-title')->each(fn ($node) => $node->text())); + // messages are escaped + $this->assertStringContainsString('fichier vide <b>é</b>', $response->getContent()); + $this->assertStringContainsString($longMessage, $response->getContent()); + // print script served by the API (no inline script) + $this->assertEquals(['/js/report.js'], $crawler->filter('script')->each(fn ($node) => $node->attr('src'))); + } + + /** + * Zip pre-validation errors (file, code, message) have no level. + */ + public function testReportZipErrors() + { + $validation = $this->updateValidation(ValidationsFixtures::VALIDATION_WITH_ARGS, Validation::STATUS_ERROR, [ + ['file' => 'data/run.exe', 'code' => 'FILE_EXTENSION_NOT_ALLOWED', 'message' => "file extension is not allowed ('run.exe')"], + ]); + + $this->client->request('GET', '/api/validations/'.$validation->getUid().'/report'); + + $this->assertStatusCode(200, $this->client); + $this->assertSelectorTextContains('h2.group-title', 'ERROR'); + $this->assertSelectorTextContains('.file', 'data/run.exe'); + } + + private function updateValidation(string $fixture, string $status, ?array $results): Validation + { + $uid = $this->getValidationFixture($fixture)->getUid(); + $validation = $this->em->getRepository(Validation::class)->findOneByUid($uid); + $validation->setStatus($status); + $validation->setResults($results); + $this->em->flush(); + + return $validation; + } +} diff --git a/tests/Controller/Api/ValidationNormDataDownloadTest.php b/tests/Controller/Api/ValidationNormDataDownloadTest.php index a39f5ab..f36c350 100644 --- a/tests/Controller/Api/ValidationNormDataDownloadTest.php +++ b/tests/Controller/Api/ValidationNormDataDownloadTest.php @@ -4,11 +4,10 @@ use App\DataFixtures\ValidationsFixtures; use App\Tests\WebTestCase; +use App\Entity\Validation; use Liip\TestFixturesBundle\Services\DatabaseToolCollection; use Liip\TestFixturesBundle\Services\DatabaseTools\AbstractDatabaseTool; -use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\KernelBrowser; -use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Filesystem\Filesystem; /** @@ -112,60 +111,32 @@ public function testDownloadValNotFound() } /** - * Trying to download normalized data after execution of validations command. + * Download of the normalized data of a finished validation. */ public function testDownload() { - $this->markTestSkipped('TODO : fix outputs'); - - // running validations command twice because there are two validations pending - static::ensureKernelShutdown(); - $kernel = static::createKernel(); - $application = new Application($kernel); - $command = $application->find('ign-validator:validations:process-one'); - $commandTester = new CommandTester($command); - $statusCode = $commandTester->execute([]); - $this->assertEquals(0, $statusCode); - - $command = $application->find('ign-validator:validations:process-one'); - $commandTester = new CommandTester($command); - $statusCode = $commandTester->execute([]); - $this->assertEquals(0, $statusCode); - - // this one has failed - $validation2 = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_BAD_ARGS); - - $this->client->request( - 'GET', - '/api/validations/'.$validation2->getUid().'/files/normalized', + $uid = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS)->getUid(); + $em = static::getContainer()->get('doctrine')->getManager(); + $validation = $em->getRepository(Validation::class)->findOneByUid($uid); + $validation->setStatus(Validation::STATUS_FINISHED); + $em->flush(); + + $storage = $this->getValidationsStorage(); + $storage->getStorage()->write( + $storage->getOutputDirectory($validation).$validation->getDatasetName().'.zip', + 'normalized-zip-content' ); - $response = $this->client->getResponse(); - $json = \json_decode($response->getContent(), true); - - $this->assertStatusCode(403, $this->client); - $this->assertEquals('Validation failed, no normalized data', $json['message']); + $this->client->request('GET', '/api/validations/'.$uid.'/files/normalized'); - // this one has succeeded - $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS); - - $this->client->request( - 'GET', - '/api/validations/'.$validation->getUid().'/files/normalized', - ); - - $response = $this->client->getResponse(); $this->assertStatusCode(200, $this->client); - - $file = $response->getFile(); - // TODO - // expected: filename suffix should be -normalized.zip - // actual: -normalized is not present in the suffix - // var_dump($file); - $headers = $response->headers->all(); - - $this->assertEquals('application/zip', $headers['content-type'][0]); - $this->assertEquals($validation->getDatasetName().'.zip', $file->getFilename()); - $this->assertEquals('zip', $file->getExtension()); + $response = $this->client->getResponse(); + $this->assertEquals('application/zip', $response->headers->get('Content-Type')); + $this->assertEquals('22', $response->headers->get('Content-Length')); + $this->assertEquals( + 'attachment; filename='.$validation->getDatasetName().'-normalized.zip', + $response->headers->get('Content-Disposition') + ); + $this->assertEquals('normalized-zip-content', $this->client->getInternalResponse()->getContent()); } } diff --git a/tests/Controller/Api/ValidationSourceDataDownloadTest.php b/tests/Controller/Api/ValidationSourceDataDownloadTest.php index 09da2b6..dabc4b9 100644 --- a/tests/Controller/Api/ValidationSourceDataDownloadTest.php +++ b/tests/Controller/Api/ValidationSourceDataDownloadTest.php @@ -6,9 +6,7 @@ use App\Tests\WebTestCase; use Doctrine\ORM\EntityManagerInterface; use Liip\TestFixturesBundle\Services\DatabaseToolCollection; -use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\KernelBrowser; -use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Filesystem\Filesystem; /** @@ -93,70 +91,24 @@ public function testDownloadValNotFound() } /** - * Trying to download source data after execution of validations command. + * Download of the uploaded archive (written to the storage by the fixtures). */ public function testDownload() { - $this->markTestSkipped('TODO : fix test'); - - // running validations command twice because there are two validations pending - static::ensureKernelShutdown(); - $kernel = static::createKernel(); - $application = new Application($kernel); - $command = $application->find('ign-validator:validations:process-one'); - $commandTester = new CommandTester($command); - $statusCode = $commandTester->execute([]); - $this->assertEquals(0, $statusCode); - - $command = $application->find('ign-validator:validations:process-one'); - $commandTester = new CommandTester($command); - $statusCode = $commandTester->execute([]); - $this->assertEquals(0, $statusCode); - - // this one has failed - $validation2 = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_BAD_ARGS); - - $this->client->request( - 'GET', - '/api/validations/'.$validation2->getUid().'/files/source', - ); - - $response = $this->client->getResponse(); - $json = \json_decode($response->getContent(), true); - - $this->assertStatusCode(200, $this->client); - - $file = $response->getFile(); - // TODO - // expected: filename suffix should be -source.zip - // actual: -source is not present in the suffix - // var_dump($file); - $headers = $response->headers->all(); - - $this->assertEquals('application/zip', $headers['content-type'][0]); - $this->assertEquals($validation2->getDatasetName().'.zip', $file->getFilename()); - $this->assertEquals('zip', $file->getExtension()); - - // this one has succeeded $validation = $this->getValidationFixture(ValidationsFixtures::VALIDATION_WITH_ARGS); - $this->client->request( - 'GET', - '/api/validations/'.$validation->getUid().'/files/source', - ); + $this->client->request('GET', '/api/validations/'.$validation->getUid().'/files/source'); - $response = $this->client->getResponse(); $this->assertStatusCode(200, $this->client); - - $file = $response->getFile(); - // TODO - // expected: filename suffix should be -source.zip - // actual: -source is not present in the suffix - // var_dump($file); - $headers = $response->headers->all(); - - $this->assertEquals('application/zip', $headers['content-type'][0]); - $this->assertEquals($validation->getDatasetName().'.zip', $file->getFilename()); - $this->assertEquals('zip', $file->getExtension()); + $response = $this->client->getResponse(); + $this->assertEquals('application/zip', $response->headers->get('Content-Type')); + $this->assertEquals( + 'attachment; filename='.$validation->getDatasetName().'-source.zip', + $response->headers->get('Content-Disposition') + ); + $this->assertStringEqualsFile( + $this->getTestDataDir().'/'.ValidationsFixtures::FILENAME_SUP_PM3, + $this->client->getInternalResponse()->getContent() + ); } } diff --git a/tests/Entity/ValidationTest.php b/tests/Entity/ValidationTest.php new file mode 100644 index 0000000..f41c4ef --- /dev/null +++ b/tests/Entity/ValidationTest.php @@ -0,0 +1,31 @@ +setDatasetName('130010853_PM3_60_20180516.v2'); + + $this->assertEquals('130010853_PM3_60_20180516.v2', $validation->getDatasetName()); + } + + /** + * Names that could escape the validation directory or be parsed as command options are rejected. + */ + public function testSetDatasetNameRejectsUnsafeNames() + { + foreach (['', '.', '..', '../other', 'a/b', '-r', 'a b', str_repeat('a', 101)] as $name) { + try { + (new Validation())->setDatasetName($name); + $this->fail(sprintf("'%s' should be rejected", $name)); + } catch (\InvalidArgumentException $ex) { + $this->assertEquals(sprintf("Invalid dataset name '%s'", $name), $ex->getMessage()); + } + } + } +} diff --git a/tests/EventListener/SecurityHeadersListenerTest.php b/tests/EventListener/SecurityHeadersListenerTest.php new file mode 100644 index 0000000..a3973d8 --- /dev/null +++ b/tests/EventListener/SecurityHeadersListenerTest.php @@ -0,0 +1,65 @@ +createStub(HttpKernelInterface::class), new Request(), $type, $response); + (new SecurityHeadersListener($debug))($event); + + return $response; + } + + public function testHtmlPage() + { + $response = $this->dispatch(new Response('')); + + $this->assertEquals('nosniff', $response->headers->get('X-Content-Type-Options')); + $this->assertEquals('DENY', $response->headers->get('X-Frame-Options')); + $this->assertEquals('strict-origin-when-cross-origin', $response->headers->get('Referrer-Policy')); + $this->assertStringContainsString("script-src 'self';", $response->headers->get('Content-Security-Policy')); + $this->assertStringContainsString("frame-ancestors 'none'", $response->headers->get('Content-Security-Policy')); + } + + public function testJsonResponseHasNoCsp() + { + $response = $this->dispatch(new JsonResponse([])); + + $this->assertEquals('nosniff', $response->headers->get('X-Content-Type-Options')); + $this->assertFalse($response->headers->has('Content-Security-Policy')); + } + + /** + * The web profiler toolbar injects inline scripts in debug mode. + */ + public function testNoCspInDebug() + { + $response = $this->dispatch(new Response(''), true); + + $this->assertFalse($response->headers->has('Content-Security-Policy')); + } + + public function testExistingHeadersAreKept() + { + $response = $this->dispatch(new Response('', 200, ['X-Frame-Options' => 'SAMEORIGIN'])); + + $this->assertEquals('SAMEORIGIN', $response->headers->get('X-Frame-Options')); + } + + public function testSubRequestsAreIgnored() + { + $response = $this->dispatch(new Response(''), false, HttpKernelInterface::SUB_REQUEST); + + $this->assertFalse($response->headers->has('X-Frame-Options')); + } +} diff --git a/tests/Export/CsvReportWriterTest.php b/tests/Export/CsvReportWriterTest.php index dcb034b..9d4d8e1 100644 --- a/tests/Export/CsvReportWriterTest.php +++ b/tests/Export/CsvReportWriterTest.php @@ -82,4 +82,26 @@ public function testRegressValidator44() ]) ); } + + /** + * Values from the dataset can't be interpreted as formulas by spreadsheet applications. + */ + public function testFormulaInjection() + { + $validation = new Validation(); + $validation->setResults([ + ['code' => 'ATTRIBUTE_INVALID', 'message' => '=HYPERLINK("http://evil.example","x")', 'id' => '-12.5', 'attribute' => '@SUM(A1)', 'featureId' => '+33'], + ]); + $targetPath = $this->createTempDirectory('export-').'/export.csv'; + + (new CsvReportWriter())->write($validation, $targetPath); + + $rows = array_map(fn ($line) => str_getcsv($line, escape: '\\'), file($targetPath, FILE_IGNORE_NEW_LINES)); + $row = array_combine($rows[0], $rows[1]); + $this->assertEquals('\'=HYPERLINK("http://evil.example","x")', $row['message']); + $this->assertEquals('\'@SUM(A1)', $row['attribute']); + // numbers are kept as is + $this->assertEquals('-12.5', $row['id']); + $this->assertEquals('+33', $row['feat_id']); + } } diff --git a/tests/Storage/ValidationsStorageTest.php b/tests/Storage/ValidationsStorageTest.php index afc67ab..d81ccce 100644 --- a/tests/Storage/ValidationsStorageTest.php +++ b/tests/Storage/ValidationsStorageTest.php @@ -2,7 +2,9 @@ namespace App\Tests\Storage; +use App\Storage\ValidationsStorage; use App\Tests\WebTestCase; +use League\Flysystem\FilesystemOperator; class ValidationsStorageTest extends WebTestCase { @@ -16,4 +18,17 @@ public function testPathIsSuffixed() $storage = $this->getValidationsStorage(); $this->assertStringEndsWith('data/validations-test', $storage->getPath()); } + + /** + * STORAGE_TYPE selects the flysystem storage (it was read with getenv(), ignoring the .env files). + */ + public function testStorageType() + { + $s3 = $this->createStub(FilesystemOperator::class); + $local = $this->createStub(FilesystemOperator::class); + + $this->assertSame($s3, (new ValidationsStorage('/tmp', $s3, $local, 'S3'))->getStorage()); + $this->assertSame($local, (new ValidationsStorage('/tmp', $s3, $local, 'local'))->getStorage()); + $this->assertSame($local, (new ValidationsStorage('/tmp', $s3, $local))->getStorage()); + } } diff --git a/tests/Validation/FileContentValidatorTest.php b/tests/Validation/FileContentValidatorTest.php new file mode 100644 index 0000000..1311465 --- /dev/null +++ b/tests/Validation/FileContentValidatorTest.php @@ -0,0 +1,76 @@ + ["%PDF-1.7\n", 'pdf'], + 'pdf with junk before signature' => ["\r\n%PDF-1.4", 'pdf'], + 'shp' => ["\x00\x00\x27\x0A\x00", 'shp'], + 'dbf' => ["\x03\x7A\x01\x01", 'dbf'], + 'gpkg' => ["SQLite format 3\x00...", 'gpkg'], + 'xml with BOM' => ["\xEF\xBB\xBF ", 'xml'], + 'gml' => ['', 'gml'], + 'geojson' => ['{"type":"FeatureCollection"}', 'geojson'], + 'csv' => ['id;name', 'csv'], + 'png' => ["\x89PNG\r\n\x1A\n...", 'png'], + 'tif' => ["II*\x00...", 'tif'], + 'empty file' => ['', 'pdf'], + ]; + } + + #[DataProvider('validProvider')] + public function testValid(string $header, string $extension) + { + $this->assertNull((new FileContentValidator())->validateHeader($header, $extension)); + } + + public static function forbiddenProvider(): array + { + return [ + 'ELF as dbf' => ["\x7FELF\x02\x01", 'dbf', 'ELF executable'], + 'PE as csv' => ["MZ\x90\x00", 'csv', 'Windows executable'], + 'Mach-O as dat' => ["\xCF\xFA\xED\xFE", 'dat', 'Mach-O executable'], + 'script as txt' => ["#!/bin/bash\nrm -rf /", 'txt', 'script'], + 'zip as pdf' => ["PK\x03\x04", 'pdf', 'zip archive'], + 'java class as id' => ["\xCA\xFE\xBA\xBE", 'id', 'Java class or Mach-O executable'], + ]; + } + + #[DataProvider('forbiddenProvider')] + public function testForbidden(string $header, string $extension, string $type) + { + $this->assertEquals( + sprintf('content is not allowed (%s detected)', $type), + (new FileContentValidator())->validateHeader($header, $extension) + ); + } + + public static function signatureMismatchProvider(): array + { + return [ + 'html as pdf' => ['', 'pdf'], + 'text as shp' => ['not a shapefile', 'shp'], + 'text as dbf' => ['id;name', 'dbf'], + 'php as json' => [' ['hello', 'gml'], + 'pdf as png' => ['%PDF-1.7', 'png'], + ]; + } + + #[DataProvider('signatureMismatchProvider')] + public function testSignatureMismatch(string $header, string $extension) + { + $this->assertEquals( + sprintf('content does not match the .%s extension', $extension), + (new FileContentValidator())->validateHeader($header, $extension) + ); + } +} diff --git a/tests/Validation/ValidationManagerTest.php b/tests/Validation/ValidationManagerTest.php new file mode 100644 index 0000000..b891ecf --- /dev/null +++ b/tests/Validation/ValidationManagerTest.php @@ -0,0 +1,178 @@ +validation = (new Validation())->setDatasetName('dataset'); + $this->validation->setStatus(Validation::STATUS_PROCESSING); + + $this->repository = $this->createMock(ValidationRepository::class); + $this->repository->method('popNextPending')->willReturn($this->validation); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($this->repository); + + $zipArchiveValidator = $this->createStub(ZipArchiveValidator::class); + $zipArchiveValidator->method('validate')->willReturn([]); + + $this->workspace = $this->createMock(ValidationWorkspace::class); + $this->validatorCli = $this->createMock(ValidatorCLI::class); + + $this->manager = new ValidationManager( + $em, + $this->workspace, + $this->validatorCli, + $zipArchiveValidator, + new NullLogger(), + $this->repository + ); + } + + public function testProcessSuccess() + { + $this->workspace->expects($this->once())->method('saveNormalizedData'); + $this->workspace->expects($this->once())->method('saveLog'); + $this->workspace->expects($this->once())->method('removeLocalDirectory'); + $this->workspace->expects($this->never())->method('removePersistedFiles'); + + $this->manager->processOne(); + + $this->assertEquals(Validation::STATUS_FINISHED, $this->validation->getStatus()); + $this->assertNotNull($this->validation->getDateFinish()); + } + + public static function errorProvider(): array + { + $process = new Process(['false']); + $process->run(); + + return [ + 'validator-cli failure (command line and output are hidden)' => [ + new ProcessFailedException($process), + 'Validation failed (exit code 1)', + ], + 'internal error (raw message is hidden)' => [ + new \RuntimeException('Unable to write /opt/validator-api/var/data/secret'), + 'Validation failed (internal error)', + ], + 'error designed for the user' => [ + new ValidationProcessException('Zip decompression failed'), + 'Zip decompression failed', + ], + ]; + } + + /** + * Raw error messages are not exposed and logs are saved / local files removed on failure. + */ + #[DataProvider('errorProvider')] + public function testProcessError(\Throwable $error, string $expectedMessage) + { + $this->validatorCli->method('process')->willThrowException($error); + $this->workspace->expects($this->never())->method('saveNormalizedData'); + $this->workspace->expects($this->once())->method('saveLog'); + $this->workspace->expects($this->once())->method('removeLocalDirectory'); + + $this->manager->processOne(); + + $this->assertEquals(Validation::STATUS_ERROR, $this->validation->getStatus()); + $this->assertEquals($expectedMessage, $this->validation->getMessage()); + } + + /** + * delete-data : files are removed whatever the result. + */ + public function testProcessDeleteData() + { + $this->validation->setDeleteData(true); + $this->workspace->expects($this->once())->method('removePersistedFiles'); + $this->repository->expects($this->once())->method('dropSchema'); + + $this->manager->processOne(); + + $this->assertEquals(Validation::STATUS_ARCHIVED, $this->validation->getStatus()); + } + + /** + * delete-data on failure : files are removed but the error status is kept. + */ + public function testProcessDeleteDataOnError() + { + $this->validation->setDeleteData(true); + $this->validatorCli->method('process')->willThrowException(new \RuntimeException('boom')); + $this->workspace->expects($this->once())->method('removePersistedFiles'); + + $this->manager->processOne(); + + $this->assertEquals(Validation::STATUS_ERROR, $this->validation->getStatus()); + } + + /** + * A cleanup failure doesn't prevent the status from being saved. + */ + public function testProcessCleanupFailure() + { + $this->workspace->method('saveLog')->willThrowException(new \RuntimeException('storage down')); + $this->workspace->method('removeLocalDirectory')->willThrowException(new \RuntimeException('disk error')); + + $this->manager->processOne(); + + $this->assertEquals(Validation::STATUS_FINISHED, $this->validation->getStatus()); + } + + /** + * SIGTERM while processing : validator-cli.jar is stopped and the validation is restarted later. + */ + public function testCancelProcessing() + { + $this->validatorCli->method('process')->willReturnCallback(function () { + $this->manager->cancelProcessing(); + }); + $this->validatorCli->expects($this->once())->method('stop'); + $this->workspace->expects($this->atLeastOnce())->method('removeLocalDirectory'); + + $this->manager->processOne(); + + // processOne normally doesn't return (the console application exits after the signal handler) + $this->assertNotNull($this->validation->getStatus()); + } + + /** + * Without validation in progress, SIGTERM has no effect. + */ + public function testCancelProcessingWithoutValidation() + { + $this->validatorCli->expects($this->never())->method('stop'); + + $this->manager->cancelProcessing(); + } +} diff --git a/tests/Validation/ZipArchiveExtractorTest.php b/tests/Validation/ZipArchiveExtractorTest.php new file mode 100644 index 0000000..dcf18c6 --- /dev/null +++ b/tests/Validation/ZipArchiveExtractorTest.php @@ -0,0 +1,119 @@ +extractor = new ZipArchiveExtractor( + new ZipArchiveValidator(new NullLogger()), + new FileContentValidator(), + new NullLogger() + ); + } + + /** + * Valid archive : files are extracted, OS metadata entries are skipped. + */ + public function testExtract() + { + $zipPath = $this->createZip([ + 'data/' => '', + 'data/table.csv' => 'id;name', + 'data/sub/table.dbf' => "\x03\x7A", + '__MACOSX/data/._table.csv' => "\x00\x05\x16\x07", + ]); + $targetPath = dirname($zipPath).'/extracted'; + + $this->extractor->extract($zipPath, $targetPath); + + $this->assertStringEqualsFile($targetPath.'/data/table.csv', 'id;name'); + $this->assertStringEqualsFile($targetPath.'/data/sub/table.dbf', "\x03\x7A"); + $this->assertDirectoryDoesNotExist($targetPath.'/__MACOSX'); + $this->assertFalse(is_executable($targetPath.'/data/table.csv')); + } + + /** + * Sample dataset archive is extracted as with ZipArchive::extractTo. + */ + public function testExtractSampleDataset() + { + $zipPath = $this->getTestDataDir().'/130010853_PM3_60_20180516.zip'; + $targetPath = $this->createTempDirectory('extract-'); + + $this->extractor->extract($zipPath, $targetPath); + + $zip = new \ZipArchive(); + $zip->open($zipPath); + for ($i = 0; $i < $zip->numFiles; ++$i) { + $name = $zip->getNameIndex($i); + if (!str_ends_with($name, '/')) { + $this->assertStringEqualsFile($targetPath.'/'.$name, $zip->getFromIndex($i)); + } + } + $zip->close(); + } + + /** + * Executables disguised with an allowed extension and fake files are rejected, + * nothing is left in the target directory. + */ + public function testExtractForbiddenContent() + { + $zipPath = $this->createZip([ + 'data/table.csv' => 'id;name', + 'data/table.dbf' => "\x7FELF\x02\x01\x01".str_repeat("\0", 2000), + 'data/doc.pdf' => '', + ]); + $targetPath = dirname($zipPath).'/extracted'; + + try { + $this->extractor->extract($zipPath, $targetPath); + $this->fail('ZipArchiveValidationException expected'); + } catch (ZipArchiveValidationException $ex) { + $this->assertEquals([ + [ + 'file' => 'data/table.dbf', + 'code' => ZipArchiveValidator::ERROR_FILE_CONTENT_NOT_ALLOWED, + 'message' => 'content is not allowed (ELF executable detected)', + ], + [ + 'file' => 'data/doc.pdf', + 'code' => ZipArchiveValidator::ERROR_FILE_CONTENT_NOT_ALLOWED, + 'message' => 'content does not match the .pdf extension', + ], + ], $ex->getErrors()); + } + + $this->assertDirectoryDoesNotExist($targetPath); + } + + /** + * Unsafe paths are never written, even without pre-validation. + */ + public function testExtractPathTraversal() + { + $zipPath = $this->createZip(['data/../../evil.csv' => 'a']); + $targetPath = dirname($zipPath).'/extracted'; + + $this->expectExceptionMessage("Zip decompression failed (path is not allowed 'data/../../evil.csv')"); + try { + $this->extractor->extract($zipPath, $targetPath); + } finally { + $this->assertFileDoesNotExist(dirname($zipPath, 2).'/evil.csv'); + } + } +} diff --git a/tests/Validation/ZipArchiveValidatorTest.php b/tests/Validation/ZipArchiveValidatorTest.php index 5ed11e5..2cd8389 100644 --- a/tests/Validation/ZipArchiveValidatorTest.php +++ b/tests/Validation/ZipArchiveValidatorTest.php @@ -106,4 +106,145 @@ public function testZipFilesInvalidName() $this->assertEquals(ZipArchiveValidator::ERROR_BAD_ARCHIVE_FILENAME, $error['code']); } } + + /** + * zip contains executables, scripts or nested archives. + */ + public function testZipFilesExtensionNotAllowed() + { + $zipPath = $this->createZip([ + 'data/table.dbf' => "\x03", + 'data/run.exe' => 'MZ', + 'data/run.sh' => '#!/bin/sh', + 'data/nested.zip' => "PK\x03\x04", + 'data/README' => 'no extension', + ]); + + $errors = $this->zipArchiveValidator->validate($zipPath); + + $this->assertEquals( + ['data/run.exe', 'data/run.sh', 'data/nested.zip', 'data/README'], + array_column($errors, 'file') + ); + foreach ($errors as $error) { + $this->assertEquals(ZipArchiveValidator::ERROR_FILE_EXTENSION_NOT_ALLOWED, $error['code']); + } + } + + /** + * extensions are not case sensitive and OS metadata entries are ignored. + */ + public function testZipFilesAllowed() + { + $zipPath = $this->createZip([ + 'data/TABLE.DBF' => "\x03", + 'data/.DS_Store' => 'mac', + '__MACOSX/data/._TABLE.DBF' => 'mac', + 'data/Thumbs.db' => 'windows', + ]); + + $this->assertEmpty($this->zipArchiveValidator->validate($zipPath)); + } + + /** + * zip contains entries which would be extracted outside the target directory. + */ + public function testZipPathTraversal() + { + $zipPath = $this->createZip([ + '../evil.csv' => 'a', + '/tmp/evil.csv' => 'a', + 'data/../../evil.csv' => 'a', + ]); + + $errors = $this->zipArchiveValidator->validate($zipPath); + + $this->assertEquals(3, count($errors)); + foreach ($errors as $error) { + $this->assertEquals(ZipArchiveValidator::ERROR_BAD_ARCHIVE_ENTRY, $error['code']); + $this->assertStringStartsWith('path is not allowed', $error['message']); + } + } + + /** + * zip contains a symbolic link. + */ + public function testZipSymlink() + { + $zipPath = $this->createZip(['data/link.csv' => '/etc/passwd']); + $zip = new \ZipArchive(); + $zip->open($zipPath); + $zip->setExternalAttributesName('data/link.csv', \ZipArchive::OPSYS_UNIX, 0o120777 << 16); + $zip->close(); + + $errors = $this->zipArchiveValidator->validate($zipPath); + + $this->assertEquals(1, count($errors)); + $this->assertEquals(ZipArchiveValidator::ERROR_BAD_ARCHIVE_ENTRY, $errors[0]['code']); + $this->assertStringStartsWith('symbolic links are not allowed', $errors[0]['message']); + } + + /** + * zip contains an encrypted file. + */ + public function testZipEncrypted() + { + $zipPath = $this->createZip(['data/table.csv' => 'a,b']); + $zip = new \ZipArchive(); + $zip->open($zipPath); + $zip->setEncryptionName('data/table.csv', \ZipArchive::EM_AES_256, 'secret'); + $zip->close(); + + $errors = $this->zipArchiveValidator->validate($zipPath); + + $this->assertEquals(1, count($errors)); + $this->assertEquals(ZipArchiveValidator::ERROR_BAD_ARCHIVE_ENTRY, $errors[0]['code']); + $this->assertStringStartsWith('encrypted files are not allowed', $errors[0]['message']); + } + + /** + * zip bomb protection : number of entries. + */ + public function testZipTooManyEntries() + { + $validator = new ZipArchiveValidator(new NullLogger(), maxEntries: 2); + $zipPath = $this->createZip(['a.csv' => 'a', 'b.csv' => 'b', 'c.csv' => 'c']); + + $errors = $validator->validate($zipPath); + + $this->assertEquals(1, count($errors)); + $this->assertEquals('test.zip', $errors[0]['file']); + $this->assertEquals(ZipArchiveValidator::ERROR_ARCHIVE_TOO_LARGE, $errors[0]['code']); + $this->assertEquals('archive contains too many entries (3 > 2)', $errors[0]['message']); + } + + /** + * zip bomb protection : total uncompressed size. + */ + public function testZipUncompressedSizeTooLarge() + { + $validator = new ZipArchiveValidator(new NullLogger(), maxUncompressedSize: 1000); + $zipPath = $this->createZip(['a.csv' => str_repeat('a', 600), 'b.csv' => str_repeat('b', 600)]); + + $errors = $validator->validate($zipPath); + + $this->assertEquals(1, count($errors)); + $this->assertEquals(ZipArchiveValidator::ERROR_ARCHIVE_TOO_LARGE, $errors[0]['code']); + $this->assertEquals('archive uncompressed size exceeds 1000 bytes', $errors[0]['message']); + } + + /** + * zip bomb protection : compression ratio. + */ + public function testZipCompressionRatioTooHigh() + { + $validator = new ZipArchiveValidator(new NullLogger(), maxCompressionRatio: 100); + $zipPath = $this->createZip(['bomb.csv' => str_repeat("\0", 2 * 1024 * 1024)]); + + $errors = $validator->validate($zipPath); + + $this->assertEquals(1, count($errors)); + $this->assertEquals(ZipArchiveValidator::ERROR_ARCHIVE_TOO_LARGE, $errors[0]['code']); + $this->assertEquals("compression ratio of 'bomb.csv' exceeds 100", $errors[0]['message']); + } } diff --git a/tests/WebTestCase.php b/tests/WebTestCase.php index 4841151..f2425bb 100644 --- a/tests/WebTestCase.php +++ b/tests/WebTestCase.php @@ -14,6 +14,16 @@ */ abstract class WebTestCase extends BaseWebTestCase { + /** + * Removes the files written to the storage (see flysystem.yaml, when@test). + */ + protected function tearDown(): void + { + parent::tearDown(); + + (new Filesystem())->remove(dirname(__DIR__).'/var/data-test'); + } + /** * @var AbstractExecutor */ @@ -32,9 +42,9 @@ protected function getValidationFixture($name): Validation { if ($this->fixtures->getReferenceRepository()->hasReference($name, Validation::class)) { return $this->fixtures->getReferenceRepository()->getReference($name, Validation::class); - } else { - throw new \Exception("No reference found for $name"); } + throw new \Exception("No reference found for $name"); + } /** @@ -45,6 +55,31 @@ protected function getValidationsStorage() return $this->getContainer()->get(ValidationsStorage::class); } + /** + * Create a zip archive in a temp directory. + * + * @param array $files content by entry name (names ending with "/" are directories) + * + * @return string path to the zip archive + */ + protected function createZip(array $files, string $zipName = 'test.zip'): string + { + $zipPath = $this->createTempDirectory('zip-').'/'.$zipName; + + $zip = new \ZipArchive(); + $this->assertTrue($zip->open($zipPath, \ZipArchive::CREATE)); + foreach ($files as $name => $content) { + if (str_ends_with($name, '/')) { + $zip->addEmptyDir($name); + } else { + $zip->addFromString($name, $content); + } + } + $this->assertTrue($zip->close()); + + return $zipPath; + } + /** * Create a temp directory. * diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 469dcce..8634d03 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -9,3 +9,9 @@ } elseif (method_exists(Dotenv::class, 'bootEnv')) { (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); } + +// rate limiter counters are persisted in the test cache (cache.rate_limiter) : reset them for each run +$kernel = new App\Kernel('test', true); +$kernel->boot(); +$kernel->getContainer()->get('test.service_container')->get('cache.rate_limiter')->clear(); +$kernel->shutdown(); diff --git a/webpack.config.js b/webpack.config.js index 2cffdb1..6a712d6 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,34 +1,55 @@ const path = require('path'); const CopyWebpackPlugin = require('copy-webpack-plugin'); -module.exports = [ - { - entry: './assets/demo.js', - output: { - path: path.resolve(__dirname, 'public'), - filename: 'build/demo.js' +/* + * Demo client (IGNF/validator-api-client) : the built bundle and its static files (css, fonts, + * images) are copied in public/ and loaded by templates/demo.html.twig. + */ +const clientDir = path.dirname(require.resolve('@ignf/validator-client/package.json')); + +// directories of public/ produced by this build (the other files of public/ are kept) +const GENERATED_DIRS = ['build/', 'vendor/', 'css/', 'font/', 'img/']; + +module.exports = { + entry: './assets/demo.js', + output: { + path: path.resolve(__dirname, 'public'), + filename: 'build/demo.js', + // removes the outdated files (ex : chunks of a previous version of the client) + clean: { + keep: (asset) => !GENERATED_DIRS.some((dir) => asset.startsWith(dir)), }, - plugins: [ - new CopyWebpackPlugin({ - patterns: [ - { - from: "./node_modules/@ignf/validator-client/dist", - to: "./vendor/validator-api-client" - }, - { - from: "./node_modules/@ignf/validator-client/public/css/", - to: "css/" - }, - { - from: "./node_modules/@ignf/validator-client/public/img/", - to: "img/" + }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { + from: path.join(clientDir, 'dist'), + to: 'vendor/validator-api-client', + // already minified by the client build (not processed again by terser) + info: { minimized: true }, + }, + { + from: path.join(clientDir, 'public/css'), + to: 'css', + }, + { + from: path.join(clientDir, 'public/img'), + to: 'img', + }, + { + from: path.join(clientDir, 'public/font'), + to: 'font', + // only woff2, woff, ttf and svg are referenced by style-carto.css + globOptions: { + ignore: ['**/*.eot', '**/*.otf', '**/selection.json'], }, - { - from: "./node_modules/@ignf/validator-client/public/font/", - to: "font/" - } - ] - }) - ] - } -]; + }, + ], + }), + ], + // the copied files of the client are large (swagger-ui), demo.js is tiny + performance: { + hints: false, + }, +};