From 9e7549ad18b2b39534433c6e2359d990a8b08779 Mon Sep 17 00:00:00 2001 From: Unay Santisteban Date: Tue, 14 Oct 2025 14:54:07 +1000 Subject: [PATCH 1/2] Refactor/Domain and application namespaces for consistency --- README.md | 58 +++++++---- composer.json | 6 +- src/Application/Command/Command.php | 18 ++++ .../Handler}/CommandHandler.php | 8 +- .../Handler}/EventHandler.php | 8 +- .../Handler}/QueryHandler.php | 9 +- .../Messaging}/CommandBus.php | 8 +- .../Messaging}/EventBus.php | 8 +- .../Messaging}/QueryBus.php | 9 +- .../Messaging}/ServiceBus.php | 6 +- src/Application/Query/Query.php | 18 ++++ src/Application/Query/QueryResponse.php | 18 ++++ src/Domain/Contracts/ServiceBus/Command.php | 16 ---- src/Domain/Contracts/ServiceBus/Query.php | 16 ---- .../Contracts/ServiceBus/QueryResponse.php | 16 ---- .../ServiceBus => Events}/Event.php | 21 ++-- src/Domain/Events/Sourceable.php | 96 +++++++++++++++++++ src/Domain/Events/Traceable.php | 68 +++++++++++++ .../{Contracts => }/Model/Aggregate.php | 8 +- src/Domain/{Contracts => }/Model/Entity.php | 6 +- .../{Contracts => }/Model/Identifier.php | 6 +- .../{Contracts => }/Model/ValueObject.php | 6 +- 22 files changed, 316 insertions(+), 117 deletions(-) create mode 100644 src/Application/Command/Command.php rename src/{Domain/Contracts/ServiceBus => Application/Handler}/CommandHandler.php (56%) rename src/{Domain/Contracts/ServiceBus => Application/Handler}/EventHandler.php (56%) rename src/{Domain/Contracts/ServiceBus => Application/Handler}/QueryHandler.php (54%) rename src/{Domain/Contracts/ServiceBus => Application/Messaging}/CommandBus.php (54%) rename src/{Domain/Contracts/ServiceBus => Application/Messaging}/EventBus.php (54%) rename src/{Domain/Contracts/ServiceBus => Application/Messaging}/QueryBus.php (51%) rename src/{Domain/Contracts/ServiceBus => Application/Messaging}/ServiceBus.php (75%) create mode 100644 src/Application/Query/Query.php create mode 100644 src/Application/Query/QueryResponse.php delete mode 100644 src/Domain/Contracts/ServiceBus/Command.php delete mode 100644 src/Domain/Contracts/ServiceBus/Query.php delete mode 100644 src/Domain/Contracts/ServiceBus/QueryResponse.php rename src/Domain/{Contracts/ServiceBus => Events}/Event.php (57%) create mode 100644 src/Domain/Events/Sourceable.php create mode 100644 src/Domain/Events/Traceable.php rename src/Domain/{Contracts => }/Model/Aggregate.php (67%) rename src/Domain/{Contracts => }/Model/Entity.php (58%) rename src/Domain/{Contracts => }/Model/Identifier.php (73%) rename src/Domain/{Contracts => }/Model/ValueObject.php (72%) diff --git a/README.md b/README.md index c54b39f..910ff73 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,41 @@ Common interfaces for PHP Complex Heart SDK. -## Domain Modeling - -- Aggregate -- Entity -- ValueObject -- Identity - -## Service Bus - -- ServiceBus -- Command -- CommandBus -- CommandHandler -- Event -- EventBus -- EventHandler -- Query -- QueryBus -- QueryHandler -- QueryResponse +## Domain Layer + +### Model +Core building blocks for domain-driven design: +- **Aggregate** - Root entity with domain event publishing +- **Entity** - Domain object with unique identity +- **ValueObject** - Immutable domain value with equality +- **Identifier** - Unique identifier representation + +### Events +Domain event interfaces following ISP (Interface Segregation Principle): +- **Event** - Base domain event (eventId, eventName, payload, occurredOn) +- **Traceable** - Distributed tracing (correlationId, causationId) +- **Sourceable** - Event sourcing (aggregateId, aggregateType, eventVersion) + +## Application Layer + +### Command +Write operations (CQRS): +- **Command** - Marker interface for state-changing operations + +### Query +Read operations (CQRS): +- **Query** - Marker interface for data retrieval +- **QueryResponse** - Marker interface for query results + +### Handler +Message handlers: +- **CommandHandler** - Executes commands +- **QueryHandler** - Executes queries and returns responses +- **EventHandler** - Reacts to domain events + +### Messaging +Message bus implementations: +- **CommandBus** - Dispatches commands +- **QueryBus** - Asks queries +- **EventBus** - Publishes events +- **ServiceBus** - Unified bus facade diff --git a/composer.json b/composer.json index 7de492c..c4f1832 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ }, "require-dev": { "phpstan/phpstan": "^1.0", - "phpstan/extension-installer": "^1.3" + "phpstan/extension-installer": "^1.3", + "laravel/pint": "^1.25" }, "autoload": { "psr-4": { @@ -23,7 +24,8 @@ } }, "scripts": { - "analyse": "vendor/bin/phpstan analyse src --no-progress --level=9" + "analyse": "vendor/bin/phpstan analyse src --no-progress --level=9", + "pint": "vendor/bin/pint --preset psr12" }, "config": { "allow-plugins": { diff --git a/src/Application/Command/Command.php b/src/Application/Command/Command.php new file mode 100644 index 0000000..4526dad --- /dev/null +++ b/src/Application/Command/Command.php @@ -0,0 +1,18 @@ + + * @package ComplexHeart\Application\Command + */ +interface Command +{ +} diff --git a/src/Domain/Contracts/ServiceBus/CommandHandler.php b/src/Application/Handler/CommandHandler.php similarity index 56% rename from src/Domain/Contracts/ServiceBus/CommandHandler.php rename to src/Application/Handler/CommandHandler.php index ffabd39..4ddbc4e 100644 --- a/src/Domain/Contracts/ServiceBus/CommandHandler.php +++ b/src/Application/Handler/CommandHandler.php @@ -2,13 +2,15 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\ServiceBus; +namespace ComplexHeart\Application\Handler; + +use ComplexHeart\Application\Command\Command; /** * Interface CommandHandler * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\ServiceBus + * @author Unay Santisteban + * @package ComplexHeart\Application\Handler */ interface CommandHandler { diff --git a/src/Domain/Contracts/ServiceBus/EventHandler.php b/src/Application/Handler/EventHandler.php similarity index 56% rename from src/Domain/Contracts/ServiceBus/EventHandler.php rename to src/Application/Handler/EventHandler.php index 270fe51..bfc0ed5 100644 --- a/src/Domain/Contracts/ServiceBus/EventHandler.php +++ b/src/Application/Handler/EventHandler.php @@ -2,13 +2,15 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\ServiceBus; +namespace ComplexHeart\Application\Handler; + +use ComplexHeart\Domain\Events\Event; /** * Interface EventHandler * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\ServiceBus + * @author Unay Santisteban + * @package ComplexHeart\Application\Handler */ interface EventHandler { diff --git a/src/Domain/Contracts/ServiceBus/QueryHandler.php b/src/Application/Handler/QueryHandler.php similarity index 54% rename from src/Domain/Contracts/ServiceBus/QueryHandler.php rename to src/Application/Handler/QueryHandler.php index f4765c0..0af0f98 100644 --- a/src/Domain/Contracts/ServiceBus/QueryHandler.php +++ b/src/Application/Handler/QueryHandler.php @@ -2,13 +2,16 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\ServiceBus; +namespace ComplexHeart\Application\Handler; + +use ComplexHeart\Application\Query\Query; +use ComplexHeart\Application\Query\QueryResponse; /** * Interface QueryHandler * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\ServiceBus + * @author Unay Santisteban + * @package ComplexHeart\Application\Handler */ interface QueryHandler { diff --git a/src/Domain/Contracts/ServiceBus/CommandBus.php b/src/Application/Messaging/CommandBus.php similarity index 54% rename from src/Domain/Contracts/ServiceBus/CommandBus.php rename to src/Application/Messaging/CommandBus.php index 1c88b30..9df5235 100644 --- a/src/Domain/Contracts/ServiceBus/CommandBus.php +++ b/src/Application/Messaging/CommandBus.php @@ -2,13 +2,15 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\ServiceBus; +namespace ComplexHeart\Application\Messaging; + +use ComplexHeart\Application\Command\Command; /** * Interface CommandBus * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\ServiceBus + * @author Unay Santisteban + * @package ComplexHeart\Application\Messaging */ interface CommandBus { diff --git a/src/Domain/Contracts/ServiceBus/EventBus.php b/src/Application/Messaging/EventBus.php similarity index 54% rename from src/Domain/Contracts/ServiceBus/EventBus.php rename to src/Application/Messaging/EventBus.php index f8a8608..2381bee 100644 --- a/src/Domain/Contracts/ServiceBus/EventBus.php +++ b/src/Application/Messaging/EventBus.php @@ -2,13 +2,15 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\ServiceBus; +namespace ComplexHeart\Application\Messaging; + +use ComplexHeart\Domain\Events\Event; /** * Interface EventBus * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\ServiceBus + * @author Unay Santisteban + * @package ComplexHeart\Application\Messaging */ interface EventBus { diff --git a/src/Domain/Contracts/ServiceBus/QueryBus.php b/src/Application/Messaging/QueryBus.php similarity index 51% rename from src/Domain/Contracts/ServiceBus/QueryBus.php rename to src/Application/Messaging/QueryBus.php index 2420f28..3375211 100644 --- a/src/Domain/Contracts/ServiceBus/QueryBus.php +++ b/src/Application/Messaging/QueryBus.php @@ -2,13 +2,16 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\ServiceBus; +namespace ComplexHeart\Application\Messaging; + +use ComplexHeart\Application\Query\Query; +use ComplexHeart\Application\Query\QueryResponse; /** * Interface QueryBus * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\ServiceBus + * @author Unay Santisteban + * @package ComplexHeart\Application\Messaging */ interface QueryBus { diff --git a/src/Domain/Contracts/ServiceBus/ServiceBus.php b/src/Application/Messaging/ServiceBus.php similarity index 75% rename from src/Domain/Contracts/ServiceBus/ServiceBus.php rename to src/Application/Messaging/ServiceBus.php index 21101f6..d26d392 100644 --- a/src/Domain/Contracts/ServiceBus/ServiceBus.php +++ b/src/Application/Messaging/ServiceBus.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\ServiceBus; +namespace ComplexHeart\Application\Messaging; /** * Interface ServiceBus * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\ServiceBus + * @author Unay Santisteban + * @package ComplexHeart\Application\Messaging */ interface ServiceBus { diff --git a/src/Application/Query/Query.php b/src/Application/Query/Query.php new file mode 100644 index 0000000..ee3a5e1 --- /dev/null +++ b/src/Application/Query/Query.php @@ -0,0 +1,18 @@ + + * @package ComplexHeart\Application\Query + */ +interface Query +{ +} diff --git a/src/Application/Query/QueryResponse.php b/src/Application/Query/QueryResponse.php new file mode 100644 index 0000000..c36d74f --- /dev/null +++ b/src/Application/Query/QueryResponse.php @@ -0,0 +1,18 @@ + + * @package ComplexHeart\Application\Query + */ +interface QueryResponse +{ +} diff --git a/src/Domain/Contracts/ServiceBus/Command.php b/src/Domain/Contracts/ServiceBus/Command.php deleted file mode 100644 index 2b469eb..0000000 --- a/src/Domain/Contracts/ServiceBus/Command.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @package ComplexHeart\Domain\Contracts\ServiceBus - */ -interface Command -{ - -} diff --git a/src/Domain/Contracts/ServiceBus/Query.php b/src/Domain/Contracts/ServiceBus/Query.php deleted file mode 100644 index 9c2b411..0000000 --- a/src/Domain/Contracts/ServiceBus/Query.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @package ComplexHeart\Domain\Contracts\ServiceBus - */ -interface Query -{ - -} diff --git a/src/Domain/Contracts/ServiceBus/QueryResponse.php b/src/Domain/Contracts/ServiceBus/QueryResponse.php deleted file mode 100644 index dc11996..0000000 --- a/src/Domain/Contracts/ServiceBus/QueryResponse.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @package ComplexHeart\Domain\Contracts\ServiceBus - */ -interface QueryResponse -{ - -} diff --git a/src/Domain/Contracts/ServiceBus/Event.php b/src/Domain/Events/Event.php similarity index 57% rename from src/Domain/Contracts/ServiceBus/Event.php rename to src/Domain/Events/Event.php index 8c32bda..7ca5b28 100644 --- a/src/Domain/Contracts/ServiceBus/Event.php +++ b/src/Domain/Events/Event.php @@ -2,13 +2,15 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\ServiceBus; +namespace ComplexHeart\Domain\Events; /** * Interface Event * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\ServiceBus + * Represents a domain event - something that happened in the domain. + * + * @author Unay Santisteban + * @package ComplexHeart\Domain\Events */ interface Event { @@ -22,19 +24,12 @@ public function eventId(): string; /** * The unique domain event name, commonly is a dotted string. * - * i.e: this.object.event.name + * Examples: order.placed, user.registered, payment.processed * * @return string */ public function eventName(): string; - /** - * The aggregate id that has registered the domain event (UUID). - * - * @return string - */ - public function aggregateId(): string; - /** * Returns the event payload. * @@ -43,9 +38,9 @@ public function aggregateId(): string; public function payload(): array; /** - * The timestamp on when the domain event is registered in ISO-8601. + * The timestamp when the domain event occurred in ISO-8601. * - * i.e: 2005-08-15T15:52:01+0000 + * Example: 2005-08-15T15:52:01+0000 * * @return string */ diff --git a/src/Domain/Events/Sourceable.php b/src/Domain/Events/Sourceable.php new file mode 100644 index 0000000..e23def1 --- /dev/null +++ b/src/Domain/Events/Sourceable.php @@ -0,0 +1,96 @@ + + * @package ComplexHeart\Domain\Events + */ +interface Sourceable +{ + /** + * The unique identifier of the aggregate that emitted this event. + * + * This method provides explicit aggregate identification rather than + * relying on convention-based extraction from the payload, offering: + * - Type safety (guaranteed string, not array lookup) + * - IDE support (autocomplete, type hints, refactoring) + * - Contract enforcement at compile time + * - Clear event store stream naming + * + * Event stores typically use this to build stream names: + * Stream name = aggregateType() . "-" . aggregateId() + * + * Example: + * For an Order aggregate with ID "order-123", this returns "order-123" + * Stream name becomes: "Order-order-123" + * + * @return string The aggregate identifier (UUID, integer, or custom ID) + */ + public function aggregateId(): string; + + /** + * The type of aggregate that emitted this event. + * + * Identifies which aggregate class/type this event belongs to. + * Used by event stores for stream organization and aggregate reconstruction. + * + * Typical values (choose what works for your event store): + * - Fully qualified class name: "App\\Domain\\Order\\Order" + * - Short class name: "Order" + * - Type identifier: "order" + * - Custom identifier: "sales.order" + * + * Consistency is key - use the same format across all events. + * + * @return string The aggregate type identifier + */ + public function aggregateType(): string; + + /** + * The schema version of this event. + * + * Critical for event sourcing systems where events are stored permanently + * and aggregates must be rebuilt from historical events over time. + * + * Versioning strategies: + * - Semantic versioning: "1.0.0", "1.1.0", "2.0.0" + * - Simple incrementing: "1", "2", "3" + * - Date-based: "2024-01-15", "2025-10-14" + * + * When to increment the version: + * - Adding new fields: minor version (1.0 → 1.1) + * - Removing fields: major version (1.0 → 2.0) + * - Changing field types: major version (1.0 → 2.0) + * - Renaming fields: major version (1.0 → 2.0) + * + * Event stores use this for: + * - Upcasting old events to new schema + * - Maintaining multiple versions simultaneously + * - Migration strategies and backward compatibility + * + * Example evolution: + * - OrderPlaced v1.0: {orderId, total} + * - OrderPlaced v1.1: {orderId, total, customerId} (added field) + * - OrderPlaced v2.0: {orderId, total, customerId, items[]} (added array) + * + * @return string Version identifier (semantic, numeric, or date-based) + */ + public function eventVersion(): string; +} diff --git a/src/Domain/Events/Traceable.php b/src/Domain/Events/Traceable.php new file mode 100644 index 0000000..64c0326 --- /dev/null +++ b/src/Domain/Events/Traceable.php @@ -0,0 +1,68 @@ + + * @package ComplexHeart\Domain\Events + */ +interface Traceable +{ + /** + * The correlation ID that traces the entire request/workflow. + * + * The same correlation ID flows through all services and events + * in a single business transaction, enabling end-to-end tracing. + * + * Example flow: + * - HTTP Request (correlationId: abc-123) + * - OrderPlaced event (correlationId: abc-123) + * - PaymentRequested event (correlationId: abc-123) + * - PaymentProcessed event (correlationId: abc-123) + * + * Use cases: + * - Distributed tracing across microservices + * - Debugging async workflows + * - APM tools integration + * - Request flow visualization + * + * @return string UUID or custom trace ID + */ + public function correlationId(): string; + + /** + * The causation ID - the immediate parent event that caused this event. + * + * Forms a causal chain showing parent-child relationships between events. + * Each event's causationId points to the eventId of its immediate parent. + * + * Example chain: + * - OrderPlaced (eventId: ev-001, causationId: null) + * ↓ + * - PaymentRequested (eventId: ev-002, causationId: ev-001) + * ↓ + * - PaymentProcessed (eventId: ev-003, causationId: ev-002) + * + * Use cases: + * - Event chain reconstruction + * - Root cause analysis (trace back to origin) + * - Understanding event dependencies + * - Building event graphs and visualizations + * + * @return string Event ID of the parent event that triggered this event + */ + public function causationId(): string; +} diff --git a/src/Domain/Contracts/Model/Aggregate.php b/src/Domain/Model/Aggregate.php similarity index 67% rename from src/Domain/Contracts/Model/Aggregate.php rename to src/Domain/Model/Aggregate.php index b93fca9..2b333d2 100644 --- a/src/Domain/Contracts/Model/Aggregate.php +++ b/src/Domain/Model/Aggregate.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\Model; +namespace ComplexHeart\Domain\Model; -use ComplexHeart\Domain\Contracts\ServiceBus\EventBus; +use ComplexHeart\Application\Messaging\EventBus; /** * Interface Aggregate * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\Model + * @author Unay Santisteban + * @package ComplexHeart\Domain\Model */ interface Aggregate extends Entity { diff --git a/src/Domain/Contracts/Model/Entity.php b/src/Domain/Model/Entity.php similarity index 58% rename from src/Domain/Contracts/Model/Entity.php rename to src/Domain/Model/Entity.php index 5521e5f..824aaf2 100644 --- a/src/Domain/Contracts/Model/Entity.php +++ b/src/Domain/Model/Entity.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\Model; +namespace ComplexHeart\Domain\Model; /** * Interface Entity * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\Model + * @author Unay Santisteban + * @package ComplexHeart\Domain\Model */ interface Entity { diff --git a/src/Domain/Contracts/Model/Identifier.php b/src/Domain/Model/Identifier.php similarity index 73% rename from src/Domain/Contracts/Model/Identifier.php rename to src/Domain/Model/Identifier.php index 46db6ab..97f5e51 100644 --- a/src/Domain/Contracts/Model/Identifier.php +++ b/src/Domain/Model/Identifier.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\Model; +namespace ComplexHeart\Domain\Model; /** * Interface Identifier * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\Model + * @author Unay Santisteban + * @package ComplexHeart\Domain\Model */ interface Identifier { diff --git a/src/Domain/Contracts/Model/ValueObject.php b/src/Domain/Model/ValueObject.php similarity index 72% rename from src/Domain/Contracts/Model/ValueObject.php rename to src/Domain/Model/ValueObject.php index 5c12815..ffc5b46 100644 --- a/src/Domain/Contracts/Model/ValueObject.php +++ b/src/Domain/Model/ValueObject.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace ComplexHeart\Domain\Contracts\Model; +namespace ComplexHeart\Domain\Model; /** * Interface ValueObject * - * @author Unay Santisteban - * @package ComplexHeart\Domain\Contracts\Model + * @author Unay Santisteban + * @package ComplexHeart\Domain\Model */ interface ValueObject { From 2cbbe87076de62a7aa7a4e00db5cb642331cebec Mon Sep 17 00:00:00 2001 From: Unay Santisteban Date: Tue, 14 Oct 2025 16:21:29 +1000 Subject: [PATCH 2/2] Refactor/Update namespaces and interfaces for clarity and consistency --- .github/workflows/test.yml | 17 +++-- .gitignore | 10 ++- README.md | 39 ++++++++--- composer.json | 13 ++-- phpunit.xml | 23 +++++++ src/Application/Command/Command.php | 1 - .../{Messaging => Command}/CommandBus.php | 7 +- src/Application/Handler/CommandHandler.php | 3 - src/Application/Handler/EventHandler.php | 3 - src/Application/Handler/QueryHandler.php | 5 -- src/Application/Messaging/QueryBus.php | 26 -------- src/Application/Query/Query.php | 1 - src/Application/Query/QueryBus.php | 18 +++++ src/Application/Query/QueryResponse.php | 1 - .../{Messaging => }/ServiceBus.php | 13 ++-- src/Domain/Events/Event.php | 7 -- .../Messaging => Domain/Events}/EventBus.php | 7 +- src/Domain/Events/Sourceable.php | 1 - src/Domain/Events/Traceable.php | 1 - src/Domain/Model/Aggregate.php | 6 +- src/Domain/Model/Entity.php | 3 - src/Domain/Model/Identifier.php | 5 -- src/Domain/Model/ValueObject.php | 5 -- tests/ArchTest.php | 65 +++++++++++++++++++ tests/Pest.php | 3 + 25 files changed, 176 insertions(+), 107 deletions(-) create mode 100644 phpunit.xml rename src/Application/{Messaging => Command}/CommandBus.php (58%) delete mode 100644 src/Application/Messaging/QueryBus.php create mode 100644 src/Application/Query/QueryBus.php rename src/Application/{Messaging => }/ServiceBus.php (71%) rename src/{Application/Messaging => Domain/Events}/EventBus.php (58%) create mode 100644 tests/ArchTest.php create mode 100644 tests/Pest.php diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dfcd0d3..0aa8b46 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,11 +8,12 @@ on: jobs: test: + name: Test on PHP ${{ matrix.php-version }} runs-on: ubuntu-latest strategy: matrix: - php-version: [ '8.1', '8.2', '8.3' ] + php-version: [ '8.2', '8.3', '8.4' ] steps: - name: Checkout source code @@ -28,7 +29,7 @@ jobs: tools: composer:v2 - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v4.3.0 with: path: ~/.composer/cache key: php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} @@ -37,9 +38,15 @@ jobs: - name: Validate composer.json and composer.lock run: composer validate - - name: Install dependencies + - name: Install Dependencies if: steps.composer-cache.outputs.cache-hit != 'true' - run: composer install --prefer-dist --no-progress --no-suggest + run: composer install --prefer-dist --no-progress + + - name: Check Code Style + run: composer pint-test - name: Execute Static Code analysis - run: composer analyse \ No newline at end of file + run: composer analyse + + - name: Execute Unit, Integration and Acceptance Tests + run: composer test diff --git a/.gitignore b/.gitignore index 43cae52..9f4ffa7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,14 @@ composer.phar /vendor/ .idea/ -/infrastructure +/infrastructure/ +/coverage/ *[N|n]o[G|g]it* -*coverage* +coverage.xml +test.xml .phpunit.result.cache composer.lock +wiki/Home.md +/.phpunit.cache +.phpactor.json +/.serena/ \ No newline at end of file diff --git a/README.md b/README.md index 910ff73..aa40d95 100644 --- a/README.md +++ b/README.md @@ -20,27 +20,48 @@ Domain event interfaces following ISP (Interface Segregation Principle): - **Event** - Base domain event (eventId, eventName, payload, occurredOn) - **Traceable** - Distributed tracing (correlationId, causationId) - **Sourceable** - Event sourcing (aggregateId, aggregateType, eventVersion) +- **EventBus** - Publishes domain events ## Application Layer ### Command Write operations (CQRS): - **Command** - Marker interface for state-changing operations +- **CommandBus** - Dispatches commands to handlers +- **CommandHandler** - Executes commands ### Query Read operations (CQRS): - **Query** - Marker interface for data retrieval - **QueryResponse** - Marker interface for query results +- **QueryBus** - Routes queries to handlers +- **QueryHandler** - Executes queries and returns responses ### Handler -Message handlers: -- **CommandHandler** - Executes commands -- **QueryHandler** - Executes queries and returns responses +Event handlers: - **EventHandler** - Reacts to domain events -### Messaging -Message bus implementations: -- **CommandBus** - Dispatches commands -- **QueryBus** - Asks queries -- **EventBus** - Publishes events -- **ServiceBus** - Unified bus facade +### Service Bus +Unified message bus facade: +- **ServiceBus** - Provides access to CommandBus, QueryBus, and EventBus + +## Architecture + +This library follows Clean Architecture principles with explicit layer separation: +- **Domain → Application** - Domain layer is independent, Application depends on Domain +- **Layer-Explicit Namespaces** - Clear architectural boundaries in namespace structure +- **Interface Segregation** - Compose only needed capabilities (e.g., Event + Traceable + Sourceable) + +### Architecture Testing + +The project includes automated architecture tests using Pest PHP: + +```bash +composer test +``` + +Tests enforce: +- Domain layer independence (no Application dependencies) +- Correct interface placement and usage +- Clean Architecture dependency rules +- PHP and security best practices (via arch presets) diff --git a/composer.json b/composer.json index c4f1832..6c6e28d 100644 --- a/composer.json +++ b/composer.json @@ -11,12 +11,14 @@ ], "minimum-stability": "stable", "require": { - "php": "^8.1" + "php": "^8.2" }, "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/extension-installer": "^1.3", - "laravel/pint": "^1.25" + "laravel/pint": "^1.25", + "pestphp/pest": "^3.8.4", + "pestphp/pest-plugin-arch": "^3.1.1" }, "autoload": { "psr-4": { @@ -24,11 +26,14 @@ } }, "scripts": { - "analyse": "vendor/bin/phpstan analyse src --no-progress --level=9", - "pint": "vendor/bin/pint --preset psr12" + "test": "vendor/bin/pest --configuration=phpunit.xml --coverage --coverage-clover=coverage.xml --log-junit=test.xml", + "analyse": "vendor/bin/phpstan analyse src --no-progress --memory-limit=4G --level=9", + "pint-test": "vendor/bin/pint --preset=psr12 --test", + "pint": "vendor/bin/pint --preset=psr12" }, "config": { "allow-plugins": { + "pestphp/pest-plugin": true, "phpstan/extension-installer": true } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e266abd --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,23 @@ + + + + + + + + + + ./tests + + + + + + ./src + + + diff --git a/src/Application/Command/Command.php b/src/Application/Command/Command.php index 4526dad..658b6ad 100644 --- a/src/Application/Command/Command.php +++ b/src/Application/Command/Command.php @@ -11,7 +11,6 @@ * Commands represent the intention to change the system state. * * @author Unay Santisteban - * @package ComplexHeart\Application\Command */ interface Command { diff --git a/src/Application/Messaging/CommandBus.php b/src/Application/Command/CommandBus.php similarity index 58% rename from src/Application/Messaging/CommandBus.php rename to src/Application/Command/CommandBus.php index 9df5235..5ba8995 100644 --- a/src/Application/Messaging/CommandBus.php +++ b/src/Application/Command/CommandBus.php @@ -2,22 +2,17 @@ declare(strict_types=1); -namespace ComplexHeart\Application\Messaging; - -use ComplexHeart\Application\Command\Command; +namespace ComplexHeart\Application\Command; /** * Interface CommandBus * * @author Unay Santisteban - * @package ComplexHeart\Application\Messaging */ interface CommandBus { /** * Dispatch the given command. - * - * @param Command $command */ public function dispatch(Command $command): void; } diff --git a/src/Application/Handler/CommandHandler.php b/src/Application/Handler/CommandHandler.php index 4ddbc4e..f822032 100644 --- a/src/Application/Handler/CommandHandler.php +++ b/src/Application/Handler/CommandHandler.php @@ -10,14 +10,11 @@ * Interface CommandHandler * * @author Unay Santisteban - * @package ComplexHeart\Application\Handler */ interface CommandHandler { /** * Handle the command execution. - * - * @param Command $command */ public function __invoke(Command $command): void; } diff --git a/src/Application/Handler/EventHandler.php b/src/Application/Handler/EventHandler.php index bfc0ed5..a681e32 100644 --- a/src/Application/Handler/EventHandler.php +++ b/src/Application/Handler/EventHandler.php @@ -10,14 +10,11 @@ * Interface EventHandler * * @author Unay Santisteban - * @package ComplexHeart\Application\Handler */ interface EventHandler { /** * Handle the event execution. - * - * @param Event $event */ public function __invoke(Event $event): void; } diff --git a/src/Application/Handler/QueryHandler.php b/src/Application/Handler/QueryHandler.php index 0af0f98..b807f25 100644 --- a/src/Application/Handler/QueryHandler.php +++ b/src/Application/Handler/QueryHandler.php @@ -11,16 +11,11 @@ * Interface QueryHandler * * @author Unay Santisteban - * @package ComplexHeart\Application\Handler */ interface QueryHandler { /** * Handle the query execution. - * - * @param Query $query - * - * @return QueryResponse */ public function __invoke(Query $query): QueryResponse; } diff --git a/src/Application/Messaging/QueryBus.php b/src/Application/Messaging/QueryBus.php deleted file mode 100644 index 3375211..0000000 --- a/src/Application/Messaging/QueryBus.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @package ComplexHeart\Application\Messaging - */ -interface QueryBus -{ - /** - * Ask the given query. - * - * @param Query $query - * - * @return QueryResponse - */ - public function ask(Query $query): QueryResponse; -} diff --git a/src/Application/Query/Query.php b/src/Application/Query/Query.php index ee3a5e1..047455f 100644 --- a/src/Application/Query/Query.php +++ b/src/Application/Query/Query.php @@ -11,7 +11,6 @@ * Queries represent the intention to read data without side effects. * * @author Unay Santisteban - * @package ComplexHeart\Application\Query */ interface Query { diff --git a/src/Application/Query/QueryBus.php b/src/Application/Query/QueryBus.php new file mode 100644 index 0000000..a33f445 --- /dev/null +++ b/src/Application/Query/QueryBus.php @@ -0,0 +1,18 @@ + + */ +interface QueryBus +{ + /** + * Ask the given query. + */ + public function ask(Query $query): QueryResponse; +} diff --git a/src/Application/Query/QueryResponse.php b/src/Application/Query/QueryResponse.php index c36d74f..dad9c2f 100644 --- a/src/Application/Query/QueryResponse.php +++ b/src/Application/Query/QueryResponse.php @@ -11,7 +11,6 @@ * Represents the data returned from a query execution. * * @author Unay Santisteban - * @package ComplexHeart\Application\Query */ interface QueryResponse { diff --git a/src/Application/Messaging/ServiceBus.php b/src/Application/ServiceBus.php similarity index 71% rename from src/Application/Messaging/ServiceBus.php rename to src/Application/ServiceBus.php index d26d392..2da19bd 100644 --- a/src/Application/Messaging/ServiceBus.php +++ b/src/Application/ServiceBus.php @@ -2,34 +2,31 @@ declare(strict_types=1); -namespace ComplexHeart\Application\Messaging; +namespace ComplexHeart\Application; + +use ComplexHeart\Application\Command\CommandBus; +use ComplexHeart\Application\Query\QueryBus; +use ComplexHeart\Domain\Events\EventBus; /** * Interface ServiceBus * * @author Unay Santisteban - * @package ComplexHeart\Application\Messaging */ interface ServiceBus { /** * Return the Command Bus implementation. - * - * @return CommandBus */ public function commandBus(): CommandBus; /** * Return the Query Bus implementation. - * - * @return QueryBus */ public function queryBus(): QueryBus; /** * Return the Event Bus implementation. - * - * @return EventBus */ public function eventBus(): EventBus; } diff --git a/src/Domain/Events/Event.php b/src/Domain/Events/Event.php index 7ca5b28..e8fbcb2 100644 --- a/src/Domain/Events/Event.php +++ b/src/Domain/Events/Event.php @@ -10,14 +10,11 @@ * Represents a domain event - something that happened in the domain. * * @author Unay Santisteban - * @package ComplexHeart\Domain\Events */ interface Event { /** * The unique id for the current domain event (UUID). - * - * @return string */ public function eventId(): string; @@ -25,8 +22,6 @@ public function eventId(): string; * The unique domain event name, commonly is a dotted string. * * Examples: order.placed, user.registered, payment.processed - * - * @return string */ public function eventName(): string; @@ -41,8 +36,6 @@ public function payload(): array; * The timestamp when the domain event occurred in ISO-8601. * * Example: 2005-08-15T15:52:01+0000 - * - * @return string */ public function occurredOn(): string; } diff --git a/src/Application/Messaging/EventBus.php b/src/Domain/Events/EventBus.php similarity index 58% rename from src/Application/Messaging/EventBus.php rename to src/Domain/Events/EventBus.php index 2381bee..f4d9962 100644 --- a/src/Application/Messaging/EventBus.php +++ b/src/Domain/Events/EventBus.php @@ -2,22 +2,17 @@ declare(strict_types=1); -namespace ComplexHeart\Application\Messaging; - -use ComplexHeart\Domain\Events\Event; +namespace ComplexHeart\Domain\Events; /** * Interface EventBus * * @author Unay Santisteban - * @package ComplexHeart\Application\Messaging */ interface EventBus { /** * Publish event list. - * - * @param Event ...$events */ public function publish(Event ...$events): void; } diff --git a/src/Domain/Events/Sourceable.php b/src/Domain/Events/Sourceable.php index e23def1..e9d6a1c 100644 --- a/src/Domain/Events/Sourceable.php +++ b/src/Domain/Events/Sourceable.php @@ -20,7 +20,6 @@ * - Event store stream organization * * @author Unay Santisteban - * @package ComplexHeart\Domain\Events */ interface Sourceable { diff --git a/src/Domain/Events/Traceable.php b/src/Domain/Events/Traceable.php index 64c0326..966d279 100644 --- a/src/Domain/Events/Traceable.php +++ b/src/Domain/Events/Traceable.php @@ -17,7 +17,6 @@ * - APM tool integration (Datadog, New Relic, Zipkin) * * @author Unay Santisteban - * @package ComplexHeart\Domain\Events */ interface Traceable { diff --git a/src/Domain/Model/Aggregate.php b/src/Domain/Model/Aggregate.php index 2b333d2..9d496d7 100644 --- a/src/Domain/Model/Aggregate.php +++ b/src/Domain/Model/Aggregate.php @@ -4,13 +4,12 @@ namespace ComplexHeart\Domain\Model; -use ComplexHeart\Application\Messaging\EventBus; +use ComplexHeart\Domain\Events\EventBus; /** * Interface Aggregate * * @author Unay Santisteban - * @package ComplexHeart\Domain\Model */ interface Aggregate extends Entity { @@ -20,9 +19,6 @@ interface Aggregate extends Entity * $aggregate = new Aggregate(); * // do things and generate events * $aggregate->publishDomainEvents($eventBus); - * - * @param EventBus $eventBus - * @return void */ public function publishDomainEvents(EventBus $eventBus): void; } diff --git a/src/Domain/Model/Entity.php b/src/Domain/Model/Entity.php index 824aaf2..38f57b8 100644 --- a/src/Domain/Model/Entity.php +++ b/src/Domain/Model/Entity.php @@ -8,14 +8,11 @@ * Interface Entity * * @author Unay Santisteban - * @package ComplexHeart\Domain\Model */ interface Entity { /** * Return the Identifier instance. - * - * @return Identifier */ public function id(): Identifier; } diff --git a/src/Domain/Model/Identifier.php b/src/Domain/Model/Identifier.php index 97f5e51..edffc22 100644 --- a/src/Domain/Model/Identifier.php +++ b/src/Domain/Model/Identifier.php @@ -8,7 +8,6 @@ * Interface Identifier * * @author Unay Santisteban - * @package ComplexHeart\Domain\Model */ interface Identifier { @@ -16,15 +15,11 @@ interface Identifier * Return the id value. * $id->value() // "7a79bffb-4b4e-4d82-932e-c1524723622b" * $id->value() // "459" - * - * @return string */ public function value(): string; /** * Represents the id as string. - * - * @return string */ public function __toString(): string; } diff --git a/src/Domain/Model/ValueObject.php b/src/Domain/Model/ValueObject.php index ffc5b46..e6d1abe 100644 --- a/src/Domain/Model/ValueObject.php +++ b/src/Domain/Model/ValueObject.php @@ -8,7 +8,6 @@ * Interface ValueObject * * @author Unay Santisteban - * @package ComplexHeart\Domain\Model */ interface ValueObject { @@ -21,10 +20,6 @@ public function values(): array; /** * Compare $this object with $other object. - * - * @param object $other - * - * @return bool */ public function equals(object $other): bool; } diff --git a/tests/ArchTest.php b/tests/ArchTest.php new file mode 100644 index 0000000..14923c4 --- /dev/null +++ b/tests/ArchTest.php @@ -0,0 +1,65 @@ +expect('ComplexHeart\Domain') + ->not->toUse('ComplexHeart\Application'); + +arch('Application layer can depend on Domain layer') + ->expect('ComplexHeart\Application') + ->toUse('ComplexHeart\Domain'); + +arch('Domain Model interfaces are correctly placed') + ->expect('ComplexHeart\Domain\Model') + ->toBeInterfaces() + ->toOnlyBeUsedIn([ + 'ComplexHeart\Domain\Model', + 'ComplexHeart\Domain\Events', + 'ComplexHeart\Application', + ]); + +arch('Domain Event interfaces are correctly placed') + ->expect('ComplexHeart\Domain\Events') + ->toBeInterfaces() + ->toOnlyBeUsedIn([ + 'ComplexHeart\Domain', + 'ComplexHeart\Application', + ]); + +arch('Application Command interfaces are correctly placed') + ->expect('ComplexHeart\Application\Command') + ->toBeInterfaces() + ->toOnlyBeUsedIn([ + 'ComplexHeart\Application', + ]); + +arch('Application Query interfaces are correctly placed') + ->expect('ComplexHeart\Application\Query') + ->toBeInterfaces() + ->toOnlyBeUsedIn([ + 'ComplexHeart\Application', + ]); + +arch('Application Handler interfaces are correctly placed') + ->expect('ComplexHeart\Application\Handler') + ->toBeInterfaces() + ->toOnlyBeUsedIn([ + 'ComplexHeart\Application', + ]); + +arch('Application Messaging interfaces are correctly placed') + ->expect('ComplexHeart\Application\Messaging') + ->toBeInterfaces() + ->toOnlyBeUsedIn([ + 'ComplexHeart\Application', + 'ComplexHeart\Domain\Model', // Aggregate uses EventBus + ]); + +arch('all interfaces are suffixed correctly') + ->expect('ComplexHeart') + ->classes() + ->toBeInterfaces(); + +arch()->preset()->php(); +arch()->preset()->security(); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..174d7fd --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,3 @@ +