diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..e76a996 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: 'composer' + directory: '/' + schedule: + interval: 'daily' + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..a0daf2c --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,30 @@ +name: Test + +on: + push: + branches: + - main + pull_request: ~ + +jobs: + test: + name: Test + strategy: + matrix: + php-version: ['8.3'] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer + + - name: Install composer dependencies + run: composer install --prefer-dist --no-progress + + - name: Run tests + run: composer test diff --git a/.gitignore b/.gitignore index a67f91e..1506387 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - ###> symfony/framework-bundle ### /.env.local /.env.local.php diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..6cfc600 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,37 @@ +in(['src', 'bin', 'config', 'public', 'migrations']); + +return (new PhpCsFixer\Config()) + ->setCacheFile('var/.php-cs-fixer.cache') + ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) + ->setRiskyAllowed(true) + ->setRules([ + '@PhpCsFixer' => true, + '@Symfony' => true, + '@PER-CS2.0' => true, + 'binary_operator_spaces' => false, + 'cast_spaces' => true, + 'concat_space' => ['spacing' => 'one'], + 'declare_strict_types' => true, + 'global_namespace_import' => [ + 'import_classes' => true, + 'import_constants' => null, + 'import_functions' => null, + ], + 'multiline_whitespace_before_semicolons' => false, + 'phpdoc_align' => ['align' => 'left'], + 'phpdoc_order' => true, + 'phpdoc_to_comment' => false, + 'php_unit_test_class_requires_covers' => false, + 'single_line_throw' => false, + 'trailing_comma_in_multiline' => [ + 'elements' => ['arrays', 'arguments', 'parameters'], + ], + 'void_return' => true, + 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], + ]) + ->setFinder($finder); diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..31de356 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Gember + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..df76865 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Event Sourcing with DCB in PHP +Example project (Proof of Concept) with Event Sourcing in PHP using the 'Dynamic Consistency Boundary' pattern (DCB). + +## Background +'Dynamic Consistency Boundary' pattern is introduced by Sara Pellegrini in 2023 as a thought process (rethinking Event Sourcing). +Explained in her talk: ["The Aggregate is dead. Long live the Aggregate!"](https://sara.event-thinking.io/2023/04/kill-aggregate-chapter-1-I-am-here-to-kill-the-aggregate.html). + +Currently, existing Event Sourcing frameworks rely on a consistent boundary within _aggregates_ (primary citizen), as explained in 'The blue book': + +> Cluster the ENTITIES and VALUE OBJECTS into AGGREGATES and define boundaries around each. +Choose one ENTITY to be the root of each AGGREGATE, and control all access to the objects inside the boundary through the root. Allow external objects to hold reference to the root only. + +_From "Domain-Driven Design: Tackling Complexity in the Heart of Software" by Eric Evans (the 'blue book')._ + +However, Event Sourcing with DCB has some advantages/solves some pitfalls over a traditional aggregate setup. + +### Reusable domain events +Traditionally, a defined boundary enforces a strict 1-on-1 relation between an aggregate and a domain event. +This makes the aggregate internally consistent. The DCB pattern removes this strict 1-on-1 relation. Instead, a domain event is considered as a _Pure event_. + +This allows us to create business decision/domain models based on a **subset of specific domain events** as well as from **multiple domain identities**. + +These business decision models fit very well in how [EventStorming](https://github.com/ddd-crew/eventstorming-glossary-cheat-sheet) considers aggregates currently. +Instead of aggregates, in EventStorming, these are now called 'system' or 'consistent business rule', +as the term 'aggregate' is difficult to explain for non-technical actors in a software design process. + +### No aggregate synchronisation nor domain services needed +As a domain event can be used by multiple business decision models, there is no synchronization needed between aggregates. +Duplicated events as well as domain services (typically to handle business logic spanning multiple aggregates) are not needed anymore. + +### Concurrency problems +As an aggregate is based on internal consistency, two concurrent modification are impossible, +even when the modified data is (domain-wise) independent of each other. +The DCB pattern removes this blocking behavior when handled by multiple business decision models. + +**But overall it removes accidental complexity which aggregates introduced, allowing to build software closer to the real world.** + +More details, pros and cons explained (highly recommended): +- https://sara.event-thinking.io/2023/04/kill-aggregate-chapter-1-I-am-here-to-kill-the-aggregate.html +- https://www.youtube.com/watch?v=wXt54BawI-8 +- https://www.axoniq.io/blog/rethinking-microservices-architecture-through-dynamic-consistency-boundaries + +## This example project +_The DCB pattern is an interesting concept, but this does not advocate to remove aggregates completely. +Instead, a hybrid solution with aggregates and business decision models is probably more likely, depending on your domain._ + +This example project is using a fictive domain (taken from Sara Pellegrini's blog) where students can subscribe to courses (of any kind). +Deliberately this is all what is defined for this domain, to focus on how this could be implemented when using Event Sourcing with the DCB pattern in mind. + +It contains both classic aggregates (e.g. [Course](src/Domain/Course/Course.php), [Student](src/Domain/Student/Student.php)) as well as business decision models (e.g. [ChangeCourseCapacity](src/Domain/Course/ChangeCourseCapacity.php), [SubscribeStudentToCourse](src/Domain/StudentToCourseSubscription/SubscribeStudentToCourse.php), [UnsubscribeStudentFromCourse](src/Domain/StudentToCourseSubscription/UnsubscribeStudentFromCourse.php)). + +Inspired by other PHP libraries such as [Broadway](https://github.com/broadway), [EventSauce](https://github.com/EventSaucePHP), [Prooph](https://github.com/prooph) and [Ecotone](https://github.com/ecotoneframework) as well as [Axon Framework](https://github.com/AxonFramework) for Java. + +### How to run +First start docker compose for a database +``` +docker compose up +``` + +Then run migrations on your local machine: +``` +bin/console doctrine:migrations:migrate +``` + +You're all set, see what commands you can run: +``` +bin/console gember +``` \ No newline at end of file diff --git a/captainhook.json b/captainhook.json new file mode 100644 index 0000000..4b06b38 --- /dev/null +++ b/captainhook.json @@ -0,0 +1,19 @@ +{ + "pre-push": { + "enabled": true, + "actions": [ + { + "action": "composer cs:dry-run" + }, + { + "action": "composer rector:dry-run" + }, + { + "action": "composer phpstan" + }, + { + "action": "composer dependency-analyser" + } + ] + } +} diff --git a/composer.json b/composer.json index 73fada6..bf7eba8 100644 --- a/composer.json +++ b/composer.json @@ -31,9 +31,17 @@ "symfony/dotenv": "7.1.*", "symfony/flex": "^2", "symfony/framework-bundle": "7.1.*", + "symfony/property-access": "7.1.*", "symfony/runtime": "7.1.*", "symfony/yaml": "7.1.*" }, + "require-dev": { + "captainhook/captainhook": "^5.23", + "friendsofphp/php-cs-fixer": "^3.58", + "phpstan/phpstan": "^1.11", + "rector/rector": "^1.2", + "shipmonk/composer-dependency-analyser": "^1.5" + }, "replace": { "symfony/polyfill-ctype": "*", "symfony/polyfill-iconv": "*", @@ -54,11 +62,6 @@ "Gember\\ExampleEventSourcingDcb\\": "src/" } }, - "autoload-dev": { - "psr-4": { - "Gember\\ExampleEventSourcingDcb\\Test\\": "tests/" - } - }, "config": { "allow-plugins": { "php-http/discovery": true, @@ -83,6 +86,19 @@ "auto-scripts": { "cache:clear": "symfony-cmd", "assets:install %PUBLIC_DIR%": "symfony-cmd" - } + }, + "cs": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php", + "cs:dry-run": "vendor/bin/php-cs-fixer fix --diff --dry-run --config=.php-cs-fixer.php", + "dependency-analyser": "vendor/bin/composer-dependency-analyser", + "phpstan": "vendor/bin/phpstan analyse -c phpstan.neon", + "phpstan:baseline": "vendor/bin/phpstan analyse -c phpstan.neon --generate-baseline phpstan-baseline.php src", + "rector": "vendor/bin/rector process --ansi", + "rector:dry-run": "vendor/bin/rector process --ansi --dry-run", + "test": [ + "@rector:dry-run", + "@cs:dry-run", + "@phpstan", + "@dependency-analyser" + ] } } diff --git a/composer.lock b/composer.lock index 9c5858c..26e36cc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "301434e5976f06e137bef692791c6e6d", + "content-hash": "a06fffd8bd171f374163866b77cccb50", "packages": [ { "name": "doctrine/cache", @@ -187,16 +187,16 @@ }, { "name": "doctrine/dbal", - "version": "4.1.1", + "version": "4.2.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "7a8252418689feb860ea8dfeab66d64a56a64df8" + "reference": "dadd35300837a3a2184bd47d403333b15d0a9bd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/7a8252418689feb860ea8dfeab66d64a56a64df8", - "reference": "7a8252418689feb860ea8dfeab66d64a56a64df8", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/dadd35300837a3a2184bd47d403333b15d0a9bd0", + "reference": "dadd35300837a3a2184bd47d403333b15d0a9bd0", "shasum": "" }, "require": { @@ -209,7 +209,7 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.2", - "phpstan/phpstan": "1.12.0", + "phpstan/phpstan": "1.12.6", "phpstan/phpstan-phpunit": "1.4.0", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "10.5.30", @@ -275,7 +275,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.1.1" + "source": "https://github.com/doctrine/dbal/tree/4.2.1" }, "funding": [ { @@ -291,7 +291,7 @@ "type": "tidelift" } ], - "time": "2024-09-03T08:58:39+00:00" + "time": "2024-10-10T18:01:27+00:00" }, { "name": "doctrine/deprecations", @@ -4274,6 +4274,166 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/property-access", + "version": "v7.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "6c709f97103355016e5782d0622437ae381012ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/6c709f97103355016e5782d0622437ae381012ad", + "reference": "6c709f97103355016e5782d0622437ae381012ad", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/property-info": "^6.4|^7.0" + }, + "require-dev": { + "symfony/cache": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v7.1.4" + }, + "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-08-30T16:12:47+00:00" + }, + { + "name": "symfony/property-info", + "version": "v7.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "88a279df2db5b7919cac6f35d6a5d1d7147e6a9b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/88a279df2db5b7919cac6f35d6a5d1d7147e6a9b", + "reference": "88a279df2db5b7919cac6f35d6a5d1d7147e6a9b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/string": "^6.4|^7.0", + "symfony/type-info": "^7.1" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0", + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v7.1.3" + }, + "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-07-26T07:36:36+00:00" + }, { "name": "symfony/routing", "version": "v7.1.4", @@ -4765,6 +4925,88 @@ ], "time": "2024-09-20T08:28:38+00:00" }, + { + "name": "symfony/type-info", + "version": "v7.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "9f6094aa900d2c06bd61576a6f279d4ac441515f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/9f6094aa900d2c06bd61576a6f279d4ac441515f", + "reference": "9f6094aa900d2c06bd61576a6f279d4ac441515f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.0", + "symfony/dependency-injection": "<6.4", + "symfony/property-info": "<6.4" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v7.1.5" + }, + "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-19T21:48:23+00:00" + }, { "name": "symfony/uid", "version": "v7.1.5", @@ -5070,7 +5312,1733 @@ "time": "2024-09-17T12:49:58+00:00" } ], - "packages-dev": [], + "packages-dev": [ + { + "name": "captainhook/captainhook", + "version": "5.23.5", + "source": { + "type": "git", + "url": "https://github.com/captainhookphp/captainhook.git", + "reference": "8b39418081b0db0c8a2996f8740ea44345af6888" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/captainhookphp/captainhook/zipball/8b39418081b0db0c8a2996f8740ea44345af6888", + "reference": "8b39418081b0db0c8a2996f8740ea44345af6888", + "shasum": "" + }, + "require": { + "captainhook/secrets": "^0.9.4", + "ext-json": "*", + "ext-spl": "*", + "ext-xml": "*", + "php": ">=8.0", + "sebastianfeldmann/camino": "^0.9.2", + "sebastianfeldmann/cli": "^3.3", + "sebastianfeldmann/git": "^3.10", + "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "replace": { + "sebastianfeldmann/captainhook": "*" + }, + "require-dev": { + "composer/composer": "~1 || ^2.0", + "mikey179/vfsstream": "~1" + }, + "bin": [ + "bin/captainhook" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0.x-dev" + }, + "captainhook": { + "config": "captainhook.json" + } + }, + "autoload": { + "psr-4": { + "CaptainHook\\App\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sebastian Feldmann", + "email": "sf@sebastian-feldmann.info" + } + ], + "description": "PHP git hook manager", + "homepage": "http://php.captainhook.info/", + "keywords": [ + "commit-msg", + "git", + "hooks", + "post-merge", + "pre-commit", + "pre-push", + "prepare-commit-msg" + ], + "support": { + "issues": "https://github.com/captainhookphp/captainhook/issues", + "source": "https://github.com/captainhookphp/captainhook/tree/5.23.5" + }, + "funding": [ + { + "url": "https://github.com/sponsors/sebastianfeldmann", + "type": "github" + } + ], + "time": "2024-09-05T15:44:55+00:00" + }, + { + "name": "captainhook/secrets", + "version": "0.9.5", + "source": { + "type": "git", + "url": "https://github.com/captainhookphp/secrets.git", + "reference": "8aa90d5b9b7892abd11b9da2fc172a7b32b90cbe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/captainhookphp/secrets/zipball/8aa90d5b9b7892abd11b9da2fc172a7b32b90cbe", + "reference": "8aa90d5b9b7892abd11b9da2fc172a7b32b90cbe", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "CaptainHook\\Secrets\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sebastian Feldmann", + "email": "sf@sebastian-feldmann.info" + } + ], + "description": "Utility classes to detect secrets", + "keywords": [ + "commit-msg", + "keys", + "passwords", + "post-merge", + "prepare-commit-msg", + "secrets", + "tokens" + ], + "support": { + "issues": "https://github.com/captainhookphp/secrets/issues", + "source": "https://github.com/captainhookphp/secrets/tree/0.9.5" + }, + "funding": [ + { + "url": "https://github.com/sponsors/sebastianfeldmann", + "type": "github" + } + ], + "time": "2023-11-30T18:10:18+00:00" + }, + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.10", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-08-27T18:44:43+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.64.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "58dd9c931c785a79739310aef5178928305ffa67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67", + "reference": "58dd9c931c785a79739310aef5178928305ffa67", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.0", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.0", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.28", + "symfony/polyfill-php80": "^1.28", + "symfony/polyfill-php81": "^1.28", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3 || ^2.3", + "infection/infection": "^0.29.5", + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.11", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", + "phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2", + "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2024-08-30T23:09:38+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.12.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc4d2f145a88ea7141ae698effd64d9df46527ae", + "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-10-06T15:03:59+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.5", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/socket": "^1.8", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.5" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-09-16T13:41:56+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "rector/rector", + "version": "1.2.6", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/6ca85da28159dbd3bb36211c5104b7bc91278e99", + "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "phpstan/phpstan": "^1.12.5" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/1.2.6" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2024-10-03T08:56:44+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": "sebastianfeldmann/camino", + "version": "0.9.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianfeldmann/camino.git", + "reference": "bf2e4c8b2a029e9eade43666132b61331e3e8184" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianfeldmann/camino/zipball/bf2e4c8b2a029e9eade43666132b61331e3e8184", + "reference": "bf2e4c8b2a029e9eade43666132b61331e3e8184", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "SebastianFeldmann\\Camino\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sebastian Feldmann", + "email": "sf@sebastian-feldmann.info" + } + ], + "description": "Path management the OO way", + "homepage": "https://github.com/sebastianfeldmann/camino", + "keywords": [ + "file system", + "path" + ], + "support": { + "issues": "https://github.com/sebastianfeldmann/camino/issues", + "source": "https://github.com/sebastianfeldmann/camino/tree/0.9.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianfeldmann", + "type": "github" + } + ], + "time": "2022-01-03T13:15:10+00:00" + }, + { + "name": "sebastianfeldmann/cli", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianfeldmann/cli.git", + "reference": "8a932e99e9455981fb32fa6c085492462fe8f8cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianfeldmann/cli/zipball/8a932e99e9455981fb32fa6c085492462fe8f8cf", + "reference": "8a932e99e9455981fb32fa6c085492462fe8f8cf", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "symfony/process": "^4.3 | ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "SebastianFeldmann\\Cli\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sebastian Feldmann", + "email": "sf@sebastian-feldmann.info" + } + ], + "description": "PHP cli helper classes", + "homepage": "https://github.com/sebastianfeldmann/cli", + "keywords": [ + "cli" + ], + "support": { + "issues": "https://github.com/sebastianfeldmann/cli/issues", + "source": "https://github.com/sebastianfeldmann/cli/tree/3.4.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianfeldmann", + "type": "github" + } + ], + "time": "2021-12-20T14:59:49+00:00" + }, + { + "name": "sebastianfeldmann/git", + "version": "3.11.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianfeldmann/git.git", + "reference": "5cb1ea94f65c7420419abe8f12c45cc7eb094790" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianfeldmann/git/zipball/5cb1ea94f65c7420419abe8f12c45cc7eb094790", + "reference": "5cb1ea94f65c7420419abe8f12c45cc7eb094790", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "php": ">=8.0", + "sebastianfeldmann/cli": "^3.0" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "SebastianFeldmann\\Git\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sebastian Feldmann", + "email": "sf@sebastian-feldmann.info" + } + ], + "description": "PHP git wrapper", + "homepage": "https://github.com/sebastianfeldmann/git", + "keywords": [ + "git" + ], + "support": { + "issues": "https://github.com/sebastianfeldmann/git/issues", + "source": "https://github.com/sebastianfeldmann/git/tree/3.11.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianfeldmann", + "type": "github" + } + ], + "time": "2024-01-23T09:11:14+00:00" + }, + { + "name": "shipmonk/composer-dependency-analyser", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/shipmonk-rnd/composer-dependency-analyser.git", + "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/shipmonk-rnd/composer-dependency-analyser/zipball/bca862b2830a453734aee048eb0cdab82e5c9da3", + "reference": "bca862b2830a453734aee048eb0cdab82e5c9da3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "editorconfig-checker/editorconfig-checker": "^10.3.0", + "ergebnis/composer-normalize": "^2.19", + "ext-dom": "*", + "ext-libxml": "*", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.10.63", + "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan-strict-rules": "^1.2.3", + "phpunit/phpunit": "^8.5.28 || ^9.5.20", + "shipmonk/name-collision-detector": "^2.0.0", + "slevomat/coding-standard": "^8.0.1" + }, + "bin": [ + "bin/composer-dependency-analyser" + ], + "type": "library", + "autoload": { + "psr-4": { + "ShipMonk\\ComposerDependencyAnalyser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Fast detection of composer dependency issues (dead dependencies, shadow dependencies, misplaced dependencies)", + "keywords": [ + "analyser", + "composer", + "composer dependency", + "dead code", + "dead dependency", + "detector", + "dev", + "misplaced dependency", + "shadow dependency", + "static analysis", + "unused code", + "unused dependency" + ], + "support": { + "issues": "https://github.com/shipmonk-rnd/composer-dependency-analyser/issues", + "source": "https://github.com/shipmonk-rnd/composer-dependency-analyser/tree/1.7.0" + }, + "time": "2024-08-08T08:12:32+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.1.1" + }, + "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-05-31T14:57:53+00:00" + }, + { + "name": "symfony/process", + "version": "v7.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "5c03ee6369281177f07f7c68252a280beccba847" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", + "reference": "5c03ee6369281177f07f7c68252a280beccba847", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.1.5" + }, + "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-19T21:48:23+00:00" + } + ], "aliases": [], "minimum-stability": "dev", "stability-flags": { @@ -5083,6 +7051,6 @@ "ext-ctype": "*", "ext-iconv": "*" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/config/bundles.php b/config/bundles.php index c1fa06a..d877efa 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -1,7 +1,10 @@ ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Gember\EventSourcingSymfonyBundle\GemberEventSourcingBundle::class => ['all' => true], ]; diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 968c77d..3bdd1b1 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -15,13 +15,7 @@ doctrine: validate_xml_mapping: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true - mappings: - Gember\ExampleEventSourcingDcb: - type: attribute - is_bundle: false - dir: '%kernel.project_dir%/src/Entity' - prefix: 'Gember\ExampleEventSourcingDcb\Entity' - alias: Gember\ExampleEventSourcingDcb + mappings: ~ controller_resolver: auto_mapping: false diff --git a/config/packages/gember_event_sourcing.yaml b/config/packages/gember_event_sourcing.yaml new file mode 100644 index 0000000..50e4b5f --- /dev/null +++ b/config/packages/gember_event_sourcing.yaml @@ -0,0 +1,21 @@ +gember_event_sourcing: + cache: + enabled: true + psr6: + service: '@cache.app' + event_registry: + reflector: + path: '%kernel.project_dir%/src/Domain' + message_bus: + symfony: + event_bus: '@event.bus' + event_store: + rdbms: + doctrine_dbal: + connection: '@doctrine.dbal.default_connection' + generator: + identity: + service: '@gember.identity_generator_symfony.uuid.symfony_uuid_identity_generator' + serializer: + symfony: + serializer: '@serializer' diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index 672b6c4..de123eb 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -1,5 +1,12 @@ framework: messenger: + default_bus: command.bus + buses: + command.bus: + event.bus: + default_middleware: + allow_no_handlers: true + # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. # failure_transport: failed diff --git a/config/preload.php b/config/preload.php index 5ebcdb2..69de615 100644 --- a/config/preload.php +++ b/config/preload.php @@ -1,5 +1,7 @@ addSql( + <<<'SQL' + CREATE TABLE `event_store` ( + `id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `event_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` json NOT NULL, + `metadata` json NOT NULL, + `applied_at` timestamp(6) NOT NULL, + PRIMARY KEY (`id`), + KEY `event_name` (`event_name`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + SQL + ); + + $this->addSql( + <<<'SQL' + CREATE TABLE `event_store_relation` ( + `event_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `domain_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + UNIQUE KEY `event_id_domain_id` (`event_id`,`domain_id`), + CONSTRAINT `event_store_x_event_store_relation` FOREIGN KEY (`event_id`) REFERENCES `event_store` (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + SQL + ); + } +} diff --git a/phpstan-baseline.php b/phpstan-baseline.php new file mode 100644 index 0000000..cd9c598 --- /dev/null +++ b/phpstan-baseline.php @@ -0,0 +1,155 @@ + '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\Course\\\\ChangeCourseCapacity\\:\\:onCourseCapacityChangedEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/Course/ChangeCourseCapacity.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\Course\\\\ChangeCourseCapacity\\:\\:onCourseCreatedEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/Course/ChangeCourseCapacity.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\Course\\\\Course\\:\\:onCourseCreatedEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/Course/Course.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\Course\\\\Course\\:\\:onCourseNameChangedEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/Course/Course.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\Student\\\\Student\\:\\:onStudentCreatedEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/Student/Student.php', +]; +$ignoreErrors[] = [ + // identifier: property.onlyWritten + 'message' => '#^Property Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\Student\\\\Student\\:\\:\\$studentId is never read, only written\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/Student/Student.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\StudentToCourseSubscription\\\\SubscribeStudentToCourse\\:\\:onCourseCapacityChangedEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/StudentToCourseSubscription/SubscribeStudentToCourse.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\StudentToCourseSubscription\\\\SubscribeStudentToCourse\\:\\:onCourseCreatedEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/StudentToCourseSubscription/SubscribeStudentToCourse.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\StudentToCourseSubscription\\\\SubscribeStudentToCourse\\:\\:onStudentCreatedEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/StudentToCourseSubscription/SubscribeStudentToCourse.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\StudentToCourseSubscription\\\\SubscribeStudentToCourse\\:\\:onStudentSubscribedToCourseEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/StudentToCourseSubscription/SubscribeStudentToCourse.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\StudentToCourseSubscription\\\\SubscribeStudentToCourse\\:\\:onStudentUnsubscribedFromCourseEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/StudentToCourseSubscription/SubscribeStudentToCourse.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\StudentToCourseSubscription\\\\UnsubscribeStudentFromCourse\\:\\:onCourseCreatedEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/StudentToCourseSubscription/UnsubscribeStudentFromCourse.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\StudentToCourseSubscription\\\\UnsubscribeStudentFromCourse\\:\\:onStudentCreatedEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/StudentToCourseSubscription/UnsubscribeStudentFromCourse.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\StudentToCourseSubscription\\\\UnsubscribeStudentFromCourse\\:\\:onStudentSubscribedToCourseEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/StudentToCourseSubscription/UnsubscribeStudentFromCourse.php', +]; +$ignoreErrors[] = [ + // identifier: method.unused + 'message' => '#^Method Gember\\\\ExampleEventSourcingDcb\\\\Domain\\\\StudentToCourseSubscription\\\\UnsubscribeStudentFromCourse\\:\\:onStudentUnsubscribedFromCourseEvent\\(\\) is unused\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Domain/StudentToCourseSubscription/UnsubscribeStudentFromCourse.php', +]; +$ignoreErrors[] = [ + // identifier: cast.int + 'message' => '#^Cannot cast mixed to int\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Infrastructure/Api/Cli/Command/Course/ChangeCourseCapacityCliCommand.php', +]; +$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Parameter \\#1 \\$courseId of class Gember\\\\ExampleEventSourcingDcb\\\\Application\\\\Command\\\\Course\\\\ChangeCourseCapacityCommand constructor expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Infrastructure/Api/Cli/Command/Course/ChangeCourseCapacityCliCommand.php', +]; +$ignoreErrors[] = [ + // identifier: cast.int + 'message' => '#^Cannot cast mixed to int\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Infrastructure/Api/Cli/Command/Course/CreateCourseCliCommand.php', +]; +$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Parameter \\#2 \\$name of class Gember\\\\ExampleEventSourcingDcb\\\\Application\\\\Command\\\\Course\\\\CreateCourseCommand constructor expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Infrastructure/Api/Cli/Command/Course/CreateCourseCliCommand.php', +]; +$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Parameter \\#1 \\$courseId of class Gember\\\\ExampleEventSourcingDcb\\\\Application\\\\Command\\\\Course\\\\RenameCourseCommand constructor expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Infrastructure/Api/Cli/Command/Course/RenameCourseCliCommand.php', +]; +$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Parameter \\#2 \\$name of class Gember\\\\ExampleEventSourcingDcb\\\\Application\\\\Command\\\\Course\\\\RenameCourseCommand constructor expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Infrastructure/Api/Cli/Command/Course/RenameCourseCliCommand.php', +]; +$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Parameter \\#1 \\$studentId of class Gember\\\\ExampleEventSourcingDcb\\\\Application\\\\Command\\\\StudentToCourseSubscription\\\\SubscribeStudentToCourseCommand constructor expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Infrastructure/Api/Cli/Command/StudentToCourseSubscription/SubscribeStudentToCourseCliCommand.php', +]; +$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Parameter \\#2 \\$courseId of class Gember\\\\ExampleEventSourcingDcb\\\\Application\\\\Command\\\\StudentToCourseSubscription\\\\SubscribeStudentToCourseCommand constructor expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Infrastructure/Api/Cli/Command/StudentToCourseSubscription/SubscribeStudentToCourseCliCommand.php', +]; +$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Parameter \\#1 \\$studentId of class Gember\\\\ExampleEventSourcingDcb\\\\Application\\\\Command\\\\StudentToCourseSubscription\\\\UnsubscribeStudentFromCourseCommand constructor expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Infrastructure/Api/Cli/Command/StudentToCourseSubscription/UnsubscribeStudentFromCourseCliCommand.php', +]; +$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Parameter \\#2 \\$courseId of class Gember\\\\ExampleEventSourcingDcb\\\\Application\\\\Command\\\\StudentToCourseSubscription\\\\UnsubscribeStudentFromCourseCommand constructor expects string, mixed given\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Infrastructure/Api/Cli/Command/StudentToCourseSubscription/UnsubscribeStudentFromCourseCliCommand.php', +]; + +return ['parameters' => ['ignoreErrors' => $ignoreErrors]]; diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..bb289a0 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.php + +parameters: + level: max + paths: + - src/ diff --git a/public/index.php b/public/index.php index e8bec62..31d1813 100644 --- a/public/index.php +++ b/public/index.php @@ -1,8 +1,10 @@ withCache(__DIR__ . '/var/rector') + ->withPaths([ + __DIR__ . '/src', + ]) + ->withParallel() + ->withRules([ + // Misc + PrivatizeFinalClassPropertyRector::class, + PrivatizeFinalClassMethodRector::class, + UnwrapSprintfOneArgumentRector::class, + RemoveSoleValueSprintfRector::class, + CountArrayToEmptyArrayComparisonRector::class, + // See withConfiguredRule below for more + + // Misc - Deadcode + RemoveUnusedForeachKeyRector::class, + RemoveDuplicatedArrayKeyRector::class, + RecastingRemovalRector::class, + RemoveUnusedNonEmptyArrayBeforeForeachRector::class, + TernaryToBooleanOrFalseToBooleanAndRector::class, + RemoveUselessParamTagRector::class, + RemoveUselessReturnTagRector::class, + RemoveUselessReadOnlyTagRector::class, + RemoveNonExistingVarAnnotationRector::class, + RemoveUselessVarTagRector::class, + ReduceAlwaysFalseIfOrRector::class, + + // PHP 8.0 + ClassOnThisVariableObjectRector::class, + ClassOnObjectRector::class, + StrStartsWithRector::class, + StrEndsWithRector::class, + StrContainsRector::class, + RemoveUnusedVariableInCatchRector::class, + + // PHP 8.1 + ReadOnlyPropertyRector::class, + FirstClassCallableRector::class, + + // PHP 8.2 + ReadOnlyClassRector::class, + + // PHP 8.3 + AddTypeToConstRector::class, + AddOverrideAttributeToOverriddenMethodsRector::class, + ]) + ->withConfiguredRule(SimplifyUselessVariableRector::class, [ + SimplifyUselessVariableRector::ONLY_DIRECT_ASSIGN => true, + ]); + +return $config; diff --git a/src/Application/Command/Course/ChangeCourseCapacityCommand.php b/src/Application/Command/Course/ChangeCourseCapacityCommand.php new file mode 100644 index 0000000..8a8cfd9 --- /dev/null +++ b/src/Application/Command/Course/ChangeCourseCapacityCommand.php @@ -0,0 +1,16 @@ + $repository + */ + public function __construct( + private DomainContextRepository $repository, + ) {} + + /** + * @throws CourseNotFoundException + * @throws DomainContextNotFoundException + * @throws DomainContextRepositoryFailedException + */ + #[AsMessageHandler(bus: 'command.bus')] + public function __invoke(ChangeCourseCapacityCommand $command): void + { + $context = $this->repository->get(ChangeCourseCapacity::class, new CourseId($command->courseId)); + + $context->changeCapacity($command->capacity); + + $this->repository->save($context); + } +} diff --git a/src/Application/Command/Course/CreateCourseCommand.php b/src/Application/Command/Course/CreateCourseCommand.php new file mode 100644 index 0000000..02cd27b --- /dev/null +++ b/src/Application/Command/Course/CreateCourseCommand.php @@ -0,0 +1,17 @@ + $repository + */ + public function __construct( + private DomainContextRepository $repository, + ) {} + + /** + * @throws DomainContextRepositoryFailedException + */ + #[AsMessageHandler(bus: 'command.bus')] + public function __invoke(CreateCourseCommand $command): void + { + $courseId = new CourseId($command->courseId); + + if ($this->repository->has(Course::class, $courseId)) { + return; + } + + $course = Course::create($courseId, $command->name, $command->capacity); + + $this->repository->save($course); + } +} diff --git a/src/Application/Command/Course/RenameCourseCommand.php b/src/Application/Command/Course/RenameCourseCommand.php new file mode 100644 index 0000000..1ab6123 --- /dev/null +++ b/src/Application/Command/Course/RenameCourseCommand.php @@ -0,0 +1,16 @@ + $repository + */ + public function __construct( + private DomainContextRepository $repository, + ) {} + + /** + * @throws DomainContextNotFoundException + * @throws DomainContextRepositoryFailedException + */ + #[AsMessageHandler(bus: 'command.bus')] + public function __invoke(RenameCourseCommand $command): void + { + $course = $this->repository->get(Course::class, new CourseId($command->courseId)); + + $course->rename($command->name); + + $this->repository->save($course); + } +} diff --git a/src/Application/Command/Student/CreateStudentCommand.php b/src/Application/Command/Student/CreateStudentCommand.php new file mode 100644 index 0000000..7c5520d --- /dev/null +++ b/src/Application/Command/Student/CreateStudentCommand.php @@ -0,0 +1,15 @@ + $repository + */ + public function __construct( + private DomainContextRepository $repository, + ) {} + + /** + * @throws DomainContextRepositoryFailedException + */ + #[AsMessageHandler(bus: 'command.bus')] + public function __invoke(CreateStudentCommand $command): void + { + $studentId = new StudentId($command->studentId); + + if ($this->repository->has(Student::class, $studentId)) { + return; + } + + $course = Student::create($studentId); + + $this->repository->save($course); + } +} diff --git a/src/Application/Command/StudentToCourseSubscription/SubscribeStudentToCourseCommand.php b/src/Application/Command/StudentToCourseSubscription/SubscribeStudentToCourseCommand.php new file mode 100644 index 0000000..d929b52 --- /dev/null +++ b/src/Application/Command/StudentToCourseSubscription/SubscribeStudentToCourseCommand.php @@ -0,0 +1,16 @@ + $repository + */ + public function __construct( + private DomainContextRepository $repository, + ) {} + + /** + * @throws CourseCannotAcceptMoreStudentsException + * @throws CourseNotFoundException + * @throws StudentCannotSubscribeToMoreCoursesException + * @throws StudentNotFoundException + * @throws DomainContextNotFoundException + * @throws DomainContextRepositoryFailedException + */ + #[AsMessageHandler(bus: 'command.bus')] + public function __invoke(SubscribeStudentToCourseCommand $command): void + { + $context = $this->repository->get( + SubscribeStudentToCourse::class, + new CourseId($command->courseId), + new StudentId($command->studentId), + ); + + $context->subscribe(); + + $this->repository->save($context); + } +} diff --git a/src/Application/Command/StudentToCourseSubscription/UnsubscribeStudentFromCourseCommand.php b/src/Application/Command/StudentToCourseSubscription/UnsubscribeStudentFromCourseCommand.php new file mode 100644 index 0000000..78fe9a9 --- /dev/null +++ b/src/Application/Command/StudentToCourseSubscription/UnsubscribeStudentFromCourseCommand.php @@ -0,0 +1,16 @@ + $repository + */ + public function __construct( + private DomainContextRepository $repository, + ) {} + + /** + * @throws CourseNotFoundException + * @throws StudentNotFoundException + * @throws DomainContextNotFoundException + * @throws DomainContextRepositoryFailedException + */ + #[AsMessageHandler(bus: 'command.bus')] + public function __invoke(UnsubscribeStudentFromCourseCommand $command): void + { + $context = $this->repository->get( + UnsubscribeStudentFromCourse::class, + new CourseId($command->courseId), + new StudentId($command->studentId), + ); + + $context->unsubscribe(); + + $this->repository->save($context); + } +} diff --git a/src/Controller/.gitignore b/src/Controller/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Domain/Course/ChangeCourseCapacity.php b/src/Domain/Course/ChangeCourseCapacity.php new file mode 100644 index 0000000..c6e379f --- /dev/null +++ b/src/Domain/Course/ChangeCourseCapacity.php @@ -0,0 +1,76 @@ + + */ +final class ChangeCourseCapacity implements EventSourcedDomainContext +{ + /** + * @use EventSourcedDomainContextBehaviorTrait + */ + use EventSourcedDomainContextBehaviorTrait; + + /* + * Define to which domain identifiers this context belongs to. + */ + #[DomainId] + private CourseId $courseId; + + /* + * Use private properties to guard idempotency and protect invariants. + */ + private int $capacity; + + /** + * @throws CourseNotFoundException + */ + public function changeCapacity(int $capacity): void + { + /* + * Guard for idempotency. + */ + if ($this->capacity === $capacity) { + return; + } + + /* + * Protect invariants (business rules). + */ + if (!isset($this->courseId)) { + throw CourseNotFoundException::create(); + } + + /* + * Apply events when all business rules are met. + */ + $this->apply(new CourseCapacityChangedEvent((string) $this->courseId, $capacity)); + } + + /* + * Change internal state by subscribing to relevant domain events for any of the domain identifiers, + * so that this context can apply its business rules. + */ + #[DomainEventSubscriber] + private function onCourseCreatedEvent(CourseCreatedEvent $event): void + { + $this->courseId = new CourseId($event->courseId); + $this->capacity = $event->capacity; + } + + #[DomainEventSubscriber] + private function onCourseCapacityChangedEvent(CourseCapacityChangedEvent $event): void + { + $this->capacity = $event->capacity; + } +} diff --git a/src/Domain/Course/Course.php b/src/Domain/Course/Course.php new file mode 100644 index 0000000..628faed --- /dev/null +++ b/src/Domain/Course/Course.php @@ -0,0 +1,74 @@ + + */ +final class Course implements EventSourcedDomainContext +{ + /** + * @use EventSourcedDomainContextBehaviorTrait + */ + use EventSourcedDomainContextBehaviorTrait; + + /* + * Define to which domain identifiers this context belongs to. + */ + #[DomainId] + private CourseId $courseId; + + /* + * Use private properties to guard idempotency and protect invariants. + */ + private string $name; + + public static function create(CourseId $courseId, string $name, int $capacity): self + { + $course = new self(); + $course->apply(new CourseCreatedEvent((string) $courseId, $name, $capacity)); + + return $course; + } + + public function rename(string $name): void + { + /* + * Guard for idempotency. + */ + if ($this->name === $name) { + return; + } + + /* + * Apply events when all business rules are met. + */ + $this->apply(new CourseRenamedEvent((string) $this->courseId, $name)); + } + + /* + * Change internal state by subscribing to relevant domain events for any of the domain identifiers, + * so that this context can apply its business rules. + */ + #[DomainEventSubscriber] + private function onCourseCreatedEvent(CourseCreatedEvent $event): void + { + $this->courseId = new CourseId($event->courseId); + $this->name = $event->name; + } + + #[DomainEventSubscriber] + private function onCourseNameChangedEvent(CourseRenamedEvent $event): void + { + $this->name = $event->name; + } +} diff --git a/src/Domain/Course/CourseCapacityChangedEvent.php b/src/Domain/Course/CourseCapacityChangedEvent.php new file mode 100644 index 0000000..5b05418 --- /dev/null +++ b/src/Domain/Course/CourseCapacityChangedEvent.php @@ -0,0 +1,18 @@ +id; + } + + public function equals(CourseId $courseId): bool + { + return $this->id === $courseId->id; + } +} diff --git a/src/Domain/Course/CourseNotFoundException.php b/src/Domain/Course/CourseNotFoundException.php new file mode 100644 index 0000000..f22e226 --- /dev/null +++ b/src/Domain/Course/CourseNotFoundException.php @@ -0,0 +1,15 @@ + + */ +final class Student implements EventSourcedDomainContext +{ + /** + * @use EventSourcedDomainContextBehaviorTrait + */ + use EventSourcedDomainContextBehaviorTrait; + + /* + * Define to which domain identifiers this context belongs to. + */ + #[DomainId] + private StudentId $studentId; + + public static function create(StudentId $studentId): self + { + $student = new self(); + $student->apply(new StudentCreatedEvent((string) $studentId)); + + return $student; + } + + /* + * Change internal state by subscribing to relevant domain events for any of the domain identifiers, + * so that this context can apply its business rules. + */ + #[DomainEventSubscriber] + private function onStudentCreatedEvent(StudentCreatedEvent $event): void + { + $this->studentId = new StudentId($event->studentId); + } +} diff --git a/src/Domain/Student/StudentCreatedEvent.php b/src/Domain/Student/StudentCreatedEvent.php new file mode 100644 index 0000000..bebef64 --- /dev/null +++ b/src/Domain/Student/StudentCreatedEvent.php @@ -0,0 +1,17 @@ +id; + } + + public function equals(StudentId $studentId): bool + { + return $this->id === $studentId->id; + } +} diff --git a/src/Domain/Student/StudentNotFoundException.php b/src/Domain/Student/StudentNotFoundException.php new file mode 100644 index 0000000..5f0ce03 --- /dev/null +++ b/src/Domain/Student/StudentNotFoundException.php @@ -0,0 +1,15 @@ + + */ +final class SubscribeStudentToCourse implements EventSourcedDomainContext +{ + /** + * @use EventSourcedDomainContextBehaviorTrait + */ + use EventSourcedDomainContextBehaviorTrait; + + /* + * Define to which domain identifiers this context belongs to. + */ + #[DomainId] + private CourseId $courseId; + #[DomainId] + private StudentId $studentId; + + /* + * Use private properties to guard idempotency and protect invariants. + */ + private bool $isStudentSubscribedToCourse; + private int $courseCapacity; + private int $totalCountSubscriptionsForCourse; + private int $totalCountSubscriptionsForStudent; + + /** + * @throws CourseCannotAcceptMoreStudentsException + * @throws CourseNotFoundException + * @throws StudentCannotSubscribeToMoreCoursesException + * @throws StudentNotFoundException + */ + public function subscribe(): void + { + /* + * Guard for idempotency. + */ + if ($this->isStudentSubscribedToCourse) { + return; + } + + /* + * Protect invariants (business rules). + */ + if (!isset($this->courseId)) { + throw CourseNotFoundException::create(); + } + + if (!isset($this->studentId)) { + throw StudentNotFoundException::create(); + } + + if ($this->totalCountSubscriptionsForCourse >= $this->courseCapacity) { + throw CourseCannotAcceptMoreStudentsException::create(); + } + + if ($this->totalCountSubscriptionsForStudent >= 10) { + throw StudentCannotSubscribeToMoreCoursesException::create(); + } + + /* + * Apply events when all business rules are met. + */ + $this->apply(new StudentSubscribedToCourseEvent((string) $this->courseId, (string) $this->studentId)); + + if ($this->totalCountSubscriptionsForCourse+1 >= $this->courseCapacity) { + $this->apply(new CourseFullyBookedEvent((string) $this->courseId)); + } + } + + /* + * Change internal state by subscribing to relevant domain events for any of the domain identifiers, + * so that this context can apply its business rules. + */ + #[DomainEventSubscriber] + private function onCourseCreatedEvent(CourseCreatedEvent $event): void + { + $this->courseId = new CourseId($event->courseId); + $this->courseCapacity = $event->capacity; + $this->totalCountSubscriptionsForCourse = 0; + } + + #[DomainEventSubscriber] + private function onStudentCreatedEvent(StudentCreatedEvent $event): void + { + $this->studentId = new StudentId($event->studentId); + $this->totalCountSubscriptionsForStudent = 0; + $this->isStudentSubscribedToCourse = false; + } + + #[DomainEventSubscriber] + private function onCourseCapacityChangedEvent(CourseCapacityChangedEvent $event): void + { + $this->courseCapacity = $event->capacity; + } + + #[DomainEventSubscriber] + private function onStudentSubscribedToCourseEvent(StudentSubscribedToCourseEvent $event): void + { + if (isset($this->studentId) + && $this->studentId->equals(new StudentId($event->studentId)) + && $this->courseId->equals(new CourseId($event->courseId)) + ) { + ++$this->totalCountSubscriptionsForStudent; + $this->isStudentSubscribedToCourse = true; + } + + ++$this->totalCountSubscriptionsForCourse; + } + + #[DomainEventSubscriber] + private function onStudentUnsubscribedFromCourseEvent(StudentUnsubscribedFromCourseEvent $event): void + { + if (isset($this->studentId) + && $this->studentId->equals(new StudentId($event->studentId)) + && $this->courseId->equals(new CourseId($event->courseId)) + ) { + --$this->totalCountSubscriptionsForStudent; + $this->isStudentSubscribedToCourse = false; + } + + --$this->totalCountSubscriptionsForCourse; + } +} diff --git a/src/Domain/StudentToCourseSubscription/UnsubscribeStudentFromCourse.php b/src/Domain/StudentToCourseSubscription/UnsubscribeStudentFromCourse.php new file mode 100644 index 0000000..cf4b4ae --- /dev/null +++ b/src/Domain/StudentToCourseSubscription/UnsubscribeStudentFromCourse.php @@ -0,0 +1,111 @@ + + */ +final class UnsubscribeStudentFromCourse implements EventSourcedDomainContext +{ + /** + * @use EventSourcedDomainContextBehaviorTrait + */ + use EventSourcedDomainContextBehaviorTrait; + + /* + * Define to which domain identifiers this context belongs to. + */ + #[DomainId] + private CourseId $courseId; + #[DomainId] + private StudentId $studentId; + + /* + * Use private properties to guard idempotency and protect invariants. + */ + private bool $isStudentSubscribedToCourse; + + /** + * @throws CourseNotFoundException + * @throws StudentNotFoundException + */ + public function unsubscribe(): void + { + /* + * Guard for idempotency. + */ + if (!$this->isStudentSubscribedToCourse) { + return; + } + + /* + * Protect invariants (business rules). + */ + if (!isset($this->courseId)) { + throw CourseNotFoundException::create(); + } + + if (!isset($this->studentId)) { + throw StudentNotFoundException::create(); + } + + /* + * Apply events when all business rules are met. + */ + $this->apply(new StudentUnsubscribedFromCourseEvent((string) $this->courseId, (string) $this->studentId)); + } + + /* + * Change internal state by subscribing to relevant domain events for any of the domain identifiers, + * so that this context can apply its business rules. + */ + #[DomainEventSubscriber] + private function onCourseCreatedEvent(CourseCreatedEvent $event): void + { + $this->courseId = new CourseId($event->courseId); + } + + #[DomainEventSubscriber] + private function onStudentCreatedEvent(StudentCreatedEvent $event): void + { + $this->studentId = new StudentId($event->studentId); + $this->isStudentSubscribedToCourse = false; + } + + #[DomainEventSubscriber] + private function onStudentSubscribedToCourseEvent(StudentSubscribedToCourseEvent $event): void + { + if (isset($this->studentId) + && $this->studentId->equals(new StudentId($event->studentId)) + && $this->courseId->equals(new CourseId($event->courseId)) + ) { + $this->isStudentSubscribedToCourse = true; + } + } + + #[DomainEventSubscriber] + private function onStudentUnsubscribedFromCourseEvent(StudentUnsubscribedFromCourseEvent $event): void + { + if (isset($this->studentId) + && $this->studentId->equals(new StudentId($event->studentId)) + && $this->courseId->equals(new CourseId($event->courseId)) + ) { + $this->isStudentSubscribedToCourse = false; + } + } +} diff --git a/src/Entity/.gitignore b/src/Entity/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Infrastructure/Api/Cli/Command/Course/ChangeCourseCapacityCliCommand.php b/src/Infrastructure/Api/Cli/Command/Course/ChangeCourseCapacityCliCommand.php new file mode 100644 index 0000000..6fbc896 --- /dev/null +++ b/src/Infrastructure/Api/Cli/Command/Course/ChangeCourseCapacityCliCommand.php @@ -0,0 +1,47 @@ +addArgument('courseId', InputArgument::REQUIRED, 'Course ID'); + $this->addArgument('capacity', InputArgument::REQUIRED, 'Course capacity'); + } + + #[Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->commandBus->dispatch(new ChangeCourseCapacityCommand( + $input->getArgument('courseId'), + (int) $input->getArgument('capacity'), + )); + + $output->write('Course capacity updated to ' . $input->getArgument('capacity')); + + return self::SUCCESS; + } +} diff --git a/src/Infrastructure/Api/Cli/Command/Course/CreateCourseCliCommand.php b/src/Infrastructure/Api/Cli/Command/Course/CreateCourseCliCommand.php new file mode 100644 index 0000000..9bb0593 --- /dev/null +++ b/src/Infrastructure/Api/Cli/Command/Course/CreateCourseCliCommand.php @@ -0,0 +1,52 @@ +addArgument('name', InputArgument::REQUIRED, 'Course name'); + $this->addArgument('capacity', InputArgument::REQUIRED, 'Course capacity'); + } + + #[Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + $courseId = $this->identityGenerator->generate(); + + $this->commandBus->dispatch(new CreateCourseCommand( + $courseId, + $input->getArgument('name'), + (int) $input->getArgument('capacity'), + )); + + $output->write('Created: course #' . $courseId); + + return self::SUCCESS; + } +} diff --git a/src/Infrastructure/Api/Cli/Command/Course/RenameCourseCliCommand.php b/src/Infrastructure/Api/Cli/Command/Course/RenameCourseCliCommand.php new file mode 100644 index 0000000..8e82c59 --- /dev/null +++ b/src/Infrastructure/Api/Cli/Command/Course/RenameCourseCliCommand.php @@ -0,0 +1,47 @@ +addArgument('courseId', InputArgument::REQUIRED, 'Course ID'); + $this->addArgument('name', InputArgument::REQUIRED, 'Course name'); + } + + #[Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->commandBus->dispatch(new RenameCourseCommand( + $input->getArgument('courseId'), + $input->getArgument('name'), + )); + + $output->write('Course renamed to ' . $input->getArgument('name')); + + return self::SUCCESS; + } +} diff --git a/src/Infrastructure/Api/Cli/Command/Student/CreateStudentCliCommand.php b/src/Infrastructure/Api/Cli/Command/Student/CreateStudentCliCommand.php new file mode 100644 index 0000000..95dbd75 --- /dev/null +++ b/src/Infrastructure/Api/Cli/Command/Student/CreateStudentCliCommand.php @@ -0,0 +1,42 @@ +identityGenerator->generate(); + + $this->commandBus->dispatch(new CreateStudentCommand( + $studentId, + )); + + $output->write('Created: student #' . $studentId); + + return self::SUCCESS; + } +} diff --git a/src/Infrastructure/Api/Cli/Command/StudentToCourseSubscription/SubscribeStudentToCourseCliCommand.php b/src/Infrastructure/Api/Cli/Command/StudentToCourseSubscription/SubscribeStudentToCourseCliCommand.php new file mode 100644 index 0000000..d4b3218 --- /dev/null +++ b/src/Infrastructure/Api/Cli/Command/StudentToCourseSubscription/SubscribeStudentToCourseCliCommand.php @@ -0,0 +1,47 @@ +addArgument('studentId', InputArgument::REQUIRED, 'Student ID'); + $this->addArgument('courseId', InputArgument::REQUIRED, 'Course ID'); + } + + #[Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->commandBus->dispatch(new SubscribeStudentToCourseCommand( + $input->getArgument('studentId'), + $input->getArgument('courseId'), + )); + + $output->write('Student # ' . $input->getArgument('studentId') . ' subscribed to course #' . $input->getArgument('courseId')); + + return self::SUCCESS; + } +} diff --git a/src/Infrastructure/Api/Cli/Command/StudentToCourseSubscription/UnsubscribeStudentFromCourseCliCommand.php b/src/Infrastructure/Api/Cli/Command/StudentToCourseSubscription/UnsubscribeStudentFromCourseCliCommand.php new file mode 100644 index 0000000..e30de79 --- /dev/null +++ b/src/Infrastructure/Api/Cli/Command/StudentToCourseSubscription/UnsubscribeStudentFromCourseCliCommand.php @@ -0,0 +1,47 @@ +addArgument('studentId', InputArgument::REQUIRED, 'Student ID'); + $this->addArgument('courseId', InputArgument::REQUIRED, 'Course ID'); + } + + #[Override] + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->commandBus->dispatch(new UnsubscribeStudentFromCourseCommand( + $input->getArgument('studentId'), + $input->getArgument('courseId'), + )); + + $output->write('Student # ' . $input->getArgument('studentId') . ' unsubscribed from course #' . $input->getArgument('courseId')); + + return self::SUCCESS; + } +} diff --git a/migrations/.gitignore b/src/Infrastructure/Api/Http/Action/.gitignore similarity index 100% rename from migrations/.gitignore rename to src/Infrastructure/Api/Http/Action/.gitignore diff --git a/src/Kernel.php b/src/Kernel.php index be5f331..4b03e78 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -1,5 +1,7 @@