From 855044f063af05afb170c732d751dd7b12f862ac Mon Sep 17 00:00:00 2001 From: Markus Reinhold Date: Mon, 9 Feb 2026 20:49:30 +0100 Subject: [PATCH 1/3] Add copilot instructions --- .github/copilot-instructions.md | 64 +++++++++++++++++++++++++ src/Common/Domain/Test/DomainAssert.php | 41 ++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 src/Common/Domain/Test/DomainAssert.php diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..d7c5e23e0 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,64 @@ +# Copilot Instructions + +## Test/Lint/Analyze +After every code change, run `./project unit`, `./project sniffer`, and `./project analyzer`. + +## Architecture +This is a modular, domain-driven PHP platform with multiple bounded contexts that communicate via messaging. + +### Bounded Contexts +Every directory in `src/` except `Common` is a bounded context in the gaming platform domain. +`Common` contains shared libraries (Bus, EventStore, Timer, etc.). + +Architectural patterns vary per context but must stay consistent within a context. Do not change a context's +pattern unless explicitly asked to refactor. + +### UI Composition +The UI is composed using SSI (Server Side Includes) and Custom Elements. SSI renders server-side fragments from +different contexts into a page. Custom Elements encapsulate client-side behavior. Both serve as transclusion +boundaries between contexts. + +## Conventions + +### Wiring +All services are explicitly wired in `config/` YAML files. There is no autowiring. + +### Exceptions +The context/aggregate base exception type (e.g., `ChallengeException`) must extend `DomainException`. Use named +constructors on that base exception rather than individual exception classes: +```php +class ChallengeException extends DomainException +{ + public static function notFound(): self + { + return new self(new Violations(new Violation('challenge_not_found'))); + } + + public static function alreadyClosed(): self + { + return new self(new Violations(new Violation('challenge_already_closed'))); + } +} +``` +When an exception is explicitly caught to drive alternate control flow, use a dedicated exception class that is +`final` and extends the context base (e.g., `final class ChallengeNotFoundException extends ChallengeException`). +Every domain exception must populate `Violations` (and any parameters) so error translation is always available. + +### Cleanup +Remove unused code introduced by changes (classes, methods, config, assets) unless explicitly asked to keep it. + +### Tests +- Unit tests mirror the `src/` structure in `tests/unit/` +- Use PHPUnit attributes: `#[Test]` +- Test namespace: `Gaming\Tests\Unit\{Context}\...` +- For domain exceptions, assert via `DomainAssert::expectViolation` with the expected identifier, + violation parameters as `['name' => value]`, and optionally the concrete exception class. + Use `expectException` only for non-domain exceptions. +- The closure passed to `DomainAssert::expectViolation` must contain only the single action that + triggers the exception. All setup (creating objects, calling prior methods) belongs before the call. + +### Code Style +- PSR-12 coding standard +- PHPStan level 8 +- `declare(strict_types=1)` in all PHP files +- Readonly properties and constructor promotion preferred diff --git a/src/Common/Domain/Test/DomainAssert.php b/src/Common/Domain/Test/DomainAssert.php new file mode 100644 index 000000000..03dda7388 --- /dev/null +++ b/src/Common/Domain/Test/DomainAssert.php @@ -0,0 +1,41 @@ + $expectedParameters + * @param class-string $expectedException + */ + public static function expectViolation( + Closure $action, + string $expectedIdentifier, + array $expectedParameters = [], + string $expectedException = DomainException::class + ): void { + try { + $action(); + Assert::fail('Expected a domain exception.'); + } catch (DomainException $exception) { + Assert::assertInstanceOf($expectedException, $exception); + + $violation = $exception->violations->first(); + Assert::assertNotNull($violation); + Assert::assertSame($expectedIdentifier, $violation->identifier); + Assert::assertSame( + $expectedParameters, + array_combine( + array_column($violation->parameters, 'name'), + array_column($violation->parameters, 'value') + ) + ); + } + } +} From b5660bc8c4de0d124223f9a589b324cd2581d656 Mon Sep 17 00:00:00 2001 From: Markus Reinhold Date: Mon, 9 Feb 2026 22:46:22 +0100 Subject: [PATCH 2/3] Be more precise with exceptions --- .github/copilot-instructions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d7c5e23e0..c0023d453 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -42,6 +42,9 @@ class ChallengeException extends DomainException ``` When an exception is explicitly caught to drive alternate control flow, use a dedicated exception class that is `final` and extends the context base (e.g., `final class ChallengeNotFoundException extends ChallengeException`). +These control-flow exception classes must declare their own constructor, and that constructor may accept scalar +violation parameters to pass through, so the concrete type is always instantiated directly when needed. +Each named constructor should instantiate `Violations`/`ViolationParameter` directly, following the example above. Every domain exception must populate `Violations` (and any parameters) so error translation is always available. ### Cleanup From 2794f7b7cef382dc93e393d1c6a5b8e12f6dbf90 Mon Sep 17 00:00:00 2001 From: Markus Reinhold Date: Mon, 9 Feb 2026 23:14:31 +0100 Subject: [PATCH 3/3] Change parameter order for checking exceptions --- .github/copilot-instructions.md | 6 +++--- src/Common/Domain/Test/DomainAssert.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c0023d453..1085f87a5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -54,9 +54,9 @@ Remove unused code introduced by changes (classes, methods, config, assets) unle - Unit tests mirror the `src/` structure in `tests/unit/` - Use PHPUnit attributes: `#[Test]` - Test namespace: `Gaming\Tests\Unit\{Context}\...` -- For domain exceptions, assert via `DomainAssert::expectViolation` with the expected identifier, - violation parameters as `['name' => value]`, and optionally the concrete exception class. - Use `expectException` only for non-domain exceptions. +- For domain exceptions, assert via `DomainAssert::expectViolation` with the expected exception + class, identifier, and optional violation parameters as `['name' => value]` (omit the + parameters argument when empty). Use `expectException` only for non-domain exceptions. - The closure passed to `DomainAssert::expectViolation` must contain only the single action that triggers the exception. All setup (creating objects, calling prior methods) belongs before the call. diff --git a/src/Common/Domain/Test/DomainAssert.php b/src/Common/Domain/Test/DomainAssert.php index 03dda7388..9910b3b6b 100644 --- a/src/Common/Domain/Test/DomainAssert.php +++ b/src/Common/Domain/Test/DomainAssert.php @@ -11,14 +11,14 @@ final class DomainAssert { /** - * @param array $expectedParameters * @param class-string $expectedException + * @param array $expectedParameters */ public static function expectViolation( Closure $action, + string $expectedException, string $expectedIdentifier, - array $expectedParameters = [], - string $expectedException = DomainException::class + array $expectedParameters = [] ): void { try { $action();