From 686d3b526740b0ece84b324b400dcecfcba63f38 Mon Sep 17 00:00:00 2001 From: mmalac Date: Sat, 11 Oct 2025 21:26:02 +0200 Subject: [PATCH 1/4] chore: add PHPStan baseline config and update dependencies to latest versions --- composer.json | 5 +- phpstan-baseline.neon | 49 +++++++++++++++ phpstan.neon.dist | 5 +- src/CommandHandling/SequentialCommandBus.php | 3 + .../Message/GenericDomainEventMessage.php | 6 ++ src/Domain/Message/GenericEventMessage.php | 6 ++ src/Domain/Message/GenericMessage.php | 6 ++ src/Domain/Message/Metadata.php | 20 +++++++ src/Domain/Model/AggregateRootTrait.php | 1 + src/Domain/Model/EventContainer.php | 1 + .../Model/EventSourcedAggregateRootTrait.php | 1 + src/EventHandling/EventExecutionFailed.php | 3 + .../PsrContainerEventHandlerLocator.php | 60 ++++++++++--------- .../Publisher/SimpleEventPublisher.php | 6 ++ src/EventHandling/SynchronousEventBus.php | 7 --- src/EventStore/RedisEventRecord.php | 28 ++++++++- src/EventStore/RedisEventStore.php | 2 +- .../Doctrine/EventStore/TableEventStore.php | 3 + src/Serializer/SymfonySerializer.php | 3 + 19 files changed, 172 insertions(+), 43 deletions(-) create mode 100644 phpstan-baseline.neon diff --git a/composer.json b/composer.json index d68bacf..9cea517 100644 --- a/composer.json +++ b/composer.json @@ -30,9 +30,9 @@ "doctrine/orm": "^2.8", "guzzlehttp/guzzle": "^7.2", "php-parallel-lint/php-parallel-lint": "^1.3", - "phpstan/phpstan": "^1.9", + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^11.0.0", - "rector/rector": "^1.2", + "rector/rector": "^2.0", "squizlabs/php_codesniffer": "^3.7", "symfony/cache": "^6.0", "symfony/serializer": "^6.0" @@ -65,6 +65,7 @@ "rector:clear": "rector -n --clear-cache -vv", "rector:fix": "rector -vv", "stan": "phpstan analyse --no-progress", + "stan:b": "@stan -b --allow-empty-baseline", "test": "phpunit", "test-coverage": "phpunit --coverage-clover clover.xml" } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..0f59775 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,49 @@ +parameters: + ignoreErrors: + - + rawMessage: 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.' + identifier: foreach.nonIterable + count: 1 + path: src/EventHandling/PsrContainerEventHandlerLocator.php + + - + rawMessage: 'Offset ''*'' on array>> on left side of ?? does not exist.' + identifier: nullCoalesce.offset + count: 1 + path: src/EventHandling/PsrContainerEventHandlerLocator.php + + - + rawMessage: 'Parameter #1 $id of method Psr\Container\ContainerInterface::get() expects string, mixed given.' + identifier: argument.type + count: 1 + path: src/EventHandling/PsrContainerEventHandlerLocator.php + + - + rawMessage: 'Parameter #2 ...$values of function sprintf expects bool|float|int|string|null, mixed given.' + identifier: argument.type + count: 1 + path: src/EventHandling/PsrContainerEventHandlerLocator.php + + - + rawMessage: 'Property CQRS\EventHandling\PsrContainerEventHandlerLocator::$handlers (array>>) does not accept array, string>>>.' + identifier: assign.propertyType + count: 3 + path: src/EventHandling/PsrContainerEventHandlerLocator.php + + - + rawMessage: 'Property CQRS\EventHandling\PsrContainerEventHandlerLocator::$handlers (array>>) does not accept non-empty-array>>.' + identifier: assign.propertyType + count: 3 + path: src/EventHandling/PsrContainerEventHandlerLocator.php + + - + rawMessage: 'Call to function is_string() with string will always evaluate to true.' + identifier: function.alreadyNarrowedType + count: 1 + path: src/EventStore/RedisEventRecord.php + + - + rawMessage: 'Method CQRS\EventStore\RedisEventRecord::toArray() should return array{id: string, timestamp: string, payload: array{data: string, type: string}, metadata: array{data: array, types: array}, aggregate?: array{type: string, id: mixed, seq: int}} but returns mixed.' + identifier: return.type + count: 1 + path: src/EventStore/RedisEventRecord.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 85c28e1..8cc855e 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,7 @@ +includes: + - phar://phpstan.phar/conf/bleedingEdge.neon + - phpstan-baseline.neon parameters: level: max - checkGenericClassInNonGenericObjectType: false - checkMissingIterableValueType: false paths: - src diff --git a/src/CommandHandling/SequentialCommandBus.php b/src/CommandHandling/SequentialCommandBus.php index 9180b0a..34ece45 100644 --- a/src/CommandHandling/SequentialCommandBus.php +++ b/src/CommandHandling/SequentialCommandBus.php @@ -24,6 +24,9 @@ */ class SequentialCommandBus implements CommandBusInterface { + /** + * @var array + */ private array $commandStack = []; private bool $executing = false; diff --git a/src/Domain/Message/GenericDomainEventMessage.php b/src/Domain/Message/GenericDomainEventMessage.php index 8c19bc1..ed16863 100644 --- a/src/Domain/Message/GenericDomainEventMessage.php +++ b/src/Domain/Message/GenericDomainEventMessage.php @@ -9,6 +9,9 @@ class GenericDomainEventMessage extends GenericEventMessage implements DomainEventMessageInterface { + /** + * @param array|Metadata $metadata + */ public function __construct( private readonly string $aggregateType, private readonly mixed $aggregateId, @@ -21,6 +24,9 @@ public function __construct( parent::__construct($payload, $metadata, $id, $timestamp); } + /** + * @return array + */ #[\Override] public function jsonSerialize(): array { diff --git a/src/Domain/Message/GenericEventMessage.php b/src/Domain/Message/GenericEventMessage.php index d9bdedf..81140c8 100644 --- a/src/Domain/Message/GenericEventMessage.php +++ b/src/Domain/Message/GenericEventMessage.php @@ -19,6 +19,9 @@ public static function setClock(ClockInterface $clock): void self::$clock = $clock; } + /** + * @param array|Metadata $metadata + */ public function __construct( object $payload, Metadata|array $metadata = [], @@ -29,6 +32,9 @@ public function __construct( $this->timestamp = $timestamp ?? self::$clock?->now() ?? new DateTimeImmutable(); } + /** + * @return array + */ #[\Override] public function jsonSerialize(): array { diff --git a/src/Domain/Message/GenericMessage.php b/src/Domain/Message/GenericMessage.php index 6188cc0..0eb68ab 100644 --- a/src/Domain/Message/GenericMessage.php +++ b/src/Domain/Message/GenericMessage.php @@ -20,6 +20,9 @@ class GenericMessage implements MessageInterface private Metadata $metadata; + /** + * @param Metadata|array $metadata + */ public function __construct(object $payload, Metadata|array $metadata = [], ?UuidInterface $id = null) { $this->id = $id ?? Uuid::uuid4(); @@ -28,6 +31,9 @@ public function __construct(object $payload, Metadata|array $metadata = [], ?Uui $this->metadata = Metadata::from($metadata); } + /** + * @return array + */ #[\Override] public function jsonSerialize(): array { diff --git a/src/Domain/Message/Metadata.php b/src/Domain/Message/Metadata.php index ecac850..fa7b053 100644 --- a/src/Domain/Message/Metadata.php +++ b/src/Domain/Message/Metadata.php @@ -12,10 +12,18 @@ use JsonSerializable; use Traversable; +/** + * @implements IteratorAggregate + * @implements ArrayAccess + */ class Metadata implements IteratorAggregate, ArrayAccess, Countable, JsonSerializable { + /** @var array */ private array $data; + /** + * @param self|array $metadata + */ public static function from(self|array $metadata = []): static { if ($metadata instanceof static) { @@ -29,11 +37,17 @@ public static function from(self|array $metadata = []): static return new static($metadata); } + /** + * @param array $data + */ public static function jsonDeserialize(array $data): static { return new static($data); } + /** + * @param array $data + */ final private function __construct(array $data) { ksort($data); @@ -46,11 +60,17 @@ public function jsonSerialize(): object return (object) $this->data; } + /** + * @return array + */ public function toArray(): array { return $this->data; } + /** + * @return Traversable + */ #[\Override] public function getIterator(): Traversable { diff --git a/src/Domain/Model/AggregateRootTrait.php b/src/Domain/Model/AggregateRootTrait.php index d26e7f8..9cb2966 100644 --- a/src/Domain/Model/AggregateRootTrait.php +++ b/src/Domain/Model/AggregateRootTrait.php @@ -27,6 +27,7 @@ abstract public function getId(): mixed; /** * Registers an event to be published when the aggregate is saved, containing the given payload and optional * metadata. + * @param array|Metadata $metadata */ protected function registerEvent(object $payload, Metadata|array $metadata = []): DomainEventMessageInterface { diff --git a/src/Domain/Model/EventContainer.php b/src/Domain/Model/EventContainer.php index dc2715f..9ec8228 100644 --- a/src/Domain/Model/EventContainer.php +++ b/src/Domain/Model/EventContainer.php @@ -38,6 +38,7 @@ public function __construct( /** * Add an event to this container. + * @param array|Metadata $metadata */ public function addEvent(object $payload, Metadata|array $metadata = []): GenericDomainEventMessage { diff --git a/src/Domain/Model/EventSourcedAggregateRootTrait.php b/src/Domain/Model/EventSourcedAggregateRootTrait.php index bf43135..300dae7 100644 --- a/src/Domain/Model/EventSourcedAggregateRootTrait.php +++ b/src/Domain/Model/EventSourcedAggregateRootTrait.php @@ -16,6 +16,7 @@ trait EventSourcedAggregateRootTrait /** * @throws DomainException + * @param array|Metadata $metadata */ protected function apply(object $payload, Metadata|array $metadata = []): void { diff --git a/src/EventHandling/EventExecutionFailed.php b/src/EventHandling/EventExecutionFailed.php index 7e79b25..ac96918 100644 --- a/src/EventHandling/EventExecutionFailed.php +++ b/src/EventHandling/EventExecutionFailed.php @@ -24,6 +24,9 @@ public function getException(): Exception return $this->exception; } + /** + * @return array + */ #[\Override] public function jsonSerialize(): array { diff --git a/src/EventHandling/PsrContainerEventHandlerLocator.php b/src/EventHandling/PsrContainerEventHandlerLocator.php index b38afcb..f329cc7 100644 --- a/src/EventHandling/PsrContainerEventHandlerLocator.php +++ b/src/EventHandling/PsrContainerEventHandlerLocator.php @@ -8,6 +8,9 @@ class PsrContainerEventHandlerLocator implements EventHandlerLocatorInterface { + /** + * @var array>> + */ protected array $handlers = []; /** @@ -17,26 +20,24 @@ class PsrContainerEventHandlerLocator implements EventHandlerLocatorInterface public function __construct(protected ContainerInterface $container, array $handlers = []) { foreach ($handlers as $eventType => $eventHandlers) { - if (!is_array($eventHandlers)) { - throw new Exception\InvalidArgumentException(sprintf( - 'Handlers for event %s must be specified as array; got %s', - $eventType, - get_debug_type($eventHandlers) - )); - } - foreach ($eventHandlers as $handler) { $priority = 1; + $handlerId = $handler; - if (is_array($handler) && array_key_exists('handler', $handler)) { - if (array_key_exists('priority', $handler)) { - $priority = $handler['priority']; - } + if (is_array($handler)) { + $priority = isset($handler['priority']) ? (int) $handler['priority'] : 1; + $handlerId = $handler['handler']; + } - $handler = $handler['handler']; + if (!is_string($handlerId)) { + throw new Exception\InvalidArgumentException(sprintf( + 'Handler id for event %s must be string; got %s', + $eventType, + get_debug_type($handlerId) + )); } - $this->add($eventType, $handler, $priority); + $this->add($eventType, $handlerId, $priority); } } } @@ -44,29 +45,34 @@ public function __construct(protected ContainerInterface $container, array $hand /** * @throws Exception\InvalidArgumentException */ - public function add(string $eventType, mixed $handler, int $priority = 1): void + public function add(string $eventType, string $handler, int $priority = 1): void { - if ( - array_key_exists($eventType, $this->handlers) - && array_key_exists($priority, $this->handlers[$eventType]) - && in_array($handler, $this->handlers[$eventType], true) - ) { + if (!isset($this->handlers[$eventType])) { + $this->handlers[$eventType] = []; + } + + $priorityKey = $priority . '.0'; + + if (!isset($this->handlers[$eventType][$priorityKey])) { + $this->handlers[$eventType][$priorityKey] = []; + } + + if (in_array($handler, $this->handlers[$eventType][$priorityKey], true)) { return; } - // Make sure the priority index is string so we can use array_merge_recursive - $this->handlers[$eventType][$priority . '.0'][] = $handler; + $this->handlers[$eventType][$priorityKey][] = $handler; } /** * @throws Exception\InvalidArgumentException */ - public function remove(mixed $handler, ?string $eventType = null): void + public function remove(string $handler, ?string $eventType = null): void { // If event type is not specified, we need to iterate through each event type - if (null === $eventType) { - foreach ($this->handlers as $eventType => $unused) { - $this->remove($handler, $eventType); + if ($eventType === null) { + foreach ($this->handlers as $type => $_) { + $this->remove($handler, $type); } return; @@ -89,8 +95,6 @@ public function remove(mixed $handler, ?string $eventType = null): void // If the queue for the given priority is empty, remove it. if (empty($this->handlers[$eventType][$priority])) { unset($this->handlers[$eventType][$priority]); - - break; } } diff --git a/src/EventHandling/Publisher/SimpleEventPublisher.php b/src/EventHandling/Publisher/SimpleEventPublisher.php index 6b092bb..d4c4d5f 100644 --- a/src/EventHandling/Publisher/SimpleEventPublisher.php +++ b/src/EventHandling/Publisher/SimpleEventPublisher.php @@ -13,6 +13,9 @@ class SimpleEventPublisher implements EventPublisherInterface { private ?Metadata $additionalMetadata = null; + /** + * @param Metadata|array|null $additionalMetadata + */ public function __construct( private readonly EventBusInterface $eventBus, private readonly ?EventQueueInterface $queue = null, @@ -29,6 +32,9 @@ public function getEventBus(): EventBusInterface return $this->eventBus; } + /** + * @param Metadata|array $additionalMetadata + */ public function setAdditionalMetadata(Metadata|array $additionalMetadata): void { $this->additionalMetadata = Metadata::from($additionalMetadata); diff --git a/src/EventHandling/SynchronousEventBus.php b/src/EventHandling/SynchronousEventBus.php index 3d43f1b..78686c4 100644 --- a/src/EventHandling/SynchronousEventBus.php +++ b/src/EventHandling/SynchronousEventBus.php @@ -25,13 +25,6 @@ public function publish(EventMessageInterface $event): void $eventHandlers = $this->locator->get($eventType); foreach ($eventHandlers as $handler) { - if (!is_callable($handler)) { - throw new Exception\RuntimeException(sprintf( - 'Event handler %s is not invokable', - get_debug_type($handler) - )); - } - $this->invokeEventHandler($handler, $event); } } diff --git a/src/EventStore/RedisEventRecord.php b/src/EventStore/RedisEventRecord.php index f7b6863..2a5fef0 100644 --- a/src/EventStore/RedisEventRecord.php +++ b/src/EventStore/RedisEventRecord.php @@ -34,8 +34,8 @@ public static function fromMessage(EventMessageInterface $event, SerializerInter 'type' => $event->getPayloadType(), ], 'metadata' => [ - 'data' => (object) $metadata, - 'types' => (object) $metadataTypes, + 'data' => $metadata, + 'types' => $metadataTypes, ], ]; @@ -61,9 +61,28 @@ public function __toString(): string return $this->data; } + /** + * @return array{ + * id: string, + * timestamp: string, + * payload: array{ + * data: string, + * type: string, + * }, + * metadata: array{ + * data: array, + * types: array, + * }, + * aggregate?: array{ + * type: string, + * id: mixed, + * seq: int, + * }, + * } + */ public function toArray(): array { - return (array) json_decode($this->data, true, 512, JSON_THROW_ON_ERROR); + return json_decode($this->data, true, 512, JSON_THROW_ON_ERROR); } public function toMessage(SerializerInterface $serializer): GenericEventMessage|GenericDomainEventMessage @@ -76,6 +95,9 @@ public function toMessage(SerializerInterface $serializer): GenericEventMessage| $metadata = $data['metadata']['data']; foreach ($data['metadata']['types'] as $key => $type) { + if (!is_string($type) || !array_key_exists($key, $metadata) || !is_string($metadata[$key])) { + continue; + } $metadata[$key] = $serializer->deserialize($metadata[$key], $type); } diff --git a/src/EventStore/RedisEventStore.php b/src/EventStore/RedisEventStore.php index 13f6227..f3b1971 100644 --- a/src/EventStore/RedisEventStore.php +++ b/src/EventStore/RedisEventStore.php @@ -33,7 +33,7 @@ public function pop(int $timeout = 0): ?RedisEventRecord { $data = $this->redis->brPop($this->key, $timeout); - if (!array_key_exists(1, $data)) { + if (!is_array($data) || !array_key_exists(1, $data) || !is_string($data[1])) { return null; } diff --git a/src/Plugin/Doctrine/EventStore/TableEventStore.php b/src/Plugin/Doctrine/EventStore/TableEventStore.php index f465173..6ba46b0 100644 --- a/src/Plugin/Doctrine/EventStore/TableEventStore.php +++ b/src/Plugin/Doctrine/EventStore/TableEventStore.php @@ -32,6 +32,9 @@ public function store(EventMessageInterface $event): void $this->connection->insert($this->table, $data); } + /** + * @return array + */ private function toArray(EventMessageInterface $event): array { $dateTimeFormat = $this->connection->getDatabasePlatform()->getDateTimeFormatString(); diff --git a/src/Serializer/SymfonySerializer.php b/src/Serializer/SymfonySerializer.php index 0c6d017..19e1a43 100644 --- a/src/Serializer/SymfonySerializer.php +++ b/src/Serializer/SymfonySerializer.php @@ -9,6 +9,9 @@ final readonly class SymfonySerializer implements SerializerInterface { + /** + * @param array $context + */ public function __construct( private SymfonySerializerInterface $serializer, private string $format, From 519af7c5773e7eba0950934604251ff38bd72fee Mon Sep 17 00:00:00 2001 From: mmalac Date: Sat, 11 Oct 2025 21:28:18 +0200 Subject: [PATCH 2/4] chore: add phpstan-phpunit extension and update static analysis configs --- composer.json | 1 + phpstan-baseline.neon | 174 ++++++++++++++++++++++++++++++++++++++++++ phpstan.neon.dist | 1 + 3 files changed, 176 insertions(+) diff --git a/composer.json b/composer.json index 9cea517..e408a84 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "guzzlehttp/guzzle": "^7.2", "php-parallel-lint/php-parallel-lint": "^1.3", "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^11.0.0", "rector/rector": "^2.0", "squizlabs/php_codesniffer": "^3.7", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0f59775..e099faf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -47,3 +47,177 @@ parameters: identifier: return.type count: 1 path: src/EventStore/RedisEventRecord.php + + - + rawMessage: 'Parameter #2 $handlers of class CQRS\CommandHandling\PsrContainerCommandHandlerLocator constructor expects array, array given.' + identifier: argument.type + count: 1 + path: tests/CommandHandling/PsrContainerCommandHandlerLocatorTest.php + + - + rawMessage: 'Method CQRSTest\CommandHandling\Stubs\DummyCommandHandlerLocator::get() should return callable(): mixed but returns mixed.' + identifier: return.type + count: 1 + path: tests/CommandHandling/Stubs/DummyCommandHandlerLocator.php + + - + rawMessage: Property CQRSTest\CommandHandling\Stubs\DummyCommandHandlerLocator::$handlers type has no value type specified in iterable type array. + identifier: missingType.iterableValue + count: 1 + path: tests/CommandHandling/Stubs/DummyCommandHandlerLocator.php + + - + rawMessage: 'Method CQRSTest\EventHandling\Stubs\DummyEventHandler::onEventExecutionFailed() has parameter $event with no type specified.' + identifier: missingType.parameter + count: 1 + path: tests/EventHandling/Stubs/DummyEventHandler.php + + - + rawMessage: 'Property CQRSTest\EventHandling\Stubs\DummyEventHandler::$failureEvent (CQRS\EventHandling\EventExecutionFailed|null) does not accept mixed.' + identifier: assign.propertyType + count: 1 + path: tests/EventHandling/Stubs/DummyEventHandler.php + + - + rawMessage: 'Match expression does not handle remaining value: string' + identifier: match.unhandled + count: 1 + path: tests/EventHandling/Stubs/DummyEventHandlerLocator.php + + - + rawMessage: 'Call to static method PHPUnit\Framework\Assert::assertTrue() with true will always evaluate to true.' + identifier: staticMethod.alreadyNarrowedType + count: 1 + path: tests/EventHandling/SynchronousEventBusTest.php + + - + rawMessage: Cannot access offset 0 on mixed. + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: tests/EventStore/RedisEventStoreTest.php + + - + rawMessage: Cannot access offset 1 on mixed. + identifier: offsetAccess.nonOffsetAccessible + count: 1 + path: tests/EventStore/RedisEventStoreTest.php + + - + rawMessage: 'Cannot call method toMessage() on CQRS\EventStore\RedisEventRecord|null.' + identifier: method.nonObject + count: 1 + path: tests/EventStore/RedisEventStoreTest.php + + - + rawMessage: 'Method CQRSTest\EventStore\RedisEventStoreTest::getData() return type has no value type specified in iterable type array.' + identifier: missingType.iterableValue + count: 1 + path: tests/EventStore/RedisEventStoreTest.php + + - + rawMessage: 'Parameter #2 $actualJson of static method PHPUnit\Framework\Assert::assertJsonStringEqualsJsonString() expects string, mixed given.' + identifier: argument.type + count: 1 + path: tests/EventStore/RedisEventStoreTest.php + + - + rawMessage: 'Method CQRSTest\EventStore\SomeSerializer::deserialize() should return object but returns array|CQRS\Domain\Message\Metadata|CQRSTest\EventStore\SomeEvent|null.' + identifier: return.type + count: 1 + path: tests/EventStore/SomeSerializer.php + + - + rawMessage: PHPDoc tag @var with type Doctrine\ORM\EntityManagerInterface is not subtype of native type PHPUnit\Framework\MockObject\MockObject. + identifier: varTag.nativeType + count: 3 + path: tests/Plugin/Doctrine/CommandHandling/ExplicitOrmTransactionManagerTest.php + + - + rawMessage: PHPDoc tag @var with type Doctrine\ORM\EntityManagerInterface is not subtype of native type PHPUnit\Framework\MockObject\MockObject. + identifier: varTag.nativeType + count: 3 + path: tests/Plugin/Doctrine/CommandHandling/ImplicitOrmTransactionManagerTest.php + + - + rawMessage: 'Property CQRSTest\Serializer\Model\FloatObject::$value is never read, only written.' + identifier: property.onlyWritten + count: 1 + path: tests/Serializer/Model/FloatObject.php + + - + rawMessage: 'Property CQRSTest\Serializer\Model\IntObject::$value is never read, only written.' + identifier: property.onlyWritten + count: 1 + path: tests/Serializer/Model/IntObject.php + + - + rawMessage: 'Property CQRSTest\Serializer\Model\IntegerObject::$value is never read, only written.' + identifier: property.onlyWritten + count: 1 + path: tests/Serializer/Model/IntegerObject.php + + - + rawMessage: 'Property CQRSTest\Serializer\Model\StringObject::$value is never read, only written.' + identifier: property.onlyWritten + count: 1 + path: tests/Serializer/Model/StringObject.php + + - + rawMessage: Property CQRSTest\Serializer\SomeEvent2::$foo has no type specified. + identifier: missingType.property + count: 1 + path: tests/Serializer/SomeEvent2.php + + - + rawMessage: Property CQRSTest\Serializer\SomeEvent2::$id has no type specified. + identifier: missingType.property + count: 1 + path: tests/Serializer/SomeEvent2.php + + - + rawMessage: Property CQRSTest\Serializer\SomeEvent2::$object has no type specified. + identifier: missingType.property + count: 1 + path: tests/Serializer/SomeEvent2.php + + - + rawMessage: Property CQRSTest\Serializer\SomeEvent2::$time has no type specified. + identifier: missingType.property + count: 1 + path: tests/Serializer/SomeEvent2.php + + - + rawMessage: 'Property CQRSTest\Serializer\SomeEvent3::$int is never read, only written.' + identifier: property.onlyWritten + count: 1 + path: tests/Serializer/SomeEvent3.php + + - + rawMessage: 'Property CQRSTest\Serializer\SomeEvent3::$string1 is never read, only written.' + identifier: property.onlyWritten + count: 1 + path: tests/Serializer/SomeEvent3.php + + - + rawMessage: 'Property CQRSTest\Serializer\SomeEvent3::$string2 is never read, only written.' + identifier: property.onlyWritten + count: 1 + path: tests/Serializer/SomeEvent3.php + + - + rawMessage: 'Property CQRSTest\Serializer\SomeEvent3::$uuid is never read, only written.' + identifier: property.onlyWritten + count: 1 + path: tests/Serializer/SomeEvent3.php + + - + rawMessage: Constructor of class CQRSTest\Serializer\TestEventWithCustomConstructor has an unused parameter $someAggregate. + identifier: constructor.unusedParameter + count: 1 + path: tests/Serializer/TestEventWithCustomConstructor.php + + - + rawMessage: 'Method CQRSTest\Stubs\MemoryContainer::__construct() has parameter $services with no value type specified in iterable type array.' + identifier: missingType.iterableValue + count: 1 + path: tests/Stubs/MemoryContainer.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 8cc855e..37fe200 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,3 +5,4 @@ parameters: level: max paths: - src + - tests From 9a6aa89c0d6ab64328ebce471b95ade953f2b50c Mon Sep 17 00:00:00 2001 From: mmalac Date: Sat, 11 Oct 2025 21:36:26 +0200 Subject: [PATCH 3/4] chore: streamline CI workflows, update GitHub Actions and refactor dependency management --- .github/workflows/continuous-integration.yml | 106 +++++-------------- 1 file changed, 29 insertions(+), 77 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b672f46..0b8b1e8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -6,77 +6,38 @@ on: branches: - master +env: + default_php: 8.4 + jobs: coding-standards: name: Coding Standards runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 + - uses: actions/checkout@v4.2.2 + - uses: shivammathur/setup-php@2.34.1 with: - php-version: 8.4 + php-version: ${{ env.default_php }} coverage: none - extensions: redis - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies - run: composer install --no-interaction --no-progress --prefer-dist - - - name: Run the linter - run: composer lint - - - name: Check coding standards - run: composer cs-check + - uses: ramsey/composer-install@v3 + - run: composer lint + - run: composer cs-check static-analysis: name: Static Analysis runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 + - uses: actions/checkout@v4.2.2 + - uses: shivammathur/setup-php@2.34.1 with: coverage: none - php-version: 8.4 - extensions: redis - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies - run: composer update --no-interaction --no-progress --prefer-dist - - - name: Run static analysis - run: php vendor/bin/phpstan analyse + php-version: ${{ env.default_php }} + - uses: ramsey/composer-install@v3 + - run: php vendor/bin/phpstan analyse unit-tests: name: Unit Tests runs-on: ubuntu-latest - strategy: matrix: php-versions: ['8.3', '8.4'] @@ -90,32 +51,23 @@ jobs: options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 + - uses: actions/checkout@v4.2.2 + - uses: shivammathur/setup-php@2.34.1 with: php-version: "${{ matrix.php-versions }}" coverage: xdebug - extensions: redis - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - uses: ramsey/composer-install@v3 + - run: ./vendor/bin/phpunit + - uses: codecov/codecov-action@v1 - - name: Cache dependencies - uses: actions/cache@v2 + rector: + runs-on: ubuntu-latest + name: Run Rector on PHP + steps: + - uses: actions/checkout@v4.2.2 + - uses: shivammathur/setup-php@2.34.1 with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies - run: composer install --no-interaction --no-progress --prefer-dist --optimize-autoloader - - - name: Run unit tests - run: ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml --coverage-text - - - name: Publish coverage report to Codecov - uses: codecov/codecov-action@v1 + php-version: ${{ env.default_php }} + tools: composer + - uses: ramsey/composer-install@v3 + - run: composer rector \ No newline at end of file From e6372390789fa5bd83d357bbaff25476596f07c6 Mon Sep 17 00:00:00 2001 From: mmalac Date: Sat, 11 Oct 2025 21:46:41 +0200 Subject: [PATCH 4/4] chore: update test fixtures to use consistent array format and add missing `@throws` docblocks in container stubs --- tests/EventStore/RedisEventStoreTest.php | 8 ++++---- tests/Stubs/DummyCallableContainer.php | 3 +++ tests/Stubs/MemoryContainer.php | 3 +++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/EventStore/RedisEventStoreTest.php b/tests/EventStore/RedisEventStoreTest.php index 57a6139..ae35158 100644 --- a/tests/EventStore/RedisEventStoreTest.php +++ b/tests/EventStore/RedisEventStoreTest.php @@ -92,8 +92,8 @@ public static function getData(): array "type": "CQRSTest\\EventStore\\SomeEvent" }, "metadata": { - "data": {}, - "types": {} + "data": [], + "types": [] } } JSON, @@ -117,8 +117,8 @@ public static function getData(): array "type": "CQRSTest\\EventStore\\SomeEvent" }, "metadata": { - "data": {}, - "types": {} + "data": [], + "types": [] }, "aggregate": { "type": "SomeAggregate", diff --git a/tests/Stubs/DummyCallableContainer.php b/tests/Stubs/DummyCallableContainer.php index 7c17809..76b6eb5 100644 --- a/tests/Stubs/DummyCallableContainer.php +++ b/tests/Stubs/DummyCallableContainer.php @@ -8,6 +8,9 @@ final class DummyCallableContainer implements ContainerInterface { + /** + * @throws void + */ #[\Override] public function get(string $id): callable { diff --git a/tests/Stubs/MemoryContainer.php b/tests/Stubs/MemoryContainer.php index d1cd804..9f75e02 100644 --- a/tests/Stubs/MemoryContainer.php +++ b/tests/Stubs/MemoryContainer.php @@ -12,6 +12,9 @@ public function __construct(private array $services) { } + /** + * @throws void + */ #[\Override] public function get(string $id): mixed {