Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, mixed>` page data and `list<string>` 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:
Expand Down
25 changes: 25 additions & 0 deletions src/ResolvedPageObserver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace PHPForge\Inertia;

use Closure;

/**
* Forwards a resolved page payload and its shared-prop keys to an application callback.
*
* Framework adapters may implement their observer contracts by extending this class.
*/
readonly class ResolvedPageObserver
{
/**
* @param Closure(array<string, mixed>, list<string>): 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());
}
}
87 changes: 87 additions & 0 deletions tests/ResolvedPageObserverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace PHPForge\Inertia\Tests;

use PHPForge\Inertia\{Page, PageMetadata, ResolvedPageObserver};
use PHPUnit\Framework\TestCase;
use RuntimeException;

/**
* Verifies the framework-neutral resolved-page callback adapter.
*/
final class ResolvedPageObserverTest extends TestCase
{
public function testForwardsEmptySharedKeys(): void
{
$observed = null;
$observer = new ResolvedPageObserver(
static function (array $data, array $sharedKeys) use (&$observed): void {
$observed = $sharedKeys;
},
);

$observer->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', [], '/', ''));
}
}
Loading