commit a4a14e441b73006569f89aa7c416a9fc0c361e13 Author: Mathias STRASSER Date: Wed Mar 4 00:27:15 2026 +0100 Step 00 — Squelette + intégration naïve 3 Bounded Contexts (Sales, Invoicing, LegacyFulfillment) avec : - Domaines complets (agrégats, VOs, événements, invariants) - Couche application (commands, queries, ports) - Infrastructure in-memory (repos, gateway fake) - Controllers HTTP Symfony - Couplage naïf synchrone entre BC via NaiveSalesEventPublisher - 20 tests unitaires et d'intégration passants diff --git a/.env b/.env new file mode 100644 index 0000000..9556311 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +APP_ENV=dev +APP_DEBUG=1 +APP_SECRET=minishop-boilerplate-change-me diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88d2dab --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor/ +/apps/symfony/var/ +/.env.local +/.phpunit.cache/ +/.phpunit.result.cache diff --git a/README.md b/README.md new file mode 100644 index 0000000..c79ec07 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# DDDBoilerplate - MiniShop + +Boilerplate DDD minimaliste pour tutoriels Blue Book, pense pour PHP/Symfony et transposable. + +Document de reference: + +- [docs/MINISHOP_DDD_BLUEBOOK_BOILERPLATE.md](docs/MINISHOP_DDD_BLUEBOOK_BOILERPLATE.md) + +Patterns cibles: + +- OHS +- ACL +- Conformist +- Published Language + +Progression recommandee via tags Git: + +- `step/00-naive` +- `step/01-published-language` +- `step/02-conformist` +- `step/03-acl` +- `step/04-ohs` +- `step/05-hardening` diff --git a/apps/symfony/config/packages/framework.yaml b/apps/symfony/config/packages/framework.yaml new file mode 100644 index 0000000..aea08ae --- /dev/null +++ b/apps/symfony/config/packages/framework.yaml @@ -0,0 +1,11 @@ +framework: + secret: '%env(APP_SECRET)%' + test: ~ + http_method_override: false + handle_all_throwables: true + php_errors: + log: true + +when@test: + framework: + test: true diff --git a/apps/symfony/config/routes.yaml b/apps/symfony/config/routes.yaml new file mode 100644 index 0000000..7e296b6 --- /dev/null +++ b/apps/symfony/config/routes.yaml @@ -0,0 +1,5 @@ +controllers: + resource: + path: ../../../src/ + namespace: MiniShop + type: attribute diff --git a/apps/symfony/config/services.yaml b/apps/symfony/config/services.yaml new file mode 100644 index 0000000..a9c9a58 --- /dev/null +++ b/apps/symfony/config/services.yaml @@ -0,0 +1,62 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + # --- Shared --- + MiniShop\Shared\: + resource: '%kernel.project_dir%/src/Shared/' + + MiniShop\Shared\Technical\Clock: + alias: MiniShop\Shared\Technical\SystemClock + + # --- Sales --- + MiniShop\Sales\Application\: + resource: '%kernel.project_dir%/src/Sales/Application/' + + MiniShop\Sales\Infrastructure\: + resource: '%kernel.project_dir%/src/Sales/Infrastructure/' + + MiniShop\Sales\Interfaces\: + resource: '%kernel.project_dir%/src/Sales/Interfaces/' + tags: ['controller.service_arguments'] + + MiniShop\Sales\Application\Port\OrderRepository: + alias: MiniShop\Sales\Infrastructure\Persistence\InMemoryOrderRepository + + MiniShop\Sales\Application\Port\SalesEventPublisher: + alias: MiniShop\Sales\Infrastructure\Messaging\NaiveSalesEventPublisher + + # --- Invoicing --- + MiniShop\Invoicing\Application\: + resource: '%kernel.project_dir%/src/Invoicing/Application/' + + MiniShop\Invoicing\Infrastructure\: + resource: '%kernel.project_dir%/src/Invoicing/Infrastructure/' + + MiniShop\Invoicing\Interfaces\: + resource: '%kernel.project_dir%/src/Invoicing/Interfaces/' + tags: ['controller.service_arguments'] + + MiniShop\Invoicing\Application\Port\InvoiceRepository: + alias: MiniShop\Invoicing\Infrastructure\Persistence\InMemoryInvoiceRepository + + MiniShop\Invoicing\Application\Port\InvoiceNumberGenerator: + alias: MiniShop\Invoicing\Infrastructure\SequentialInvoiceNumberGenerator + + # --- LegacyFulfillment --- + MiniShop\LegacyFulfillment\Application\: + resource: '%kernel.project_dir%/src/LegacyFulfillment/Application/' + + MiniShop\LegacyFulfillment\Infrastructure\: + resource: '%kernel.project_dir%/src/LegacyFulfillment/Infrastructure/' + + MiniShop\LegacyFulfillment\Interfaces\: + resource: '%kernel.project_dir%/src/LegacyFulfillment/Interfaces/' + tags: ['controller.service_arguments'] + + MiniShop\LegacyFulfillment\Application\Port\ShipmentRequestRepository: + alias: MiniShop\LegacyFulfillment\Infrastructure\Persistence\InMemoryShipmentRequestRepository + + MiniShop\LegacyFulfillment\Application\Port\LegacyFulfillmentGateway: + alias: MiniShop\LegacyFulfillment\Infrastructure\Gateway\FakeLegacyFulfillmentGateway diff --git a/apps/symfony/public/index.php b/apps/symfony/public/index.php new file mode 100644 index 0000000..c25a4bb --- /dev/null +++ b/apps/symfony/public/index.php @@ -0,0 +1,17 @@ +bootEnv(dirname(__DIR__, 3) . '/.env'); + +$kernel = new Kernel($_SERVER['APP_ENV'] ?? 'dev', (bool) ($_SERVER['APP_DEBUG'] ?? true)); +$request = Request::createFromGlobals(); +$response = $kernel->handle($request); +$response->send(); +$kernel->terminate($request, $response); diff --git a/apps/symfony/src/Kernel.php b/apps/symfony/src/Kernel.php new file mode 100644 index 0000000..6a941a1 --- /dev/null +++ b/apps/symfony/src/Kernel.php @@ -0,0 +1,28 @@ +getProjectDir() . '/apps/symfony/config'; + } + + public function getCacheDir(): string + { + return $this->getProjectDir() . '/apps/symfony/var/cache/' . $this->environment; + } + + public function getLogDir(): string + { + return $this->getProjectDir() . '/apps/symfony/var/log'; + } +} diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..022bbf0 --- /dev/null +++ b/bin/console @@ -0,0 +1,21 @@ +#!/usr/bin/env php +bootEnv(dirname(__DIR__) . '/.env'); + +$input = new ArgvInput(); +$env = $input->getParameterOption(['--env', '-e'], $_SERVER['APP_ENV'] ?? 'dev', true); +$debug = (bool) ($_SERVER['APP_DEBUG'] ?? ($env !== 'prod')); + +$kernel = new Kernel($env, $debug); +$application = new Application($kernel); +$application->run($input); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a7c71ab --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "roukmoute/ddd-boilerplate", + "description": "MiniShop - DDD Blue Book Boilerplate", + "type": "project", + "license": "MIT", + "require": { + "php": ">=8.5", + "symfony/framework-bundle": "^7.2", + "symfony/console": "^7.2", + "symfony/dotenv": "^7.2", + "symfony/yaml": "^7.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "autoload": { + "psr-4": { + "MiniShop\\Sales\\": "src/Sales/", + "MiniShop\\Invoicing\\": "src/Invoicing/", + "MiniShop\\LegacyFulfillment\\": "src/LegacyFulfillment/", + "MiniShop\\Shared\\": "src/Shared/", + "MiniShop\\Contracts\\": "contracts/", + "App\\": "apps/symfony/src/" + } + }, + "autoload-dev": { + "psr-4": { + "MiniShop\\Tests\\": "tests/" + } + }, + "config": { + "sort-packages": true + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..94b1433 --- /dev/null +++ b/composer.lock @@ -0,0 +1,4220 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "cf876267ce49272867204aaa70702ed4", + "packages": [ + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/cache", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "59184fa14658d7724cd9b8743d91c1b1aa618bff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/59184fa14658d7724cd9b8743d91c1b1aa618bff", + "reference": "59184fa14658d7724cd9b8743d91c1b1aa618bff", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^7.4|^8.0" + }, + "conflict": { + "doctrine/dbal": "<4.3", + "ext-redis": "<6.1", + "ext-relay": "<0.12.1" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v8.0.6" + }, + "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-02-21T23:29:37+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T15:25:07+00:00" + }, + { + "name": "symfony/config", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "94ea198de42f93dffa920a098cac3961a82e63b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/94ea198de42f93dffa920a098cac3961a82e63b7", + "reference": "94ea198de42f93dffa920a098cac3961a82e63b7", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v8.0.6" + }, + "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-02-25T16:59:43+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "6d643a93b47398599124022eb24d97c153c12f27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27", + "reference": "6d643a93b47398599124022eb24d97c153c12f27", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.6" + }, + "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-02-25T17:02:47+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "edd98864a7b9eaaa10f389bd414e7d9e816bb59d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/edd98864a7b9eaaa10f389bd414e7d9e816bb59d", + "reference": "edd98864a7b9eaaa10f389bd414e7d9e816bb59d", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^7.4|^8.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v8.0.6" + }, + "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-02-25T16:59:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "db374255a1c99511d34d5e009dce5be75d0d9c23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/db374255a1c99511d34d5e009dce5be75d0d9c23", + "reference": "db374255a1c99511d34d5e009dce5be75d0d9c23", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/console": "<6.4", + "symfony/process": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v7.4.6" + }, + "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-02-13T11:43:08+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/7620b97ec0ab1d2d6c7fb737aa55da411bea776a", + "reference": "7620b97ec0ab1d2d6c7fb737aa55da411bea776a", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-23T11:07:10+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T11:45:55+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.0.6" + }, + "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-02-25T16:59:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:41:02+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "a4022da7530f794aa64cea34b388439afb6323a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a4022da7530f794aa64cea34b388439afb6323a3", + "reference": "a4022da7530f794aa64cea34b388439afb6323a3", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/cache": "^6.4.12|^7.0|^8.0", + "symfony/config": "^7.4.4|^8.0.4", + "symfony/dependency-injection": "^7.4.4|^8.0.4", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.3|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/filesystem": "^7.1|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/routing": "^7.4|^8.0" + }, + "conflict": { + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/asset": "<6.4", + "symfony/asset-mapper": "<6.4", + "symfony/clock": "<6.4", + "symfony/console": "<6.4", + "symfony/dom-crawler": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/form": "<7.4", + "symfony/http-client": "<6.4", + "symfony/lock": "<6.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<7.4", + "symfony/mime": "<6.4", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", + "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", + "symfony/security-core": "<6.4", + "symfony/security-csrf": "<7.2", + "symfony/serializer": "<7.2.5", + "symfony/stopwatch": "<6.4", + "symfony/translation": "<7.3", + "symfony/twig-bridge": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4", + "symfony/web-profiler-bundle": "<6.4", + "symfony/webhook": "<7.2", + "symfony/workflow": "<7.4" + }, + "require-dev": { + "doctrine/persistence": "^1.3|^2|^3", + "dragonmantank/cron-expression": "^3.1", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "seld/jsonlint": "^1.10", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/dotenv": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/json-streamer": "^7.3|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/mailer": "^6.4|^7.0|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/notifier": "^6.4|^7.0|^8.0", + "symfony/object-mapper": "^7.3|^8.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/scheduler": "^6.4.4|^7.0.4|^8.0", + "symfony/security-bundle": "^6.4|^7.0|^8.0", + "symfony/semaphore": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.2.5|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^7.3|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/webhook": "^7.2|^8.0", + "symfony/workflow": "^7.4|^8.0", + "symfony/yaml": "^7.3|^8.0", + "twig/twig": "^3.12" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.6" + }, + "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-02-25T16:50:00+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "7745ff1aad45d855fe25b08969269ef83b1ad8bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7745ff1aad45d855fe25b08969269ef83b1ad8bc", + "reference": "7745ff1aad45d855fe25b08969269ef83b1ad8bc", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" + }, + "require-dev": { + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v8.0.6" + }, + "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-02-21T16:28:39+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "b567e571e74b5774b3d3cb4d35bdafa5f37e51a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b567e571e74b5774b3d3cb4d35bdafa5f37e51a9", + "reference": "b567e571e74b5774b3d3cb4d35bdafa5f37e51a9", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/log": "^1|^2|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/translation-contracts": "<2.5", + "twig/twig": "<3.21" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v8.0.6" + }, + "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-02-26T08:36:42+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/routing", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "053c40fd46e1d19c5c5a94cada93ce6c3facdd55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/053c40fd46e1d19c5c5a94cada93ce6c3facdd55", + "reference": "053c40fd46e1d19c5c5a94cada93ce6c3facdd55", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v8.0.6" + }, + "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-02-25T16:59:43+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.6" + }, + "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-02-09T10:14:57+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", + "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.0.6" + }, + "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-02-15T10:53:29+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T18:53:00+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.6" + }, + "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-02-09T09:33:46+00:00" + } + ], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.5" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/docs/MINISHOP_DDD_BLUEBOOK_BOILERPLATE.md b/docs/MINISHOP_DDD_BLUEBOOK_BOILERPLATE.md new file mode 100644 index 0000000..5640403 --- /dev/null +++ b/docs/MINISHOP_DDD_BLUEBOOK_BOILERPLATE.md @@ -0,0 +1,404 @@ +# MiniShop DDD Blue Book Boilerplate + +Document de reference pour un boilerplate DDD minimaliste, pense pour PHP/Symfony, mais transposable. + +## 1. But du boilerplate + +Construire une base unique, reutilisable pour des tutoriels progressifs sur les patterns du Blue Book: + +- `OHS` (Open Host Service) +- `ACL` (Anti-Corruption Layer) +- `Conformist` +- `Published Language` + +Contraintes voulues: + +- un seul repo +- `2 a 3 Bounded Contexts` maximum +- interfaces explicites entre contextes +- integration volontairement naive au debut +- evolution pas a pas, concept par concept + +## 2. Positionnement et principes + +Ce boilerplate privilegie la purete tactique/strategique du DDD, pas la sophistication technique. + +Principes directeurs: + +1. Un `Bounded Context` = un modele autonome + un langage propre. +2. Aucune entite/VO partagee entre BC par defaut. +3. Les relations entre BC sont explicites via une `Context Map`. +4. Le code inter-BC passe par des contrats (DTO, messages, API), jamais par les modeles internes. +5. Tout pattern (Conformist, ACL, OHS, Published Language) est rendu visible dans l'arborescence et dans les tests. +6. Les choix techniques Symfony/Doctrine restent remplaçables. + +## 3. Domaine MiniShop (scope minimal) + +### 3.1 Bounded Contexts retenus + +- `Sales` (coeur metier): prise de commande +- `Invoicing`: emission de facture +- `LegacyFulfillment`: expedition via systeme legacy volontairement "sale" + +`Shipping` peut remplacer `LegacyFulfillment` dans une variante "moderne", mais la version pedagogique recommande `LegacyFulfillment` pour illustrer `ACL`. + +### 3.2 Scenario fil rouge + +1. Un client passe une commande dans `Sales`. +2. `Sales` notifie les autres BC qu'une commande est confirmee. +3. `Invoicing` produit une facture. +4. `LegacyFulfillment` cree une demande d'expedition dans un format legacy. + +## 4. Ubiquitous Language par BC + +### 4.1 Sales + +- Entites/Aggregats: `Order` +- Value Objects: `OrderId`, `CustomerId`, `Money`, `OrderLine` +- Evenements de domaine: `OrderPlaced`, `OrderConfirmed` +- Invariants: + - une commande a au moins une ligne + - total > 0 + - confirmation impossible si commande vide + +### 4.2 Invoicing + +- Entites/Aggregats: `Invoice` +- Value Objects: `InvoiceId`, `BillingParty`, `InvoiceLine`, `TaxRate` +- Evenements de domaine: `InvoiceIssued` +- Invariants: + - une facture refere une commande externe + - montant facture coherent avec ses lignes + +### 4.3 LegacyFulfillment + +- Entites/Aggregats: `ShipmentRequest` +- Value Objects: `LegacyOrderRef`, `ShippingAddress`, `ParcelSpec` +- Evenements de domaine: `ShipmentRequested` +- Contraintes: + - format legacy impose (champs abrégés, codifications date/status) + - mapping explicite depuis le langage publie + +## 5. Context Map cible (pedagogique) + +Etat initial (naif): + +- `Sales` appelle directement des services applicatifs externes +- schemas ad hoc, non versionnes +- couplage fort et fragile + +Etat cible (apres tutoriels): + +- `Sales` expose un `OHS` pour consultation/commande (option REST ou message) +- `Sales` publie un `Published Language` versionne (`sales.v1`) +- `Invoicing` suit `Conformist` sur ce langage publie +- `LegacyFulfillment` introduit un `ACL` entre langage publie et modele legacy + +Representation synthese: + +```text +Sales (Upstream) + ├── Published Language (sales.v1.*) + ├── OHS (query/command API) + ├──> Invoicing (Conformist) + └──> LegacyFulfillment (ACL) +``` + +## 6. Architecture de code recommandee + +```text +. +├── apps/ +│ └── symfony/ +│ ├── config/ +│ └── public/ +├── src/ +│ ├── Sales/ +│ │ ├── Domain/ +│ │ ├── Application/ +│ │ ├── Infrastructure/ +│ │ └── Interfaces/ +│ ├── Invoicing/ +│ │ ├── Domain/ +│ │ ├── Application/ +│ │ ├── Infrastructure/ +│ │ └── Interfaces/ +│ ├── LegacyFulfillment/ +│ │ ├── Domain/ +│ │ ├── Application/ +│ │ ├── Infrastructure/ +│ │ └── Interfaces/ +│ └── Shared/ +│ └── Technical/ # utilitaires purement techniques (Clock, UUID) +├── contracts/ +│ └── sales/ +│ └── v1/ +│ ├── Event/ +│ └── Api/ +├── tests/ +│ ├── Unit/ +│ ├── Integration/ +│ └── Contract/ +└── docs/ + ├── MINISHOP_DDD_BLUEBOOK_BOILERPLATE.md + └── tutorials/ +``` + +Regle stricte: + +- `src/Shared` ne contient aucun concept metier transverse. +- tout objet dans `contracts/` est stable, versionnable et "publique" entre BC. + +## 7. Convention de couches par BC + +- `Domain`: + - modeles metier, invariants, services de domaine, evenements de domaine + - aucune dependance framework +- `Application`: + - use cases (commands/queries), orchestration, ports (interfaces) +- `Infrastructure`: + - persistance, bus, clients HTTP, adaptateurs externes +- `Interfaces`: + - HTTP CLI Messenger, serializers, controleurs + +## 8. Contrats inter-BC (Published Language) + +Le contrat publie minimal recommande: + +- `contracts/sales/v1/Event/OrderPlaced.php` +- `contracts/sales/v1/Event/OrderConfirmed.php` +- `contracts/sales/v1/Api/OrderView.php` + +Regles: + +1. Contrats immutables. +2. Version explicite (`v1`, puis `v2`). +3. Compatibilite retro geree par ajout, pas mutation destructive. +4. Tests de contrat obligatoires (`tests/Contract`). + +## 9. Evolution tutorielle (roadmap) + +### Episode 00 - Squelette + integration naive + +- BC en place +- workflow `PlaceOrder -> Invoice -> LegacyShipment` +- appels synchrones directs inter-BC +- aucun Published Language officiel + +Tag Git: `step/00-naive` + +### Episode 01 - Published Language + +- extraction des DTO publics dans `contracts/sales/v1` +- premiere version de messages d'integration +- tests de schema/serialisation + +Tag Git: `step/01-published-language` + +### Episode 02 - Conformist (Invoicing) + +- `Invoicing` consomme tel quel `sales.v1` +- documentation explicite de la dependance upstream +- tests de non-regression de compatibilite + +Tag Git: `step/02-conformist` + +### Episode 03 - ACL (LegacyFulfillment) + +- creation d'une couche `AntiCorruption` cote `LegacyFulfillment` +- mapping `sales.v1 -> legacy command model` +- isolation des bizarreries legacy dans l'ACL + +Tag Git: `step/03-acl` + +### Episode 04 - OHS (Sales) + +- exposition d'un service hote explicite (`/api/sales/v1/...` ou endpoint message) +- documentation des points d'entree supportes +- modeles internes toujours non exposes + +Tag Git: `step/04-ohs` + +### Episode 05 - Hardening + +- outbox/event relay (optionnel selon niveau tuto) +- idempotence des consommateurs +- monitoring minimal (logs de correlation) + +Tag Git: `step/05-hardening` + +## 10. Definition of Done par episode + +Chaque episode doit fournir: + +1. code compilable et testable +2. diagramme context map a jour (`docs/tutorials/`) +3. note "ce qui change" + "pourquoi Blue Book" +4. tag git pour reset rapide + +## 11. Strategie de reset pour les tutos + +Approche recommandee: + +- une branche `main` stable (etat final ou etat de base choisi) +- des tags immuables `step/xx-*` +- une branche de travail recreee a chaque tuto depuis le tag + +Exemple: + +```bash +git fetch --tags +git switch -C play/episode-03 step/03-acl +``` + +## 12. Regles de purete Blue Book (checklist) + +- Le modele de domaine de chaque BC est independant des autres BC. +- La relation upstream/downstream est documentee. +- Les dependances de code suivent la context map, pas l'inverse. +- Les traductions de langage sont localisees (ACL), jamais diffuses partout. +- Les contrats publies sont versionnes et testes. +- Les cas d'usage ne contournent pas les invariants d'agregat. + +## 13. Gabarits minimaux utiles (pseudo-code) + +### 13.1 Port de publication d'evenement (Application) + +```php +interface SalesEventPublisher +{ + public function publishOrderConfirmed(OrderConfirmedMessage $message): void; +} +``` + +### 13.2 ACL vers legacy (Infrastructure) + +```php +final class LegacyShipmentAcl +{ + public function fromSalesOrderConfirmed(OrderConfirmedMessage $message): LegacyCreateShipmentCommand + { + // Mapping explicite du langage publie vers le modele legacy. + } +} +``` + +### 13.3 Use case de Sales (Application) + +```php +final class ConfirmOrderHandler +{ + public function __invoke(ConfirmOrderCommand $command): void + { + // Charge l'agregat, applique l'invariant, persiste, publie l'evenement d'integration. + } +} +``` + +## 14. Decoupage recommande des tests + +- `tests/Unit/Sales/Domain/*`: invariants d'agregat +- `tests/Unit/Invoicing/Domain/*`: regles de facturation +- `tests/Unit/LegacyFulfillment/Infrastructure/AntiCorruption/*`: mapping ACL +- `tests/Contract/Sales/v1/*`: stabilite du langage publie +- `tests/Integration/*`: wiring Symfony, bus, persistence + +## 15. Ce que ce boilerplate n'essaie pas de faire + +- couvrir tous les patterns strategiques DDD d'un coup +- fournir une architecture microservices complete +- optimiser prematurement la performance + +La priorite est la clarte pedagogique, la reproductibilite et la fidelite Blue Book. + +## 16. Specification minimale "codegen-ready" + +### 16.1 Sales - cas d'usage + +Commandes: + +- `PlaceOrder` +- `ConfirmOrder` +- `CancelOrder` + +Queries: + +- `GetOrderById` +- `ListOrdersByCustomer` + +Ports sortants: + +- `OrderRepository` +- `SalesEventPublisher` +- `Clock` +- `TransactionManager` + +Evenements d'integration publies: + +- `sales.v1.OrderPlaced` +- `sales.v1.OrderConfirmed` +- `sales.v1.OrderCancelled` + +### 16.2 Invoicing - cas d'usage + +Commandes: + +- `IssueInvoiceForExternalOrder` +- `MarkInvoiceAsSent` + +Queries: + +- `GetInvoiceByOrderRef` + +Ports sortants: + +- `InvoiceRepository` +- `InvoiceNumberGenerator` + +Evenements d'integration consommes: + +- `sales.v1.OrderConfirmed` (Conformist) + +### 16.3 LegacyFulfillment - cas d'usage + +Commandes: + +- `RequestShipmentFromSalesOrder` +- `MarkShipmentDispatched` + +Queries: + +- `GetShipmentByExternalOrderRef` + +Ports sortants: + +- `ShipmentRequestRepository` +- `LegacyFulfillmentGateway` + +Evenements d'integration consommes: + +- `sales.v1.OrderConfirmed` (via ACL) + +## 17. Matrice d'integration inter-BC + +| Producteur | Contrat | Consommateur | Pattern | Notes | +|---|---|---|---|---| +| Sales | `sales.v1.OrderPlaced` | (optionnel) Invoicing | Published Language | Peut etre ignore en phase initiale | +| Sales | `sales.v1.OrderConfirmed` | Invoicing | Conformist | Invoicing s'aligne sur le schema upstream | +| Sales | `sales.v1.OrderConfirmed` | LegacyFulfillment | ACL | Traduction vers format legacy | +| Sales | `sales.v1 API` | clients externes | OHS | API stable et versionnee | + +## 18. Plan d'implementation concret (ordre recommande) + +1. Implementer le domaine `Sales` (aggregate `Order`, invariants, repository in-memory). +2. Ajouter use case `PlaceOrder`, puis `ConfirmOrder`. +3. Brancher `Invoicing` en appel direct naif (episode 00). +4. Introduire `contracts/sales/v1` et migrer les echanges (episode 01). +5. Formaliser `Invoicing` en Conformist avec tests de contrat (episode 02). +6. Introduire `LegacyFulfillment` ACL et isoler tout mapping legacy (episode 03). +7. Exposer `Sales` via OHS versionne (episode 04). +8. Ajouter robustesse (idempotence, outbox optionnelle, correlation-id) (episode 05). + +--- + +Ce document sert de "constitution" du repo MiniShop. Toute evolution de structure doit justifier son impact sur la context map et sur les patterns pedagogiques cibles. diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..b3eec0d --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,34 @@ + + + + + + + + + + + + + tests/Unit + + + tests/Integration + + + tests/Contract + + + + + src + contracts + + + diff --git a/src/Invoicing/Application/Command/IssueInvoiceForExternalOrder.php b/src/Invoicing/Application/Command/IssueInvoiceForExternalOrder.php new file mode 100644 index 0000000..de8249d --- /dev/null +++ b/src/Invoicing/Application/Command/IssueInvoiceForExternalOrder.php @@ -0,0 +1,18 @@ + $lines + */ + public function __construct( + public string $externalOrderId, + public string $customerName, + public string $customerAddress, + public array $lines, + ) {} +} diff --git a/src/Invoicing/Application/Command/IssueInvoiceForExternalOrderHandler.php b/src/Invoicing/Application/Command/IssueInvoiceForExternalOrderHandler.php new file mode 100644 index 0000000..4dc6101 --- /dev/null +++ b/src/Invoicing/Application/Command/IssueInvoiceForExternalOrderHandler.php @@ -0,0 +1,49 @@ + new InvoiceLine( + $line['description'], + $line['quantity'], + $line['unitPriceInCents'], + $line['currency'], + TaxRate::standard(), + ), + $command->lines, + ); + + $invoice = Invoice::issueForExternalOrder( + InvoiceId::fromString(UuidGenerator::generate()), + $this->invoiceNumberGenerator->next(), + $command->externalOrderId, + new BillingParty($command->customerName, $command->customerAddress), + $lines, + $this->clock->now(), + ); + + $this->invoiceRepository->save($invoice); + } +} diff --git a/src/Invoicing/Application/Command/MarkInvoiceAsSent.php b/src/Invoicing/Application/Command/MarkInvoiceAsSent.php new file mode 100644 index 0000000..c17cff2 --- /dev/null +++ b/src/Invoicing/Application/Command/MarkInvoiceAsSent.php @@ -0,0 +1,12 @@ +invoiceRepository->get(InvoiceId::fromString($command->invoiceId)); + $invoice->markAsSent(); + $this->invoiceRepository->save($invoice); + } +} diff --git a/src/Invoicing/Application/Port/InvoiceNumberGenerator.php b/src/Invoicing/Application/Port/InvoiceNumberGenerator.php new file mode 100644 index 0000000..f591d4b --- /dev/null +++ b/src/Invoicing/Application/Port/InvoiceNumberGenerator.php @@ -0,0 +1,10 @@ +invoiceRepository->findByExternalOrderId($query->externalOrderId); + } +} diff --git a/src/Invoicing/Domain/Event/InvoiceIssued.php b/src/Invoicing/Domain/Event/InvoiceIssued.php new file mode 100644 index 0000000..163b472 --- /dev/null +++ b/src/Invoicing/Domain/Event/InvoiceIssued.php @@ -0,0 +1,18 @@ + */ + private array $domainEvents = []; + + /** + * @param list $lines + */ + private function __construct( + public readonly InvoiceId $id, + public readonly string $invoiceNumber, + public readonly string $externalOrderId, + public readonly BillingParty $billingParty, + private readonly array $lines, + private readonly \DateTimeImmutable $issuedAt, + ) { + $this->status = InvoiceStatus::Issued; + } + + /** + * @param list $lines + */ + public static function issueForExternalOrder( + InvoiceId $id, + string $invoiceNumber, + string $externalOrderId, + BillingParty $billingParty, + array $lines, + \DateTimeImmutable $issuedAt, + ): self { + if ($lines === []) { + throw new \DomainException('An invoice must have at least one line.'); + } + + $invoice = new self($id, $invoiceNumber, $externalOrderId, $billingParty, $lines, $issuedAt); + + $invoice->recordEvent(new InvoiceIssued( + $id, + $invoiceNumber, + $externalOrderId, + $invoice->totalInclTax(), + $issuedAt, + )); + + return $invoice; + } + + public function markAsSent(): void + { + if ($this->status === InvoiceStatus::Sent) { + throw new \DomainException('Invoice is already sent.'); + } + + $this->status = InvoiceStatus::Sent; + } + + public function totalExclTax(): int + { + return array_sum(array_map( + static fn (InvoiceLine $line): int => $line->lineTotalExclTax(), + $this->lines, + )); + } + + public function totalInclTax(): int + { + return array_sum(array_map( + static fn (InvoiceLine $line): int => $line->lineTotalInclTax(), + $this->lines, + )); + } + + public function status(): InvoiceStatus + { + return $this->status; + } + + /** @return list */ + public function lines(): array + { + return $this->lines; + } + + public function issuedAt(): \DateTimeImmutable + { + return $this->issuedAt; + } + + /** @return list */ + public function releaseEvents(): array + { + $events = $this->domainEvents; + $this->domainEvents = []; + + return $events; + } + + private function recordEvent(object $event): void + { + $this->domainEvents[] = $event; + } +} diff --git a/src/Invoicing/Domain/Model/InvoiceId.php b/src/Invoicing/Domain/Model/InvoiceId.php new file mode 100644 index 0000000..eac2474 --- /dev/null +++ b/src/Invoicing/Domain/Model/InvoiceId.php @@ -0,0 +1,25 @@ +value; + } + + public function equals(self $other): bool + { + return $this->value === $other->value; + } +} diff --git a/src/Invoicing/Domain/Model/InvoiceLine.php b/src/Invoicing/Domain/Model/InvoiceLine.php new file mode 100644 index 0000000..070e593 --- /dev/null +++ b/src/Invoicing/Domain/Model/InvoiceLine.php @@ -0,0 +1,30 @@ +quantity * $this->unitPriceInCents; + } + + public function lineTotalInclTax(): int + { + return (int) round($this->lineTotalExclTax() * (1 + $this->taxRate->rate)); + } +} diff --git a/src/Invoicing/Domain/Model/InvoiceStatus.php b/src/Invoicing/Domain/Model/InvoiceStatus.php new file mode 100644 index 0000000..8f467f2 --- /dev/null +++ b/src/Invoicing/Domain/Model/InvoiceStatus.php @@ -0,0 +1,11 @@ + 1) { + throw new \InvalidArgumentException('Tax rate must be between 0 and 1.'); + } + } + + public static function standard(): self + { + return new self(0.20, 'TVA 20%'); + } + + public static function zero(): self + { + return new self(0.0, 'Exonere'); + } +} diff --git a/src/Invoicing/Infrastructure/Persistence/InMemoryInvoiceRepository.php b/src/Invoicing/Infrastructure/Persistence/InMemoryInvoiceRepository.php new file mode 100644 index 0000000..85505da --- /dev/null +++ b/src/Invoicing/Infrastructure/Persistence/InMemoryInvoiceRepository.php @@ -0,0 +1,37 @@ + */ + private array $invoices = []; + + public function save(Invoice $invoice): void + { + $this->invoices[$invoice->id->toString()] = $invoice; + } + + public function get(InvoiceId $id): Invoice + { + return $this->invoices[$id->toString()] + ?? throw new \RuntimeException(sprintf('Invoice "%s" not found.', $id->toString())); + } + + public function findByExternalOrderId(string $externalOrderId): ?Invoice + { + foreach ($this->invoices as $invoice) { + if ($invoice->externalOrderId === $externalOrderId) { + return $invoice; + } + } + + return null; + } +} diff --git a/src/Invoicing/Infrastructure/SequentialInvoiceNumberGenerator.php b/src/Invoicing/Infrastructure/SequentialInvoiceNumberGenerator.php new file mode 100644 index 0000000..e61cef7 --- /dev/null +++ b/src/Invoicing/Infrastructure/SequentialInvoiceNumberGenerator.php @@ -0,0 +1,17 @@ +counter); + } +} diff --git a/src/Invoicing/Interfaces/Http/GetInvoiceController.php b/src/Invoicing/Interfaces/Http/GetInvoiceController.php new file mode 100644 index 0000000..633b2d3 --- /dev/null +++ b/src/Invoicing/Interfaces/Http/GetInvoiceController.php @@ -0,0 +1,52 @@ +handler)(new GetInvoiceByOrderRef($externalOrderId)); + + if ($invoice === null) { + return new JsonResponse(['error' => 'Invoice not found.'], Response::HTTP_NOT_FOUND); + } + + return new JsonResponse([ + 'invoiceId' => $invoice->id->toString(), + 'invoiceNumber' => $invoice->invoiceNumber, + 'externalOrderId' => $invoice->externalOrderId, + 'billingParty' => [ + 'name' => $invoice->billingParty->name, + 'address' => $invoice->billingParty->address, + ], + 'status' => $invoice->status()->value, + 'totalExclTax' => $invoice->totalExclTax(), + 'totalInclTax' => $invoice->totalInclTax(), + 'lines' => array_map( + static fn (InvoiceLine $line): array => [ + 'description' => $line->description, + 'quantity' => $line->quantity, + 'unitPriceInCents' => $line->unitPriceInCents, + 'lineTotalExclTax' => $line->lineTotalExclTax(), + 'lineTotalInclTax' => $line->lineTotalInclTax(), + ], + $invoice->lines(), + ), + ]); + } +} diff --git a/src/LegacyFulfillment/Application/Command/MarkShipmentDispatched.php b/src/LegacyFulfillment/Application/Command/MarkShipmentDispatched.php new file mode 100644 index 0000000..88cda83 --- /dev/null +++ b/src/LegacyFulfillment/Application/Command/MarkShipmentDispatched.php @@ -0,0 +1,12 @@ +externalOrderId); + $request = $this->shipmentRequestRepository->findByOrderRef($ref) + ?? throw new \RuntimeException(sprintf('Shipment for order "%s" not found.', $command->externalOrderId)); + + $request->markDispatched(); + $this->shipmentRequestRepository->save($request); + } +} diff --git a/src/LegacyFulfillment/Application/Command/RequestShipmentFromSalesOrder.php b/src/LegacyFulfillment/Application/Command/RequestShipmentFromSalesOrder.php new file mode 100644 index 0000000..d590148 --- /dev/null +++ b/src/LegacyFulfillment/Application/Command/RequestShipmentFromSalesOrder.php @@ -0,0 +1,19 @@ +externalOrderId), + new ShippingAddress( + $command->recipientName, + $command->street, + $command->city, + $command->postalCode, + $command->country, + ), + new ParcelSpec($command->totalWeightInGrams, $command->description), + $this->clock->now(), + ); + + $this->shipmentRequestRepository->save($request); + $this->gateway->sendShipmentRequest($request); + } +} diff --git a/src/LegacyFulfillment/Application/Port/LegacyFulfillmentGateway.php b/src/LegacyFulfillment/Application/Port/LegacyFulfillmentGateway.php new file mode 100644 index 0000000..47f9033 --- /dev/null +++ b/src/LegacyFulfillment/Application/Port/LegacyFulfillmentGateway.php @@ -0,0 +1,12 @@ +shipmentRequestRepository->findByOrderRef( + LegacyOrderRef::fromExternalId($query->externalOrderId), + ); + } +} diff --git a/src/LegacyFulfillment/Domain/Event/ShipmentRequested.php b/src/LegacyFulfillment/Domain/Event/ShipmentRequested.php new file mode 100644 index 0000000..b889639 --- /dev/null +++ b/src/LegacyFulfillment/Domain/Event/ShipmentRequested.php @@ -0,0 +1,15 @@ +value; + } + + public function equals(self $other): bool + { + return $this->value === $other->value; + } +} diff --git a/src/LegacyFulfillment/Domain/Model/ParcelSpec.php b/src/LegacyFulfillment/Domain/Model/ParcelSpec.php new file mode 100644 index 0000000..6e24fe0 --- /dev/null +++ b/src/LegacyFulfillment/Domain/Model/ParcelSpec.php @@ -0,0 +1,17 @@ + */ + private array $domainEvents = []; + + private function __construct( + public readonly LegacyOrderRef $orderRef, + public readonly ShippingAddress $address, + public readonly ParcelSpec $parcel, + private readonly \DateTimeImmutable $requestedAt, + ) { + $this->status = ShipmentStatus::Requested; + } + + public static function fromSalesOrder( + LegacyOrderRef $orderRef, + ShippingAddress $address, + ParcelSpec $parcel, + \DateTimeImmutable $requestedAt, + ): self { + $request = new self($orderRef, $address, $parcel, $requestedAt); + + $request->recordEvent(new ShipmentRequested($orderRef, $requestedAt)); + + return $request; + } + + public function markDispatched(): void + { + if ($this->status === ShipmentStatus::Dispatched) { + throw new \DomainException('Shipment already dispatched.'); + } + + $this->status = ShipmentStatus::Dispatched; + } + + public function status(): ShipmentStatus + { + return $this->status; + } + + public function requestedAt(): \DateTimeImmutable + { + return $this->requestedAt; + } + + /** @return list */ + public function releaseEvents(): array + { + $events = $this->domainEvents; + $this->domainEvents = []; + + return $events; + } + + private function recordEvent(object $event): void + { + $this->domainEvents[] = $event; + } +} diff --git a/src/LegacyFulfillment/Domain/Model/ShipmentStatus.php b/src/LegacyFulfillment/Domain/Model/ShipmentStatus.php new file mode 100644 index 0000000..b0b3f1d --- /dev/null +++ b/src/LegacyFulfillment/Domain/Model/ShipmentStatus.php @@ -0,0 +1,11 @@ + */ + private array $sentRequests = []; + + public function sendShipmentRequest(ShipmentRequest $request): void + { + $this->sentRequests[] = $request; + } + + /** @return list */ + public function sentRequests(): array + { + return $this->sentRequests; + } +} diff --git a/src/LegacyFulfillment/Infrastructure/Persistence/InMemoryShipmentRequestRepository.php b/src/LegacyFulfillment/Infrastructure/Persistence/InMemoryShipmentRequestRepository.php new file mode 100644 index 0000000..687f601 --- /dev/null +++ b/src/LegacyFulfillment/Infrastructure/Persistence/InMemoryShipmentRequestRepository.php @@ -0,0 +1,25 @@ + */ + private array $requests = []; + + public function save(ShipmentRequest $request): void + { + $this->requests[$request->orderRef->toString()] = $request; + } + + public function findByOrderRef(LegacyOrderRef $orderRef): ?ShipmentRequest + { + return $this->requests[$orderRef->toString()] ?? null; + } +} diff --git a/src/LegacyFulfillment/Interfaces/Http/GetShipmentController.php b/src/LegacyFulfillment/Interfaces/Http/GetShipmentController.php new file mode 100644 index 0000000..cd89300 --- /dev/null +++ b/src/LegacyFulfillment/Interfaces/Http/GetShipmentController.php @@ -0,0 +1,44 @@ +handler)(new GetShipmentByExternalOrderRef($externalOrderId)); + + if ($shipment === null) { + return new JsonResponse(['error' => 'Shipment not found.'], Response::HTTP_NOT_FOUND); + } + + return new JsonResponse([ + 'orderRef' => $shipment->orderRef->toString(), + 'status' => $shipment->status()->value, + 'address' => [ + 'recipientName' => $shipment->address->recipientName, + 'street' => $shipment->address->street, + 'city' => $shipment->address->city, + 'postalCode' => $shipment->address->postalCode, + 'country' => $shipment->address->country, + ], + 'parcel' => [ + 'weightInGrams' => $shipment->parcel->weightInGrams, + 'description' => $shipment->parcel->description, + ], + ]); + } +} diff --git a/src/Sales/Application/Command/CancelOrder.php b/src/Sales/Application/Command/CancelOrder.php new file mode 100644 index 0000000..e3095c4 --- /dev/null +++ b/src/Sales/Application/Command/CancelOrder.php @@ -0,0 +1,12 @@ +orderRepository->get(OrderId::fromString($command->orderId)); + $order->cancel(); + $this->orderRepository->save($order); + + foreach ($order->releaseEvents() as $event) { + if ($event instanceof OrderCancelled) { + $this->publisher->publishOrderCancelled($event); + } + } + } +} diff --git a/src/Sales/Application/Command/ConfirmOrder.php b/src/Sales/Application/Command/ConfirmOrder.php new file mode 100644 index 0000000..979833a --- /dev/null +++ b/src/Sales/Application/Command/ConfirmOrder.php @@ -0,0 +1,12 @@ +orderRepository->get(OrderId::fromString($command->orderId)); + $order->confirm(); + $this->orderRepository->save($order); + + foreach ($order->releaseEvents() as $event) { + if ($event instanceof OrderConfirmed) { + $this->publisher->publishOrderConfirmed($event); + } + } + } +} diff --git a/src/Sales/Application/Command/PlaceOrder.php b/src/Sales/Application/Command/PlaceOrder.php new file mode 100644 index 0000000..eacb3b2 --- /dev/null +++ b/src/Sales/Application/Command/PlaceOrder.php @@ -0,0 +1,17 @@ + $lines + */ + public function __construct( + public string $orderId, + public string $customerId, + public array $lines, + ) {} +} diff --git a/src/Sales/Application/Command/PlaceOrderHandler.php b/src/Sales/Application/Command/PlaceOrderHandler.php new file mode 100644 index 0000000..1ea1cc1 --- /dev/null +++ b/src/Sales/Application/Command/PlaceOrderHandler.php @@ -0,0 +1,51 @@ + new OrderLine( + $line['productName'], + $line['quantity'], + new Money($line['unitPriceInCents'], $line['currency']), + ), + $command->lines, + ); + + $order = Order::place( + OrderId::fromString($command->orderId), + CustomerId::fromString($command->customerId), + $lines, + $this->clock->now(), + ); + + $this->orderRepository->save($order); + + foreach ($order->releaseEvents() as $event) { + if ($event instanceof OrderPlaced) { + $this->publisher->publishOrderPlaced($event); + } + } + } +} diff --git a/src/Sales/Application/Port/OrderRepository.php b/src/Sales/Application/Port/OrderRepository.php new file mode 100644 index 0000000..f0b23cd --- /dev/null +++ b/src/Sales/Application/Port/OrderRepository.php @@ -0,0 +1,19 @@ + */ + public function findByCustomer(CustomerId $customerId): array; +} diff --git a/src/Sales/Application/Port/SalesEventPublisher.php b/src/Sales/Application/Port/SalesEventPublisher.php new file mode 100644 index 0000000..13b7dc2 --- /dev/null +++ b/src/Sales/Application/Port/SalesEventPublisher.php @@ -0,0 +1,18 @@ +orderRepository->get(OrderId::fromString($query->orderId)); + } +} diff --git a/src/Sales/Application/Query/ListOrdersByCustomer.php b/src/Sales/Application/Query/ListOrdersByCustomer.php new file mode 100644 index 0000000..3bc8592 --- /dev/null +++ b/src/Sales/Application/Query/ListOrdersByCustomer.php @@ -0,0 +1,12 @@ + */ + public function __invoke(ListOrdersByCustomer $query): array + { + return $this->orderRepository->findByCustomer(CustomerId::fromString($query->customerId)); + } +} diff --git a/src/Sales/Domain/Event/OrderCancelled.php b/src/Sales/Domain/Event/OrderCancelled.php new file mode 100644 index 0000000..4ea2eb8 --- /dev/null +++ b/src/Sales/Domain/Event/OrderCancelled.php @@ -0,0 +1,14 @@ + $lines + */ + public function __construct( + public OrderId $orderId, + public CustomerId $customerId, + public Money $total, + public array $lines, + ) {} +} diff --git a/src/Sales/Domain/Event/OrderPlaced.php b/src/Sales/Domain/Event/OrderPlaced.php new file mode 100644 index 0000000..d0ae628 --- /dev/null +++ b/src/Sales/Domain/Event/OrderPlaced.php @@ -0,0 +1,19 @@ +value)); + } + + public static function cannotCancel(OrderStatus $current): self + { + return new self(sprintf('Cannot cancel order in "%s" state.', $current->value)); + } +} diff --git a/src/Sales/Domain/Model/CustomerId.php b/src/Sales/Domain/Model/CustomerId.php new file mode 100644 index 0000000..a59c61c --- /dev/null +++ b/src/Sales/Domain/Model/CustomerId.php @@ -0,0 +1,25 @@ +value; + } + + public function equals(self $other): bool + { + return $this->value === $other->value; + } +} diff --git a/src/Sales/Domain/Model/Money.php b/src/Sales/Domain/Model/Money.php new file mode 100644 index 0000000..7312b46 --- /dev/null +++ b/src/Sales/Domain/Model/Money.php @@ -0,0 +1,39 @@ +currency !== $other->currency) { + throw new \InvalidArgumentException( + sprintf('Cannot add %s to %s.', $other->currency, $this->currency), + ); + } + + return new self($this->amount + $other->amount, $this->currency); + } + + public function isPositive(): bool + { + return $this->amount > 0; + } + + public function equals(self $other): bool + { + return $this->amount === $other->amount && $this->currency === $other->currency; + } +} diff --git a/src/Sales/Domain/Model/Order.php b/src/Sales/Domain/Model/Order.php new file mode 100644 index 0000000..d5cfde4 --- /dev/null +++ b/src/Sales/Domain/Model/Order.php @@ -0,0 +1,115 @@ + */ + private array $lines; + /** @var list */ + private array $domainEvents = []; + + private function __construct( + public readonly OrderId $id, + public readonly CustomerId $customerId, + private readonly \DateTimeImmutable $placedAt, + ) { + $this->status = OrderStatus::Draft; + $this->lines = []; + } + + /** + * @param list $lines + */ + public static function place( + OrderId $id, + CustomerId $customerId, + array $lines, + \DateTimeImmutable $placedAt, + ): self { + if ($lines === []) { + throw EmptyOrderException::noLines(); + } + + $order = new self($id, $customerId, $placedAt); + $order->lines = $lines; + $order->status = OrderStatus::Placed; + + $order->recordEvent(new OrderPlaced($id, $customerId, $order->total(), $placedAt)); + + return $order; + } + + public function confirm(): void + { + if ($this->status !== OrderStatus::Placed) { + throw InvalidOrderStateException::cannotConfirm($this->status); + } + + $this->status = OrderStatus::Confirmed; + $this->recordEvent(new OrderConfirmed( + $this->id, + $this->customerId, + $this->total(), + $this->lines, + )); + } + + public function cancel(): void + { + if ($this->status !== OrderStatus::Placed) { + throw InvalidOrderStateException::cannotCancel($this->status); + } + + $this->status = OrderStatus::Cancelled; + $this->recordEvent(new OrderCancelled($this->id)); + } + + public function total(): Money + { + return array_reduce( + $this->lines, + static fn (Money $carry, OrderLine $line): Money => $carry->add($line->lineTotal()), + Money::zero($this->lines[0]->unitPrice->currency), + ); + } + + public function status(): OrderStatus + { + return $this->status; + } + + /** @return list */ + public function lines(): array + { + return $this->lines; + } + + public function placedAt(): \DateTimeImmutable + { + return $this->placedAt; + } + + /** @return list */ + public function releaseEvents(): array + { + $events = $this->domainEvents; + $this->domainEvents = []; + + return $events; + } + + private function recordEvent(object $event): void + { + $this->domainEvents[] = $event; + } +} diff --git a/src/Sales/Domain/Model/OrderId.php b/src/Sales/Domain/Model/OrderId.php new file mode 100644 index 0000000..5d663df --- /dev/null +++ b/src/Sales/Domain/Model/OrderId.php @@ -0,0 +1,25 @@ +value; + } + + public function equals(self $other): bool + { + return $this->value === $other->value; + } +} diff --git a/src/Sales/Domain/Model/OrderLine.php b/src/Sales/Domain/Model/OrderLine.php new file mode 100644 index 0000000..cdcfb82 --- /dev/null +++ b/src/Sales/Domain/Model/OrderLine.php @@ -0,0 +1,26 @@ +unitPrice->amount * $this->quantity, + $this->unitPrice->currency, + ); + } +} diff --git a/src/Sales/Domain/Model/OrderStatus.php b/src/Sales/Domain/Model/OrderStatus.php new file mode 100644 index 0000000..26f879c --- /dev/null +++ b/src/Sales/Domain/Model/OrderStatus.php @@ -0,0 +1,13 @@ +issueInvoiceHandler)(new IssueInvoiceForExternalOrder( + externalOrderId: $event->orderId->toString(), + customerName: 'Customer ' . $event->customerId->toString(), + customerAddress: 'N/A', + lines: array_map( + static fn (OrderLine $line): array => [ + 'description' => $line->productName, + 'quantity' => $line->quantity, + 'unitPriceInCents' => $line->unitPrice->amount, + 'currency' => $line->unitPrice->currency, + ], + $event->lines, + ), + )); + + // Appel direct vers LegacyFulfillment — couplage naif + ($this->requestShipmentHandler)(new RequestShipmentFromSalesOrder( + externalOrderId: $event->orderId->toString(), + recipientName: 'Customer ' . $event->customerId->toString(), + street: '1 Rue du Commerce', + city: 'Paris', + postalCode: '75001', + country: 'FR', + totalWeightInGrams: 1000, + description: sprintf('Order %s', $event->orderId->toString()), + )); + } + + public function publishOrderCancelled(OrderCancelled $event): void + { + // Pas d'action downstream sur OrderCancelled dans le scenario naif. + } +} diff --git a/src/Sales/Infrastructure/Persistence/InMemoryOrderRepository.php b/src/Sales/Infrastructure/Persistence/InMemoryOrderRepository.php new file mode 100644 index 0000000..c4e3ec0 --- /dev/null +++ b/src/Sales/Infrastructure/Persistence/InMemoryOrderRepository.php @@ -0,0 +1,38 @@ + */ + private array $orders = []; + + public function save(Order $order): void + { + $this->orders[$order->id->toString()] = $order; + } + + public function get(OrderId $id): Order + { + return $this->orders[$id->toString()] + ?? throw new \RuntimeException(sprintf('Order "%s" not found.', $id->toString())); + } + + /** @return list */ + public function findByCustomer(CustomerId $customerId): array + { + return array_values( + array_filter( + $this->orders, + static fn (Order $order): bool => $order->customerId->equals($customerId), + ), + ); + } +} diff --git a/src/Sales/Interfaces/Http/CancelOrderController.php b/src/Sales/Interfaces/Http/CancelOrderController.php new file mode 100644 index 0000000..7183288 --- /dev/null +++ b/src/Sales/Interfaces/Http/CancelOrderController.php @@ -0,0 +1,26 @@ +handler)(new CancelOrder(orderId: $orderId)); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Sales/Interfaces/Http/ConfirmOrderController.php b/src/Sales/Interfaces/Http/ConfirmOrderController.php new file mode 100644 index 0000000..121452f --- /dev/null +++ b/src/Sales/Interfaces/Http/ConfirmOrderController.php @@ -0,0 +1,26 @@ +handler)(new ConfirmOrder(orderId: $orderId)); + + return new JsonResponse(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Sales/Interfaces/Http/GetOrderController.php b/src/Sales/Interfaces/Http/GetOrderController.php new file mode 100644 index 0000000..f09268e --- /dev/null +++ b/src/Sales/Interfaces/Http/GetOrderController.php @@ -0,0 +1,44 @@ +handler)(new GetOrderById($orderId)); + + return new JsonResponse([ + 'orderId' => $order->id->toString(), + 'customerId' => $order->customerId->toString(), + 'status' => $order->status()->value, + 'total' => [ + 'amount' => $order->total()->amount, + 'currency' => $order->total()->currency, + ], + 'lines' => array_map( + static fn (OrderLine $line): array => [ + 'productName' => $line->productName, + 'quantity' => $line->quantity, + 'unitPrice' => $line->unitPrice->amount, + 'currency' => $line->unitPrice->currency, + 'lineTotal' => $line->lineTotal()->amount, + ], + $order->lines(), + ), + ]); + } +} diff --git a/src/Sales/Interfaces/Http/ListCustomerOrdersController.php b/src/Sales/Interfaces/Http/ListCustomerOrdersController.php new file mode 100644 index 0000000..6042e7b --- /dev/null +++ b/src/Sales/Interfaces/Http/ListCustomerOrdersController.php @@ -0,0 +1,36 @@ +handler)(new ListOrdersByCustomer($customerId)); + + return new JsonResponse(array_map( + static fn (Order $order): array => [ + 'orderId' => $order->id->toString(), + 'status' => $order->status()->value, + 'total' => [ + 'amount' => $order->total()->amount, + 'currency' => $order->total()->currency, + ], + ], + $orders, + )); + } +} diff --git a/src/Sales/Interfaces/Http/PlaceOrderController.php b/src/Sales/Interfaces/Http/PlaceOrderController.php new file mode 100644 index 0000000..f1fa4f7 --- /dev/null +++ b/src/Sales/Interfaces/Http/PlaceOrderController.php @@ -0,0 +1,37 @@ +} $data */ + $data = json_decode($request->getContent(), true, flags: JSON_THROW_ON_ERROR); + + $orderId = UuidGenerator::generate(); + + ($this->handler)(new PlaceOrder( + orderId: $orderId, + customerId: $data['customerId'], + lines: $data['lines'], + )); + + return new JsonResponse(['orderId' => $orderId], Response::HTTP_CREATED); + } +} diff --git a/src/Shared/Technical/Clock.php b/src/Shared/Technical/Clock.php new file mode 100644 index 0000000..e4d3755 --- /dev/null +++ b/src/Shared/Technical/Clock.php @@ -0,0 +1,10 @@ + ConfirmOrder -> Invoice + Shipment. + * Illustre le couplage direct entre BC (episode 00). + */ +final class NaiveWorkflowTest extends TestCase +{ + public function test_full_naive_workflow(): void + { + // --- Setup : cablage naif synchrone --- + $clock = new SystemClock(); + $orderRepo = new InMemoryOrderRepository(); + $invoiceRepo = new InMemoryInvoiceRepository(); + $shipmentRepo = new InMemoryShipmentRequestRepository(); + $gateway = new FakeLegacyFulfillmentGateway(); + + $issueInvoiceHandler = new IssueInvoiceForExternalOrderHandler( + $invoiceRepo, + new SequentialInvoiceNumberGenerator(), + $clock, + ); + $requestShipmentHandler = new RequestShipmentFromSalesOrderHandler( + $shipmentRepo, + $gateway, + $clock, + ); + $publisher = new NaiveSalesEventPublisher($issueInvoiceHandler, $requestShipmentHandler); + + $placeOrderHandler = new PlaceOrderHandler($orderRepo, $publisher, $clock); + $confirmOrderHandler = new ConfirmOrderHandler($orderRepo, $publisher); + + $orderId = 'test-order-001'; + + // --- Act : passer et confirmer une commande --- + ($placeOrderHandler)(new PlaceOrder( + orderId: $orderId, + customerId: 'cust-001', + lines: [ + ['productName' => 'Widget', 'quantity' => 2, 'unitPriceInCents' => 1500, 'currency' => 'EUR'], + ['productName' => 'Gadget', 'quantity' => 1, 'unitPriceInCents' => 2500, 'currency' => 'EUR'], + ], + )); + + ($confirmOrderHandler)(new ConfirmOrder(orderId: $orderId)); + + // --- Assert : ordre confirme --- + $order = $orderRepo->get(OrderId::fromString($orderId)); + self::assertSame(OrderStatus::Confirmed, $order->status()); + + // --- Assert : facture creee --- + $invoice = $invoiceRepo->findByExternalOrderId($orderId); + self::assertNotNull($invoice, 'An invoice should have been created for the confirmed order.'); + self::assertSame($orderId, $invoice->externalOrderId); + self::assertCount(2, $invoice->lines()); + + // --- Assert : expedition demandee --- + $shipment = $shipmentRepo->findByOrderRef(LegacyOrderRef::fromExternalId($orderId)); + self::assertNotNull($shipment, 'A shipment request should have been created.'); + self::assertSame(ShipmentStatus::Requested, $shipment->status()); + + // --- Assert : gateway legacy appele --- + self::assertCount(1, $gateway->sentRequests()); + } +} diff --git a/tests/Unit/Invoicing/Domain/InvoiceTest.php b/tests/Unit/Invoicing/Domain/InvoiceTest.php new file mode 100644 index 0000000..d618625 --- /dev/null +++ b/tests/Unit/Invoicing/Domain/InvoiceTest.php @@ -0,0 +1,97 @@ +issueInvoice(); + + self::assertSame(InvoiceStatus::Issued, $invoice->status()); + self::assertSame('ext-order-1', $invoice->externalOrderId); + self::assertCount(1, $invoice->lines()); + } + + public function test_issue_invoice_records_event(): void + { + $invoice = $this->issueInvoice(); + + $events = $invoice->releaseEvents(); + self::assertCount(1, $events); + self::assertInstanceOf(InvoiceIssued::class, $events[0]); + } + + public function test_invoice_without_lines_throws(): void + { + $this->expectException(\DomainException::class); + + Invoice::issueForExternalOrder( + InvoiceId::fromString('inv-1'), + 'INV-000001', + 'ext-order-1', + new BillingParty('Acme', '1 Rue Test'), + [], + new \DateTimeImmutable(), + ); + } + + public function test_total_coherent_with_lines(): void + { + $invoice = Invoice::issueForExternalOrder( + InvoiceId::fromString('inv-1'), + 'INV-000001', + 'ext-order-1', + new BillingParty('Acme', '1 Rue Test'), + [ + new InvoiceLine('Widget', 2, 1000, 'EUR', TaxRate::standard()), + new InvoiceLine('Gadget', 1, 2500, 'EUR', TaxRate::standard()), + ], + new \DateTimeImmutable(), + ); + + self::assertSame(4500, $invoice->totalExclTax()); + self::assertSame(5400, $invoice->totalInclTax()); + } + + public function test_mark_as_sent(): void + { + $invoice = $this->issueInvoice(); + + $invoice->markAsSent(); + + self::assertSame(InvoiceStatus::Sent, $invoice->status()); + } + + public function test_mark_as_sent_twice_throws(): void + { + $this->expectException(\DomainException::class); + + $invoice = $this->issueInvoice(); + $invoice->markAsSent(); + $invoice->markAsSent(); + } + + private function issueInvoice(): Invoice + { + return Invoice::issueForExternalOrder( + InvoiceId::fromString('inv-1'), + 'INV-000001', + 'ext-order-1', + new BillingParty('Acme', '1 Rue Test'), + [new InvoiceLine('Widget', 2, 1500, 'EUR', TaxRate::standard())], + new \DateTimeImmutable(), + ); + } +} diff --git a/tests/Unit/LegacyFulfillment/Domain/ShipmentRequestTest.php b/tests/Unit/LegacyFulfillment/Domain/ShipmentRequestTest.php new file mode 100644 index 0000000..e795e90 --- /dev/null +++ b/tests/Unit/LegacyFulfillment/Domain/ShipmentRequestTest.php @@ -0,0 +1,68 @@ +createRequest(); + + self::assertSame(ShipmentStatus::Requested, $request->status()); + self::assertSame('order-1', $request->orderRef->toString()); + } + + public function test_create_records_event(): void + { + $request = $this->createRequest(); + + $events = $request->releaseEvents(); + self::assertCount(1, $events); + self::assertInstanceOf(ShipmentRequested::class, $events[0]); + } + + public function test_mark_dispatched(): void + { + $request = $this->createRequest(); + + $request->markDispatched(); + + self::assertSame(ShipmentStatus::Dispatched, $request->status()); + } + + public function test_mark_dispatched_twice_throws(): void + { + $this->expectException(\DomainException::class); + + $request = $this->createRequest(); + $request->markDispatched(); + $request->markDispatched(); + } + + public function test_parcel_with_zero_weight_throws(): void + { + $this->expectException(\InvalidArgumentException::class); + + new ParcelSpec(0, 'Widget'); + } + + private function createRequest(): ShipmentRequest + { + return ShipmentRequest::fromSalesOrder( + LegacyOrderRef::fromExternalId('order-1'), + new ShippingAddress('John Doe', '1 Rue du Commerce', 'Paris', '75001', 'FR'), + new ParcelSpec(1000, 'Order order-1'), + new \DateTimeImmutable(), + ); + } +} diff --git a/tests/Unit/Sales/Domain/OrderTest.php b/tests/Unit/Sales/Domain/OrderTest.php new file mode 100644 index 0000000..f2ebac0 --- /dev/null +++ b/tests/Unit/Sales/Domain/OrderTest.php @@ -0,0 +1,122 @@ +placeOrder(); + + self::assertSame(OrderStatus::Placed, $order->status()); + self::assertCount(1, $order->lines()); + self::assertTrue($order->total()->isPositive()); + } + + public function test_place_order_records_order_placed_event(): void + { + $order = $this->placeOrder(); + + $events = $order->releaseEvents(); + self::assertCount(1, $events); + self::assertInstanceOf(OrderPlaced::class, $events[0]); + } + + public function test_place_order_without_lines_throws(): void + { + $this->expectException(EmptyOrderException::class); + + Order::place( + OrderId::fromString('order-1'), + CustomerId::fromString('cust-1'), + [], + new \DateTimeImmutable(), + ); + } + + public function test_confirm_placed_order(): void + { + $order = $this->placeOrder(); + $order->releaseEvents(); + + $order->confirm(); + + self::assertSame(OrderStatus::Confirmed, $order->status()); + $events = $order->releaseEvents(); + self::assertCount(1, $events); + self::assertInstanceOf(OrderConfirmed::class, $events[0]); + } + + public function test_confirm_draft_order_throws(): void + { + $this->expectException(InvalidOrderStateException::class); + + $order = $this->placeOrder(); + $order->confirm(); + $order->releaseEvents(); + + $order->confirm(); // already confirmed + } + + public function test_cancel_placed_order(): void + { + $order = $this->placeOrder(); + $order->releaseEvents(); + + $order->cancel(); + + self::assertSame(OrderStatus::Cancelled, $order->status()); + $events = $order->releaseEvents(); + self::assertCount(1, $events); + self::assertInstanceOf(OrderCancelled::class, $events[0]); + } + + public function test_cannot_cancel_confirmed_order(): void + { + $this->expectException(InvalidOrderStateException::class); + + $order = $this->placeOrder(); + $order->confirm(); + $order->cancel(); + } + + public function test_total_is_sum_of_line_totals(): void + { + $order = Order::place( + OrderId::fromString('order-1'), + CustomerId::fromString('cust-1'), + [ + new OrderLine('Widget', 2, new Money(1000, 'EUR')), + new OrderLine('Gadget', 1, new Money(2500, 'EUR')), + ], + new \DateTimeImmutable(), + ); + + self::assertTrue($order->total()->equals(new Money(4500, 'EUR'))); + } + + private function placeOrder(): Order + { + return Order::place( + OrderId::fromString('order-1'), + CustomerId::fromString('cust-1'), + [new OrderLine('Widget', 2, new Money(1500, 'EUR'))], + new \DateTimeImmutable(), + ); + } +}