From 235e33f01f6b49e4fe07907dd59168340fa6aee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Sat, 15 Jun 2024 20:36:04 +0200 Subject: [PATCH 1/9] Improving exceptions --- src/EventSourcingException.php | 14 ++++ .../Exception/ExtractorException.php | 4 +- .../ReflectionPropertyExtractorException.php | 4 +- .../AggregateFactoryInterface.php | 2 - .../AggregateFactory/ReflectionFactory.php | 73 ++++++++++++++----- .../EventSourcedRepositoryException.php | 21 ++++++ 6 files changed, 92 insertions(+), 26 deletions(-) create mode 100644 src/EventSourcingException.php create mode 100644 src/Repository/EventSourcedRepositoryException.php diff --git a/src/EventSourcingException.php b/src/EventSourcingException.php new file mode 100644 index 0000000..b2f7420 --- /dev/null +++ b/src/EventSourcingException.php @@ -0,0 +1,14 @@ + $classMap */ public function __construct( - protected readonly string $methodName = 'applyEventsFromHistory', - protected readonly array $classMap = [] + protected string $methodName = 'applyEventsFromHistory', + protected array $classMap = [] ) { } public function reconstituteFromEvents(string|object $aggregate, Iterator $events): object { if ($aggregate instanceof SnapshotInterface) { - $aggregate = $aggregate->getAggregateRoot(); - $aggregate->{$this->methodName}($events); - - return $aggregate; + return $this->fromSnapshot($aggregate, $events); } if (is_string($aggregate)) { - if (isset($this->classMap[$aggregate])) { - $aggregate = $this->classMap[$aggregate]; - } + return $this->fromString($aggregate, $events); + } - /** @phpstan-ignore-next-line */ - $reflectionClass = new ReflectionClass($aggregate); - $aggregate = $reflectionClass->newInstanceWithoutConstructor(); - $aggregate->{$this->methodName}($events); + throw EventSourcedRepositoryException::couldNotReconstituteAggregate($aggregate); + } + + /** + * @param object $aggregate + * @param Iterator $events + * @return mixed + * @throws EventSourcedRepositoryException + */ + protected function fromSnapshot(object $aggregate, Iterator $events) + { + $aggregate = $aggregate->getAggregateRoot(); + $this->assertAggregateHasMethod($aggregate); + $aggregate->{$this->methodName}($events); + + return $aggregate; + } - return $aggregate; + /** + * @param string $aggregate + * @param Iterator $events + * @return object|string + * @throws EventSourcedRepositoryException + * @throws \ReflectionException + */ + protected function fromString(string $aggregate, Iterator $events) { + if (isset($this->classMap[$aggregate])) { + $aggregate = $this->classMap[$aggregate]; } - throw new RuntimeException(sprintf( - 'Could not reconstitute aggregate of type: %s', - gettype($aggregate) - )); + /** @phpstan-ignore-next-line */ + $reflectionClass = new ReflectionClass($aggregate); + $aggregate = $reflectionClass->newInstanceWithoutConstructor(); + $this->assertAggregateHasMethod($aggregate); + + $aggregate->{$this->methodName}($events); + + return $aggregate; + } + + protected function assertAggregateHasMethod(object $aggregate): void + { + if (!method_exists($aggregate, $this->methodName)) { + throw new EventSourcedRepositoryException (sprintf( + 'Aggregate class `%s` does not have a method `%s` to reconstruct the aggregate state.', + get_class($aggregate), + $this->methodName + )); + } } } diff --git a/src/Repository/EventSourcedRepositoryException.php b/src/Repository/EventSourcedRepositoryException.php new file mode 100644 index 0000000..528136c --- /dev/null +++ b/src/Repository/EventSourcedRepositoryException.php @@ -0,0 +1,21 @@ + Date: Mon, 24 Jun 2024 01:04:47 +0200 Subject: [PATCH 2/9] Fixing using an attribute to define the domain events property. --- .../AbstractEventSourcedAggregate.php | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) mode change 100644 => 100755 src/Aggregate/AbstractEventSourcedAggregate.php diff --git a/src/Aggregate/AbstractEventSourcedAggregate.php b/src/Aggregate/AbstractEventSourcedAggregate.php old mode 100644 new mode 100755 index 5e5dbb4..7b7df9e --- a/src/Aggregate/AbstractEventSourcedAggregate.php +++ b/src/Aggregate/AbstractEventSourcedAggregate.php @@ -6,10 +6,14 @@ use Generator; use Iterator; +use Phauthentic\EventSourcing\Aggregate\Attribute\DomainEvents; use Phauthentic\EventSourcing\Aggregate\Exception\AggregateEventVersionMismatchException; use Phauthentic\EventSourcing\Aggregate\Exception\EventMismatchException; use Phauthentic\EventSourcing\Aggregate\Exception\MissingEventHandlerException; use Phauthentic\EventSourcing\DomainEvent\AggregateIdentityProvidingEventInterface; +use Phauthentic\EventSourcing\Repository\EventSourcedRepositoryException; +use ReflectionClass; +use ReflectionProperty; /** * @@ -48,7 +52,34 @@ protected function recordThat(object $event): void $this->aggregateVersion++; } - $this->{$this->domainEventsProperty}[$this->aggregateVersion] = $event; + $reflectionClass = new ReflectionClass($this); + $domainEventsProperty = $this->findDomainEventsProperty($reflectionClass); + + if ($domainEventsProperty !== null) { + if ($domainEventsProperty->isPrivate()) { + $domainEventsProperty->setAccessible(true); + } + + $events = $domainEventsProperty->getValue($this); + $events[] = $event; + $domainEventsProperty->setValue($this, $events); + } else { + throw new EventSourcedRepositoryException(sprintf( + 'Could not find domain events property %s', + $this->domainEventsProperty + )); + } + } + + private function findDomainEventsProperty(ReflectionClass $reflectionClass): ?ReflectionProperty + { + foreach ($reflectionClass->getProperties() as $property) { + if (!empty($property->getAttributes(DomainEvents::class))) { + return $property; + } + } + + return null; } protected function getEventNameFromEvent(object $event): string From 8fc5235ceb356c7895151688d3efad8927e7beee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Mon, 24 Jun 2024 01:10:03 +0200 Subject: [PATCH 3/9] Refactoring the base repository class into a trait. --- .../AbstractEventSourcedAggregate.php | 164 +--------------- src/Aggregate/EventSourcedAggregateTrait.php | 175 ++++++++++++++++++ 2 files changed, 176 insertions(+), 163 deletions(-) create mode 100644 src/Aggregate/EventSourcedAggregateTrait.php diff --git a/src/Aggregate/AbstractEventSourcedAggregate.php b/src/Aggregate/AbstractEventSourcedAggregate.php index 7b7df9e..878e54a 100755 --- a/src/Aggregate/AbstractEventSourcedAggregate.php +++ b/src/Aggregate/AbstractEventSourcedAggregate.php @@ -4,172 +4,10 @@ namespace Phauthentic\EventSourcing\Aggregate; -use Generator; -use Iterator; -use Phauthentic\EventSourcing\Aggregate\Attribute\DomainEvents; -use Phauthentic\EventSourcing\Aggregate\Exception\AggregateEventVersionMismatchException; -use Phauthentic\EventSourcing\Aggregate\Exception\EventMismatchException; -use Phauthentic\EventSourcing\Aggregate\Exception\MissingEventHandlerException; -use Phauthentic\EventSourcing\DomainEvent\AggregateIdentityProvidingEventInterface; -use Phauthentic\EventSourcing\Repository\EventSourcedRepositoryException; -use ReflectionClass; -use ReflectionProperty; - /** * */ abstract class AbstractEventSourcedAggregate { - protected string $aggregateId = ''; - - /** - * @var array - */ - protected array $aggregateEvents = []; - - protected string $domainEventsProperty = 'aggregateEvents'; - - protected int $aggregateVersion = 0; - - protected const EVENT_METHOD_PREFIX = 'when'; - - protected const EVENT_METHOD_SUFFIX = ''; - - protected bool $applyEventOnRecordThat = false; - - /** - * Applies and records the event - * - * @param object $event - * @return void - * @throws EventMismatchException|MissingEventHandlerException - */ - protected function recordThat(object $event): void - { - if ($this->applyEventOnRecordThat) { - $this->applyEvent($event); - } else { - $this->aggregateVersion++; - } - - $reflectionClass = new ReflectionClass($this); - $domainEventsProperty = $this->findDomainEventsProperty($reflectionClass); - - if ($domainEventsProperty !== null) { - if ($domainEventsProperty->isPrivate()) { - $domainEventsProperty->setAccessible(true); - } - - $events = $domainEventsProperty->getValue($this); - $events[] = $event; - $domainEventsProperty->setValue($this, $events); - } else { - throw new EventSourcedRepositoryException(sprintf( - 'Could not find domain events property %s', - $this->domainEventsProperty - )); - } - } - - private function findDomainEventsProperty(ReflectionClass $reflectionClass): ?ReflectionProperty - { - foreach ($reflectionClass->getProperties() as $property) { - if (!empty($property->getAttributes(DomainEvents::class))) { - return $property; - } - } - - return null; - } - - protected function getEventNameFromEvent(object $event): string - { - $eventClass = get_class($event); - $eventName = ucfirst(substr($eventClass, strrpos($eventClass, '\\') + 1)); - - if (!empty(static::EVENT_METHOD_SUFFIX)) { - $eventName = substr($eventName, 0, -strlen(static::EVENT_METHOD_SUFFIX)); - } - - return static::EVENT_METHOD_PREFIX . $eventName; - } - - /** - * @param object $event - * @param string $eventName - * @return void - * @throws MissingEventHandlerException - */ - protected function assertEventHandlerExists(object $event, string $eventName): void - { - if (method_exists($this, $eventName)) { - return; - } - - throw MissingEventHandlerException::withNameAndClass( - $eventName, - get_class($event), - get_class($this) - ); - } - - protected function assertEventMatchesAggregate(object $event): void - { - if ( - $event instanceof AggregateIdentityProvidingEventInterface - && $this->aggregateId !== $event->getAggregateId() - ) { - throw EventMismatchException::eventDoesNotMatchAggregateWith( - $event, - $this, - ); - } - } - - /** - * @param object $event - * @return void - * @throws EventMismatchException|MissingEventHandlerException - */ - protected function applyEvent(object $event): void - { - $eventName = $this->getEventNameFromEvent($event); - - $this->assertEventHandlerExists($event, $eventName); - $this->assertEventMatchesAggregate($event); - - $this->{$eventName}($event); - $this->aggregateVersion++; - } - - /** - * @param Iterator|array|Generator $events - * @return void - * @throws EventMismatchException|AggregateEventVersionMismatchException|MissingEventHandlerException - */ - public function applyEventsFromHistory(Iterator|array|Generator $events): void - { - foreach ($events as $event) { - $this->assertNextVersion($event); - $this->applyEvent($event->getPayload()); - } - } - - /** - * @param object $event - * @return void - * @throws AggregateEventVersionMismatchException - */ - protected function assertNextVersion(object $event): void - { - if ( - $event instanceof AggregateIdentityProvidingEventInterface - && $this->aggregateVersion + 1 !== $event->getAggregateVersion() - ) { - throw AggregateEventVersionMismatchException::fromVersions( - $this->aggregateVersion, - $event->getAggregateVersion() - ); - } - } + use EventSourcedAggregateTrait; } diff --git a/src/Aggregate/EventSourcedAggregateTrait.php b/src/Aggregate/EventSourcedAggregateTrait.php new file mode 100644 index 0000000..3dead35 --- /dev/null +++ b/src/Aggregate/EventSourcedAggregateTrait.php @@ -0,0 +1,175 @@ + + */ + protected array $aggregateEvents = []; + + protected string $domainEventsProperty = 'aggregateEvents'; + + protected int $aggregateVersion = 0; + + protected const EVENT_METHOD_PREFIX = 'when'; + + protected const EVENT_METHOD_SUFFIX = ''; + + protected bool $applyEventOnRecordThat = false; + + /** + * Applies and records the event + * + * @param object $event + * @return void + * @throws EventMismatchException|MissingEventHandlerException + */ + protected function recordThat(object $event): void + { + if ($this->applyEventOnRecordThat) { + $this->applyEvent($event); + } else { + $this->aggregateVersion++; + } + + $reflectionClass = new ReflectionClass($this); + $domainEventsProperty = $this->findDomainEventsProperty($reflectionClass); + + if ($domainEventsProperty !== null) { + if ($domainEventsProperty->isPrivate()) { + $domainEventsProperty->setAccessible(true); + } + + $events = $domainEventsProperty->getValue($this); + $events[] = $event; + $domainEventsProperty->setValue($this, $events); + } else { + throw new EventSourcedRepositoryException(sprintf( + 'Could not find domain events property %s', + $this->domainEventsProperty + )); + } + } + + private function findDomainEventsProperty(ReflectionClass $reflectionClass): ?ReflectionProperty + { + foreach ($reflectionClass->getProperties() as $property) { + if (!empty($property->getAttributes(DomainEvents::class))) { + return $property; + } + } + + return null; + } + + protected function getEventNameFromEvent(object $event): string + { + $eventClass = get_class($event); + $eventName = ucfirst(substr($eventClass, strrpos($eventClass, '\\') + 1)); + + if (!empty(static::EVENT_METHOD_SUFFIX)) { + $eventName = substr($eventName, 0, -strlen(static::EVENT_METHOD_SUFFIX)); + } + + return static::EVENT_METHOD_PREFIX . $eventName; + } + + /** + * @param object $event + * @param string $eventName + * @return void + * @throws MissingEventHandlerException + */ + protected function assertEventHandlerExists(object $event, string $eventName): void + { + if (method_exists($this, $eventName)) { + return; + } + + throw MissingEventHandlerException::withNameAndClass( + $eventName, + get_class($event), + get_class($this) + ); + } + + protected function assertEventMatchesAggregate(object $event): void + { + if ( + $event instanceof AggregateIdentityProvidingEventInterface + && $this->aggregateId !== $event->getAggregateId() + ) { + throw EventMismatchException::eventDoesNotMatchAggregateWith( + $event, + $this, + ); + } + } + + /** + * @param object $event + * @return void + * @throws EventMismatchException|MissingEventHandlerException + */ + protected function applyEvent(object $event): void + { + $eventName = $this->getEventNameFromEvent($event); + + $this->assertEventHandlerExists($event, $eventName); + $this->assertEventMatchesAggregate($event); + + $this->{$eventName}($event); + $this->aggregateVersion++; + } + + /** + * @param Iterator|array|Generator $events + * @return void + * @throws EventMismatchException|AggregateEventVersionMismatchException|MissingEventHandlerException + */ + public function applyEventsFromHistory(Iterator|array|Generator $events): void + { + foreach ($events as $event) { + $this->assertNextVersion($event); + $this->applyEvent($event->getPayload()); + } + } + + /** + * @param object $event + * @return void + * @throws AggregateEventVersionMismatchException + */ + protected function assertNextVersion(object $event): void + { + if ( + $event instanceof AggregateIdentityProvidingEventInterface + && $this->aggregateVersion + 1 !== $event->getAggregateVersion() + ) { + throw AggregateEventVersionMismatchException::fromVersions( + $this->aggregateVersion, + $event->getAggregateVersion() + ); + } + } +} From 9ffc34ffd6c3cbc195cc5c1c23ad7875589b5863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Thu, 27 Jun 2024 00:36:43 +0200 Subject: [PATCH 4/9] Working on the tests --- .github/workflows/ci.yaml | 15 +- .gitignore | 1 + .idea/eventsourcing.iml | 19 + .idea/php.xml | 26 +- composer.json | 10 +- composer.lock | 1906 +++++++++++++++-- infection.json5 | 14 + phpunit.xml.dist | 6 + .../AbstractEventSourcedAggregate.php | 164 +- src/Aggregate/EventSourcedAggregateTrait.php | 175 -- .../DomainEvent/AbstractDomainEventTest.php | 56 + 11 files changed, 1972 insertions(+), 420 deletions(-) create mode 100644 infection.json5 delete mode 100644 src/Aggregate/EventSourcedAggregateTrait.php create mode 100644 tests/Aggregate/DomainEvent/AbstractDomainEventTest.php diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cfc67db..7225d3d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,6 @@ jobs: --health-timeout 5s --health-retries 5 ports: - # Maps port 6379 on service container to the host - 6379:6379 strategy: @@ -54,7 +53,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: mbstring, json, fileinfo + extensions: json, fileinfo tools: pecl coverage: pcov @@ -68,15 +67,21 @@ jobs: - name: Run PHPUnit run: | - if [[ ${{ matrix.php-version }} == '8.1' ]]; then + if [[ ${{ matrix.php-version }} == '8.2' ]]; then bin/phpunit --coverage-clover=coverage.xml else bin/phpunit fi - name: Code Coverage Report - if: success() && matrix.php-version == '8.1' + if: success() && matrix.php-version == '8.2' uses: codecov/codecov-action@v4 + - name: Run Infection + run: | + if [[ ${{ matrix.php-version }} == '8.2' ]]; then + bin/infection + fi + cs-stan: name: Coding Standard & Static Analysis runs-on: ubuntu-20.04 @@ -90,7 +95,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: mbstring, json, fileinfo + extensions: json, fileinfo coverage: none tools: pecl diff --git a/.gitignore b/.gitignore index 46ccc86..92290bf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /tmp .idea .phpunit.result.cache +infection.log *.drawio.bkp *.drawio.dtmp diff --git a/.idea/eventsourcing.iml b/.idea/eventsourcing.iml index e1b535e..7d5c755 100644 --- a/.idea/eventsourcing.iml +++ b/.idea/eventsourcing.iml @@ -60,6 +60,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/.idea/php.xml b/.idea/php.xml index 09f5427..b0cc9de 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -11,11 +11,6 @@ - - @@ -78,6 +73,25 @@ + + + + + + + + + + + + + + + + + + + @@ -94,4 +108,4 @@ - \ No newline at end of file + diff --git a/composer.json b/composer.json index cbc2f13..bc241b1 100644 --- a/composer.json +++ b/composer.json @@ -17,16 +17,17 @@ }, "require-dev": { "ext-pdo": "*", + "infection/infection": "^0.29.6", "phpmd/phpmd": "^2.15", "phpro/grumphp-shim": "^2.5", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.6", + "psr/container": "^1.0||^2.0", + "psr/log": "^2.0||^3.0", "ramsey/uuid": "^4.7", "squizlabs/php_codesniffer": "^4.0", "symfony/messenger": "^6.0||^7.0", - "symfony/var-dumper": "^6.0||^7.1", - "psr/container": "^1.0||^2.0", - "psr/log": "^2.0||^3.0" + "symfony/var-dumper": "^6.0||^7.1" }, "suggest": { "psr/container": "If you want to use the repository factory.", @@ -49,7 +50,8 @@ "sort-packages": true, "bin-dir": "./bin", "allow-plugins": { - "phpro/grumphp-shim": true + "phpro/grumphp-shim": true, + "infection/extension-installer": true } }, "scripts": { diff --git a/composer.lock b/composer.lock index a66d73a..2592766 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": "9a9437ab7eff3ca7bb6cf59872c60942", + "content-hash": "cb6cfb3c9e7069894d5ca18b3c999124", "packages": [ { "name": "phauthentic/event-store", @@ -146,6 +146,94 @@ ], "time": "2023-01-15T23:15:59+00:00" }, + { + "name": "colinodell/json5", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/colinodell/json5.git", + "reference": "5724d21bc5c910c2560af1b8915f0cc0163579c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/colinodell/json5/zipball/5724d21bc5c910c2560af1b8915f0cc0163579c8", + "reference": "5724d21bc5c910c2560af1b8915f0cc0163579c8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.0" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "^1.7.0", + "phpstan/phpstan": "^1.10.57", + "scrutinizer/ocular": "^1.9", + "squizlabs/php_codesniffer": "^3.8.1", + "symfony/finder": "^6.0|^7.0", + "symfony/phpunit-bridge": "^7.0.3" + }, + "bin": [ + "bin/json5" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "files": [ + "src/global.php" + ], + "psr-4": { + "ColinODell\\Json5\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Developer" + } + ], + "description": "UTF-8 compatible JSON5 parser for PHP", + "homepage": "https://github.com/colinodell/json5", + "keywords": [ + "JSON5", + "json", + "json5_decode", + "json_decode" + ], + "support": { + "issues": "https://github.com/colinodell/json5/issues", + "source": "https://github.com/colinodell/json5/tree/v3.0.0" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://www.patreon.com/colinodell", + "type": "patreon" + } + ], + "time": "2024-02-09T13:06:12+00:00" + }, { "name": "composer/pcre", "version": "3.1.3", @@ -354,100 +442,92 @@ "time": "2022-12-30T00:23:10+00:00" }, { - "name": "myclabs/deep-copy", - "version": "1.11.1", + "name": "fidry/cpu-core-counter", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + "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": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], "psr-4": { - "DeepCopy\\": "src/DeepCopy/" + "Fidry\\CpuCoreCounter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "CPU", + "core" ], "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://github.com/theofidry", + "type": "github" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-02-07T09:43:46+00:00" }, { - "name": "nikic/php-parser", - "version": "v5.0.2", + "name": "infection/abstract-testframework-adapter", + "version": "0.5.0", "source": { "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + "url": "https://github.com/infection/abstract-testframework-adapter.git", + "reference": "18925e20d15d1a5995bb85c9dc09e8751e1e069b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "url": "https://api.github.com/repos/infection/abstract-testframework-adapter/zipball/18925e20d15d1a5995bb85c9dc09e8751e1e069b", + "reference": "18925e20d15d1a5995bb85c9dc09e8751e1e069b", "shasum": "" }, "require": { - "ext-ctype": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "php": ">=7.4" + "php": "^7.4 || ^8.0" }, "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^2.17", + "phpunit/phpunit": "^9.5" }, - "bin": [ - "bin/php-parse" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, "autoload": { "psr-4": { - "PhpParser\\": "lib/PhpParser" + "Infection\\AbstractTestFramework\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -456,115 +536,121 @@ ], "authors": [ { - "name": "Nikita Popov" + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" } ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], + "description": "Abstract Test Framework Adapter for Infection", "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + "issues": "https://github.com/infection/abstract-testframework-adapter/issues", + "source": "https://github.com/infection/abstract-testframework-adapter/tree/0.5.0" }, - "time": "2024-03-05T20:51:40+00:00" + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" + } + ], + "time": "2021-08-17T18:49:12+00:00" }, { - "name": "pdepend/pdepend", - "version": "2.16.2", + "name": "infection/extension-installer", + "version": "0.1.2", "source": { "type": "git", - "url": "https://github.com/pdepend/pdepend.git", - "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" + "url": "https://github.com/infection/extension-installer.git", + "reference": "9b351d2910b9a23ab4815542e93d541e0ca0cdcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", - "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "url": "https://api.github.com/repos/infection/extension-installer/zipball/9b351d2910b9a23ab4815542e93d541e0ca0cdcf", + "reference": "9b351d2910b9a23ab4815542e93d541e0ca0cdcf", "shasum": "" }, "require": { - "php": ">=5.3.7", - "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0", - "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0", - "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0", - "symfony/polyfill-mbstring": "^1.19" + "composer-plugin-api": "^1.1 || ^2.0" }, "require-dev": { - "easy-doc/easy-doc": "0.0.0|^1.2.3", - "gregwar/rst": "^1.0", - "squizlabs/php_codesniffer": "^2.0.0" + "composer/composer": "^1.9 || ^2.0", + "friendsofphp/php-cs-fixer": "^2.18, <2.19", + "infection/infection": "^0.15.2", + "php-coveralls/php-coveralls": "^2.4", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.10", + "phpstan/phpstan-phpunit": "^0.12.6", + "phpstan/phpstan-strict-rules": "^0.12.2", + "phpstan/phpstan-webmozart-assert": "^0.12.2", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.8" }, - "bin": [ - "src/bin/pdepend" - ], - "type": "library", + "type": "composer-plugin", "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } + "class": "Infection\\ExtensionInstaller\\Plugin" }, "autoload": { "psr-4": { - "PDepend\\": "src/main/php/PDepend" + "Infection\\ExtensionInstaller\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "description": "Official version of pdepend to be handled with Composer", - "keywords": [ - "PHP Depend", - "PHP_Depend", - "dev", - "pdepend" + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } ], + "description": "Infection Extension Installer", "support": { - "issues": "https://github.com/pdepend/pdepend/issues", - "source": "https://github.com/pdepend/pdepend/tree/2.16.2" + "issues": "https://github.com/infection/extension-installer/issues", + "source": "https://github.com/infection/extension-installer/tree/0.1.2" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", - "type": "tidelift" + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" } ], - "time": "2023-12-17T18:09:59+00:00" + "time": "2021-10-20T22:08:34+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.4", + "name": "infection/include-interceptor", + "version": "0.2.5", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" + "url": "https://github.com/infection/include-interceptor.git", + "reference": "0cc76d95a79d9832d74e74492b0a30139904bdf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", + "url": "https://api.github.com/repos/infection/include-interceptor/zipball/0cc76d95a79d9832d74e74492b0a30139904bdf7", + "reference": "0cc76d95a79d9832d74e74492b0a30139904bdf7", "shasum": "" }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "infection/infection": "^0.15.0", + "phan/phan": "^2.4 || ^3", + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "^0.12.8", + "phpunit/phpunit": "^8.5", + "vimeo/psalm": "^3.8" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Infection\\StreamWrapper\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -572,56 +658,98 @@ ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" } ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "description": "Stream Wrapper: Include Interceptor. Allows to replace included (autoloaded) file with another one.", "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" + "issues": "https://github.com/infection/include-interceptor/issues", + "source": "https://github.com/infection/include-interceptor/tree/0.2.5" }, "funding": [ { - "url": "https://github.com/theseer", + "url": "https://github.com/infection", "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" } ], - "time": "2024-03-03T12:33:53+00:00" + "time": "2021-08-09T10:03:57+00:00" }, { - "name": "phar-io/version", - "version": "3.2.1", + "name": "infection/infection", + "version": "0.29.6", "source": { "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + "url": "https://github.com/infection/infection.git", + "reference": "a8510c1d472892dda2ae32e2c4b2e795533db810" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "url": "https://api.github.com/repos/infection/infection/zipball/a8510c1d472892dda2ae32e2c4b2e795533db810", + "reference": "a8510c1d472892dda2ae32e2c4b2e795533db810", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "colinodell/json5": "^2.2 || ^3.0", + "composer-runtime-api": "^2.0", + "composer/xdebug-handler": "^2.0 || ^3.0", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "fidry/cpu-core-counter": "^0.4.0 || ^0.5.0 || ^1.0", + "infection/abstract-testframework-adapter": "^0.5.0", + "infection/extension-installer": "^0.1.0", + "infection/include-interceptor": "^0.2.5", + "infection/mutator": "^0.4", + "justinrainbow/json-schema": "^5.2.10", + "nikic/php-parser": "^5.0", + "ondram/ci-detector": "^4.1.0", + "php": "^8.1", + "sanmai/later": "^0.1.1", + "sanmai/pipeline": "^5.1 || ^6", + "sebastian/diff": "^3.0.2 || ^4.0 || ^5.0 || ^6.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "thecodingmachine/safe": "^2.1.2", + "webmozart/assert": "^1.11" + }, + "conflict": { + "antecedent/patchwork": "<2.1.25", + "dg/bypass-finals": "<1.4.1", + "phpunit/php-code-coverage": ">9,<9.1.4 || >9.2.17,<9.2.21" }, + "require-dev": { + "ext-simplexml": "*", + "fidry/makefile": "^1.0", + "helmich/phpunit-json-assert": "^3.0", + "phpspec/prophecy": "^1.15", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.1.0", + "phpstan/phpstan": "^1.10.15", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpstan/phpstan-strict-rules": "^1.1.0", + "phpstan/phpstan-webmozart-assert": "^1.0.2", + "phpunit/phpunit": "^10.5", + "rector/rector": "^1.0", + "sidz/phpstan-rules": "^0.4", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0", + "thecodingmachine/phpstan-safe-rule": "^1.2.0" + }, + "bin": [ + "bin/infection" + ], "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Infection\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -629,22 +757,552 @@ ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com", + "homepage": "https://twitter.com/maks_rafalko" }, { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "name": "Oleg Zhulnev", + "homepage": "https://github.com/sidz" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", + "name": "Gert de Pagter", + "homepage": "https://github.com/BackEndTea" + }, + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com", + "homepage": "https://twitter.com/tfidry" + }, + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com", + "homepage": "https://www.alexeykopytko.com" + }, + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Infection is a Mutation Testing framework for PHP. The mutation adequacy score can be used to measure the effectiveness of a test set in terms of its ability to detect faults.", + "keywords": [ + "coverage", + "mutant", + "mutation framework", + "mutation testing", + "testing", + "unit testing" + ], + "support": { + "issues": "https://github.com/infection/infection/issues", + "source": "https://github.com/infection/infection/tree/0.29.6" + }, + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" + } + ], + "time": "2024-06-21T10:21:05+00:00" + }, + { + "name": "infection/mutator", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/infection/mutator.git", + "reference": "51d6d01a2357102030aee9d603063c4bad86b144" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/mutator/zipball/51d6d01a2357102030aee9d603063c4bad86b144", + "reference": "51d6d01a2357102030aee9d603063c4bad86b144", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Infection\\Mutator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } + ], + "description": "Mutator interface to implement custom mutators (mutation operators) for Infection", + "support": { + "issues": "https://github.com/infection/mutator/issues", + "source": "https://github.com/infection/mutator/tree/0.4.0" + }, + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" + } + ], + "time": "2024-05-14T22:39:59+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "v5.2.13", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/v5.2.13" + }, + "time": "2023-09-26T02:20:38+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.0.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", + "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + }, + "time": "2024-03-05T20:51:40+00:00" + }, + { + "name": "ondram/ci-detector", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/OndraM/ci-detector.git", + "reference": "8b0223b5ed235fd377c75fdd1bfcad05c0f168b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/OndraM/ci-detector/zipball/8b0223b5ed235fd377c75fdd1bfcad05c0f168b8", + "reference": "8b0223b5ed235fd377c75fdd1bfcad05c0f168b8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.13.2", + "lmc/coding-standard": "^3.0.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.1.0", + "phpstan/phpstan": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpunit/phpunit": "^9.6.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "OndraM\\CiDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Machulda", + "email": "ondrej.machulda@gmail.com" + } + ], + "description": "Detect continuous integration environment and provide unified access to properties of current build", + "keywords": [ + "CircleCI", + "Codeship", + "Wercker", + "adapter", + "appveyor", + "aws", + "aws codebuild", + "azure", + "azure devops", + "azure pipelines", + "bamboo", + "bitbucket", + "buddy", + "ci-info", + "codebuild", + "continuous integration", + "continuousphp", + "devops", + "drone", + "github", + "gitlab", + "interface", + "jenkins", + "pipelines", + "sourcehut", + "teamcity", + "travis" + ], + "support": { + "issues": "https://github.com/OndraM/ci-detector/issues", + "source": "https://github.com/OndraM/ci-detector/tree/4.2.0" + }, + "time": "2024-03-12T13:22:30+00:00" + }, + { + "name": "pdepend/pdepend", + "version": "2.16.2", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "shasum": "" + }, + "require": { + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/polyfill-mbstring": "^1.19" + }, + "require-dev": { + "easy-doc/easy-doc": "0.0.0|^1.2.3", + "gregwar/rst": "^1.0", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "keywords": [ + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" + ], + "support": { + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2023-12-17T18:09:59+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", "source": "https://github.com/phar-io/version/tree/3.2.1" @@ -1605,6 +2263,135 @@ ], "time": "2023-11-08T05:53:05+00:00" }, + { + "name": "sanmai/later", + "version": "0.1.4", + "source": { + "type": "git", + "url": "https://github.com/sanmai/later.git", + "reference": "e24c4304a4b1349c2a83151a692cec0c10579f60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sanmai/later/zipball/e24c4304a4b1349c2a83151a692cec0c10579f60", + "reference": "e24c4304a4b1349c2a83151a692cec0c10579f60", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^3.35.1", + "infection/infection": ">=0.27.6", + "phan/phan": ">=2", + "php-coveralls/php-coveralls": "^2.0", + "phpstan/phpstan": ">=1.4.5", + "phpunit/phpunit": ">=9.5 <10", + "vimeo/psalm": ">=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.1.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Later\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com" + } + ], + "description": "Later: deferred wrapper object", + "support": { + "issues": "https://github.com/sanmai/later/issues", + "source": "https://github.com/sanmai/later/tree/0.1.4" + }, + "funding": [ + { + "url": "https://github.com/sanmai", + "type": "github" + } + ], + "time": "2023-10-24T00:25:28+00:00" + }, + { + "name": "sanmai/pipeline", + "version": "v6.11", + "source": { + "type": "git", + "url": "https://github.com/sanmai/pipeline.git", + "reference": "a5fa2a6c6ca93efa37e7c24aab72f47448a6b110" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sanmai/pipeline/zipball/a5fa2a6c6ca93efa37e7c24aab72f47448a6b110", + "reference": "a5fa2a6c6ca93efa37e7c24aab72f47448a6b110", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^3.17", + "infection/infection": ">=0.10.5", + "league/pipeline": "^0.3 || ^1.0", + "phan/phan": ">=1.1", + "php-coveralls/php-coveralls": "^2.4.1", + "phpstan/phpstan": ">=0.10", + "phpunit/phpunit": ">=9.4", + "vimeo/psalm": ">=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "v6.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Pipeline\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com" + } + ], + "description": "General-purpose collections pipeline", + "support": { + "issues": "https://github.com/sanmai/pipeline/issues", + "source": "https://github.com/sanmai/pipeline/tree/v6.11" + }, + "funding": [ + { + "url": "https://github.com/sanmai", + "type": "github" + } + ], + "time": "2024-06-15T03:11:19+00:00" + }, { "name": "sebastian/cli-parser", "version": "1.0.2", @@ -2672,11 +3459,91 @@ }, "type": "library", "autoload": { - "files": [ - "Resources/now.php" - ], + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.0.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-03-02T12:46:12+00:00" + }, + { + "name": "symfony/config", + "version": "v7.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "7fc7e18a73ec8125fd95928c0340470d64760deb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/7fc7e18a73ec8125fd95928c0340470d64760deb", + "reference": "7fc7e18a73ec8125fd95928c0340470d64760deb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { "psr-4": { - "Symfony\\Component\\Clock\\": "" + "Symfony\\Component\\Config\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2688,23 +3555,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Decouples applications from the system clock", + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", - "keywords": [ - "clock", - "psr20", - "time" - ], "support": { - "source": "https://github.com/symfony/clock/tree/v7.0.5" + "source": "https://github.com/symfony/config/tree/v7.0.6" }, "funding": [ { @@ -2720,43 +3582,55 @@ "type": "tidelift" } ], - "time": "2024-03-02T12:46:12+00:00" + "time": "2024-03-27T19:55:25+00:00" }, { - "name": "symfony/config", - "version": "v7.0.6", + "name": "symfony/console", + "version": "v7.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "7fc7e18a73ec8125fd95928c0340470d64760deb" + "url": "https://github.com/symfony/console.git", + "reference": "9b008f2d7b21c74ef4d0c3de6077a642bc55ece3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/7fc7e18a73ec8125fd95928c0340470d64760deb", - "reference": "7fc7e18a73ec8125fd95928c0340470d64760deb", + "url": "https://api.github.com/repos/symfony/console/zipball/9b008f2d7b21c74ef4d0c3de6077a642bc55ece3", + "reference": "9b008f2d7b21c74ef4d0c3de6077a642bc55ece3", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^6.4|^7.0", - "symfony/polyfill-ctype": "~1.8" + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0" }, "conflict": { - "symfony/finder": "<6.4", - "symfony/service-contracts": "<2.5" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", "symfony/messenger": "^6.4|^7.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0" + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Config\\": "" + "Symfony\\Component\\Console\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2776,10 +3650,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], "support": { - "source": "https://github.com/symfony/config/tree/v7.0.6" + "source": "https://github.com/symfony/console/tree/v7.1.1" }, "funding": [ { @@ -2795,7 +3675,7 @@ "type": "tidelift" } ], - "time": "2024-03-27T19:55:25+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/dependency-injection", @@ -3007,6 +3887,70 @@ ], "time": "2024-03-21T19:37:36+00:00" }, + { + "name": "symfony/finder", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/fbb0ba67688b780efbc886c1a0a0948dcf7205d6", + "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.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/messenger", "version": "v7.0.6", @@ -3034,29 +3978,178 @@ "symfony/http-kernel": "<6.4", "symfony/serializer": "<6.4" }, - "require-dev": { - "psr/cache": "^1.0|^2.0|^3.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/property-access": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0" + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Samuel Roze", + "email": "samuel.roze@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps applications send and receive messages to/from other applications or via message queues", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/messenger/tree/v7.0.6" + }, + "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-03-19T11:57:22+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Messenger\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3064,18 +4157,26 @@ ], "authors": [ { - "name": "Samuel Roze", - "email": "samuel.roze@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Helps applications send and receive messages to/from other applications or via message queues", + "description": "Symfony polyfill for intl's grapheme_* functions", "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], "support": { - "source": "https://github.com/symfony/messenger/tree/v7.0.6" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -3091,30 +4192,27 @@ "type": "tidelift" } ], - "time": "2024-03-19T11:57:22+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.30.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { "php": ">=7.1" }, - "provide": { - "ext-ctype": "*" - }, "suggest": { - "ext-ctype": "For best performance" + "ext-intl": "For best performance" }, "type": "library", "extra": { @@ -3128,8 +4226,11 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3137,24 +4238,26 @@ ], "authors": [ { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for ctype functions", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "ctype", + "intl", + "normalizer", "polyfill", - "portable" + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -3170,7 +4273,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -3409,6 +4512,67 @@ ], "time": "2024-01-29T20:11:03+00:00" }, + { + "name": "symfony/process", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "febf90124323a093c7ee06fdb30e765ca3c20028" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/febf90124323a093c7ee06fdb30e765ca3c20028", + "reference": "febf90124323a093c7ee06fdb30e765ca3c20028", + "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.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/service-contracts", "version": "v3.4.2", @@ -3491,6 +4655,93 @@ ], "time": "2023-12-19T21:51:00+00:00" }, + { + "name": "symfony/string", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "60bc311c74e0af215101235aa6f471bcbc032df2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/60bc311c74e0af215101235aa6f471bcbc032df2", + "reference": "60bc311c74e0af215101235aa6f471bcbc032df2", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.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-06-04T06:40:14+00:00" + }, { "name": "symfony/var-dumper", "version": "v6.4.6", @@ -3652,6 +4903,145 @@ ], "time": "2024-03-20T21:25:22+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "files": [ + "deprecated/apc.php", + "deprecated/array.php", + "deprecated/datetime.php", + "deprecated/libevent.php", + "deprecated/misc.php", + "deprecated/password.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "deprecated/strings.php", + "lib/special_cases.php", + "deprecated/mysqli.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "deprecated/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v2.5.0" + }, + "time": "2023-04-05T11:54:14+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.3", @@ -3701,6 +5091,64 @@ } ], "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" } ], "aliases": [], diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 0000000..e2ab411 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,14 @@ +{ + "$schema": "vendor/infection/infection/resources/schema.json", + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "infection.log" + }, + "mutators": { + "@default": true + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e66ded0..3f6a2aa 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,9 +1,15 @@ + + src/DomainEvent/Attribute/ + src/Aggregate/Attribute/ + src/ + src/ + diff --git a/src/Aggregate/AbstractEventSourcedAggregate.php b/src/Aggregate/AbstractEventSourcedAggregate.php index 878e54a..7b7df9e 100755 --- a/src/Aggregate/AbstractEventSourcedAggregate.php +++ b/src/Aggregate/AbstractEventSourcedAggregate.php @@ -4,10 +4,172 @@ namespace Phauthentic\EventSourcing\Aggregate; +use Generator; +use Iterator; +use Phauthentic\EventSourcing\Aggregate\Attribute\DomainEvents; +use Phauthentic\EventSourcing\Aggregate\Exception\AggregateEventVersionMismatchException; +use Phauthentic\EventSourcing\Aggregate\Exception\EventMismatchException; +use Phauthentic\EventSourcing\Aggregate\Exception\MissingEventHandlerException; +use Phauthentic\EventSourcing\DomainEvent\AggregateIdentityProvidingEventInterface; +use Phauthentic\EventSourcing\Repository\EventSourcedRepositoryException; +use ReflectionClass; +use ReflectionProperty; + /** * */ abstract class AbstractEventSourcedAggregate { - use EventSourcedAggregateTrait; + protected string $aggregateId = ''; + + /** + * @var array + */ + protected array $aggregateEvents = []; + + protected string $domainEventsProperty = 'aggregateEvents'; + + protected int $aggregateVersion = 0; + + protected const EVENT_METHOD_PREFIX = 'when'; + + protected const EVENT_METHOD_SUFFIX = ''; + + protected bool $applyEventOnRecordThat = false; + + /** + * Applies and records the event + * + * @param object $event + * @return void + * @throws EventMismatchException|MissingEventHandlerException + */ + protected function recordThat(object $event): void + { + if ($this->applyEventOnRecordThat) { + $this->applyEvent($event); + } else { + $this->aggregateVersion++; + } + + $reflectionClass = new ReflectionClass($this); + $domainEventsProperty = $this->findDomainEventsProperty($reflectionClass); + + if ($domainEventsProperty !== null) { + if ($domainEventsProperty->isPrivate()) { + $domainEventsProperty->setAccessible(true); + } + + $events = $domainEventsProperty->getValue($this); + $events[] = $event; + $domainEventsProperty->setValue($this, $events); + } else { + throw new EventSourcedRepositoryException(sprintf( + 'Could not find domain events property %s', + $this->domainEventsProperty + )); + } + } + + private function findDomainEventsProperty(ReflectionClass $reflectionClass): ?ReflectionProperty + { + foreach ($reflectionClass->getProperties() as $property) { + if (!empty($property->getAttributes(DomainEvents::class))) { + return $property; + } + } + + return null; + } + + protected function getEventNameFromEvent(object $event): string + { + $eventClass = get_class($event); + $eventName = ucfirst(substr($eventClass, strrpos($eventClass, '\\') + 1)); + + if (!empty(static::EVENT_METHOD_SUFFIX)) { + $eventName = substr($eventName, 0, -strlen(static::EVENT_METHOD_SUFFIX)); + } + + return static::EVENT_METHOD_PREFIX . $eventName; + } + + /** + * @param object $event + * @param string $eventName + * @return void + * @throws MissingEventHandlerException + */ + protected function assertEventHandlerExists(object $event, string $eventName): void + { + if (method_exists($this, $eventName)) { + return; + } + + throw MissingEventHandlerException::withNameAndClass( + $eventName, + get_class($event), + get_class($this) + ); + } + + protected function assertEventMatchesAggregate(object $event): void + { + if ( + $event instanceof AggregateIdentityProvidingEventInterface + && $this->aggregateId !== $event->getAggregateId() + ) { + throw EventMismatchException::eventDoesNotMatchAggregateWith( + $event, + $this, + ); + } + } + + /** + * @param object $event + * @return void + * @throws EventMismatchException|MissingEventHandlerException + */ + protected function applyEvent(object $event): void + { + $eventName = $this->getEventNameFromEvent($event); + + $this->assertEventHandlerExists($event, $eventName); + $this->assertEventMatchesAggregate($event); + + $this->{$eventName}($event); + $this->aggregateVersion++; + } + + /** + * @param Iterator|array|Generator $events + * @return void + * @throws EventMismatchException|AggregateEventVersionMismatchException|MissingEventHandlerException + */ + public function applyEventsFromHistory(Iterator|array|Generator $events): void + { + foreach ($events as $event) { + $this->assertNextVersion($event); + $this->applyEvent($event->getPayload()); + } + } + + /** + * @param object $event + * @return void + * @throws AggregateEventVersionMismatchException + */ + protected function assertNextVersion(object $event): void + { + if ( + $event instanceof AggregateIdentityProvidingEventInterface + && $this->aggregateVersion + 1 !== $event->getAggregateVersion() + ) { + throw AggregateEventVersionMismatchException::fromVersions( + $this->aggregateVersion, + $event->getAggregateVersion() + ); + } + } } diff --git a/src/Aggregate/EventSourcedAggregateTrait.php b/src/Aggregate/EventSourcedAggregateTrait.php deleted file mode 100644 index 3dead35..0000000 --- a/src/Aggregate/EventSourcedAggregateTrait.php +++ /dev/null @@ -1,175 +0,0 @@ - - */ - protected array $aggregateEvents = []; - - protected string $domainEventsProperty = 'aggregateEvents'; - - protected int $aggregateVersion = 0; - - protected const EVENT_METHOD_PREFIX = 'when'; - - protected const EVENT_METHOD_SUFFIX = ''; - - protected bool $applyEventOnRecordThat = false; - - /** - * Applies and records the event - * - * @param object $event - * @return void - * @throws EventMismatchException|MissingEventHandlerException - */ - protected function recordThat(object $event): void - { - if ($this->applyEventOnRecordThat) { - $this->applyEvent($event); - } else { - $this->aggregateVersion++; - } - - $reflectionClass = new ReflectionClass($this); - $domainEventsProperty = $this->findDomainEventsProperty($reflectionClass); - - if ($domainEventsProperty !== null) { - if ($domainEventsProperty->isPrivate()) { - $domainEventsProperty->setAccessible(true); - } - - $events = $domainEventsProperty->getValue($this); - $events[] = $event; - $domainEventsProperty->setValue($this, $events); - } else { - throw new EventSourcedRepositoryException(sprintf( - 'Could not find domain events property %s', - $this->domainEventsProperty - )); - } - } - - private function findDomainEventsProperty(ReflectionClass $reflectionClass): ?ReflectionProperty - { - foreach ($reflectionClass->getProperties() as $property) { - if (!empty($property->getAttributes(DomainEvents::class))) { - return $property; - } - } - - return null; - } - - protected function getEventNameFromEvent(object $event): string - { - $eventClass = get_class($event); - $eventName = ucfirst(substr($eventClass, strrpos($eventClass, '\\') + 1)); - - if (!empty(static::EVENT_METHOD_SUFFIX)) { - $eventName = substr($eventName, 0, -strlen(static::EVENT_METHOD_SUFFIX)); - } - - return static::EVENT_METHOD_PREFIX . $eventName; - } - - /** - * @param object $event - * @param string $eventName - * @return void - * @throws MissingEventHandlerException - */ - protected function assertEventHandlerExists(object $event, string $eventName): void - { - if (method_exists($this, $eventName)) { - return; - } - - throw MissingEventHandlerException::withNameAndClass( - $eventName, - get_class($event), - get_class($this) - ); - } - - protected function assertEventMatchesAggregate(object $event): void - { - if ( - $event instanceof AggregateIdentityProvidingEventInterface - && $this->aggregateId !== $event->getAggregateId() - ) { - throw EventMismatchException::eventDoesNotMatchAggregateWith( - $event, - $this, - ); - } - } - - /** - * @param object $event - * @return void - * @throws EventMismatchException|MissingEventHandlerException - */ - protected function applyEvent(object $event): void - { - $eventName = $this->getEventNameFromEvent($event); - - $this->assertEventHandlerExists($event, $eventName); - $this->assertEventMatchesAggregate($event); - - $this->{$eventName}($event); - $this->aggregateVersion++; - } - - /** - * @param Iterator|array|Generator $events - * @return void - * @throws EventMismatchException|AggregateEventVersionMismatchException|MissingEventHandlerException - */ - public function applyEventsFromHistory(Iterator|array|Generator $events): void - { - foreach ($events as $event) { - $this->assertNextVersion($event); - $this->applyEvent($event->getPayload()); - } - } - - /** - * @param object $event - * @return void - * @throws AggregateEventVersionMismatchException - */ - protected function assertNextVersion(object $event): void - { - if ( - $event instanceof AggregateIdentityProvidingEventInterface - && $this->aggregateVersion + 1 !== $event->getAggregateVersion() - ) { - throw AggregateEventVersionMismatchException::fromVersions( - $this->aggregateVersion, - $event->getAggregateVersion() - ); - } - } -} diff --git a/tests/Aggregate/DomainEvent/AbstractDomainEventTest.php b/tests/Aggregate/DomainEvent/AbstractDomainEventTest.php new file mode 100644 index 0000000..f23e816 --- /dev/null +++ b/tests/Aggregate/DomainEvent/AbstractDomainEventTest.php @@ -0,0 +1,56 @@ +aggregateId = $aggregateId; + $this->aggregateVersion = $aggregateVersion; + $this->domainEventType = $domainEventType; + } +} + +/** + * + */ +class AbstractDomainEventTest extends TestCase +{ + public function testGetAggregateId(): void + { + $event = new ConcreteDomainEvent('123', 1, 'TestEvent'); + $this->assertEquals('123', $event->getAggregateId()); + } + + public function testGetAggregateVersion(): void + { + $event = new ConcreteDomainEvent('123', 1, 'TestEvent'); + $this->assertEquals(1, $event->getAggregateVersion()); + } + + public function testGetEventType(): void + { + $event = new ConcreteDomainEvent('123', 1, 'TestEvent'); + $this->assertEquals('TestEvent', $event->getEventType()); + } + + public function testImplementsRequiredInterfaces(): void + { + $event = new ConcreteDomainEvent('123', 1, 'TestEvent'); + $this->assertInstanceOf(AggregateIdentityProvidingEventInterface::class, $event); + $this->assertInstanceOf(AggregateVersionProvidingEvent::class, $event); + $this->assertInstanceOf(TypeProvidingDomainEventInterface::class, $event); + } +} From 8d167b94fd716f77a90d1df93bd769637285ca60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Thu, 27 Jun 2024 23:38:13 +0200 Subject: [PATCH 5/9] Working on the tests --- .../AbstractEventSourcedAggregate.php | 25 +- .../AggregateFactory/ReflectionFactory.php | 17 +- src/Repository/EventSourcedRepository.php | 5 +- .../AbstractEventSourcedAggregateTest.php | 256 ++++++++++++++++++ 4 files changed, 281 insertions(+), 22 deletions(-) create mode 100644 tests/Aggregate/AbstractEventSourcedAggregateTest.php diff --git a/src/Aggregate/AbstractEventSourcedAggregate.php b/src/Aggregate/AbstractEventSourcedAggregate.php index 7b7df9e..51cfe04 100755 --- a/src/Aggregate/AbstractEventSourcedAggregate.php +++ b/src/Aggregate/AbstractEventSourcedAggregate.php @@ -12,6 +12,7 @@ use Phauthentic\EventSourcing\Aggregate\Exception\MissingEventHandlerException; use Phauthentic\EventSourcing\DomainEvent\AggregateIdentityProvidingEventInterface; use Phauthentic\EventSourcing\Repository\EventSourcedRepositoryException; +use Phauthentic\EventStore\EventInterface; use ReflectionClass; use ReflectionProperty; @@ -46,12 +47,6 @@ abstract class AbstractEventSourcedAggregate */ protected function recordThat(object $event): void { - if ($this->applyEventOnRecordThat) { - $this->applyEvent($event); - } else { - $this->aggregateVersion++; - } - $reflectionClass = new ReflectionClass($this); $domainEventsProperty = $this->findDomainEventsProperty($reflectionClass); @@ -69,6 +64,8 @@ protected function recordThat(object $event): void $this->domainEventsProperty )); } + + $this->aggregateVersion++; } private function findDomainEventsProperty(ReflectionClass $reflectionClass): ?ReflectionProperty @@ -143,32 +140,30 @@ protected function applyEvent(object $event): void } /** - * @param Iterator|array|Generator $events + * @param Iterator|array|Generator $events * @return void * @throws EventMismatchException|AggregateEventVersionMismatchException|MissingEventHandlerException */ public function applyEventsFromHistory(Iterator|array|Generator $events): void { + /** @var EventInterface $event */ foreach ($events as $event) { - $this->assertNextVersion($event); + $this->assertNextVersion($event->getAggregateVersion()); $this->applyEvent($event->getPayload()); } } /** - * @param object $event + * @param int $eventVersion * @return void * @throws AggregateEventVersionMismatchException */ - protected function assertNextVersion(object $event): void + protected function assertNextVersion(int $eventVersion): void { - if ( - $event instanceof AggregateIdentityProvidingEventInterface - && $this->aggregateVersion + 1 !== $event->getAggregateVersion() - ) { + if ($this->aggregateVersion + 1 !== $eventVersion) { throw AggregateEventVersionMismatchException::fromVersions( $this->aggregateVersion, - $event->getAggregateVersion() + $eventVersion ); } } diff --git a/src/Repository/AggregateFactory/ReflectionFactory.php b/src/Repository/AggregateFactory/ReflectionFactory.php index ad026c6..9ee9ec3 100644 --- a/src/Repository/AggregateFactory/ReflectionFactory.php +++ b/src/Repository/AggregateFactory/ReflectionFactory.php @@ -8,6 +8,7 @@ use Phauthentic\EventSourcing\Repository\EventSourcedRepositoryException; use Phauthentic\SnapshotStore\SnapshotInterface; use ReflectionClass; +use ReflectionException; /** * @@ -34,18 +35,22 @@ public function reconstituteFromEvents(string|object $aggregate, Iterator $event return $this->fromString($aggregate, $events); } + if (is_object($aggregate)) { + $aggregate = get_class($aggregate); + } + throw EventSourcedRepositoryException::couldNotReconstituteAggregate($aggregate); } /** - * @param object $aggregate + * @param SnapshotInterface $snapshot * @param Iterator $events * @return mixed * @throws EventSourcedRepositoryException */ - protected function fromSnapshot(object $aggregate, Iterator $events) + protected function fromSnapshot(SnapshotInterface $snapshot, Iterator $events) { - $aggregate = $aggregate->getAggregateRoot(); + $aggregate = $snapshot->getAggregateRoot(); $this->assertAggregateHasMethod($aggregate); $aggregate->{$this->methodName}($events); @@ -55,11 +60,11 @@ protected function fromSnapshot(object $aggregate, Iterator $events) /** * @param string $aggregate * @param Iterator $events - * @return object|string + * @return object * @throws EventSourcedRepositoryException - * @throws \ReflectionException + * @throws ReflectionException */ - protected function fromString(string $aggregate, Iterator $events) { + protected function fromString(string $aggregate, Iterator $events): object{ if (isset($this->classMap[$aggregate])) { $aggregate = $this->classMap[$aggregate]; } diff --git a/src/Repository/EventSourcedRepository.php b/src/Repository/EventSourcedRepository.php index 26d76b0..3752730 100644 --- a/src/Repository/EventSourcedRepository.php +++ b/src/Repository/EventSourcedRepository.php @@ -67,11 +67,14 @@ public function persist(object $aggregate, bool $takeSnapshot = false): void protected function storeEvents(AggregateDataInterface $aggregateData): void { + $version = $aggregateData->getAggregateVersion() - count($aggregateData->getDomainEvents()); + foreach ($aggregateData->getDomainEvents() as $event) { + $version++; $storeEvent = $this->eventFactory->createEventFromArray([ EventInterface::STREAM => (string)$aggregateData->getStream(), EventInterface::AGGREGATE_ID => $aggregateData->getAggregateId(), - EventInterface::VERSION => $aggregateData->getAggregateVersion(), + EventInterface::VERSION => $version, EventInterface::EVENT => get_class($event), EventInterface::PAYLOAD => $event, EventInterface::CREATED_AT => (new DateTimeImmutable())->format(EventInterface::CREATED_AT_FORMAT) diff --git a/tests/Aggregate/AbstractEventSourcedAggregateTest.php b/tests/Aggregate/AbstractEventSourcedAggregateTest.php new file mode 100644 index 0000000..892b778 --- /dev/null +++ b/tests/Aggregate/AbstractEventSourcedAggregateTest.php @@ -0,0 +1,256 @@ +aggregateId = 'test-id'; + } + + protected function whenTestEvent(TestEvent $event): void + { + $this->testProperty = $event->getText(); + } + + public function whenIdentityProvidingTestEvent(IdentityProvidingTestEvent $event) + { + $this->testProperty = $event->getText(); + } + + public function doSomething(string $data): void + { + $this->recordThat(new TestEvent($data)); + } + + public function getAggregateEvents(): array + { + return $this->aggregateEvents; + } + + public function getAggregateVersion(): int + { + return $this->aggregateVersion; + } +} + +/** + * + */ +class TestEvent +{ + public function __construct(private readonly string $text = '') {} + + public function getText() + { + return $this->text; + } +} + +class TestEvent2 extends TestEvent +{ +} + +class IdentityProvidingTestEvent extends TestEvent implements AggregateIdentityProvidingEventInterface +{ + public string $aggregateId = ''; + + public int $aggregateVersion = 1; + + public function getAggregateId(): string + { + return $this->aggregateId; + } + + public function getAggregateVersion(): int + { + return $this->aggregateVersion; + } +} + +class AbstractEventSourcedAggregateTest extends TestCase +{ + private ConcreteAggregate $aggregate; + + protected function setUp(): void + { + $this->aggregate = new ConcreteAggregate(); + } + + public function testRecordThat(): void + { + $this->aggregate->doSomething('test data'); + + $events = $this->aggregate->getAggregateEvents(); + $this->assertCount(1, $events); + $this->assertInstanceOf(TestEvent::class, $events[0]); + $this->assertEquals('test data', $events[0]->getText()); + } + + public function testApplyEvent(): void + { + $event = new Event( + aggregateId: 'test-id', + aggregateVersion: 1, + event: 'TestEvent', + payload: new TestEvent('applied data'), + createdAt: new DateTimeImmutable() + ); + + $this->aggregate->applyEventsFromHistory([$event]); + + $this->assertEquals('applied data', $this->aggregate->testProperty); + $this->assertEquals(1, $this->aggregate->getAggregateVersion()); + } + + public function testApplyEventsFromHistory(): void + { + $eventsGenerator = function(): Generator { + yield new Event( + aggregateId: 'test-id', + aggregateVersion: 1, + event: 'TestEvent', + payload: new TestEvent('data 1'), + createdAt: new DateTimeImmutable() + ); + yield new Event( + aggregateId: 'test-id', + aggregateVersion: 2, + event: 'TestEvent', + payload: new TestEvent('data 2'), + createdAt: new DateTimeImmutable() + ); + yield new Event( + aggregateId: 'test-id', + aggregateVersion: 3, + event: 'TestEvent', + payload: new TestEvent('data 3'), + createdAt: new DateTimeImmutable() + ); + }; + + $this->aggregate->applyEventsFromHistory($eventsGenerator()); + + $this->assertEquals('data 3', $this->aggregate->testProperty); + $this->assertEquals(3, $this->aggregate->getAggregateVersion()); + } + + public function testApplyEventsFromHistoryWithGenerator(): void + { + $eventsGenerator = function(): Generator { + yield new Event( + aggregateId: 'test-id', + aggregateVersion: 1, + event: 'TestEvent', + payload: new TestEvent('data 1'), + createdAt: new DateTimeImmutable() + ); + yield new Event( + aggregateId: 'test-id', + aggregateVersion: 2, + event: 'TestEvent', + payload: new TestEvent('data 2'), + createdAt: new DateTimeImmutable() + ); + yield new Event( + aggregateId: 'test-id', + aggregateVersion: 3, + event: 'TestEvent', + payload: new TestEvent('data 3'), + createdAt: new DateTimeImmutable() + ); + }; + + $this->aggregate->applyEventsFromHistory($eventsGenerator()); + + $this->assertEquals('data 3', $this->aggregate->testProperty); + $this->assertEquals(3, $this->aggregate->getAggregateVersion()); + } + + public function testEventMismatchException(): void + { + $this->expectException(EventMismatchException::class); + + $testEvent = new IdentityProvidingTestEvent('data 1'); + $testEvent->aggregateId = 'wrong-id'; + $testEvent->aggregateVersion = 1; + + $event = new Event( + aggregateId: 'test-id', + aggregateVersion: 1, + event: 'TestEvent', + payload: $testEvent, + createdAt: new DateTimeImmutable() + ); + + $this->aggregate->applyEventsFromHistory([$event]); + } + + public function testMissingEventHandlerException(): void + { + $this->expectException(MissingEventHandlerException::class); + $this->expectExceptionMessage('Handler method `whenTestEvent2` for event `Phauthentic\EventSourcing\Test\Aggregate\TestEvent2` does not exist in aggregate `Phauthentic\EventSourcing\Test\Aggregate\ConcreteAggregate`'); + + $event = new Event( + aggregateId: 'test-id', + aggregateVersion: 1, + event: 'TestEvent', + payload: new TestEvent2(), + createdAt: new DateTimeImmutable() + ); + + $this->aggregate->applyEventsFromHistory([$event]); + } + + public function testAggregateEventVersionMismatchException(): void + { + $this->expectException(AggregateEventVersionMismatchException::class); + + $event1 = new IdentityProvidingTestEvent(); + $event1->aggregateId = 'test-id'; + + $event2 = new IdentityProvidingTestEvent(); + $event2->aggregateId = 'test-id'; + + $events = [ + new Event( + aggregateId: 'test-id', + aggregateVersion: 1, + event: 'TestEvent', + payload: $event1, + createdAt: new DateTimeImmutable() + ), + new Event( + aggregateId: 'test-id', + aggregateVersion: 6, + event: 'TestEvent', + payload: $event2, + createdAt: new DateTimeImmutable() + ), + ]; + + $this->aggregate->applyEventsFromHistory($events); + } +} From 014310c99e50b69522567c6d43f2c1446c77942f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Thu, 27 Jun 2024 23:49:45 +0200 Subject: [PATCH 6/9] Working on the tests --- .../AggregateFactory/ReflectionFactory.php | 7 +- .../AbstractEventSourcedAggregateTest.php | 79 +------------------ tests/Aggregate/ConcreteAggregate.php | 49 ++++++++++++ .../DomainEvent/AbstractDomainEventTest.php | 15 +--- .../DomainEvent/ConcreteDomainEvent.php | 20 +++++ .../Aggregate/IdentityProvidingTestEvent.php | 27 +++++++ tests/Aggregate/TestEvent.php | 20 +++++ tests/Aggregate/TestEvent2.php | 9 +++ 8 files changed, 132 insertions(+), 94 deletions(-) create mode 100644 tests/Aggregate/ConcreteAggregate.php create mode 100644 tests/Aggregate/DomainEvent/ConcreteDomainEvent.php create mode 100644 tests/Aggregate/IdentityProvidingTestEvent.php create mode 100644 tests/Aggregate/TestEvent.php create mode 100644 tests/Aggregate/TestEvent2.php diff --git a/src/Repository/AggregateFactory/ReflectionFactory.php b/src/Repository/AggregateFactory/ReflectionFactory.php index 9ee9ec3..ebca1cd 100644 --- a/src/Repository/AggregateFactory/ReflectionFactory.php +++ b/src/Repository/AggregateFactory/ReflectionFactory.php @@ -21,7 +21,7 @@ */ public function __construct( protected string $methodName = 'applyEventsFromHistory', - protected array $classMap = [] + protected array $classMap = [] ) { } @@ -64,7 +64,8 @@ protected function fromSnapshot(SnapshotInterface $snapshot, Iterator $events) * @throws EventSourcedRepositoryException * @throws ReflectionException */ - protected function fromString(string $aggregate, Iterator $events): object{ + protected function fromString(string $aggregate, Iterator $events): object + { if (isset($this->classMap[$aggregate])) { $aggregate = $this->classMap[$aggregate]; } @@ -82,7 +83,7 @@ protected function fromString(string $aggregate, Iterator $events): object{ protected function assertAggregateHasMethod(object $aggregate): void { if (!method_exists($aggregate, $this->methodName)) { - throw new EventSourcedRepositoryException (sprintf( + throw new EventSourcedRepositoryException(sprintf( 'Aggregate class `%s` does not have a method `%s` to reconstruct the aggregate state.', get_class($aggregate), $this->methodName diff --git a/tests/Aggregate/AbstractEventSourcedAggregateTest.php b/tests/Aggregate/AbstractEventSourcedAggregateTest.php index 892b778..a3f485f 100644 --- a/tests/Aggregate/AbstractEventSourcedAggregateTest.php +++ b/tests/Aggregate/AbstractEventSourcedAggregateTest.php @@ -8,88 +8,13 @@ use Generator; use Phauthentic\EventStore\Event; use PHPUnit\Framework\TestCase; -use Phauthentic\EventSourcing\Aggregate\AbstractEventSourcedAggregate; -use Phauthentic\EventSourcing\Aggregate\Attribute\DomainEvents; use Phauthentic\EventSourcing\Aggregate\Exception\AggregateEventVersionMismatchException; use Phauthentic\EventSourcing\Aggregate\Exception\EventMismatchException; use Phauthentic\EventSourcing\Aggregate\Exception\MissingEventHandlerException; -use Phauthentic\EventSourcing\DomainEvent\AggregateIdentityProvidingEventInterface; /** * */ -class ConcreteAggregate extends AbstractEventSourcedAggregate -{ - #[DomainEvents] - protected array $aggregateEvents = []; - - public string $testProperty = ''; - - public function __construct() - { - $this->aggregateId = 'test-id'; - } - - protected function whenTestEvent(TestEvent $event): void - { - $this->testProperty = $event->getText(); - } - - public function whenIdentityProvidingTestEvent(IdentityProvidingTestEvent $event) - { - $this->testProperty = $event->getText(); - } - - public function doSomething(string $data): void - { - $this->recordThat(new TestEvent($data)); - } - - public function getAggregateEvents(): array - { - return $this->aggregateEvents; - } - - public function getAggregateVersion(): int - { - return $this->aggregateVersion; - } -} - -/** - * - */ -class TestEvent -{ - public function __construct(private readonly string $text = '') {} - - public function getText() - { - return $this->text; - } -} - -class TestEvent2 extends TestEvent -{ -} - -class IdentityProvidingTestEvent extends TestEvent implements AggregateIdentityProvidingEventInterface -{ - public string $aggregateId = ''; - - public int $aggregateVersion = 1; - - public function getAggregateId(): string - { - return $this->aggregateId; - } - - public function getAggregateVersion(): int - { - return $this->aggregateVersion; - } -} - class AbstractEventSourcedAggregateTest extends TestCase { private ConcreteAggregate $aggregate; @@ -127,7 +52,7 @@ public function testApplyEvent(): void public function testApplyEventsFromHistory(): void { - $eventsGenerator = function(): Generator { + $eventsGenerator = function (): Generator { yield new Event( aggregateId: 'test-id', aggregateVersion: 1, @@ -159,7 +84,7 @@ public function testApplyEventsFromHistory(): void public function testApplyEventsFromHistoryWithGenerator(): void { - $eventsGenerator = function(): Generator { + $eventsGenerator = function (): Generator { yield new Event( aggregateId: 'test-id', aggregateVersion: 1, diff --git a/tests/Aggregate/ConcreteAggregate.php b/tests/Aggregate/ConcreteAggregate.php new file mode 100644 index 0000000..a684fdc --- /dev/null +++ b/tests/Aggregate/ConcreteAggregate.php @@ -0,0 +1,49 @@ +aggregateId = 'test-id'; + } + + protected function whenTestEvent(TestEvent $event): void + { + $this->testProperty = $event->getText(); + } + + public function whenIdentityProvidingTestEvent(IdentityProvidingTestEvent $event) + { + $this->testProperty = $event->getText(); + } + + public function doSomething(string $data): void + { + $this->recordThat(new TestEvent($data)); + } + + public function getAggregateEvents(): array + { + return $this->aggregateEvents; + } + + public function getAggregateVersion(): int + { + return $this->aggregateVersion; + } +} diff --git a/tests/Aggregate/DomainEvent/AbstractDomainEventTest.php b/tests/Aggregate/DomainEvent/AbstractDomainEventTest.php index f23e816..6c2c89c 100644 --- a/tests/Aggregate/DomainEvent/AbstractDomainEventTest.php +++ b/tests/Aggregate/DomainEvent/AbstractDomainEventTest.php @@ -1,4 +1,5 @@ aggregateId = $aggregateId; - $this->aggregateVersion = $aggregateVersion; - $this->domainEventType = $domainEventType; - } -} /** * diff --git a/tests/Aggregate/DomainEvent/ConcreteDomainEvent.php b/tests/Aggregate/DomainEvent/ConcreteDomainEvent.php new file mode 100644 index 0000000..082cbd6 --- /dev/null +++ b/tests/Aggregate/DomainEvent/ConcreteDomainEvent.php @@ -0,0 +1,20 @@ +aggregateId = $aggregateId; + $this->aggregateVersion = $aggregateVersion; + $this->domainEventType = $domainEventType; + } +} diff --git a/tests/Aggregate/IdentityProvidingTestEvent.php b/tests/Aggregate/IdentityProvidingTestEvent.php new file mode 100644 index 0000000..a862a78 --- /dev/null +++ b/tests/Aggregate/IdentityProvidingTestEvent.php @@ -0,0 +1,27 @@ +aggregateId; + } + + public function getAggregateVersion(): int + { + return $this->aggregateVersion; + } +} diff --git a/tests/Aggregate/TestEvent.php b/tests/Aggregate/TestEvent.php new file mode 100644 index 0000000..9388b1d --- /dev/null +++ b/tests/Aggregate/TestEvent.php @@ -0,0 +1,20 @@ +text; + } +} diff --git a/tests/Aggregate/TestEvent2.php b/tests/Aggregate/TestEvent2.php new file mode 100644 index 0000000..8689066 --- /dev/null +++ b/tests/Aggregate/TestEvent2.php @@ -0,0 +1,9 @@ + Date: Sun, 30 Jun 2024 15:26:16 +0200 Subject: [PATCH 7/9] Working on the tests --- infection.json5 | 5 +- ...AggregateEventVersionMismatchException.php | 2 +- .../Exception/EventMismatchException.php | 2 +- .../MissingEventHandlerException.php | 2 +- .../AttributeBasedExtractor.php | 72 +++++++++++----- .../ReflectionPropertyExtractorException.php | 6 +- src/Repository/EventSourcedRepository.php | 2 +- .../AbstractEventSourcedAggregateTest.php | 5 +- tests/Aggregate/ConcreteAggregate.php | 5 ++ tests/Aggregate/MissingEventHandlerEvent.php | 9 ++ tests/Repository/AggregateDataTest.php | 85 +++++++++++++++++++ .../EventSourcedRepositoryIntegrationTest.php | 6 +- 12 files changed, 169 insertions(+), 32 deletions(-) create mode 100644 tests/Aggregate/MissingEventHandlerEvent.php create mode 100644 tests/Repository/AggregateDataTest.php diff --git a/infection.json5 b/infection.json5 index e2ab411..c49a356 100644 --- a/infection.json5 +++ b/infection.json5 @@ -9,6 +9,7 @@ "text": "infection.log" }, "mutators": { - "@default": true + "@default": true, + "Plus": false } -} \ No newline at end of file +} diff --git a/src/Aggregate/Exception/AggregateEventVersionMismatchException.php b/src/Aggregate/Exception/AggregateEventVersionMismatchException.php index 4c6336c..9447783 100644 --- a/src/Aggregate/Exception/AggregateEventVersionMismatchException.php +++ b/src/Aggregate/Exception/AggregateEventVersionMismatchException.php @@ -17,7 +17,7 @@ class AggregateEventVersionMismatchException extends AggregateException * @param int $eventVersion * @return self */ - public static function fromVersions(int $aggregateVersion, int $eventVersion) + public static function fromVersions(int $aggregateVersion, int $eventVersion): self { return new self(sprintf( self::MESSAGE_STRING, diff --git a/src/Aggregate/Exception/EventMismatchException.php b/src/Aggregate/Exception/EventMismatchException.php index 2d7c403..397e008 100644 --- a/src/Aggregate/Exception/EventMismatchException.php +++ b/src/Aggregate/Exception/EventMismatchException.php @@ -16,7 +16,7 @@ class EventMismatchException extends AggregateException * @param object $aggregate * @return EventMismatchException */ - public static function eventDoesNotMatchAggregateWith(object $event, object $aggregate): EventMismatchException + public static function eventDoesNotMatchAggregateWith(object $event, object $aggregate): self { return new self(sprintf( self::EVENT_DOES_NOT_MATCH_AGGREGATE, diff --git a/src/Aggregate/Exception/MissingEventHandlerException.php b/src/Aggregate/Exception/MissingEventHandlerException.php index 3f3e52a..481dcc8 100644 --- a/src/Aggregate/Exception/MissingEventHandlerException.php +++ b/src/Aggregate/Exception/MissingEventHandlerException.php @@ -21,7 +21,7 @@ public static function withNameAndClass( string $eventName, string $eventClass, string $aggregateClass - ): MissingEventHandlerException { + ): self { return new self(sprintf( self::WITH_NAME_AND_CLASS_MESSAGE, $eventName, diff --git a/src/Repository/AggregateExtractor/AttributeBasedExtractor.php b/src/Repository/AggregateExtractor/AttributeBasedExtractor.php index 5737c01..bd6f8d6 100644 --- a/src/Repository/AggregateExtractor/AttributeBasedExtractor.php +++ b/src/Repository/AggregateExtractor/AttributeBasedExtractor.php @@ -11,9 +11,10 @@ use Phauthentic\EventSourcing\Aggregate\Attribute\AggregateVersion; use Phauthentic\EventSourcing\Repository\AggregateData; use Phauthentic\EventSourcing\Repository\AggregateDataInterface; +use Phauthentic\EventSourcing\Repository\AggregateExtractor\Exception\ExtractorException; +use ReflectionAttribute; use ReflectionClass; use ReflectionProperty; -use RuntimeException; /** * @@ -42,17 +43,61 @@ public function extract(object $aggregate): AggregateDataInterface ); } - protected function extractAggregate(ReflectionClass $reflectionClass, object $aggregate): AggregateData + protected function assertPropertyHasName(ReflectionClass $reflectionClass, string $name): void + { + if (!$reflectionClass->hasProperty($name)) { + throw new ExtractorException(sprintf( + 'Property %s not found in %s', + $name, + $reflectionClass->getName() + )); + } + } + + protected function extractAggregateTypeFromAggregate( + object $aggregate, + EventSourcedAggregate $aggregateAttribute, + ReflectionClass $reflectionClass + ): string { + $aggregateType = get_class($aggregate); + if ($aggregateAttribute->aggregateType !== null) { + $aggregateType = $reflectionClass + ->getProperty($aggregateAttribute->aggregateType) + ->getValue($aggregate); + } + + return $aggregateType; + } + + /** + * @param ReflectionClass $reflectionClass + * + * @return array + */ + protected function getAttributes(ReflectionClass $reflectionClass): array + { + return $reflectionClass->getAttributes(EventSourcedAggregate::class); + } + + protected function assertAggregateHasAttributes(ReflectionClass $reflectionClass): void { $attributes = $reflectionClass->getAttributes(EventSourcedAggregate::class); + if (empty($attributes)) { - throw new RuntimeException(sprintf( + throw new ExtractorException(sprintf( 'Attribute `%s` found in `%s`', EventSourcedAggregate::class, $reflectionClass->getName() )); } + } + protected function extractAggregate(ReflectionClass $reflectionClass, object $aggregate): AggregateData + { + $this->assertAggregateHasAttributes($reflectionClass); + $attributes = $this->getAttributes($reflectionClass); + + /** @var EventSourcedAggregate $aggregateAttribute */ $aggregateAttribute = $attributes[0]->newInstance(); $properties = [ 'identifierProperty' => $aggregateAttribute->identifierProperty, @@ -61,21 +106,10 @@ protected function extractAggregate(ReflectionClass $reflectionClass, object $ag ]; foreach ($properties as $name) { - if (!$reflectionClass->hasProperty($name)) { - throw new RuntimeException(sprintf( - 'Property %s not found in %s', - $name, - $reflectionClass->getName() - )); - } + $this->assertPropertyHasName($reflectionClass, $name); } - $aggregateType = get_class($aggregate); - if ($aggregateAttribute->aggregateType !== null) { - $aggregateType = $reflectionClass - ->getProperty($aggregateAttribute->aggregateType) - ->getValue($aggregate); - } + $aggregateType = $this->extractAggregateTypeFromAggregate($aggregate, $aggregateAttribute, $reflectionClass); return new AggregateData( aggregateId: (string)$reflectionClass @@ -113,7 +147,7 @@ protected function extractAggregateVersion(ReflectionClass $reflectionClass, obj $value = $this->getValueFromAttribute($property, AggregateVersion::class, $aggregate); if (!is_int($value)) { - throw new RuntimeException(sprintf( + throw new ExtractorException(sprintf( 'The version property must be an integer, `%s` given.', gettype($value) )); @@ -133,7 +167,7 @@ protected function extractAggregateEvents(ReflectionClass $reflectionClass, obje $value = $this->getValueFromAttribute($property, DomainEvents::class, $aggregate); if (!is_array($value)) { - throw new RuntimeException(sprintf( + throw new ExtractorException(sprintf( 'The version property must be an integer, `%s` given.', gettype($value) )); @@ -169,7 +203,7 @@ protected function findPropertyWithRequiredAttribute( return $reflectionProperty; } - throw new RuntimeException(sprintf( + throw new ExtractorException(sprintf( 'No property with the required attribute `%s` was found class `%s`.', $attribute, $reflectionClass->getName() diff --git a/src/Repository/AggregateExtractor/Exception/ReflectionPropertyExtractorException.php b/src/Repository/AggregateExtractor/Exception/ReflectionPropertyExtractorException.php index ec1413d..75a77b3 100644 --- a/src/Repository/AggregateExtractor/Exception/ReflectionPropertyExtractorException.php +++ b/src/Repository/AggregateExtractor/Exception/ReflectionPropertyExtractorException.php @@ -13,13 +13,13 @@ class ReflectionPropertyExtractorException extends EventSourcingException { /** * @param string $className - * @param $propertyName - * @return \Phauthentic\EventSourcing\Repository\AggregateExtractor\Exception\ReflectionPropertyExtractorException + * @param string $propertyName + * @return ReflectionPropertyExtractorException */ public static function classHasMissingProperty( string $className, string $propertyName - ): ReflectionPropertyExtractorException { + ): self { return new self(sprintf('Aggregate class `%s` is missing the property `%s`', $className, $propertyName)); } } diff --git a/src/Repository/EventSourcedRepository.php b/src/Repository/EventSourcedRepository.php index 3752730..834527f 100644 --- a/src/Repository/EventSourcedRepository.php +++ b/src/Repository/EventSourcedRepository.php @@ -103,13 +103,13 @@ protected function applySnapshotStrategies(object $aggregate, AggregateDataInter public function restore(string $aggregateId, string $aggregateType): object { $snapshot = $this->getSnapshot($aggregateId); - $position = 0; if ($snapshot) { $aggregate = $snapshot->getAggregateRoot(); $position = $snapshot->getLastVersion(); } else { $aggregate = $aggregateType; + $position = 0; } $events = $this->eventStore->replyFromPosition($aggregateId, $position); diff --git a/tests/Aggregate/AbstractEventSourcedAggregateTest.php b/tests/Aggregate/AbstractEventSourcedAggregateTest.php index a3f485f..496e696 100644 --- a/tests/Aggregate/AbstractEventSourcedAggregateTest.php +++ b/tests/Aggregate/AbstractEventSourcedAggregateTest.php @@ -136,13 +136,14 @@ public function testEventMismatchException(): void public function testMissingEventHandlerException(): void { $this->expectException(MissingEventHandlerException::class); - $this->expectExceptionMessage('Handler method `whenTestEvent2` for event `Phauthentic\EventSourcing\Test\Aggregate\TestEvent2` does not exist in aggregate `Phauthentic\EventSourcing\Test\Aggregate\ConcreteAggregate`'); + // phpcs:ignore + $this->expectExceptionMessage('Handler method `whenMissingEventHandlerEvent` for event `Phauthentic\EventSourcing\Test\Aggregate\MissingEventHandlerEvent` does not exist in aggregate `Phauthentic\EventSourcing\Test\Aggregate\ConcreteAggregate`'); $event = new Event( aggregateId: 'test-id', aggregateVersion: 1, event: 'TestEvent', - payload: new TestEvent2(), + payload: new MissingEventHandlerEvent(), createdAt: new DateTimeImmutable() ); diff --git a/tests/Aggregate/ConcreteAggregate.php b/tests/Aggregate/ConcreteAggregate.php index a684fdc..08b5237 100644 --- a/tests/Aggregate/ConcreteAggregate.php +++ b/tests/Aggregate/ConcreteAggregate.php @@ -27,6 +27,11 @@ protected function whenTestEvent(TestEvent $event): void $this->testProperty = $event->getText(); } + protected function whenTestEvent2(TestEvent2 $event): void + { + $this->testProperty = $event->getText(); + } + public function whenIdentityProvidingTestEvent(IdentityProvidingTestEvent $event) { $this->testProperty = $event->getText(); diff --git a/tests/Aggregate/MissingEventHandlerEvent.php b/tests/Aggregate/MissingEventHandlerEvent.php new file mode 100644 index 0000000..81a1175 --- /dev/null +++ b/tests/Aggregate/MissingEventHandlerEvent.php @@ -0,0 +1,9 @@ +assertSame($aggregateId, $aggregateData->getAggregateId()); + $this->assertSame($aggregateType, $aggregateData->getAggregateType()); + $this->assertSame($version, $aggregateData->getAggregateVersion()); + $this->assertSame($events, $aggregateData->getDomainEvents()); + $this->assertSame($stream, $aggregateData->getStream()); + } + + public function testConstructorWithDefaults() + { + $aggregateId = 'test-id'; + $aggregateType = 'TestType'; + $version = 1; + + $aggregateData = new AggregateData($aggregateId, $aggregateType, $version); + + $this->assertSame($aggregateId, $aggregateData->getAggregateId()); + $this->assertSame($aggregateType, $aggregateData->getAggregateType()); + $this->assertSame($version, $aggregateData->getAggregateVersion()); + $this->assertSame([], $aggregateData->getDomainEvents()); + $this->assertNull($aggregateData->getStream()); + } + + public function testCreateFromArray() + { + $data = [ + 'aggregateId' => 'test-id', + 'aggregateType' => 'TestType', + 'version' => 1, + 'events' => [new stdClass(), new stdClass()], + 'stream' => 'test-stream', + ]; + + $aggregateData = AggregateData::createFromArray($data); + + $this->assertInstanceOf(AggregateData::class, $aggregateData); + $this->assertSame($data['aggregateId'], $aggregateData->getAggregateId()); + $this->assertSame($data['aggregateType'], $aggregateData->getAggregateType()); + $this->assertSame($data['version'], $aggregateData->getAggregateVersion()); + $this->assertSame($data['events'], $aggregateData->getDomainEvents()); + $this->assertSame($data['stream'], $aggregateData->getStream()); + } + + public function testCreateFromArrayWithDefaults() + { + $data = [ + 'aggregateId' => 'test-id', + 'aggregateType' => 'TestType', + 'version' => 1, + ]; + + $aggregateData = AggregateData::createFromArray($data); + + $this->assertInstanceOf(AggregateData::class, $aggregateData); + $this->assertSame($data['aggregateId'], $aggregateData->getAggregateId()); + $this->assertSame($data['aggregateType'], $aggregateData->getAggregateType()); + $this->assertSame($data['version'], $aggregateData->getAggregateVersion()); + $this->assertSame([], $aggregateData->getDomainEvents()); + $this->assertNull($aggregateData->getStream()); + } +} diff --git a/tests/Repository/EventSourcedRepositoryIntegrationTest.php b/tests/Repository/EventSourcedRepositoryIntegrationTest.php index 9e8920d..e1f9bc5 100644 --- a/tests/Repository/EventSourcedRepositoryIntegrationTest.php +++ b/tests/Repository/EventSourcedRepositoryIntegrationTest.php @@ -19,12 +19,14 @@ use Phauthentic\EventStore\InMemoryEventStore; use Phauthentic\SnapshotStore\SnapshotFactory; use Phauthentic\SnapshotStore\SnapshotFactoryInterface; -use Phauthentic\SnapshotStore\Store\NullStore; use Phauthentic\SnapshotStore\Store\SnapshotStoreInterface; use Phauthentic\SnapshotStore\Store\InMemorySnapshotStore; use PHPUnit\Framework\TestCase; use Ramsey\Uuid\Uuid; +/** + * + */ class EventSourcedRepositoryIntegrationTest extends TestCase { protected SnapshotStoreInterface $snapshotStore; @@ -86,7 +88,7 @@ public function testEventSourcedRepositoryIntegration(): void $this->assertSame(1, $invoice->lineItemCount()); // Act: Persist the aggregate and restore it - $repository->persist($invoice); + $repository->persist($invoice, true); $invoice = $repository->restore($aggregateId, Invoice::class); // Assert From cd8d30aab78388e9c334d8f420e6df54eb06a57a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Sun, 30 Jun 2024 15:32:59 +0200 Subject: [PATCH 8/9] Working on tests --- .github/workflows/ci.yaml | 3 + examples/Domain/Chess/Board.php | 127 ------------------ examples/Domain/Chess/BoardId.php | 17 --- examples/Domain/Chess/Event/Capture.php | 17 --- examples/Domain/Chess/Event/DrawOffered.php | 15 --- examples/Domain/Chess/Event/GameStarted.php | 15 --- examples/Domain/Chess/Event/PieceMoved.php | 22 --- examples/Domain/Chess/Move.php | 16 --- examples/Domain/Chess/Piece.php | 55 -------- examples/Domain/Chess/PieceType.php | 18 --- examples/Domain/Chess/Player.php | 18 --- examples/Domain/Chess/Position.php | 33 ----- examples/Domain/Chess/Side.php | 14 -- examples/Domain/Invoice/Address.php | 3 +- .../Domain/Invoice/Event/InvoiceCreated.php | 37 ++--- examples/Domain/Invoice/Event/InvoicePaid.php | 25 ++-- .../Domain/Invoice/Event/LineItemAdded.php | 35 ++--- .../Domain/Invoice/Event/LineItemRemoved.php | 11 +- examples/Domain/Invoice/LineItem.php | 3 +- .../Projection/InvoiceProjector.php | 2 +- grumphp.yml | 6 + 21 files changed, 68 insertions(+), 424 deletions(-) delete mode 100644 examples/Domain/Chess/Board.php delete mode 100644 examples/Domain/Chess/BoardId.php delete mode 100644 examples/Domain/Chess/Event/Capture.php delete mode 100644 examples/Domain/Chess/Event/DrawOffered.php delete mode 100644 examples/Domain/Chess/Event/GameStarted.php delete mode 100644 examples/Domain/Chess/Event/PieceMoved.php delete mode 100644 examples/Domain/Chess/Move.php delete mode 100644 examples/Domain/Chess/Piece.php delete mode 100644 examples/Domain/Chess/PieceType.php delete mode 100644 examples/Domain/Chess/Player.php delete mode 100644 examples/Domain/Chess/Position.php delete mode 100644 examples/Domain/Chess/Side.php create mode 100644 grumphp.yml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7225d3d..b4856da 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -107,3 +107,6 @@ jobs: - name: Run phpstan run: bin/phpstan -V && bin/phpstan --error-format=github + + - name: Run Infection + run: bin/infection diff --git a/examples/Domain/Chess/Board.php b/examples/Domain/Chess/Board.php deleted file mode 100644 index a04f738..0000000 --- a/examples/Domain/Chess/Board.php +++ /dev/null @@ -1,127 +0,0 @@ -assertPlayersDonNotHaveTheSameSide($playerOne, $playerTwo); - - $this->boardId = $boardId; - $this->playerOne = $playerOne; - $this->playerTwo = $playerTwo; - - $this->setPieces(); - $this->randomPlayerSelection(); - - $this->recordThat(new BoardCreated($boardId, $playerOne, $playerTwo)); - } - - private function randomPlayerSelection() - { - $this->activePlayer = rand(0, 1) === 0 ? $this->playerOne : $this->playerTwo; - } - - private function setPieces() - { - // Generate pawn pieces - $charCode = 96; - for ($i = 0; $i < 8; $i++) { - $charCode++; - $this->fields[chr($charCode) . 7] = new Piece(Side::BLACK, PieceType::PAWN, new Position(chr($charCode) . 7)); - $this->fields[chr($charCode) . 2] = new Piece(Side::WHITE, PieceType::PAWN, new Position(chr($charCode) . 2)); - } - - // Black pieces - $this->fields['a8'] = new Piece(Side::BLACK, PieceType::ROOK, new Position('a8')); - $this->fields['h8'] = new Piece(Side::BLACK, PieceType::ROOK, new Position('a8')); - $this->fields['b8'] = new Piece(Side::BLACK, PieceType::BISHOP, new Position('b8')); - $this->fields['g8'] = new Piece(Side::BLACK, PieceType::BISHOP, new Position('g8')); - $this->fields['c8'] = new Piece(Side::BLACK, PieceType::KNIGHT, new Position('c8')); - $this->fields['f8'] = new Piece(Side::BLACK, PieceType::KNIGHT, new Position('f8')); - $this->fields['f1'] = new Piece(Side::WHITE, PieceType::QUEEN, new Position('8d')); - $this->fields['f1'] = new Piece(Side::WHITE, PieceType::KING, new Position('8e')); - - // White pieces - $this->fields['a1'] = new Piece(Side::WHITE, PieceType::ROOK, new Position('a8')); - $this->fields['h1'] = new Piece(Side::WHITE, PieceType::ROOK, new Position('a8')); - $this->fields['b1'] = new Piece(Side::WHITE, PieceType::BISHOP, new Position('b1')); - $this->fields['g1'] = new Piece(Side::WHITE, PieceType::BISHOP, new Position('g1')); - $this->fields['c1'] = new Piece(Side::WHITE, PieceType::KNIGHT, new Position('c1')); - $this->fields['f1'] = new Piece(Side::WHITE, PieceType::KNIGHT, new Position('f1')); - $this->fields['f1'] = new Piece(Side::WHITE, PieceType::QUEEN, new Position('1e')); - $this->fields['f1'] = new Piece(Side::WHITE, PieceType::KING, new Position('1d')); - } - - private function assertPlayersDonNotHaveTheSameSide(Player $playerWhite, Player $playerBlack): void - { - assert($playerWhite->side === $playerBlack->side, 'Players must not have the same side!'); - } - - public function move(Position $from, Position $to): void - { - foreach ($this->pieces as $piece) { - if ($piece->position->equals($from)) { - $this->assertActivePlayerHasThePiece($piece); - - $piece->move($to); - $this->endTurn(); - - return; - } - } - } - - private function fieldHasPawn(Position $position): ?Piece - { - if (isset($this->fields[$position->toString()])) { - return $this->fields[$position->toString()]; - } - } - - private function endTurn() - { - $this->activePlayer = $this->activePlayer === $this->playerOne ? $this->playerTwo : $this->playerOne; - } - - private function assertActivePlayerHasThePiece(Piece $piece): void - { - if ($this->activePlayer->side !== $piece->side) { - throw new Exception('It is not your turn!'); - } - } - - private function recordThat(object $event): void - { - $this->domainEvents[] = $event; - } -} diff --git a/examples/Domain/Chess/BoardId.php b/examples/Domain/Chess/BoardId.php deleted file mode 100644 index 5164e12..0000000 --- a/examples/Domain/Chess/BoardId.php +++ /dev/null @@ -1,17 +0,0 @@ -type->value; - } - - public function toSymbol(): string - { - $isBlack = $this->side === Side::BLACK; - - switch ($this->type) { - case PieceType::PAWN: - return $isBlack ? '♟' : '♙'; - case PieceType::QUEEN: - return $isBlack ? '♛' : '♕'; - case PieceType::ROOK: - return $isBlack ? '♜' : '♖'; - case PieceType::BISHOP: - return $isBlack ? '♝' : '♗'; - case PieceType::KNIGHT: - return $isBlack ? '♞' : '♘'; - case PieceType::KING: - return $isBlack ? '♚' : '♔'; - default: - throw new \InvalidArgumentException('Invalid PieceType provided.'); - } - } - - public function promote(PieceType $pieceType): void - { - if ($this->type !== PieceType::PAWN) { - throw new \InvalidArgumentException('Only pawns can be promoted.'); - } - - $this->type = $pieceType; - } -} diff --git a/examples/Domain/Chess/PieceType.php b/examples/Domain/Chess/PieceType.php deleted file mode 100644 index e28eb89..0000000 --- a/examples/Domain/Chess/PieceType.php +++ /dev/null @@ -1,18 +0,0 @@ -assertValidField($position); - } - - public function assertValidField(): string - { - return '/^([a-h][1-8])\s*-\s*([a-h][1-8])$/'; - } - - public function __toString(): string - { - return $this->position; - } - - public function toString() - { - return $this->position; - } -} diff --git a/examples/Domain/Chess/Side.php b/examples/Domain/Chess/Side.php deleted file mode 100644 index a7eafd2..0000000 --- a/examples/Domain/Chess/Side.php +++ /dev/null @@ -1,14 +0,0 @@ - Date: Sun, 30 Jun 2024 15:36:18 +0200 Subject: [PATCH 9/9] Working on tests --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b4856da..9aaaace 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,7 +96,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: json, fileinfo - coverage: none + coverage: pcov tools: pecl - name: Composer install