diff --git a/CHANGELOG.md b/CHANGELOG.md index c159321..1cf8283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 0.2.1 Under development +- feat: add a portable resolved-page observer that forwards page payloads and shared-prop keys to callbacks. + ## 0.2.0 August 25, 2026 - docs: add `Next steps` section with links to installation, usage, configuration, and testing guides. diff --git a/README.md b/README.md index 2c40417..d0f4622 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,20 @@ $result = Protocol::create()->page($request, $input); `Protocol` does not return a framework response. An adapter inspects the result, applies `statusCode()` and `headers()`, serializes `page()` for an Inertia visit, or embeds the page JSON in the root HTML document for an initial visit. +## Resolved-page observation + +`PHPForge\Inertia\ResolvedPageObserver` forwards the resolved page payload and shared-prop keys to a callback. +Observer failures propagate to the caller; the observer does not mutate pages or hide callback failures. + +```php +$observer = new \PHPForge\Inertia\ResolvedPageObserver($collector->observe(...)); + +$observer->observe($page); +``` + +The callback receives `array` page data and `list` shared-prop keys, without depending on a debugger +or framework. The class is extensible so adapters can preserve their own observer interfaces without duplicating logic. + ## Adapter boundary The core owns protocol decisions and page shaping. A framework adapter remains responsible for: diff --git a/src/ResolvedPageObserver.php b/src/ResolvedPageObserver.php new file mode 100644 index 0000000..efac265 --- /dev/null +++ b/src/ResolvedPageObserver.php @@ -0,0 +1,25 @@ +, list): void $callback Receives the unmodified page diagnostics. + */ + public function __construct(private Closure $callback) {} + + public function observe(Page $page): void + { + ($this->callback)($page->toArray(), $page->sharedProps()); + } +} diff --git a/tests/ResolvedPageObserverTest.php b/tests/ResolvedPageObserverTest.php new file mode 100644 index 0000000..cca9cb9 --- /dev/null +++ b/tests/ResolvedPageObserverTest.php @@ -0,0 +1,87 @@ +observe(new Page('Home', [], '/', '')); + + self::assertSame( + [], + $observed, + 'A page without shared metadata must forward an empty key list.', + ); + } + + public function testForwardsPagePayloadAndSharedKeysWithoutMutation(): void + { + $page = ( + new Page( + 'Dashboard', + ['user' => ['name' => 'Ada'], 'errors' => ['email' => 'Invalid']], + '/dashboard', + 'v1' + ) + )->withMetadata((new PageMetadata())->withSharedProps(['user'])); + + $payload = $page->toArray(); + + $observed = []; + + $observer = new ResolvedPageObserver( + static function (array $data, array $sharedKeys) use (&$observed): void { + $observed[] = [$data, $sharedKeys]; + }, + ); + + $observer->observe($page); + $observer->observe($page); + + self::assertEquals( + [[$payload, ['user']], [$payload, ['user']]], + $observed, + 'Each observation must forward the original payload and shared keys exactly once.', + ); + self::assertEquals( + $payload, + $page->toArray(), + 'Observation must not change the page.', + ); + } + + public function testPropagatesCallbackFailure(): void + { + $observer = new ResolvedPageObserver( + static function (): never { + throw new RuntimeException( + 'Observer failed.', + ); + }, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage( + 'Observer failed.', + ); + + $observer->observe(new Page('Home', [], '/', '')); + } +}