From 1850c1472e171c1052301c80e6d82bb47781d4b5 Mon Sep 17 00:00:00 2001 From: martinydeAI Date: Thu, 11 Jun 2026 19:07:16 +0200 Subject: [PATCH 1/7] feat: add user authentication (#2) Wires up the application's first persistent identity and the login flow it gates. Installs Doctrine ORM + migrations + Symfony Security + dev-only MakerBundle and DoctrineFixturesBundle; switches doctrine.yaml to MariaDB and sets DATABASE_URL to the in-stack mariadb service. App surface: App\Entity\User (email, hashed password, roles), UserRepository with PasswordUpgraderInterface, App\Security\UserManager service that owns persistence + hashing, App\Controller\SecurityController exposing /login and /logout (form_login + declarative logout in security.yaml), Twig login form under templates/security/, and Danish translation keys under security.login.*. Two console commands sit on UserManager: app:user:create and app:user:change-password. App\DataFixtures\UserFixtures seeds alice@example.test and bob@example.test (password `password`) for local dev. Tests: 32 cases, 70 assertions, 100% coverage. UserManager unit-tested via KernelTestCase + a schema-reset trait; UserRepository's upgradePassword covered for the happy path and the foreign-user rejection; commands exercised through CommandTester; SecurityController's full /login + /logout + failed-login flow exercised through WebTestCase. Tests share tests/Support/ResetsDatabaseSchemaTrait which drops + recreates the db_test schema from ORM metadata in setUp. Docs: README gains a "Creating the first user" subsection covering migration, fixture load, and the two console commands. CHANGELOG entry under [Unreleased] / Added references #2. Closes #2. Co-Authored-By: Claude Opus 4.7 (1M context) --- .env | 4 + CHANGELOG.md | 8 + README.md | 18 + composer.json | 6 + composer.lock | 4307 ++++++++++++----- config/bundles.php | 5 + config/packages/doctrine.yaml | 39 + config/packages/doctrine_migrations.yaml | 6 + config/packages/security.yaml | 45 + config/reference.php | 535 ++ config/routes/security.yaml | 3 + migrations/.gitignore | 0 migrations/Version20260611124347.php | 29 + phpunit.dist.xml | 2 + src/Command/UserChangePasswordCommand.php | 75 + src/Command/UserCreateCommand.php | 77 + src/Controller/SecurityController.php | 68 + src/DataFixtures/UserFixtures.php | 44 + src/Entity/.gitignore | 0 src/Entity/User.php | 109 + src/Repository/.gitignore | 0 src/Repository/UserRepository.php | 35 + src/Security/UserManager.php | 113 + symfony.lock | 70 + templates/security/login.html.twig | 49 + .../Command/UserChangePasswordCommandTest.php | 57 + tests/Command/UserCreateCommandTest.php | 57 + tests/Controller/SecurityControllerTest.php | 110 + tests/DataFixtures/UserFixturesTest.php | 41 + tests/Repository/UserRepositoryTest.php | 69 + tests/Security/UserManagerTest.php | 104 + tests/Support/ResetsDatabaseSchemaTrait.php | 34 + translations/messages.da.yaml | 9 + 33 files changed, 4990 insertions(+), 1138 deletions(-) create mode 100644 config/packages/doctrine.yaml create mode 100644 config/packages/doctrine_migrations.yaml create mode 100644 config/packages/security.yaml create mode 100644 config/routes/security.yaml create mode 100644 migrations/.gitignore create mode 100644 migrations/Version20260611124347.php create mode 100644 src/Command/UserChangePasswordCommand.php create mode 100644 src/Command/UserCreateCommand.php create mode 100644 src/Controller/SecurityController.php create mode 100644 src/DataFixtures/UserFixtures.php create mode 100644 src/Entity/.gitignore create mode 100644 src/Entity/User.php create mode 100644 src/Repository/.gitignore create mode 100644 src/Repository/UserRepository.php create mode 100644 src/Security/UserManager.php create mode 100644 templates/security/login.html.twig create mode 100644 tests/Command/UserChangePasswordCommandTest.php create mode 100644 tests/Command/UserCreateCommandTest.php create mode 100644 tests/Controller/SecurityControllerTest.php create mode 100644 tests/DataFixtures/UserFixturesTest.php create mode 100644 tests/Repository/UserRepositoryTest.php create mode 100644 tests/Security/UserManagerTest.php create mode 100644 tests/Support/ResetsDatabaseSchemaTrait.php diff --git a/.env b/.env index 3c692d1..38bbe52 100644 --- a/.env +++ b/.env @@ -34,3 +34,7 @@ BRAND_NAME="AI Bibliotek" BRAND_TAGLINE="del & hjemtag assistenter" BRAND_INITIALS="AI" ###< brand identity ### + +###> doctrine/doctrine-bundle ### +DATABASE_URL="mysql://db:db@mariadb:3306/db?serverVersion=10.11.16-MariaDB&charset=utf8mb4" +###< doctrine/doctrine-bundle ### diff --git a/CHANGELOG.md b/CHANGELOG.md index 30aa2a5..524b90c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - PHPUnit test harness with 100% coverage gate enforced in CI via `rregeer/phpunit-coverage-check` ([#31](https://github.com/itk-dev/ai-lib/issues/31)). +- User authentication: `User` Doctrine entity (email, hashed password, + roles), `UserRepository` (with `PasswordUpgraderInterface`), the + `UserManager` service that hides persistence + hashing, form-login + firewall + `/login` + `/logout`, fixtures for two baseline users + (`alice@example.test`, `bob@example.test` — password `password`), + console commands `app:user:create` and `app:user:change-password`, + and end-to-end functional + unit tests + ([#2](https://github.com/itk-dev/ai-lib/issues/2)). ### Changed diff --git a/README.md b/README.md index ff1d30d..1bc9cb0 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,24 @@ task open The site is served through Traefik on a `*.local.itkdev.dk` domain (the exact URL is printed by the start task). +### Creating the first user + +```sh +# Apply the database schema +task console -- doctrine:migrations:migrate -n + +# Option A — load the local-dev fixtures (alice + bob, password `password`) +task console -- doctrine:fixtures:load -n + +# Option B — create a single user explicitly +task console -- app:user:create alice@example.test secret + +# Change an existing user's password +task console -- app:user:change-password alice@example.test newsecret +``` + +Then sign in at `/login`. + ## Testing Tests live under `tests/` (PSR-4 namespace `App\Tests\`) and run with diff --git a/composer.json b/composer.json index 79beba5..aa0ffec 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,9 @@ "php": ">=8.4", "ext-ctype": "*", "ext-iconv": "*", + "doctrine/doctrine-bundle": "^3.2", + "doctrine/doctrine-migrations-bundle": "^4.0", + "doctrine/orm": "^3.6", "symfony/asset": "^8.1", "symfony/asset-mapper": "^8.1", "symfony/console": "~8.1.0", @@ -14,6 +17,7 @@ "symfony/flex": "^2", "symfony/framework-bundle": "~8.1.0", "symfony/runtime": "~8.1.0", + "symfony/security-bundle": "~8.1.0", "symfony/stimulus-bundle": "^3.1", "symfony/translation": "~8.1.0", "symfony/twig-bundle": "~8.1.0", @@ -24,12 +28,14 @@ "twig/twig": "^2.12 || ^3.0" }, "require-dev": { + "doctrine/doctrine-fixtures-bundle": "^4.3", "ergebnis/composer-normalize": "^2.52", "friendsofphp/php-cs-fixer": "^3.95.5", "phpunit/phpunit": "^11.5", "rregeer/phpunit-coverage-check": "^0.3.1", "symfony/browser-kit": "~8.1.0", "symfony/css-selector": "~8.1.0", + "symfony/maker-bundle": "^1.67", "symfony/phpunit-bridge": "~8.1.0", "vincentlanglet/twig-cs-fixer": "^3.14" }, diff --git a/composer.lock b/composer.lock index abb6510..3bead4f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5e5be02b1936d9ba94164fc47d576055", + "content-hash": "515575e1ba1d454616c991cd865c269a", "packages": [ { "name": "composer/semver", @@ -84,31 +84,35 @@ "time": "2025-08-20T19:15:30+00:00" }, { - "name": "psr/cache", - "version": "3.0.0", + "name": "doctrine/collections", + "version": "2.6.0", "source": { "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + "url": "https://github.com/doctrine/collections.git", + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2", + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2", "shasum": "" }, "require": { - "php": ">=8.0.0" + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } + "require-dev": { + "doctrine/coding-standard": "^14", + "ext-json": "*", + "phpstan/phpstan": "^2.1.30", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4" }, + "type": "library", "autoload": { "psr-4": { - "Psr\\Cache\\": "src/" + "Doctrine\\Common\\Collections\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -117,47 +121,94 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "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": "Common interface for caching libraries", + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", "keywords": [ - "cache", - "psr", - "psr-6" + "array", + "collections", + "iterators", + "php" ], "support": { - "source": "https://github.com/php-fig/cache/tree/3.0.0" + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.6.0" }, - "time": "2021-02-03T23:26:27+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2026-01-15T10:01:58+00:00" }, { - "name": "psr/container", - "version": "2.0.2", + "name": "doctrine/dbal", + "version": "4.4.3", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "url": "https://github.com/doctrine/dbal.git", + "reference": "61e730f1658814821a85f2402c945f3883407dec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61e730f1658814821a85f2402c945f3883407dec", + "reference": "61e730f1658814821a85f2402c945f3883407dec", "shasum": "" }, "require": { - "php": ">=7.4.0" + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } + "require-dev": { + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "2.0.7", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "11.5.50", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." }, + "type": "library", "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "Doctrine\\DBAL\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -166,101 +217,176 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.4.3" }, - "time": "2021-11-05T16:47:00+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2026-03-20T08:52:12+00:00" }, { - "name": "psr/event-dispatcher", - "version": "1.0.0", + "name": "doctrine/deprecations", + "version": "1.1.6", "source": { "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": "^7.1 || ^8.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } + "conflict": { + "phpunit/phpunit": "<=7.5 || >=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": { - "Psr\\EventDispatcher\\": "src/" + "Doctrine\\Deprecations\\": "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" - ], + "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/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2019-01-08T18:20:26+00:00" + "time": "2026-02-07T07:09:04+00:00" }, { - "name": "psr/log", - "version": "3.0.2", + "name": "doctrine/doctrine-bundle", + "version": "3.2.4", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "75f1bf75d0ba099f23e7d43ebd804df5bec58c29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/75f1bf75d0ba099f23e7d43ebd804df5bec58c29", + "reference": "75f1bf75d0ba099f23e7d43ebd804df5bec58c29", "shasum": "" }, "require": { - "php": ">=8.0.0" + "doctrine/dbal": "^4.0", + "doctrine/deprecations": "^1.0", + "doctrine/persistence": "^4", + "doctrine/sql-formatter": "^1.0.1", + "php": "^8.4", + "symfony/cache": "^6.4 || ^7.0 || ^8.0", + "symfony/config": "^6.4 || ^7.0 || ^8.0", + "symfony/console": "^6.4 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", + "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3 || ^8.0", + "symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0", + "symfony/service-contracts": "^3" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } + "conflict": { + "doctrine/orm": "<3.0 || >=4.0", + "twig/twig": "<3.0.4" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "doctrine/orm": "^3.4.4", + "phpstan/phpstan": "^2.1.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/security-bundle": "^6.4 || ^7.0 || ^8.0", + "symfony/stopwatch": "^6.4 || ^7.0 || ^8.0", + "symfony/string": "^6.4 || ^7.0 || ^8.0", + "symfony/twig-bridge": "^6.4 || ^7.0 || ^8.0", + "symfony/validator": "^6.4 || ^7.0 || ^8.0", + "symfony/web-profiler-bundle": "^6.4 || ^7.0 || ^8.0", + "symfony/yaml": "^6.4 || ^7.0 || ^8.0", + "twig/twig": "^3.21.1" + }, + "suggest": { + "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", + "ext-pdo": "*", + "symfony/web-profiler-bundle": "To use the data collector." }, + "type": "symfony-bundle", "autoload": { "psr-4": { - "Psr\\Log\\": "src" + "Doctrine\\Bundle\\DoctrineBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -269,52 +395,96 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "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": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Symfony DoctrineBundle", + "homepage": "https://www.doctrine-project.org", "keywords": [ - "log", - "psr", - "psr-3" + "database", + "dbal", + "orm", + "persistence" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" + "issues": "https://github.com/doctrine/DoctrineBundle/issues", + "source": "https://github.com/doctrine/DoctrineBundle/tree/3.2.4" }, - "time": "2024-09-11T13:17:53+00:00" + "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-06-09T19:11:55+00:00" }, { - "name": "symfony/asset", - "version": "v8.1.0", + "name": "doctrine/doctrine-migrations-bundle", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/asset.git", - "reference": "4bd4d143b7e53f40d45877df52eb2b18282bdac4" + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "20505da78735744fb4a42a3bb9a416b345ad6f7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/asset/zipball/4bd4d143b7e53f40d45877df52eb2b18282bdac4", - "reference": "4bd4d143b7e53f40d45877df52eb2b18282bdac4", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/20505da78735744fb4a42a3bb9a416b345ad6f7c", + "reference": "20505da78735744fb4a42a3bb9a416b345ad6f7c", "shasum": "" }, "require": { - "php": ">=8.4.1" + "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": { - "symfony/http-client": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0" + "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": "library", + "type": "symfony-bundle", "autoload": { "psr-4": { - "Symfony\\Component\\Asset\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Doctrine\\Bundle\\MigrationsBundle\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -325,77 +495,73 @@ "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": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", - "homepage": "https://symfony.com", + "description": "Symfony DoctrineMigrationsBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "dbal", + "migrations", + "schema" + ], "support": { - "source": "https://github.com/symfony/asset/tree/v8.1.0" + "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/4.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://www.doctrine-project.org/sponsorship.html", "type": "custom" }, { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle", "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2025-12-05T08:14:38+00:00" }, { - "name": "symfony/asset-mapper", - "version": "v8.1.0", + "name": "doctrine/event-manager", + "version": "2.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/asset-mapper.git", - "reference": "74b1b7b7019c728cb1f8672b502260e683b6374e" + "url": "https://github.com/doctrine/event-manager.git", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/asset-mapper/zipball/74b1b7b7019c728cb1f8672b502260e683b6374e", - "reference": "74b1b7b7019c728cb1f8672b502260e683b6374e", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", "shasum": "" }, "require": { - "composer/semver": "^3.0", - "php": ">=8.4.1", - "symfony/filesystem": "^7.4|^8.0", - "symfony/http-client": "^7.4|^8.0" + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" }, "require-dev": { - "symfony/asset": "^7.4|^8.0", - "symfony/browser-kit": "^7.4|^8.0", - "symfony/console": "^7.4|^8.0", - "symfony/event-dispatcher-contracts": "^3.0", - "symfony/finder": "^7.4|^8.0", - "symfony/framework-bundle": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/process": "^7.4|^8.0", - "symfony/runtime": "^7.4|^8.0", - "symfony/web-link": "^7.4|^8.0" + "doctrine/coding-standard": "^14", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/phpstan": "^2.1.32", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\AssetMapper\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Doctrine\\Common\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -403,94 +569,88 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "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": "Maps directories of assets & makes them available in a public directory with versioned filenames.", - "homepage": "https://symfony.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": { - "source": "https://github.com/symfony/asset-mapper/tree/v8.1.0" + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.1.1" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://www.doctrine-project.org/sponsorship.html", "type": "custom" }, { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-01-29T07:11:08+00:00" }, { - "name": "symfony/cache", - "version": "v8.1.0", + "name": "doctrine/inflector", + "version": "2.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/cache.git", - "reference": "ba62e0ed9ea9bc26142844a891d4a3dfceb24aed" + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/ba62e0ed9ea9bc26142844a891d4a3dfceb24aed", - "reference": "ba62e0ed9ea9bc26142844a891d4a3dfceb24aed", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { - "php": ">=8.4.1", - "psr/cache": "^2.0|^3.0", - "psr/log": "^1.1|^2|^3", - "symfony/cache-contracts": "^3.6", - "symfony/service-contracts": "^2.5|^3", - "symfony/var-exporter": "^8.1" - }, - "conflict": { - "ext-redis": "<6.1", - "ext-relay": "<0.12.1" - }, - "provide": { - "psr/cache-implementation": "2.0|3.0", - "psr/simple-cache-implementation": "1.0|2.0|3.0", - "symfony/cache-implementation": "1.1|2.0|3.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "cache/integration-tests": "dev-master", - "doctrine/dbal": "^4.3", - "predis/predis": "^1.1|^2.0", - "psr/simple-cache": "^1.0|^2.0|^3.0", - "symfony/clock": "^7.4|^8.0", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/filesystem": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/messenger": "^7.4|^8.0", - "symfony/var-dumper": "^7.4|^8.0" + "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": { - "Symfony\\Component\\Cache\\": "" - }, - "classmap": [ - "Traits/ValueWrapper.php" - ], - "exclude-from-classmap": [ - "/Tests/" - ] + "Doctrine\\Inflector\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -498,74 +658,90 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "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": "Provides extended PSR-6, PSR-16 (and tags) implementations", - "homepage": "https://symfony.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": [ - "caching", - "psr6" + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" ], "support": { - "source": "https://github.com/symfony/cache/tree/v8.1.0" + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://www.doctrine-project.org/sponsorship.html", "type": "custom" }, { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { - "name": "symfony/cache-contracts", - "version": "v3.7.0", + "name": "doctrine/instantiator", + "version": "2.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/cache-contracts.git", - "reference": "225e8a254166bd3442e370c6f50145465db63831" + "url": "https://github.com/doctrine/instantiator.git", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/225e8a254166bd3442e370c6f50145465db63831", - "reference": "225e8a254166bd3442e370c6f50145465db63831", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/cache": "^3.0" + "php": "^8.4" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-dev" - } + "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": { - "Symfony\\Contracts\\Cache\\": "" + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" } }, "notification-url": "https://packagist.org/downloads/", @@ -574,85 +750,66 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" } ], - "description": "Generic abstractions related to caching", - "homepage": "https://symfony.com", + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "constructor", + "instantiate" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.7.0" + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://www.doctrine-project.org/sponsorship.html", "type": "custom" }, { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", "type": "tidelift" } ], - "time": "2026-05-05T15:33:14+00:00" + "time": "2026-01-05T06:47:08+00:00" }, { - "name": "symfony/config", - "version": "v8.1.0", + "name": "doctrine/lexer", + "version": "3.0.1", "source": { "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "429783a0c649696f2058ea5ab5315f082dba6de9" + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/429783a0c649696f2058ea5ab5315f082dba6de9", - "reference": "429783a0c649696f2058ea5ab5315f082dba6de9", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { - "php": ">=8.4.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^7.4|^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/service-contracts": "<2.5" + "php": "^8.1" }, "require-dev": { - "symfony/event-dispatcher": "^7.4|^8.0", - "symfony/finder": "^7.4|^8.0", - "symfony/messenger": "^7.4|^8.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^7.4|^8.0" + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Config\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Doctrine\\Common\\Lexer\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -660,93 +817,104 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" } ], - "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", - "homepage": "https://symfony.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": { - "source": "https://github.com/symfony/config/tree/v8.1.0" + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://www.doctrine-project.org/sponsorship.html", "type": "custom" }, { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { - "name": "symfony/console", - "version": "v8.1.0", + "name": "doctrine/migrations", + "version": "3.9.7", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a" + "url": "https://github.com/doctrine/migrations.git", + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f5a856c6ecb56b3c21ed94a5b7bf940d857d110a", - "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", "shasum": "" }, "require": { - "php": ">=8.4.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php85": "^1.32", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.4.6|^8.0.6" + "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": { - "symfony/dependency-injection": "<8.1", - "symfony/event-dispatcher": "<8.1" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "doctrine/orm": "<2.12 || >=4" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^8.1", - "symfony/event-dispatcher": "^8.1", - "symfony/filesystem": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/lock": "^7.4|^8.0", - "symfony/messenger": "^7.4|^8.0", - "symfony/mime": "^7.4|^8.0", - "symfony/process": "^7.4|^8.0", - "symfony/stopwatch": "^7.4|^8.0", - "symfony/uid": "^7.4|^8.0", - "symfony/validator": "^7.4|^8.0", - "symfony/var-dumper": "^7.4|^8.0" + "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": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Doctrine\\Migrations\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -754,82 +922,1484 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" } ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.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": [ - "cli", - "command-line", - "console", - "terminal" + "database", + "dbal", + "migrations" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.1.0" + "issues": "https://github.com/doctrine/migrations/issues", + "source": "https://github.com/doctrine/migrations/tree/3.9.7" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://www.doctrine-project.org/sponsorship.html", "type": "custom" }, { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fmigrations", "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-04-23T19:33:20+00:00" }, { - "name": "symfony/dependency-injection", - "version": "v8.1.0", + "name": "doctrine/orm", + "version": "3.6.7", "source": { "type": "git", - "url": "https://github.com/symfony/dependency-injection.git", - "reference": "b6ba1f45127106885de4b77558c5ecca8feb1e1b" + "url": "https://github.com/doctrine/orm.git", + "reference": "bc217c0e19c3a9eadfa67697143b87c9ba01272c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/b6ba1f45127106885de4b77558c5ecca8feb1e1b", - "reference": "b6ba1f45127106885de4b77558c5ecca8feb1e1b", + "url": "https://api.github.com/repos/doctrine/orm/zipball/bc217c0e19c3a9eadfa67697143b87c9ba01272c", + "reference": "bc217c0e19c3a9eadfa67697143b87c9ba01272c", "shasum": "" }, "require": { - "php": ">=8.4.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/service-contracts": "^3.6", - "symfony/var-exporter": "^8.1" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "provide": { - "psr/container-implementation": "1.1|2.0", - "symfony/service-implementation": "1.1|2.0|3.0" + "composer-runtime-api": "^2", + "doctrine/collections": "^2.2", + "doctrine/dbal": "^3.8.2 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^3", + "doctrine/persistence": "^3.3.1 || ^4", + "ext-ctype": "*", + "php": "^8.1", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/var-exporter": "^6.3.9 || ^7.0 || ^8.0" }, "require-dev": { - "symfony/config": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/yaml": "^7.4|^8.0" + "doctrine/coding-standard": "^14.0", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "2.1.23", + "phpstan/phpstan-deprecation-rules": "^2", + "phpunit/phpunit": "^10.5.0 || ^11.5", + "psr/log": "^1 || ^2 || ^3", + "symfony/cache": "^5.4 || ^6.2 || ^7.0 || ^8.0" }, - "type": "library", + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/3.6.7" + }, + "time": "2026-05-25T16:45:47+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": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/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/asset", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "4bd4d143b7e53f40d45877df52eb2b18282bdac4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/4bd4d143b7e53f40d45877df52eb2b18282bdac4", + "reference": "4bd4d143b7e53f40d45877df52eb2b18282bdac4", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/http-client": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset/tree/v8.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-05-29T05:06:50+00:00" + }, + { + "name": "symfony/asset-mapper", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset-mapper.git", + "reference": "74b1b7b7019c728cb1f8672b502260e683b6374e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset-mapper/zipball/74b1b7b7019c728cb1f8672b502260e683b6374e", + "reference": "74b1b7b7019c728cb1f8672b502260e683b6374e", + "shasum": "" + }, + "require": { + "composer/semver": "^3.0", + "php": ">=8.4.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0" + }, + "require-dev": { + "symfony/asset": "^7.4|^8.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/event-dispatcher-contracts": "^3.0", + "symfony/finder": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/runtime": "^7.4|^8.0", + "symfony/web-link": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\AssetMapper\\": "" + }, + "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 directories of assets & makes them available in a public directory with versioned filenames.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset-mapper/tree/v8.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-05-29T05:06:50+00:00" + }, + { + "name": "symfony/cache", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "ba62e0ed9ea9bc26142844a891d4a3dfceb24aed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/ba62e0ed9ea9bc26142844a891d4a3dfceb24aed", + "reference": "ba62e0ed9ea9bc26142844a891d4a3dfceb24aed", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^8.1" + }, + "conflict": { + "ext-redis": "<6.1", + "ext-relay": "<0.12.1" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v8.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-05-29T05:06:50+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "225e8a254166bd3442e370c6f50145465db63831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/225e8a254166bd3442e370c6f50145465db63831", + "reference": "225e8a254166bd3442e370c6f50145465db63831", + "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.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-05T15:33:14+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v8.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-05-29T05:06:50+00:00" + }, + { + "name": "symfony/config", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "429783a0c649696f2058ea5ab5315f082dba6de9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/429783a0c649696f2058ea5ab5315f082dba6de9", + "reference": "429783a0c649696f2058ea5ab5315f082dba6de9", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v8.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-05-29T05:06:50+00:00" + }, + { + "name": "symfony/console", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/f5a856c6ecb56b3c21ed94a5b7bf940d857d110a", + "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.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-05-29T05:06:50+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "b6ba1f45127106885de4b77558c5ecca8feb1e1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/b6ba1f45127106885de4b77558c5ecca8feb1e1b", + "reference": "b6ba1f45127106885de4b77558c5ecca8feb1e1b", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^8.1" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v8.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-05-29T05:06:50+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "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.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-04-13T15:52:40+00:00" + }, + { + "name": "symfony/doctrine-bridge", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "80daf848dd39d9ff5a0f39aa6f2bf5448aa662c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/80daf848dd39d9ff5a0f39aa6f2bf5448aa662c5", + "reference": "80daf848dd39d9ff5a0f39aa6f2bf5448aa662c5", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^2", + "doctrine/persistence": "^3.1|^4", + "php": ">=8.4.1", + "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": "<4.3", + "doctrine/lexer": "<1.1", + "doctrine/orm": "<3.4", + "symfony/property-info": "<8.0" + }, + "require-dev": { + "doctrine/collections": "^1.8|^2.0", + "doctrine/data-fixtures": "^1.1|^2", + "doctrine/dbal": "^4.3", + "doctrine/orm": "^3.4", + "psr/log": "^1|^2|^3", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^8.1", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/doctrine-messenger": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^8.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/type-info": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Doctrine with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-bridge/tree/v8.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-05-29T05:18:49+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "7ed4e3a11e3c98235c70ded047d7ddf9e6ae854c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/7ed4e3a11e3c98235c70ded047d7ddf9e6ae854c", + "reference": "7ed4e3a11e3c98235c70ded047d7ddf9e6ae854c", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v8.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-05-29T05:06:50+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\DependencyInjection\\": "" + "Symfony\\Component\\ErrorHandler\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -849,10 +2419,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v8.1.0" + "source": "https://github.com/symfony/error-handler/tree/v8.1.0" }, "funding": [ { @@ -875,21 +2445,108 @@ "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/deprecation-contracts", + "name": "symfony/event-dispatcher", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.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-05-29T05:06:50+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", "version": "v3.7.0", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.1", + "psr/event-dispatcher": "^1" }, "type": "library", "extra": { @@ -902,9 +2559,9 @@ } }, "autoload": { - "files": [ - "function.php" - ] + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -920,10 +2577,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "A generic function and convention to trigger deprecation notices", + "description": "Generic abstractions related to dispatching event", "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" }, "funding": [ { @@ -943,33 +2608,35 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { - "name": "symfony/dotenv", + "name": "symfony/filesystem", "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/dotenv.git", - "reference": "7ed4e3a11e3c98235c70ded047d7ddf9e6ae854c" + "url": "https://github.com/symfony/filesystem.git", + "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/7ed4e3a11e3c98235c70ded047d7ddf9e6ae854c", - "reference": "7ed4e3a11e3c98235c70ded047d7ddf9e6ae854c", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/99aec13b82b4967ec5088222c4a3ecca955949c2", + "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2", "shasum": "" }, "require": { - "php": ">=8.4.1" + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/console": "^7.4|^8.0", "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Dotenv\\": "" + "Symfony\\Component\\Filesystem\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -989,15 +2656,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Registers environment variables from a .env file", + "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "keywords": [ - "dotenv", - "env", - "environment" - ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v8.1.0" + "source": "https://github.com/symfony/filesystem/tree/v8.1.0" }, "funding": [ { @@ -1020,46 +2682,110 @@ "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/error-handler", + "name": "symfony/finder", "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/error-handler.git", - "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5" + "url": "https://github.com/symfony/finder.git", + "reference": "58d2e767a66052c1487356f953445634a8194c64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/d8aeb1abd3fef84795567850d3a567bdb5945ee5", - "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "url": "https://api.github.com/repos/symfony/finder/zipball/58d2e767a66052c1487356f953445634a8194c64", + "reference": "58d2e767a66052c1487356f953445634a8194c64", "shasum": "" }, "require": { - "php": ">=8.4.1", - "psr/log": "^1|^2|^3", - "symfony/polyfill-php85": "^1.32", - "symfony/var-dumper": "^7.4|^8.0" + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.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-05-29T05:06:50+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": { - "symfony/deprecation-contracts": "<2.5" + "composer/semver": "<1.7.2", + "symfony/dotenv": "<5.4" }, "require-dev": { - "symfony/console": "^7.4|^8.0", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/serializer": "^7.4|^8.0", - "symfony/webpack-encore-bundle": "^1.0|^2.0" + "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" }, - "bin": [ - "Resources/bin/patch-type-declarations" - ], - "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\ErrorHandler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Flex\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1068,17 +2794,13 @@ "authors": [ { "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "email": "fabien.potencier@gmail.com" } ], - "description": "Provides tools to manage errors and ease debugging PHP code", - "homepage": "https://symfony.com", + "description": "Composer plugin for Symfony", "support": { - "source": "https://github.com/symfony/error-handler/tree/v8.1.0" + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.11.0" }, "funding": [ { @@ -1098,50 +2820,107 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-05-29T17:25:22+00:00" }, { - "name": "symfony/event-dispatcher", + "name": "symfony/framework-bundle", "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102" + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "6a0953f4fd8b51db6136c2628af99b7193e63256" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f249ae3f680958b6f1f9dd76e5747cf0695b4102", - "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6a0953f4fd8b51db6136c2628af99b7193e63256", + "reference": "6a0953f4fd8b51db6136c2628af99b7193e63256", "shasum": "" }, "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", "php": ">=8.4.1", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4.4|^8.0.4", + "symfony/dependency-injection": "^8.1", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/event-dispatcher-contracts": "^2.5|^3" + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^8.1", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.33", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^3.7", + "symfony/var-exporter": "^8.1" }, "conflict": { - "symfony/security-http": "<7.4", - "symfony/service-contracts": "<2.5" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0|3.0" + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/console": "<8.1", + "symfony/form": "<7.4", + "symfony/json-streamer": "<7.4", + "symfony/messenger": "<7.4.10|>=8.0,<8.0.10", + "symfony/mime": "<7.4.9|>=8.0,<8.0.9", + "symfony/security-csrf": "<7.4", + "symfony/serializer": "<7.4", + "symfony/translation": "<7.4", + "symfony/webhook": "<7.4", + "symfony/workflow": "<7.4" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/error-handler": "^7.4|^8.0", + "doctrine/persistence": "^1.3|^2|^3", + "dragonmantank/cron-expression": "^3.1", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/asset": "^7.4|^8.0", + "symfony/asset-mapper": "^7.4|^8.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/console": "^8.1", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/dotenv": "^7.4|^8.0", "symfony/expression-language": "^7.4|^8.0", - "symfony/framework-bundle": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^7.4|^8.0" + "symfony/form": "^7.4|^8.0", + "symfony/html-sanitizer": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/json-streamer": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/mailer": "^7.4|^8.0", + "symfony/messenger": "^7.4.10|^8.0.10", + "symfony/mime": "^7.4.9|^8.0.9", + "symfony/notifier": "^7.4|^8.0", + "symfony/object-mapper": "^7.4.9|^8.0.9", + "symfony/polyfill-intl-icu": "^1.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/runtime": "^7.4|^8.0", + "symfony/scheduler": "^7.4|^8.0", + "symfony/security-bundle": "^7.4|^8.0", + "symfony/semaphore": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/string": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/twig-bundle": "^7.4|^8.0", + "symfony/type-info": "^7.4.1|^8.0.1", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^7.4|^8.0", + "symfony/webhook": "^7.4|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" }, - "type": "library", + "type": "symfony-bundle", "autoload": { "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" + "Symfony\\Bundle\\FrameworkBundle\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -1161,10 +2940,10 @@ "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", + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.0" + "source": "https://github.com/symfony/framework-bundle/tree/v8.1.0" }, "funding": [ { @@ -1187,37 +2966,59 @@ "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.0", + "name": "symfony/http-client", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + "url": "https://github.com/symfony/http-client.git", + "reference": "68a48e4c31f63fcd1bdff997a85a09e55efe8cdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "url": "https://api.github.com/repos/symfony/http-client/zipball/68a48e4c31f63fcd1bdff997a85a09e55efe8cdb", + "reference": "68a48e4c31f63fcd1bdff997a85a09e55efe8cdb", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/event-dispatcher": "^1" + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/http-client-contracts": "^3.7", + "symfony/service-contracts": "^2.5|^3" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-dev" - } + "conflict": { + "amphp/amp": "<3", + "php-http/discovery": "<1.15" + }, + "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": "^5.3.2", + "amphp/http-tunnel": "^2.0", + "guzzlehttp/guzzle": "^7.10", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1233,18 +3034,13 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to dispatching event", + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "http" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/http-client/tree/v8.1.0" }, "funding": [ { @@ -1264,38 +3060,41 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/filesystem", - "version": "v8.1.0", + "name": "symfony/http-client-contracts", + "version": "v3.7.0", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2" + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/99aec13b82b4967ec5088222c4a3ecca955949c2", - "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", + "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", "shasum": "" }, "require": { - "php": ">=8.4.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" - }, - "require-dev": { - "symfony/process": "^7.4|^8.0" + "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\\Component\\Filesystem\\": "" + "Symfony\\Contracts\\HttpClient\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Test/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1304,18 +3103,26 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides basic utilities for the filesystem", + "description": "Generic abstractions related to HTTP clients", "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.1.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.0" }, "funding": [ { @@ -1335,32 +3142,45 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-03-06T13:17:50+00:00" }, { - "name": "symfony/finder", + "name": "symfony/http-foundation", "version": "v8.1.0", "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "58d2e767a66052c1487356f953445634a8194c64" + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/58d2e767a66052c1487356f953445634a8194c64", - "reference": "58d2e767a66052c1487356f953445634a8194c64", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/af11474600f06718086c2cda4fa6fa8d0a672e7e", + "reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e", "shasum": "" }, "require": { - "php": ">=8.4.1" + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" }, "require-dev": { - "symfony/filesystem": "^7.4|^8.0" + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Finder\\": "" + "Symfony\\Component\\HttpFoundation\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -1380,10 +3200,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Finds files and directories via an intuitive fluent interface", + "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.1.0" + "source": "https://github.com/symfony/http-foundation/tree/v8.1.0" }, "funding": [ { @@ -1406,42 +3226,74 @@ "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/flex", - "version": "v2.11.0", + "name": "symfony/http-kernel", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/flex.git", - "reference": "4a6d98eea3ebc7f68d82810cb682eedca2649e99" + "url": "https://github.com/symfony/http-kernel.git", + "reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/4a6d98eea3ebc7f68d82810cb682eedca2649e99", - "reference": "4a6d98eea3ebc7f68d82810cb682eedca2649e99", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cefeb37c82eed3e0c42fa25ba64cd3a908d90f39", + "reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39", "shasum": "" }, "require": { - "composer-plugin-api": "^2.1", - "php": ">=8.1" + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "composer/semver": "<1.7.2", - "symfony/dotenv": "<5.4" + "symfony/dependency-injection": "<8.1", + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/translation-contracts": "<2.5", + "symfony/var-dumper": "<8.1", + "symfony/web-profiler-bundle": "<8.1", + "twig/twig": "<3.21" }, - "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" + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" }, - "type": "composer-plugin", - "extra": { - "class": "Symfony\\Flex\\Flex" + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^8.1", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Flex\\": "src" - } + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1450,13 +3302,17 @@ "authors": [ { "name": "Fabien Potencier", - "email": "fabien.potencier@gmail.com" + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Composer plugin for Symfony", + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.11.0" + "source": "https://github.com/symfony/http-kernel/tree/v8.1.0" }, "funding": [ { @@ -1476,107 +3332,33 @@ "type": "tidelift" } ], - "time": "2026-05-29T17:25:22+00:00" + "time": "2026-05-29T08:46:08+00:00" }, { - "name": "symfony/framework-bundle", + "name": "symfony/password-hasher", "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/framework-bundle.git", - "reference": "6a0953f4fd8b51db6136c2628af99b7193e63256" + "url": "https://github.com/symfony/password-hasher.git", + "reference": "6934d16beaa4677f2c4584229fff1b51099dd7af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6a0953f4fd8b51db6136c2628af99b7193e63256", - "reference": "6a0953f4fd8b51db6136c2628af99b7193e63256", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/6934d16beaa4677f2c4584229fff1b51099dd7af", + "reference": "6934d16beaa4677f2c4584229fff1b51099dd7af", "shasum": "" }, "require": { - "composer-runtime-api": ">=2.1", - "ext-xml": "*", - "php": ">=8.4.1", - "symfony/cache": "^7.4|^8.0", - "symfony/config": "^7.4.4|^8.0.4", - "symfony/dependency-injection": "^8.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^7.4|^8.0", - "symfony/event-dispatcher": "^8.1", - "symfony/filesystem": "^7.4|^8.0", - "symfony/finder": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/http-kernel": "^8.1", - "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php85": "^1.33", - "symfony/routing": "^7.4|^8.0", - "symfony/service-contracts": "^3.7", - "symfony/var-exporter": "^8.1" - }, - "conflict": { - "doctrine/persistence": "<1.3", - "phpdocumentor/reflection-docblock": "<5.2|>=7", - "phpdocumentor/type-resolver": "<1.5.1", - "symfony/console": "<8.1", - "symfony/form": "<7.4", - "symfony/json-streamer": "<7.4", - "symfony/messenger": "<7.4.10|>=8.0,<8.0.10", - "symfony/mime": "<7.4.9|>=8.0,<8.0.9", - "symfony/security-csrf": "<7.4", - "symfony/serializer": "<7.4", - "symfony/translation": "<7.4", - "symfony/webhook": "<7.4", - "symfony/workflow": "<7.4" + "php": ">=8.4.1" }, "require-dev": { - "doctrine/persistence": "^1.3|^2|^3", - "dragonmantank/cron-expression": "^3.1", - "phpdocumentor/reflection-docblock": "^5.2|^6.0", - "phpstan/phpdoc-parser": "^1.0|^2.0", - "seld/jsonlint": "^1.10", - "symfony/asset": "^7.4|^8.0", - "symfony/asset-mapper": "^7.4|^8.0", - "symfony/browser-kit": "^7.4|^8.0", - "symfony/clock": "^7.4|^8.0", - "symfony/console": "^8.1", - "symfony/css-selector": "^7.4|^8.0", - "symfony/dom-crawler": "^7.4|^8.0", - "symfony/dotenv": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/form": "^7.4|^8.0", - "symfony/html-sanitizer": "^7.4|^8.0", - "symfony/http-client": "^7.4|^8.0", - "symfony/json-streamer": "^7.4|^8.0", - "symfony/lock": "^7.4|^8.0", - "symfony/mailer": "^7.4|^8.0", - "symfony/messenger": "^7.4.10|^8.0.10", - "symfony/mime": "^7.4.9|^8.0.9", - "symfony/notifier": "^7.4|^8.0", - "symfony/object-mapper": "^7.4.9|^8.0.9", - "symfony/polyfill-intl-icu": "^1.0", - "symfony/process": "^7.4|^8.0", - "symfony/property-info": "^7.4|^8.0", - "symfony/rate-limiter": "^7.4|^8.0", - "symfony/runtime": "^7.4|^8.0", - "symfony/scheduler": "^7.4|^8.0", - "symfony/security-bundle": "^7.4|^8.0", - "symfony/semaphore": "^7.4|^8.0", - "symfony/serializer": "^7.4|^8.0", - "symfony/stopwatch": "^7.4|^8.0", - "symfony/string": "^7.4|^8.0", - "symfony/translation": "^7.4|^8.0", - "symfony/twig-bundle": "^7.4|^8.0", - "symfony/type-info": "^7.4.1|^8.0.1", - "symfony/uid": "^7.4|^8.0", - "symfony/validator": "^7.4|^8.0", - "symfony/web-link": "^7.4|^8.0", - "symfony/webhook": "^7.4|^8.0", - "symfony/workflow": "^7.4|^8.0", - "symfony/yaml": "^7.4|^8.0" + "symfony/console": "^7.4|^8.0", + "symfony/security-core": "^7.4|^8.0" }, - "type": "symfony-bundle", + "type": "library", "autoload": { "psr-4": { - "Symfony\\Bundle\\FrameworkBundle\\": "" + "Symfony\\Component\\PasswordHasher\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -1588,18 +3370,22 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "description": "Provides password hashing utilities", "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v8.1.0" + "source": "https://github.com/symfony/password-hasher/tree/v8.1.0" }, "funding": [ { @@ -1622,58 +3408,44 @@ "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/http-client", - "version": "v8.1.0", + "name": "symfony/polyfill-deepclone", + "version": "v1.39.0", "source": { "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "68a48e4c31f63fcd1bdff997a85a09e55efe8cdb" + "url": "https://github.com/symfony/polyfill-deepclone.git", + "reference": "1b034bc050d84cc9c187de373f744912e1e35f1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/68a48e4c31f63fcd1bdff997a85a09e55efe8cdb", - "reference": "68a48e4c31f63fcd1bdff997a85a09e55efe8cdb", - "shasum": "" - }, - "require": { - "php": ">=8.4.1", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/http-client-contracts": "^3.7", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "amphp/amp": "<3", - "php-http/discovery": "<1.15" - }, - "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": "^5.3.2", - "amphp/http-tunnel": "^2.0", - "guzzlehttp/guzzle": "^7.10", - "nyholm/psr7": "^1.0", - "php-http/httplug": "^1.0|^2.0", - "psr/http-client": "^1.0", - "symfony/cache": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/messenger": "^7.4|^8.0", - "symfony/process": "^7.4|^8.0", - "symfony/rate-limiter": "^7.4|^8.0", - "symfony/stopwatch": "^7.4|^8.0" + "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/1b034bc050d84cc9c187de373f744912e1e35f1f", + "reference": "1b034bc050d84cc9c187de373f744912e1e35f1f", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "provide": { + "ext-deepclone": "*" + }, + "suggest": { + "ext-deepclone": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\HttpClient\\": "" + "Symfony\\Polyfill\\DeepClone\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1690,13 +3462,17 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "description": "Symfony polyfill for the deepclone extension", "homepage": "https://symfony.com", "keywords": [ - "http" + "compatibility", + "deepclone", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v8.1.0" + "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.39.0" }, "funding": [ { @@ -1716,42 +3492,42 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-06-10T20:07:50+00:00" }, { - "name": "symfony/http-client-contracts", - "version": "v3.7.0", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.38.1", "source": { "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", - "reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-dev" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1767,18 +3543,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to HTTP clients", + "description": "Symfony polyfill for intl's grapheme_* functions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -1798,48 +3574,44 @@ "type": "tidelift" } ], - "time": "2026-03-06T13:17:50+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { - "name": "symfony/http-foundation", - "version": "v8.1.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/af11474600f06718086c2cda4fa6fa8d0a672e7e", - "reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { - "php": ">=8.4.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "^1.1" - }, - "conflict": { - "doctrine/dbal": "<4.3" + "php": ">=7.2" }, - "require-dev": { - "doctrine/dbal": "^4.3", - "predis/predis": "^1.1|^2.0", - "symfony/cache": "^7.4|^8.0", - "symfony/clock": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/mime": "^7.4|^8.0", - "symfony/rate-limiter": "^7.4|^8.0" + "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\\Component\\HttpFoundation\\": "" + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1848,18 +3620,26 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Defines an object-oriented layer for the HTTP specification", + "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/http-foundation/tree/v8.1.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -1879,77 +3659,46 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { - "name": "symfony/http-kernel", - "version": "v8.1.0", + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", "source": { "type": "git", - "url": "https://github.com/symfony/http-kernel.git", - "reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cefeb37c82eed3e0c42fa25ba64cd3a908d90f39", - "reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { - "php": ">=8.4.1", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^7.4|^8.0", - "symfony/event-dispatcher": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/dependency-injection": "<8.1", - "symfony/flex": "<2.10", - "symfony/http-client-contracts": "<2.5", - "symfony/translation-contracts": "<2.5", - "symfony/var-dumper": "<8.1", - "symfony/web-profiler-bundle": "<8.1", - "twig/twig": "<3.21" + "ext-iconv": "*", + "php": ">=7.2" }, "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "ext-mbstring": "*" }, - "require-dev": { - "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^7.4|^8.0", - "symfony/clock": "^7.4|^8.0", - "symfony/config": "^7.4|^8.0", - "symfony/console": "^7.4|^8.0", - "symfony/css-selector": "^7.4|^8.0", - "symfony/dependency-injection": "^8.1", - "symfony/dom-crawler": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/finder": "^7.4|^8.0", - "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^7.4|^8.0", - "symfony/property-access": "^7.4|^8.0", - "symfony/rate-limiter": "^7.4|^8.0", - "symfony/routing": "^7.4|^8.0", - "symfony/serializer": "^7.4|^8.0", - "symfony/stopwatch": "^7.4|^8.0", - "symfony/translation": "^7.4|^8.0", - "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^7.4|^8.0", - "symfony/validator": "^7.4|^8.0", - "symfony/var-dumper": "^8.1", - "symfony/var-exporter": "^7.4|^8.0", - "twig/twig": "^3.21" + "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\\Component\\HttpKernel\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1957,18 +3706,25 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides a structured process for converting a Request into a Response", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], "support": { - "source": "https://github.com/symfony/http-kernel/tree/v8.1.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -1988,30 +3744,24 @@ "type": "tidelift" } ], - "time": "2026-05-29T08:46:08+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { - "name": "symfony/polyfill-deepclone", - "version": "v1.39.0", + "name": "symfony/polyfill-php85", + "version": "v1.38.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-deepclone.git", - "reference": "1b034bc050d84cc9c187de373f744912e1e35f1f" + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-deepclone/zipball/1b034bc050d84cc9c187de373f744912e1e35f1f", - "reference": "1b034bc050d84cc9c187de373f744912e1e35f1f", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", "shasum": "" }, "require": { - "php": ">=8.1" - }, - "provide": { - "ext-deepclone": "*" - }, - "suggest": { - "ext-deepclone": "For best performance" + "php": ">=7.2" }, "type": "library", "extra": { @@ -2025,7 +3775,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\DeepClone\\": "" + "Symfony\\Polyfill\\Php85\\": "" }, "classmap": [ "Resources/stubs" @@ -2045,17 +3795,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the deepclone extension", + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "deepclone", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-deepclone/tree/v1.39.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" }, "funding": [ { @@ -2075,42 +3824,33 @@ "type": "tidelift" } ], - "time": "2026-06-10T20:07:50+00:00" + "time": "2026-05-26T02:25:22+00:00" }, { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.38.1", + "name": "symfony/process", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + "url": "https://github.com/symfony/process.git", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "url": "https://api.github.com/repos/symfony/process/zipball/c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", "shasum": "" }, "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" + "php": ">=8.4.1" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2118,26 +3858,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's grapheme_* functions", + "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + "source": "https://github.com/symfony/process/tree/v8.1.0" }, "funding": [ { @@ -2157,44 +3889,37 @@ "type": "tidelift" } ], - "time": "2026-05-26T05:58:03+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.38.0", + "name": "symfony/property-access", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + "url": "https://github.com/symfony/property-access.git", + "reference": "9261ef060f26cc7b728f67f141ba19b98a6209a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "url": "https://api.github.com/repos/symfony/property-access/zipball/9261ef060f26cc7b728f67f141ba19b98a6209a9", + "reference": "9261ef060f26cc7b728f67f141ba19b98a6209a9", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.4.1", + "symfony/property-info": "^7.4.4|^8.0.4" }, - "suggest": { - "ext-intl": "For best performance" + "require-dev": { + "symfony/cache": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + "Symfony\\Component\\PropertyAccess\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2203,26 +3928,29 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", + "description": "Provides functions to read and write from/to an object or array using a simple string notation", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + "source": "https://github.com/symfony/property-access/tree/v8.1.0" }, "funding": [ { @@ -2242,46 +3970,46 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:48:31+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.38.2", + "name": "symfony/property-info", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + "url": "https://github.com/symfony/property-info.git", + "reference": "4721e8c56d0cd2378e0ef9a9899f810008b859f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", - "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "url": "https://api.github.com/repos/symfony/property-info/zipball/4721e8c56d0cd2378e0ef9a9899f810008b859f7", + "reference": "4721e8c56d0cd2378e0ef9a9899f810008b859f7", "shasum": "" }, "require": { - "ext-iconv": "*", - "php": ">=7.2" + "php": ">=8.4.1", + "symfony/string": "^7.4|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" }, - "provide": { - "ext-mbstring": "*" + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1" }, - "suggest": { - "ext-mbstring": "For best performance" + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2289,25 +4017,26 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Extracts information about PHP class' properties using metadata of popular sources", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + "source": "https://github.com/symfony/property-info/tree/v8.1.0" }, "funding": [ { @@ -2327,41 +4056,41 @@ "type": "tidelift" } ], - "time": "2026-05-27T06:59:30+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/polyfill-php85", - "version": "v1.38.1", + "name": "symfony/routing", + "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + "url": "https://github.com/symfony/routing.git", + "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "url": "https://api.github.com/repos/symfony/routing/zipball/fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" }, + "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php85\\": "" + "Symfony\\Component\\Routing\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2370,24 +4099,24 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "description": "Maps an HTTP request to a set of configuration variables", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "router", + "routing", + "uri", + "url" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + "source": "https://github.com/symfony/routing/tree/v8.1.0" }, "funding": [ { @@ -2407,29 +4136,45 @@ "type": "tidelift" } ], - "time": "2026-05-26T02:25:22+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/process", + "name": "symfony/runtime", "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5" + "url": "https://github.com/symfony/runtime.git", + "reference": "b7ea1abe04561e814b3134db0f56c287cedb35cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", - "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "url": "https://api.github.com/repos/symfony/runtime/zipball/b7ea1abe04561e814b3134db0f56c287cedb35cc", + "reference": "b7ea1abe04561e814b3134db0f56c287cedb35cc", "shasum": "" }, "require": { + "composer-plugin-api": "^1.0|^2.0", "php": ">=8.4.1" }, - "type": "library", + "conflict": { + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "composer/composer": "^2.6", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dotenv": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + }, "autoload": { "psr-4": { - "Symfony\\Component\\Process\\": "" + "Symfony\\Component\\Runtime\\": "", + "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" }, "exclude-from-classmap": [ "/Tests/" @@ -2441,18 +4186,21 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Executes commands in sub-processes", + "description": "Enables decoupling PHP applications from global state", "homepage": "https://symfony.com", + "keywords": [ + "runtime" + ], "support": { - "source": "https://github.com/symfony/process/tree/v8.1.0" + "source": "https://github.com/symfony/runtime/tree/v8.1.0" }, "funding": [ { @@ -2475,31 +4223,62 @@ "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/property-access", + "name": "symfony/security-bundle", "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/property-access.git", - "reference": "9261ef060f26cc7b728f67f141ba19b98a6209a9" + "url": "https://github.com/symfony/security-bundle.git", + "reference": "0489a6247f729652db9b9ff408f69ac3bee3589e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/9261ef060f26cc7b728f67f141ba19b98a6209a9", - "reference": "9261ef060f26cc7b728f67f141ba19b98a6209a9", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/0489a6247f729652db9b9ff408f69ac3bee3589e", + "reference": "0489a6247f729652db9b9ff408f69ac3bee3589e", "shasum": "" }, "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", "php": ">=8.4.1", - "symfony/property-info": "^7.4.4|^8.0.4" + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/password-hasher": "^7.4|^8.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/security-csrf": "^7.4|^8.0", + "symfony/security-http": "^8.1", + "symfony/service-contracts": "^2.5|^3" }, "require-dev": { - "symfony/cache": "^7.4|^8.0", - "symfony/var-exporter": "^7.4|^8.0" + "symfony/asset": "^7.4|^8.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/ldap": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/runtime": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0", + "symfony/twig-bundle": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0", + "web-token/jwt-library": "^3.3.2|^4.0" }, - "type": "library", + "type": "symfony-bundle", "autoload": { "psr-4": { - "Symfony\\Component\\PropertyAccess\\": "" + "Symfony\\Bundle\\SecurityBundle\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2519,21 +4298,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", - "keywords": [ - "access", - "array", - "extraction", - "index", - "injection", - "object", - "property", - "property-path", - "reflection" - ], "support": { - "source": "https://github.com/symfony/property-access/tree/v8.1.0" + "source": "https://github.com/symfony/security-bundle/tree/v8.1.0" }, "funding": [ { @@ -2556,39 +4324,44 @@ "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/property-info", + "name": "symfony/security-core", "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/property-info.git", - "reference": "4721e8c56d0cd2378e0ef9a9899f810008b859f7" + "url": "https://github.com/symfony/security-core.git", + "reference": "a8239abe61dafdd0c01c0b4019138b2855717f97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/4721e8c56d0cd2378e0ef9a9899f810008b859f7", - "reference": "4721e8c56d0cd2378e0ef9a9899f810008b859f7", + "url": "https://api.github.com/repos/symfony/security-core/zipball/a8239abe61dafdd0c01c0b4019138b2855717f97", + "reference": "a8239abe61dafdd0c01c0b4019138b2855717f97", "shasum": "" }, "require": { "php": ">=8.4.1", - "symfony/string": "^7.4|^8.0", - "symfony/type-info": "^7.4.7|^8.0.7" - }, - "conflict": { - "phpdocumentor/reflection-docblock": "<5.2|>=7", - "phpdocumentor/type-resolver": "<1.5.1" + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" }, "require-dev": { - "phpdocumentor/reflection-docblock": "^5.2|^6.0", - "phpstan/phpdoc-parser": "^1.0|^2.0", + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", "symfony/cache": "^7.4|^8.0", "symfony/dependency-injection": "^7.4|^8.0", - "symfony/serializer": "^7.4|^8.0" + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/ldap": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/string": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\PropertyInfo\\": "" + "Symfony\\Component\\Security\\Core\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2600,26 +4373,18 @@ ], "authors": [ { - "name": "Kévin Dunglas", - "email": "dunglas@gmail.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Extracts information about PHP class' properties using metadata of popular sources", + "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", - "keywords": [ - "doctrine", - "phpdoc", - "property", - "symfony", - "type", - "validator" - ], "support": { - "source": "https://github.com/symfony/property-info/tree/v8.1.0" + "source": "https://github.com/symfony/security-core/tree/v8.1.0" }, "funding": [ { @@ -2642,35 +4407,33 @@ "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/routing", + "name": "symfony/security-csrf", "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/routing.git", - "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3" + "url": "https://github.com/symfony/security-csrf.git", + "reference": "c865a8ee0d30b14545d7e5349b8e443f4fa9dc3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", - "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/c865a8ee0d30b14545d7e5349b8e443f4fa9dc3f", + "reference": "c865a8ee0d30b14545d7e5349b8e443f4fa9dc3f", "shasum": "" }, "require": { "php": ">=8.4.1", - "symfony/deprecation-contracts": "^2.5|^3" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/security-core": "^7.4|^8.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", "symfony/http-foundation": "^7.4|^8.0", - "symfony/yaml": "^7.4|^8.0" + "symfony/http-kernel": "^7.4|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Routing\\": "" + "Symfony\\Component\\Security\\Csrf\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2690,16 +4453,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Maps an HTTP request to a set of configuration variables", + "description": "Symfony Security Component - CSRF Library", "homepage": "https://symfony.com", - "keywords": [ - "router", - "routing", - "uri", - "url" - ], "support": { - "source": "https://github.com/symfony/routing/tree/v8.1.0" + "source": "https://github.com/symfony/security-csrf/tree/v8.1.0" }, "funding": [ { @@ -2722,42 +4479,49 @@ "time": "2026-05-29T05:06:50+00:00" }, { - "name": "symfony/runtime", + "name": "symfony/security-http", "version": "v8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/runtime.git", - "reference": "b7ea1abe04561e814b3134db0f56c287cedb35cc" + "url": "https://github.com/symfony/security-http.git", + "reference": "e0e6c7b9e80eec37248b92359cbd6938c7086f4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/b7ea1abe04561e814b3134db0f56c287cedb35cc", - "reference": "b7ea1abe04561e814b3134db0f56c287cedb35cc", + "url": "https://api.github.com/repos/symfony/security-http/zipball/e0e6c7b9e80eec37248b92359cbd6938c7086f4b", + "reference": "e0e6c7b9e80eec37248b92359cbd6938c7086f4b", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0|^2.0", - "php": ">=8.4.1" + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^8.1", + "symfony/polyfill-mbstring": "^1.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "symfony/error-handler": "<7.4" + "symfony/http-client-contracts": "<3.0" }, "require-dev": { - "composer/composer": "^2.6", - "symfony/console": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/dotenv": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" + "psr/log": "^1|^2|^3", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/security-csrf": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "web-token/jwt-library": "^3.3.2|^4.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Runtime\\": "", - "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" + "Symfony\\Component\\Security\\Http\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2769,21 +4533,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Enables decoupling PHP applications from global state", + "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", - "keywords": [ - "runtime" - ], "support": { - "source": "https://github.com/symfony/runtime/tree/v8.1.0" + "source": "https://github.com/symfony/security-http/tree/v8.1.0" }, "funding": [ { @@ -2918,35 +4679,101 @@ "conflict": { "symfony/asset-mapper": "<6.4" }, - "require-dev": { - "phpunit/phpunit": "^11.1|^12.0", - "symfony/asset-mapper": "^7.4|^8.0", - "symfony/framework-bundle": "^7.4|^8.0", - "symfony/twig-bundle": "^7.4|^8.0", - "zenstruck/browser": "^1.9" + "require-dev": { + "phpunit/phpunit": "^11.1|^12.0", + "symfony/asset-mapper": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/twig-bundle": "^7.4|^8.0", + "zenstruck/browser": "^1.9" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\UX\\StimulusBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration with your Symfony app & Stimulus!", + "keywords": [ + "symfony-ux" + ], + "support": { + "source": "https://github.com/symfony/stimulus-bundle/tree/v3.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-05-22T05:04:55+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "21c07b026905d596e8379caeb115d87aa479499d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/21c07b026905d596e8379caeb115d87aa479499d", + "reference": "21c07b026905d596e8379caeb115d87aa479499d", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/service-contracts": "^2.5|^3" }, - "type": "symfony-bundle", + "type": "library", "autoload": { "psr-4": { - "Symfony\\UX\\StimulusBundle\\": "src" - } + "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": "Integration with your Symfony app & Stimulus!", - "keywords": [ - "symfony-ux" - ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stimulus-bundle/tree/v3.1.0" + "source": "https://github.com/symfony/stopwatch/tree/v8.1.0" }, "funding": [ { @@ -2966,7 +4793,7 @@ "type": "tidelift" } ], - "time": "2026-05-22T05:04:55+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/string", @@ -4262,6 +6089,177 @@ ], "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", @@ -7727,6 +9725,105 @@ ], "time": "2026-05-29T05:06:50+00:00" }, + { + "name": "symfony/maker-bundle", + "version": "v1.67.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/6ce8b313845f16bcf385ee3cb31d8b24e30d5516", + "reference": "6ce8b313845f16bcf385ee3cb31d8b24e30d5516", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1", + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.10|^3.0", + "doctrine/orm": "^2.15|^3", + "doctrine/persistence": "^3.1|^4.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.67.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-03-18T13:39:06+00:00" + }, { "name": "symfony/options-resolver", "version": "v8.1.0", @@ -7883,72 +9980,6 @@ ], "time": "2026-05-29T05:06:50+00:00" }, - { - "name": "symfony/stopwatch", - "version": "v8.1.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "21c07b026905d596e8379caeb115d87aa479499d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/21c07b026905d596e8379caeb115d87aa479499d", - "reference": "21c07b026905d596e8379caeb115d87aa479499d", - "shasum": "" - }, - "require": { - "php": ">=8.4.1", - "symfony/service-contracts": "^2.5|^3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides a way to profile code", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/stopwatch/tree/v8.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-05-29T05:06:50+00:00" - }, { "name": "theseer/tokenizer", "version": "1.3.1", diff --git a/config/bundles.php b/config/bundles.php index 76e2980..744b525 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -7,4 +7,9 @@ Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle::class => ['all' => true], Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], ]; diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml new file mode 100644 index 0000000..c196bab --- /dev/null +++ b/config/packages/doctrine.yaml @@ -0,0 +1,39 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + profiling_collect_backtrace: '%kernel.debug%' + orm: + validate_xml_mapping: true + naming_strategy: doctrine.orm.naming_strategy.underscore + auto_mapping: true + mappings: + App: + type: attribute + is_bundle: false + 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: + 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/doctrine_migrations.yaml b/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000..29231d9 --- /dev/null +++ b/config/packages/doctrine_migrations.yaml @@ -0,0 +1,6 @@ +doctrine_migrations: + migrations_paths: + # namespace is arbitrary but should be different from App\Migrations + # as migrations classes should NOT be autoloaded + 'DoctrineMigrations': '%kernel.project_dir%/migrations' + enable_profiler: false diff --git a/config/packages/security.yaml b/config/packages/security.yaml new file mode 100644 index 0000000..bad06f3 --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,45 @@ +security: + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + + # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email + + firewalls: + dev: + # Ensure dev tools and static assets are always allowed + pattern: ^/(_profiler|_wdt|assets|build)/ + security: false + main: + lazy: true + provider: app_user_provider + form_login: + login_path: app_login + check_path: app_login + enable_csrf: true + default_target_path: app_frontpage + logout: + path: app_logout + target: app_frontpage + + # Note: Only the *first* matching rule is applied + access_control: + - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/logout, roles: PUBLIC_ACCESS } + +when@test: + security: + password_hashers: + # Password hashers are resource-intensive by design to ensure security. + # In tests, it's safe to reduce their cost to improve performance. + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon diff --git a/config/reference.php b/config/reference.php index f852f60..40bcddb 100644 --- a/config/reference.php +++ b/config/reference.php @@ -805,6 +805,528 @@ * collect_components?: bool|Param, // Collect components instances // Default: true * }, * } + * @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?: 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 SecurityConfig = array{ + * access_denied_url?: scalar|Param|null, // Default: null + * session_fixation_strategy?: "none"|"migrate"|"invalidate"|Param, // Default: "migrate" + * expose_security_errors?: \Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::None|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::AccountStatus|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::All|Param, // Default: "none" + * erase_credentials?: bool|Param, // Deprecated: Setting the "security.erase_credentials.erase_credentials" configuration option is deprecated. It will be removed in Symfony 9.0, as the "eraseCredentials()" method was removed in Symfony 8.0. // Default: true + * access_decision_manager?: array{ + * strategy?: "affirmative"|"consensus"|"unanimous"|"priority"|Param, + * service?: scalar|Param|null, + * strategy_service?: scalar|Param|null, + * allow_if_all_abstain?: bool|Param, // Default: false + * allow_if_equal_granted_denied?: bool|Param, // Default: true + * }, + * password_hashers?: array, + * hash_algorithm?: scalar|Param|null, // Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms. // Default: "sha512" + * key_length?: scalar|Param|null, // Default: 40 + * ignore_case?: bool|Param, // Default: false + * encode_as_base64?: bool|Param, // Default: true + * iterations?: scalar|Param|null, // Default: 5000 + * cost?: int|Param, // Default: null + * memory_cost?: scalar|Param|null, // Default: null + * time_cost?: scalar|Param|null, // Default: null + * id?: scalar|Param|null, + * }>, + * providers?: array, + * }, + * entity?: array{ + * class?: scalar|Param|null, // The full entity class name of your user class. + * property?: scalar|Param|null, // Default: null + * manager_name?: scalar|Param|null, // Default: null + * }, + * memory?: array{ + * users?: array, + * }>, + * }, + * ldap?: array{ + * service?: scalar|Param|null, + * base_dn?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: null + * search_password?: scalar|Param|null, // Default: null + * extra_fields?: list, + * default_roles?: string|list, + * role_fetcher?: scalar|Param|null, // Default: null + * uid_key?: scalar|Param|null, // Default: "sAMAccountName" + * filter?: scalar|Param|null, // Default: "({uid_key}={user_identifier})" + * password_attribute?: scalar|Param|null, // Default: null + * }, + * }>, + * firewalls?: array, + * security?: bool|Param, // Default: true + * user_checker?: scalar|Param|null, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" + * request_matcher?: scalar|Param|null, + * access_denied_url?: scalar|Param|null, + * access_denied_handler?: scalar|Param|null, + * entry_point?: scalar|Param|null, // An enabled authenticator name or a service id that implements "Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface". + * provider?: scalar|Param|null, + * stateless?: bool|Param, // Default: false + * lazy?: bool|Param, // Default: false + * context?: scalar|Param|null, + * logout?: array{ + * enable_csrf?: bool|Param|null, // Default: null + * csrf_token_id?: scalar|Param|null, // Default: "logout" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_manager?: scalar|Param|null, + * path?: scalar|Param|null, // Default: "/logout" + * target?: scalar|Param|null, // Default: "/" + * invalidate_session?: bool|Param, // Default: true + * clear_site_data?: string|list<"*"|"cache"|"cookies"|"storage"|"clientHints"|"executionContexts"|"prefetchCache"|"prerenderCache"|Param>, + * delete_cookies?: string|array, + * }, + * switch_user?: array{ + * provider?: scalar|Param|null, + * parameter?: scalar|Param|null, // Default: "_switch_user" + * role?: scalar|Param|null, // Default: "ROLE_ALLOWED_TO_SWITCH" + * target_route?: scalar|Param|null, // Default: null + * }, + * required_badges?: list, + * custom_authenticators?: list, + * login_throttling?: array{ + * limiter?: scalar|Param|null, // A service id implementing "Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface". + * max_attempts?: int|Param, // Default: 5 + * interval?: scalar|Param|null, // Default: "1 minute" + * lock_factory?: scalar|Param|null, // The service ID of the lock factory used by the login rate limiter (or null to disable locking). // Default: null + * cache_pool?: string|Param, // The cache pool to use for storing the limiter state // Default: "cache.rate_limiter" + * storage_service?: string|Param, // The service ID of a custom storage implementation, this precedes any configured "cache_pool" // Default: null + * }, + * x509?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN_Email" + * credentials?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN" + * user_identifier?: scalar|Param|null, // Default: "emailAddress" + * }, + * remote_user?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "REMOTE_USER" + * }, + * login_link?: array{ + * check_route?: scalar|Param|null, // Route that will validate the login link - e.g. "app_login_link_verify". + * check_post_only?: scalar|Param|null, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false + * signature_properties?: list, + * lifetime?: int|Param, // The lifetime of the login link in seconds. // Default: 600 + * max_uses?: int|Param, // Max number of times a login link can be used - null means unlimited within lifetime. // Default: null + * used_link_cache?: scalar|Param|null, // Cache service id used to expired links of max_uses is set. + * success_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface. + * failure_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. + * provider?: scalar|Param|null, // The user provider to load users from. + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * login_path?: scalar|Param|null, // Default: "/login" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * json_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * }, + * json_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * access_token?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: null + * token_extractors?: string|list, + * token_handler?: string|array{ + * id?: scalar|Param|null, + * oidc_user_info?: string|array{ + * base_uri?: scalar|Param|null, // Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require "discovery" to be configured). + * discovery?: array{ // Enable the OIDC discovery. + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub" + * client?: scalar|Param|null, // HttpClient service id to use to call the OIDC server. + * }, + * oidc?: array{ + * discovery?: array{ // Enable the OIDC discovery. + * base_uri?: string|list, + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * enforce_key_usage_verification?: bool|Param, // When enabled (default), only keys explicitly designated for signature (via "use":"sig" or a "key_ops" entry containing "sign"/"verify") are accepted. When disabled, keys without any usage designation are also accepted; keys explicitly restricted to encryption are still rejected. // Default: true + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub" + * audience?: scalar|Param|null, // Audience set in the token, for validation purpose. + * issuers?: list, + * algorithms?: list, + * keyset?: scalar|Param|null, // JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys). + * encryption?: bool|array{ + * enabled?: bool|Param, // Default: false + * enforce?: bool|Param, // When enabled, the token shall be encrypted. // Default: false + * algorithms?: list, + * keyset?: scalar|Param|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys). + * }, + * }, + * cas?: array{ + * validation_url?: scalar|Param|null, // CAS server validation URL + * prefix?: scalar|Param|null, // CAS prefix // Default: "cas" + * http_client?: scalar|Param|null, // HTTP Client service // Default: null + * }, + * oauth2?: scalar|Param|null, + * }, + * }, + * http_basic?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * }, + * http_basic_ldap?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * remember_me?: array{ + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * service?: scalar|Param|null, + * user_providers?: string|list, + * catch_exceptions?: bool|Param, // Default: true + * signature_properties?: list, + * token_provider?: string|array{ + * service?: scalar|Param|null, // The service ID of a custom remember-me token provider. + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: false + * connection?: scalar|Param|null, // Default: null + * }, + * }, + * token_verifier?: scalar|Param|null, // The service ID of a custom rememberme token verifier. + * name?: scalar|Param|null, // Default: "REMEMBERME" + * lifetime?: int|Param, // Default: 31536000 + * path?: scalar|Param|null, // Default: "/" + * domain?: scalar|Param|null, // Default: null + * secure?: true|false|"auto"|Param, // Default: false + * httponly?: bool|Param, // Default: true + * samesite?: null|"lax"|"strict"|"none"|Param, // Default: null + * always_remember_me?: bool|Param, // Default: false + * remember_me_parameter?: scalar|Param|null, // Default: "_remember_me" + * }, + * }>, + * access_control?: list, + * attributes?: array, + * route?: scalar|Param|null, // Default: null + * methods?: string|list, + * allow_if?: scalar|Param|null, // Default: null + * roles?: string|list, + * }>, + * role_hierarchy?: array>, + * } + * @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 ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -815,6 +1337,9 @@ * stimulus?: StimulusConfig, * symfonycasts_tailwind?: SymfonycastsTailwindConfig, * twig_component?: TwigComponentConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, * "when@dev"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -825,6 +1350,10 @@ * stimulus?: StimulusConfig, * symfonycasts_tailwind?: SymfonycastsTailwindConfig, * twig_component?: TwigComponentConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, + * maker?: MakerConfig, * }, * "when@prod"?: array{ * imports?: ImportsConfig, @@ -836,6 +1365,9 @@ * stimulus?: StimulusConfig, * symfonycasts_tailwind?: SymfonycastsTailwindConfig, * twig_component?: TwigComponentConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, @@ -847,6 +1379,9 @@ * stimulus?: StimulusConfig, * symfonycasts_tailwind?: SymfonycastsTailwindConfig, * twig_component?: TwigComponentConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * security?: SecurityConfig, * }, * ...addSql('CREATE TABLE `user` (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL (email), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE `user`'); + } +} diff --git a/phpunit.dist.xml b/phpunit.dist.xml index 07d5366..e244000 100644 --- a/phpunit.dist.xml +++ b/phpunit.dist.xml @@ -33,6 +33,8 @@ + Doctrine\Deprecations\Deprecation::trigger + Doctrine\Deprecations\Deprecation::delegateTriggerToBackend trigger_deprecation diff --git a/src/Command/UserChangePasswordCommand.php b/src/Command/UserChangePasswordCommand.php new file mode 100644 index 0000000..614e5d0 --- /dev/null +++ b/src/Command/UserChangePasswordCommand.php @@ -0,0 +1,75 @@ + `. + * Delegates to {@see UserManager::changePassword()}. + */ +#[AsCommand( + name: 'app:user:change-password', + description: 'Set a new hashed password for an existing user.', +)] +final class UserChangePasswordCommand extends Command +{ + /** + * @param UserManager $userManager service that owns password hashing and + * persistence + */ + public function __construct(private readonly UserManager $userManager) + { + parent::__construct(); + } + + /** + * Declare CLI arguments. + */ + protected function configure(): void + { + $this + ->addArgument('email', InputArgument::REQUIRED, 'The e-mail of the user whose password to change.') + ->addArgument('password', InputArgument::REQUIRED, 'The new password in clear-text — will be hashed.'); + } + + /** + * Adapt console arguments to the {@see UserManager} call and render + * a success / error message. + * + * @param InputInterface $input CLI arguments + * @param OutputInterface $output Symfony console output stream + * + * @return int Command::SUCCESS on a successful change, + * Command::FAILURE when the user is not found or the + * password is rejected + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $email = (string) $input->getArgument('email'); + $password = (string) $input->getArgument('password'); + + try { + $user = $this->userManager->changePassword($email, $password); + } catch (\DomainException|\InvalidArgumentException $e) { + $io->error($e->getMessage()); + + return Command::FAILURE; + } + + $io->success(\sprintf('Updated password for user "%s".', $user->getUserIdentifier())); + + return Command::SUCCESS; + } +} diff --git a/src/Command/UserCreateCommand.php b/src/Command/UserCreateCommand.php new file mode 100644 index 0000000..c0d7974 --- /dev/null +++ b/src/Command/UserCreateCommand.php @@ -0,0 +1,77 @@ + `. + * Delegates to {@see UserManager::createUser()} for the actual + * persistence and hashing — the command itself just adapts CLI input + * and renders the outcome. + */ +#[AsCommand( + name: 'app:user:create', + description: 'Create a new application user with a hashed password.', +)] +final class UserCreateCommand extends Command +{ + /** + * @param UserManager $userManager service that owns user creation, + * password hashing, and persistence + */ + public function __construct(private readonly UserManager $userManager) + { + parent::__construct(); + } + + /** + * Declare CLI arguments. + */ + protected function configure(): void + { + $this + ->addArgument('email', InputArgument::REQUIRED, 'The user\'s e-mail address (must be unique).') + ->addArgument('password', InputArgument::REQUIRED, 'The user\'s password in clear-text — will be hashed.'); + } + + /** + * Adapt console arguments to the {@see UserManager} call and render + * a success / error message. + * + * @param InputInterface $input CLI arguments + * @param OutputInterface $output Symfony console output stream + * + * @return int Command::SUCCESS on creation, Command::FAILURE when + * {@see UserManager::createUser()} throws a domain or + * validation error + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $email = (string) $input->getArgument('email'); + $password = (string) $input->getArgument('password'); + + try { + $user = $this->userManager->createUser($email, $password); + } catch (\DomainException|\InvalidArgumentException $e) { + $io->error($e->getMessage()); + + return Command::FAILURE; + } + + $io->success(\sprintf('Created user "%s" (id=%d).', $user->getUserIdentifier(), (int) $user->getId())); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..3addc34 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,68 @@ +render('security/login.html.twig', [ + 'last_username' => $authenticationUtils->getLastUsername(), + 'error' => $authenticationUtils->getLastAuthenticationError(), + ]); + } + + /** + * Placeholder action for the `app_logout` route. + * + * The route exists so URL generation (`{{ path('app_logout') }}`) + * works in templates, but Symfony Security intercepts the request + * and handles session invalidation before this method is called. + * The unreachable body throws so that any accidental direct call + * fails loudly. + * + * @throws \LogicException always — the firewall must intercept + */ + #[Route(path: '/logout', name: 'app_logout', methods: ['GET'])] + #[IsGranted('IS_AUTHENTICATED_FULLY')] + public function logout(): never + { + throw new \LogicException('This method is intercepted by the logout key on the firewall.'); + } +} diff --git a/src/DataFixtures/UserFixtures.php b/src/DataFixtures/UserFixtures.php new file mode 100644 index 0000000..d9e536a --- /dev/null +++ b/src/DataFixtures/UserFixtures.php @@ -0,0 +1,44 @@ +userManager->createUser('alice@example.test', 'password'); + $this->userManager->createUser('bob@example.test', 'password'); + } +} diff --git a/src/Entity/.gitignore b/src/Entity/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..8fee99a --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,109 @@ + The user roles + */ + #[ORM\Column] + private array $roles = []; + + /** + * @var string The hashed password + */ + #[ORM\Column] + private ?string $password = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + /** + * @param list $roles + */ + public function setRoles(array $roles): static + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + + return $this; + } + + /** + * Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3. + */ + public function __serialize(): array + { + $data = (array) $this; + $data["\0".self::class."\0password"] = hash('crc32c', $this->password); + + return $data; + } +} diff --git a/src/Repository/.gitignore b/src/Repository/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..2a44ea0 --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,35 @@ + + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); + } + + $user->setPassword($newHashedPassword); + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } +} diff --git a/src/Security/UserManager.php b/src/Security/UserManager.php new file mode 100644 index 0000000..074a6da --- /dev/null +++ b/src/Security/UserManager.php @@ -0,0 +1,113 @@ + $roles additional roles to grant; the + * framework guarantees `ROLE_USER` + * implicitly so callers should leave + * this empty for plain users + * + * @return User the persisted user with an assigned id + * + * @throws \DomainException when a user with the same e-mail + * already exists + * @throws \InvalidArgumentException when `$plainPassword` is empty + */ + public function createUser(string $email, string $plainPassword, array $roles = []): User + { + if ('' === $plainPassword) { + throw new \InvalidArgumentException('Password must not be empty.'); + } + + if (null !== $this->userRepository->findOneBy(['email' => $email])) { + throw new \DomainException(\sprintf('A user with the e-mail "%s" already exists.', $email)); + } + + $user = (new User()) + ->setEmail($email) + ->setRoles($roles); + $user->setPassword($this->passwordHasher->hashPassword($user, $plainPassword)); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + return $user; + } + + /** + * Replace a user's password with a freshly hashed copy. + * + * @param string $email the e-mail of the user whose password + * to change + * @param string $newPlainPassword the new password in clear-text; hashed + * before persistence + * + * @return User the updated user + * + * @throws \DomainException when no user with that e-mail exists + * @throws \InvalidArgumentException when `$newPlainPassword` is empty + */ + public function changePassword(string $email, string $newPlainPassword): User + { + if ('' === $newPlainPassword) { + throw new \InvalidArgumentException('Password must not be empty.'); + } + + $user = $this->userRepository->findOneBy(['email' => $email]); + if (null === $user) { + throw new \DomainException(\sprintf('No user with the e-mail "%s" was found.', $email)); + } + + $user->setPassword($this->passwordHasher->hashPassword($user, $newPlainPassword)); + $this->entityManager->flush(); + + return $user; + } +} diff --git a/symfony.lock b/symfony.lock index f765f9e..976c559 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,4 +1,52 @@ { + "doctrine/deprecations": { + "version": "1.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fdd756167454623e21f1d769c5b814b243782a67" + } + }, + "doctrine/doctrine-bundle": { + "version": "3.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "d39a3bd844edfe90c20ae520b804a3bf4f82b4ad" + }, + "files": [ + "config/packages/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-fixtures-bundle": { + "version": "4.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "1f5514cfa15b947298df4d771e694e578d4c204d" + }, + "files": [ + "src/DataFixtures/AppFixtures.php" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "4.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.1", + "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" + }, + "files": [ + "config/packages/doctrine_migrations.yaml", + "migrations/.gitignore" + ] + }, "friendsofphp/php-cs-fixer": { "version": "3.95", "recipe": { @@ -86,6 +134,15 @@ ".editorconfig" ] }, + "symfony/maker-bundle": { + "version": "1.67", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, "symfony/phpunit-bridge": { "version": "8.1", "recipe": { @@ -120,6 +177,19 @@ "config/routes.yaml" ] }, + "symfony/security-bundle": { + "version": "8.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.4", + "ref": "c42fee7802181cdd50f61b8622715829f5d2335c" + }, + "files": [ + "config/packages/security.yaml", + "config/routes/security.yaml" + ] + }, "symfony/stimulus-bundle": { "version": "3.1", "recipe": { diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..ee1ed48 --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,49 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'security.login.title'|trans({'%brand%': brand_name}) }}{% endblock %} + +{% block body %} +
+ {{ 'security.login.eyebrow'|trans }} +

+ {{ 'security.login.heading'|trans }} +

+ + {% if error %} + + {% endif %} + +
+ + + + + + + +
+
+{% endblock %} diff --git a/tests/Command/UserChangePasswordCommandTest.php b/tests/Command/UserChangePasswordCommandTest.php new file mode 100644 index 0000000..8bf1f3a --- /dev/null +++ b/tests/Command/UserChangePasswordCommandTest.php @@ -0,0 +1,57 @@ +get(EntityManagerInterface::class)); + + $this->userManager = $container->get(UserManager::class); + + $application = new Application(self::$kernel); + $command = $application->find('app:user:change-password'); + $this->tester = new CommandTester($command); + } + + public function testChangesPassword(): void + { + $this->userManager->createUser('alice@example.test', 'old'); + + $exit = $this->tester->execute([ + 'email' => 'alice@example.test', + 'password' => 'new', + ]); + + self::assertSame(0, $exit); + self::assertStringContainsString('Updated password for user "alice@example.test"', $this->tester->getDisplay()); + } + + public function testReportsFailureWhenUserMissing(): void + { + $exit = $this->tester->execute([ + 'email' => 'nobody@example.test', + 'password' => 'new', + ]); + + self::assertSame(1, $exit); + self::assertStringContainsString('No user with the e-mail "nobody@example.test"', $this->tester->getDisplay()); + } +} diff --git a/tests/Command/UserCreateCommandTest.php b/tests/Command/UserCreateCommandTest.php new file mode 100644 index 0000000..ea39ac5 --- /dev/null +++ b/tests/Command/UserCreateCommandTest.php @@ -0,0 +1,57 @@ +get(EntityManagerInterface::class)); + + $this->userManager = $container->get(UserManager::class); + + $application = new Application(self::$kernel); + $command = $application->find('app:user:create'); + $this->tester = new CommandTester($command); + } + + public function testCreatesUser(): void + { + $exit = $this->tester->execute([ + 'email' => 'alice@example.test', + 'password' => 'secret', + ]); + + self::assertSame(0, $exit); + self::assertStringContainsString('Created user "alice@example.test"', $this->tester->getDisplay()); + } + + public function testReportsFailureWhenEmailAlreadyExists(): void + { + $this->userManager->createUser('alice@example.test', 'first'); + + $exit = $this->tester->execute([ + 'email' => 'alice@example.test', + 'password' => 'second', + ]); + + self::assertSame(1, $exit); + self::assertStringContainsString('already exists', $this->tester->getDisplay()); + } +} diff --git a/tests/Controller/SecurityControllerTest.php b/tests/Controller/SecurityControllerTest.php new file mode 100644 index 0000000..841821f --- /dev/null +++ b/tests/Controller/SecurityControllerTest.php @@ -0,0 +1,110 @@ +client = self::createClient(); + $container = self::getContainer(); + self::resetSchema($container->get(EntityManagerInterface::class)); + + $container->get(UserManager::class) + ->createUser('alice@example.test', 'secret'); + } + + public function testLoginPageRenders(): void + { + $this->client->request('GET', '/login'); + + self::assertResponseIsSuccessful(); + self::assertSelectorExists('input[name="_username"]'); + self::assertSelectorExists('input[name="_password"]'); + self::assertSelectorExists('input[name="_csrf_token"]'); + } + + public function testSuccessfulLoginRedirectsToFrontpage(): void + { + $crawler = $this->client->request('GET', '/login'); + $form = $crawler->filter('form')->form(); + $form['_username'] = 'alice@example.test'; + $form['_password'] = 'secret'; + $this->client->submit($form); + + self::assertResponseRedirects('/'); + $this->client->followRedirect(); + self::assertResponseIsSuccessful(); + + /** @var User|null $token */ + $token = $this->client->getContainer()->get('security.token_storage')->getToken()?->getUser(); + self::assertInstanceOf(User::class, $token); + self::assertSame('alice@example.test', $token->getUserIdentifier()); + } + + public function testFailedLoginShowsErrorAndStaysOnLoginPage(): void + { + $crawler = $this->client->request('GET', '/login'); + $form = $crawler->filter('form')->form(); + $form['_username'] = 'alice@example.test'; + $form['_password'] = 'wrong'; + $this->client->submit($form); + + // Form login redirects back to the login page on failure. + self::assertResponseRedirects('/login'); + $this->client->followRedirect(); + self::assertResponseIsSuccessful(); + self::assertNull( + $this->client->getContainer()->get('security.token_storage')->getToken(), + ); + } + + public function testLogoutActionThrowsWhenInvokedDirectly(): void + { + // The firewall intercepts /logout in production, so the method body + // is unreachable through HTTP. Calling it directly proves the + // defensive throw is wired correctly. + $controller = new SecurityController(); + + $this->expectException(\LogicException::class); + $controller->logout(); + } + + public function testLogoutClearsTheSession(): void + { + // Sign in first. + $crawler = $this->client->request('GET', '/login'); + $form = $crawler->filter('form')->form(); + $form['_username'] = 'alice@example.test'; + $form['_password'] = 'secret'; + $this->client->submit($form); + $this->client->followRedirect(); + + $this->client->request('GET', '/logout'); + + // Symfony intercepts /logout and redirects to the configured target. + self::assertResponseRedirects('/'); + $this->client->followRedirect(); + self::assertNull( + $this->client->getContainer()->get('security.token_storage')->getToken(), + ); + } +} diff --git a/tests/DataFixtures/UserFixturesTest.php b/tests/DataFixtures/UserFixturesTest.php new file mode 100644 index 0000000..9190abd --- /dev/null +++ b/tests/DataFixtures/UserFixturesTest.php @@ -0,0 +1,41 @@ +get(EntityManagerInterface::class); + self::resetSchema($em); + + $container->get(UserFixtures::class)->load($em); + + $repository = $container->get(UserRepository::class); + $alice = $repository->findOneBy(['email' => 'alice@example.test']); + $bob = $repository->findOneBy(['email' => 'bob@example.test']); + + self::assertInstanceOf(User::class, $alice); + self::assertInstanceOf(User::class, $bob); + } +} diff --git a/tests/Repository/UserRepositoryTest.php b/tests/Repository/UserRepositoryTest.php new file mode 100644 index 0000000..06255de --- /dev/null +++ b/tests/Repository/UserRepositoryTest.php @@ -0,0 +1,69 @@ +get(EntityManagerInterface::class)); + + $this->repository = $container->get(UserRepository::class); + $this->userManager = $container->get(UserManager::class); + } + + public function testUpgradePasswordWritesTheNewHash(): void + { + $user = $this->userManager->createUser('alice@example.test', 'old'); + $oldHash = $user->getPassword(); + + $this->repository->upgradePassword($user, 'a-new-hash'); + + self::assertSame('a-new-hash', $user->getPassword()); + + $reloaded = $this->repository->find($user->getId()); + self::assertNotNull($reloaded); + self::assertSame('a-new-hash', $reloaded->getPassword()); + self::assertNotSame($oldHash, $reloaded->getPassword()); + } + + public function testUpgradePasswordRejectsForeignUserType(): void + { + $foreignUser = new class implements PasswordAuthenticatedUserInterface { + public function getPassword(): ?string + { + return null; + } + }; + + $this->expectException(UnsupportedUserException::class); + + $this->repository->upgradePassword($foreignUser, 'irrelevant'); + } +} diff --git a/tests/Security/UserManagerTest.php b/tests/Security/UserManagerTest.php new file mode 100644 index 0000000..1a5b90f --- /dev/null +++ b/tests/Security/UserManagerTest.php @@ -0,0 +1,104 @@ +get(EntityManagerInterface::class)); + + $this->userManager = $container->get(UserManager::class); + $this->userRepository = $container->get(UserRepository::class); + $this->passwordHasher = $container->get(UserPasswordHasherInterface::class); + } + + public function testCreatesAndPersistsUserWithHashedPassword(): void + { + $user = $this->userManager->createUser('alice@example.test', 'secret'); + + self::assertNotNull($user->getId()); + self::assertSame('alice@example.test', $user->getEmail()); + self::assertSame(['ROLE_USER'], $user->getRoles()); + self::assertNotSame('secret', $user->getPassword(), 'Password must be hashed.'); + self::assertTrue( + $this->passwordHasher->isPasswordValid($user, 'secret'), + 'Hashed password must verify against the original plain text.', + ); + self::assertSame($user->getId(), $this->userRepository->findOneBy(['email' => 'alice@example.test'])?->getId()); + } + + public function testCreateUserStoresExtraRoles(): void + { + $user = $this->userManager->createUser('admin@example.test', 'secret', ['ROLE_ADMIN']); + + self::assertSame(['ROLE_ADMIN', 'ROLE_USER'], $user->getRoles()); + } + + public function testCreateUserRejectsDuplicateEmail(): void + { + $this->userManager->createUser('alice@example.test', 'secret'); + + $this->expectException(\DomainException::class); + $this->expectExceptionMessage('alice@example.test'); + + $this->userManager->createUser('alice@example.test', 'other'); + } + + public function testCreateUserRejectsEmptyPassword(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Password must not be empty.'); + + $this->userManager->createUser('alice@example.test', ''); + } + + public function testChangePasswordReplacesTheHash(): void + { + $user = $this->userManager->createUser('alice@example.test', 'old'); + $oldHash = $user->getPassword(); + + $updated = $this->userManager->changePassword('alice@example.test', 'new'); + + self::assertSame($user->getId(), $updated->getId()); + self::assertNotSame($oldHash, $updated->getPassword()); + self::assertTrue($this->passwordHasher->isPasswordValid($updated, 'new')); + self::assertFalse($this->passwordHasher->isPasswordValid($updated, 'old')); + } + + public function testChangePasswordFailsWhenUserMissing(): void + { + $this->expectException(\DomainException::class); + $this->expectExceptionMessage('nobody@example.test'); + + $this->userManager->changePassword('nobody@example.test', 'whatever'); + } + + public function testChangePasswordRejectsEmptyPassword(): void + { + $this->userManager->createUser('alice@example.test', 'secret'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Password must not be empty.'); + + $this->userManager->changePassword('alice@example.test', ''); + } +} diff --git a/tests/Support/ResetsDatabaseSchemaTrait.php b/tests/Support/ResetsDatabaseSchemaTrait.php new file mode 100644 index 0000000..4e9619a --- /dev/null +++ b/tests/Support/ResetsDatabaseSchemaTrait.php @@ -0,0 +1,34 @@ +getMetadataFactory()->getAllMetadata(); + $schemaTool->dropDatabase(); + $schemaTool->createSchema($metadata); + } +} diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index d7804b2..b69723a 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -33,6 +33,15 @@ tag: badge_default: "snart" title_default: "Kommer snart" +security: + login: + title: "Log ind – %brand%" + eyebrow: "Log ind" + heading: "Velkommen tilbage" + email_label: "E-mail" + password_label: "Adgangskode" + submit: "Log ind" + frontpage: title: "%brand% – forhåndsvisning" hero: From 349e098994810a16fddaf6e68b8634a7bb0a05be Mon Sep 17 00:00:00 2001 From: martinydeAI Date: Thu, 11 Jun 2026 19:25:06 +0200 Subject: [PATCH 2/7] fix(ci): grant db user access to db_test* via mariadb init SQL PHPUnit's test environment uses Symfony's dbname_suffix to talk to a separate test database (db_test, optionally db_test_paratest_N). The itkdev/mariadb image only grants MYSQL_USER on MYSQL_DATABASE, so the test runner hits 'Access denied for user db@% to database db_test' on a fresh container. Mount .docker/mariadb/init/ as /docker-entrypoint-initdb.d/ so the included GRANT runs on first container initialisation. The wildcard is escaped as db\_test% so it only matches db_test... not unrelated names like dbXtest. Local devs with an already-initialised mariadb volume can either recreate the container (task compose -- down -v && task site-install) or apply the grant once manually. Co-Authored-By: Claude Opus 4.7 (1M context) --- .docker/mariadb/init/01-grant-test-databases.sql | 14 ++++++++++++++ docker-compose.yml | 5 +++++ 2 files changed, 19 insertions(+) create mode 100644 .docker/mariadb/init/01-grant-test-databases.sql diff --git a/.docker/mariadb/init/01-grant-test-databases.sql b/.docker/mariadb/init/01-grant-test-databases.sql new file mode 100644 index 0000000..c21040a --- /dev/null +++ b/.docker/mariadb/init/01-grant-test-databases.sql @@ -0,0 +1,14 @@ +-- Grant the application user access to the per-environment test databases +-- Symfony's `when@test` doctrine config appends `_test` (plus an optional +-- ParaTest suffix) to the configured database name. The MariaDB image only +-- grants MYSQL_USER on MYSQL_DATABASE by default, so without this the +-- test suite fails with "Access denied for user 'db'@'%' to database +-- 'db_test'". The `\_test` escapes the SQL wildcard so the grant matches +-- `db_test`, `db_test_paratest_1`, etc. — but not unrelated names like +-- `dbXtest`. +-- +-- This file is mounted into `/docker-entrypoint-initdb.d/` and runs once +-- when the container's data volume is first initialised. + +GRANT ALL PRIVILEGES ON `db\_test%`.* TO `db`@`%`; +FLUSH PRIVILEGES; diff --git a/docker-compose.yml b/docker-compose.yml index 4c8f69a..178a1d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,11 @@ services: - MYSQL_PASSWORD=db - MYSQL_DATABASE=db #- ENCRYPT=1 # Uncomment to enable database encryption. + volumes: + # Grant the application user access to the `db_test*` databases + # Symfony creates in the test environment. See the file for + # details. + - ./.docker/mariadb/init:/docker-entrypoint-initdb.d:ro phpfpm: image: itkdev/php8.4-fpm:latest From 9523285ade3f8d3f1190bb8b58806b0ccb159063 Mon Sep 17 00:00:00 2001 From: martinydeAI Date: Thu, 11 Jun 2026 19:29:53 +0200 Subject: [PATCH 3/7] chore(taskfile): self-heal test DB privileges before running PHPUnit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds task db-prepare-test which re-applies the GRANT init SQL and ensures db_test exists. test and test-coverage depend on it so any local dev whose mariadb data volume predates the new /docker-entrypoint-initdb.d mount can run tests without first recreating the container. The grant is idempotent and only touches privileges — no data is read or written. Co-Authored-By: Claude Opus 4.7 (1M context) --- Taskfile.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Taskfile.yml b/Taskfile.yml index 700ad33..e12f655 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -91,15 +91,27 @@ tasks: # ---- Tests ------------------------------------------------------------ + db-prepare-test: + desc: 'Ensure the test database exists and the app user can write to it (idempotent).' + cmds: + # Re-apply the init SQL on every invocation so local devs whose + # mariadb data volume predates the init mount also pick the grant up. + # Touches privileges only — no row data is read or written. + - cat .docker/mariadb/init/01-grant-test-databases.sql | task compose-exec -- mariadb mariadb -uroot -ppassword + - task console -- doctrine:database:create --if-not-exists --env=test + silent: true + test: desc: 'Run the PHPUnit test suite (no coverage).' cmds: + - task: db-prepare-test - task compose-exec -- phpfpm vendor/bin/phpunit silent: true test-coverage: desc: 'Run PHPUnit with coverage and enforce the 100% gate.' cmds: + - task: db-prepare-test - task compose -- exec -e XDEBUG_MODE=coverage phpfpm vendor/bin/phpunit --coverage-clover=coverage/clover.xml - task compose-exec -- phpfpm vendor/bin/coverage-check coverage/clover.xml 100 silent: true From e2c8c80ddeb01eaf70521e6ca1f3bf816526ce74 Mon Sep 17 00:00:00 2001 From: martinydeAI Date: Thu, 11 Jun 2026 19:33:29 +0200 Subject: [PATCH 4/7] fix(ci): create db_test in the mariadb init SQL too The earlier init script only granted privileges on db_test* but never created the database. CI runs vendor/bin/phpunit directly (not task test), so the db-prepare-test target couldn't backfill the CREATE either, and the Tests workflow failed with 'Unknown database db_test'. Folds CREATE DATABASE IF NOT EXISTS db_test into the init SQL so both fresh CI containers and the local db-prepare-test target reach the same state. The doctrine:database:create call in db-prepare-test is now redundant and removed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../mariadb/init/01-grant-test-databases.sql | 26 ++++++++++++------- Taskfile.yml | 6 ++--- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.docker/mariadb/init/01-grant-test-databases.sql b/.docker/mariadb/init/01-grant-test-databases.sql index c21040a..1763ae7 100644 --- a/.docker/mariadb/init/01-grant-test-databases.sql +++ b/.docker/mariadb/init/01-grant-test-databases.sql @@ -1,14 +1,20 @@ --- Grant the application user access to the per-environment test databases --- Symfony's `when@test` doctrine config appends `_test` (plus an optional --- ParaTest suffix) to the configured database name. The MariaDB image only --- grants MYSQL_USER on MYSQL_DATABASE by default, so without this the --- test suite fails with "Access denied for user 'db'@'%' to database --- 'db_test'". The `\_test` escapes the SQL wildcard so the grant matches --- `db_test`, `db_test_paratest_1`, etc. — but not unrelated names like --- `dbXtest`. +-- Create the test database used by PHPUnit and grant the application +-- user access to it (plus any future ParaTest-suffixed siblings). -- --- This file is mounted into `/docker-entrypoint-initdb.d/` and runs once --- when the container's data volume is first initialised. +-- Symfony's `when@test` doctrine config appends `_test` to the configured +-- database name, and the MariaDB image only creates MYSQL_DATABASE and +-- grants MYSQL_USER on it. Without this file the test suite fails with +-- either "Unknown database 'db_test'" (database absent) or "Access +-- denied for user 'db'@'%'" (database present but no grant). +-- +-- The `\_test` escape on the GRANT pattern matches `db_test`, +-- `db_test_paratest_1`, etc. — but not unrelated names like `dbXtest`. +-- +-- This file is mounted into `/docker-entrypoint-initdb.d/` and runs +-- once when the container's data volume is first initialised. The +-- `task db-prepare-test` target re-applies the same logic for local +-- devs whose volume predates the mount. +CREATE DATABASE IF NOT EXISTS `db_test`; GRANT ALL PRIVILEGES ON `db\_test%`.* TO `db`@`%`; FLUSH PRIVILEGES; diff --git a/Taskfile.yml b/Taskfile.yml index e12f655..f417bdb 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -95,10 +95,10 @@ tasks: desc: 'Ensure the test database exists and the app user can write to it (idempotent).' cmds: # Re-apply the init SQL on every invocation so local devs whose - # mariadb data volume predates the init mount also pick the grant up. - # Touches privileges only — no row data is read or written. + # mariadb data volume predates the init mount also pick up the + # CREATE DATABASE + GRANT. The script is IF-NOT-EXISTS / additive + # only — no row data is read or written. - cat .docker/mariadb/init/01-grant-test-databases.sql | task compose-exec -- mariadb mariadb -uroot -ppassword - - task console -- doctrine:database:create --if-not-exists --env=test silent: true test: From 4f88b53882ec7e40c705f7743f9515f19dd5bc38 Mon Sep 17 00:00:00 2001 From: martinydeAI Date: Thu, 11 Jun 2026 20:07:56 +0200 Subject: [PATCH 5/7] chore(taskfile): apply migrations as part of site-install Adds doctrine:migrations:migrate as the final step of site-install so a fresh check-out's database schema lands automatically. Drops the manual 'Apply the database schema' snippet from the README's user-creation section since the schema is now in place by the time anyone reaches it. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 6 +++--- Taskfile.yml | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1bc9cb0..8aaed9a 100644 --- a/README.md +++ b/README.md @@ -147,10 +147,10 @@ URL is printed by the start task). ### Creating the first user -```sh -# Apply the database schema -task console -- doctrine:migrations:migrate -n +`task site-install` applies the database schema, so by the time you +get here the `user` table already exists. +```sh # Option A — load the local-dev fixtures (alice + bob, password `password`) task console -- doctrine:fixtures:load -n diff --git a/Taskfile.yml b/Taskfile.yml index f417bdb..7e5ebb2 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -36,11 +36,12 @@ tasks: # ---- Lifecycle -------------------------------------------------------- site-install: - desc: 'Pull images, start the stack, and install Composer dependencies.' + desc: 'Pull images, start the stack, install Composer dependencies, and apply database migrations.' cmds: - task compose -- pull - task compose -- up --detach --remove-orphans --wait - task composer-install + - task console -- doctrine:migrations:migrate --no-interaction silent: true # ---- Composer --------------------------------------------------------- From 6b712de2f283d9fe9efe565f5868da14b31ac293 Mon Sep 17 00:00:00 2001 From: martinydeAI Date: Thu, 11 Jun 2026 20:08:17 +0200 Subject: [PATCH 6/7] docs: trim the redundant site-install note from user-creation block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'Creating the first user' section doesn't need to remind readers that site-install ran migrations — that's documented in the section above. Removes the explanatory paragraph and leaves just the commands. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 8aaed9a..ee3501c 100644 --- a/README.md +++ b/README.md @@ -147,9 +147,6 @@ URL is printed by the start task). ### Creating the first user -`task site-install` applies the database schema, so by the time you -get here the `user` table already exists. - ```sh # Option A — load the local-dev fixtures (alice + bob, password `password`) task console -- doctrine:fixtures:load -n From 46e417c546e3596b4231a4bfffd04b57b68e9d2c Mon Sep 17 00:00:00 2001 From: martinydeAI Date: Thu, 11 Jun 2026 20:20:49 +0200 Subject: [PATCH 7/7] style: drop vertical phpdoc alignment; shorten @param/@return blurbs @Symfony enables phpdoc_align with align=vertical, which padded @param and @return so columns lined up and pushed descriptions onto extra wrapped lines. Override to align=left and rewrite the verbose blurbs in the user-auth code so each tag fits on one line. Net 46 lines removed across UserManager, the two console commands, SecurityController, UserFixtures, and DevTemplateMarkerNodeVisitor (the last one fell out of the same cs-fixer pass). Co-Authored-By: Claude Opus 4.7 (1M context) --- .php-cs-fixer.dist.php | 5 +++ src/Command/UserChangePasswordCommand.php | 16 +++----- src/Command/UserCreateCommand.php | 18 +++------ src/Controller/SecurityController.php | 32 +++++---------- src/DataFixtures/UserFixtures.php | 17 +++----- src/Security/UserManager.php | 48 +++++++---------------- src/Twig/DevTemplateMarkerNodeVisitor.php | 12 +++--- 7 files changed, 51 insertions(+), 97 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index c23b927..bb2e707 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -15,6 +15,11 @@ $config->setRules([ '@Symfony' => true, + // Override the Symfony default that vertically aligns @param / @return + // columns by padding names and descriptions with spaces. We keep tags + // left-aligned so descriptions don't get pushed into hard-to-read + // multi-line wraps. + 'phpdoc_align' => ['align' => 'left'], ]); return $config; diff --git a/src/Command/UserChangePasswordCommand.php b/src/Command/UserChangePasswordCommand.php index 614e5d0..3e6d49d 100644 --- a/src/Command/UserChangePasswordCommand.php +++ b/src/Command/UserChangePasswordCommand.php @@ -15,8 +15,8 @@ /** * Console command that updates an existing user's password. * + * Thin adapter over {@see UserManager::changePassword()}. * Usage: `task console -- app:user:change-password `. - * Delegates to {@see UserManager::changePassword()}. */ #[AsCommand( name: 'app:user:change-password', @@ -25,8 +25,7 @@ final class UserChangePasswordCommand extends Command { /** - * @param UserManager $userManager service that owns password hashing and - * persistence + * @param UserManager $userManager service that owns password rotation */ public function __construct(private readonly UserManager $userManager) { @@ -44,15 +43,12 @@ protected function configure(): void } /** - * Adapt console arguments to the {@see UserManager} call and render - * a success / error message. + * Adapt console arguments to the {@see UserManager} call. * - * @param InputInterface $input CLI arguments - * @param OutputInterface $output Symfony console output stream + * @param InputInterface $input CLI arguments + * @param OutputInterface $output console output stream * - * @return int Command::SUCCESS on a successful change, - * Command::FAILURE when the user is not found or the - * password is rejected + * @return int Command::SUCCESS on a successful change, Command::FAILURE otherwise */ protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/src/Command/UserCreateCommand.php b/src/Command/UserCreateCommand.php index c0d7974..ba4de44 100644 --- a/src/Command/UserCreateCommand.php +++ b/src/Command/UserCreateCommand.php @@ -15,10 +15,8 @@ /** * Console command that creates a new application user. * + * Thin adapter over {@see UserManager::createUser()}. * Usage: `task console -- app:user:create `. - * Delegates to {@see UserManager::createUser()} for the actual - * persistence and hashing — the command itself just adapts CLI input - * and renders the outcome. */ #[AsCommand( name: 'app:user:create', @@ -27,8 +25,7 @@ final class UserCreateCommand extends Command { /** - * @param UserManager $userManager service that owns user creation, - * password hashing, and persistence + * @param UserManager $userManager service that owns user creation */ public function __construct(private readonly UserManager $userManager) { @@ -46,15 +43,12 @@ protected function configure(): void } /** - * Adapt console arguments to the {@see UserManager} call and render - * a success / error message. + * Adapt console arguments to the {@see UserManager} call. * - * @param InputInterface $input CLI arguments - * @param OutputInterface $output Symfony console output stream + * @param InputInterface $input CLI arguments + * @param OutputInterface $output console output stream * - * @return int Command::SUCCESS on creation, Command::FAILURE when - * {@see UserManager::createUser()} throws a domain or - * validation error + * @return int Command::SUCCESS on creation, Command::FAILURE on domain or validation error */ protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 3addc34..bfd3bba 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -13,31 +13,19 @@ /** * Routes for interactive authentication. * - * `GET/POST /login` renders the login form and surfaces the last - * authentication error and pre-filled username via Symfony Security's - * {@see AuthenticationUtils}. The actual credential check is performed - * by the `form_login` authenticator declared in `security.yaml`, not - * here — this controller stays in the "inject service → render" - * shape required by project conventions. - * - * `/logout` is wired declaratively in the firewall's `logout` block; - * the method body is never executed because Symfony intercepts the - * request and clears the session. + * `/login` renders the form and surfaces the previous error / last + * username via {@see AuthenticationUtils}. The credential check is + * performed by the `form_login` authenticator declared in + * `security.yaml`, not here. `/logout` is intercepted by the firewall. */ final class SecurityController extends AbstractController { /** * Render the login form. * - * @param AuthenticationUtils $authenticationUtils symfony Security helper - * providing the previous - * login error (if any) - * and the last entered - * username so the form - * can be re-rendered with - * user input preserved + * @param AuthenticationUtils $authenticationUtils Security helper for the previous error + last username * - * @return Response the rendered `security/login.html.twig` template + * @return Response the rendered login template */ #[Route(path: '/login', name: 'app_login', methods: ['GET', 'POST'])] public function login(AuthenticationUtils $authenticationUtils): Response @@ -51,11 +39,9 @@ public function login(AuthenticationUtils $authenticationUtils): Response /** * Placeholder action for the `app_logout` route. * - * The route exists so URL generation (`{{ path('app_logout') }}`) - * works in templates, but Symfony Security intercepts the request - * and handles session invalidation before this method is called. - * The unreachable body throws so that any accidental direct call - * fails loudly. + * The firewall intercepts the request and clears the session, so + * the body is unreachable in production. The throw guards against + * an accidental direct call. * * @throws \LogicException always — the firewall must intercept */ diff --git a/src/DataFixtures/UserFixtures.php b/src/DataFixtures/UserFixtures.php index d9e536a..67e4dd7 100644 --- a/src/DataFixtures/UserFixtures.php +++ b/src/DataFixtures/UserFixtures.php @@ -11,19 +11,14 @@ /** * Seed two baseline users for local development. * - * `alice@example.test` and `bob@example.test` both get the same - * intentionally weak password (`password`) so they're convenient to - * paste into the login form. The fixture is excluded from prod by - * Doctrine's standard fixtures workflow (`bin/console - * doctrine:fixtures:load` is a dev/test command). + * `alice@example.test` and `bob@example.test`, both with the + * intentionally-weak password `password`, so they're easy to paste + * into the login form. */ final class UserFixtures extends Fixture { /** - * @param UserManager $userManager service that creates the persisted - * {@see \App\Entity\User} entities, - * keeping fixture code from duplicating - * the hashing wiring + * @param UserManager $userManager service that creates the persisted users */ public function __construct(private readonly UserManager $userManager) { @@ -32,9 +27,7 @@ public function __construct(private readonly UserManager $userManager) /** * Persist the two baseline users via {@see UserManager::createUser()}. * - * @param ObjectManager $manager unused — the {@see UserManager} - * flushes through its own injected - * entity manager + * @param ObjectManager $manager unused — UserManager flushes its own entity manager */ public function load(ObjectManager $manager): void { diff --git a/src/Security/UserManager.php b/src/Security/UserManager.php index 074a6da..25ca58e 100644 --- a/src/Security/UserManager.php +++ b/src/Security/UserManager.php @@ -10,29 +10,18 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; /** - * Create users and change passwords without leaking persistence and - * password-hashing wiring out into controllers or console commands. + * Create users and rotate their passwords. * - * Callers ({@see \App\Command\UserCreateCommand}, - * {@see \App\Command\UserChangePasswordCommand}, and any future signup - * flow) work with strings and get a persisted {@see User} back — the - * service hides the {@see EntityManagerInterface}, - * {@see UserRepository}, and {@see UserPasswordHasherInterface} - * collaborators. + * Hides the Doctrine + password-hasher wiring from callers + * (controllers, console commands, fixtures) so they work with plain + * strings and get a persisted {@see User} back. */ final class UserManager { /** - * @param EntityManagerInterface $entityManager doctrine entity manager - * used to persist and - * flush the {@see User} - * aggregate - * @param UserRepository $userRepository read-side lookup of - * users by email - * @param UserPasswordHasherInterface $passwordHasher Symfony Security - * password hasher, - * configured via - * `security.yaml`. + * @param EntityManagerInterface $entityManager Doctrine entity manager + * @param UserRepository $userRepository read-side lookup of users by email + * @param UserPasswordHasherInterface $passwordHasher Symfony Security hasher */ public function __construct( private readonly EntityManagerInterface $entityManager, @@ -44,20 +33,13 @@ public function __construct( /** * Create a new persisted user with a hashed password. * - * @param string $email the user's e-mail address; must be - * unique across the table - * @param string $plainPassword the password in clear-text — it is - * hashed before persistence and never - * stored as-is - * @param list $roles additional roles to grant; the - * framework guarantees `ROLE_USER` - * implicitly so callers should leave - * this empty for plain users + * @param string $email user e-mail; must be unique + * @param string $plainPassword clear-text password, hashed before persistence + * @param list $roles additional roles beyond the implicit `ROLE_USER` * * @return User the persisted user with an assigned id * - * @throws \DomainException when a user with the same e-mail - * already exists + * @throws \DomainException when a user with the same e-mail already exists * @throws \InvalidArgumentException when `$plainPassword` is empty */ public function createUser(string $email, string $plainPassword, array $roles = []): User @@ -84,14 +66,12 @@ public function createUser(string $email, string $plainPassword, array $roles = /** * Replace a user's password with a freshly hashed copy. * - * @param string $email the e-mail of the user whose password - * to change - * @param string $newPlainPassword the new password in clear-text; hashed - * before persistence + * @param string $email e-mail of the user to update + * @param string $newPlainPassword new clear-text password, hashed before persistence * * @return User the updated user * - * @throws \DomainException when no user with that e-mail exists + * @throws \DomainException when no user with that e-mail exists * @throws \InvalidArgumentException when `$newPlainPassword` is empty */ public function changePassword(string $email, string $newPlainPassword): User diff --git a/src/Twig/DevTemplateMarkerNodeVisitor.php b/src/Twig/DevTemplateMarkerNodeVisitor.php index 40aa315..d274713 100644 --- a/src/Twig/DevTemplateMarkerNodeVisitor.php +++ b/src/Twig/DevTemplateMarkerNodeVisitor.php @@ -37,8 +37,8 @@ public function enterNode(Node $node, Environment $env): Node * Wrap the template body (or its `body` block, for extending * templates) with begin and end marker TextNodes. * - * @param Node $node the node being left - * @param Environment $env the Twig environment (unused) + * @param Node $node the node being left + * @param Environment $env the Twig environment (unused) * * @return Node the original node, mutated in place when applicable */ @@ -77,9 +77,9 @@ public function leaveNode(Node $node, Environment $env): Node * `BlockNode`; we iterate to find the BlockNode and replace its * body with the wrapped sequence. * - * @param ModuleNode $node the extending module - * @param TextNode $prefix the opening marker - * @param TextNode $suffix the closing marker + * @param ModuleNode $node the extending module + * @param TextNode $prefix the opening marker + * @param TextNode $suffix the closing marker */ private function wrapExtendingBody(ModuleNode $node, TextNode $prefix, TextNode $suffix): void { @@ -111,7 +111,7 @@ public function getPriority(): int * Replace `$node`'s `body` child with a sequence: prefix, original * body, suffix. * - * @param Node $node the node whose body to wrap + * @param Node $node the node whose body to wrap * @param TextNode $prefix the opening marker * @param TextNode $suffix the closing marker */