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.5.1 Under development

- docs: describe the automatic debugger wiring `yii2-extensions/debug` and `yii3/debug` provide for this package.

## 0.5.0 September 18, 2026

- build!: require `php-forge/debug` `^0.3`.
Expand Down
82 changes: 51 additions & 31 deletions docs/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,55 +23,75 @@ The package ships one collector and one panel, `PHPForge\Inertia\Debug\InertiaCo
`PHPForge\Inertia\Debug\InertiaPanel`; the host never reimplements collection or presentation. What differs between
frameworks is only how the dispatcher reaches the `PHPForge\Inertia\Protocol` service.

### Yii3, one flag and no application code
### Yii3, from `yii3/debug` 0.1.0

`yii3/debug` registers the collector and panel behind a flag and attaches the collector as a listener; the container
autowires `Psr\EventDispatcher\EventDispatcherInterface` into `Protocol`, so the application adds nothing else.
`yii3/debug` registers the collector and the panel in its packaged parameters as soon as this package is installed,
routes `ProtocolResultCreated` to that collector from its packaged `events-web` group, and builds the collector with
the host `CapturePolicy` redaction callbacks. The container autowires `Psr\EventDispatcher\EventDispatcherInterface`
into `Protocol`, so the application adds nothing. Disable both packaged entries with `enabled => false` when the
capture is unwanted.

```php
return [
'yii3/debug' => [
'extensions' => [
'inertia' => true,
],
],
];
```
### Yii2, from `yii2-extensions/debug` 0.2.0

Enabling the flag without `php-forge/inertia` installed fails with an explicit container error. The collector receives
the host `CapturePolicy` redaction callbacks automatically.
Yii2 has no framework-native PSR-14 dispatcher, and its DI container does not autowire optional constructor arguments.
`Protocol` emits exactly one event type, so `InertiaCollector` is its own single-listener dispatcher.
`yii2-extensions/debug` registers the collector and the panel once this package is installed, builds the collector
with the module capture policy, and hands it to the `inertia` component through
`yii\inertia\Manager::$eventDispatcher` before the request runs. A component that configures `eventDispatcher` or
`protocol` itself keeps what it configures; disable the packaged entries with `enabled => false` when the capture is
unwanted.

### Yii2, one registration
If the application already owns a real PSR-14 dispatcher, register the collector as a listener on it and inject that
dispatcher instead; never replace a populated dispatcher with an empty one. Inject the dispatcher into the **actual**
`Protocol` service, not into a duplicate diagnostic-only one. Omitting it is safe: the protocol behaves normally and
the panel simply stays empty.

Yii2 has no framework-native PSR-14 dispatcher, and its DI container does not autowire optional constructor arguments.
`Protocol` emits exactly one event type, so `InertiaCollector` is its own single-listener dispatcher and the
application writes no PSR-14 code.
### Earlier debugger releases

Earlier releases package no Inertia entry: they wrap the portable objects in their generic adapters and group them
under Extensions, and the `inertia` ID is free for the application to register. Without that registration the panel
stays empty, and without the redaction callbacks `InertiaCollector` keeps the captured values unchanged.

Yii3, in the application `params`, `events-web`, and `di-web` groups:

Inside the existing `YII_DEBUG` configuration guard:
```php
use PHPForge\Debug\Capture\CapturePolicy;
use PHPForge\Inertia\Debug\{InertiaCollector, InertiaPanel};
use PHPForge\Inertia\Event\ProtocolResultCreated;

// params.php
'yii3/debug' => [
'collectors' => ['inertia' => InertiaCollector::class],
'panels' => ['inertia' => InertiaPanel::class],
],

// events-web.php
ProtocolResultCreated::class => [InertiaCollector::class],

// di-web.php
InertiaCollector::class => static fn(CapturePolicy $policy): InertiaCollector => new InertiaCollector(
$policy->redact(...),
$policy->redactUrl(...),
),
```

Yii2, inside the `YII_DEBUG` configuration guard, keeping the component ID the application already uses:

```php
use PHPForge\Inertia\Debug\{InertiaCollector, InertiaPanel};
use PHPForge\Inertia\Protocol;

$inertiaCollector = new InertiaCollector();

// Keep the component ID already used by the application.
$config['components']['inertia']['protocol'] = Protocol::create(eventDispatcher: $inertiaCollector);
$config['modules']['debug']['collectors']['inertia'] = $inertiaCollector;
$config['modules']['debug']['panels']['inertia'] = new InertiaPanel();
```

If the application already owns a real PSR-14 dispatcher, register the collector as a listener on it and inject that
dispatcher instead; never replace a populated dispatcher with an empty one.

`yii2-extensions/debug` no longer ships an Inertia collector or panel, so the `inertia` ID is free: the module wraps
the portable objects in its generic adapters and groups them under Extensions. Pass `$policy->redact(...)` and
`$policy->redactUrl(...)` to `InertiaCollector` to apply the host redaction rules; the default keeps captured values
unchanged. Retain the existing module bootstrap, routing, access rules, and asset configuration. The public
`yii\inertia\Manager::$protocol` property accepts the instrumented core service.

Inject the dispatcher into the **actual** Protocol service, not into a duplicate diagnostic-only one. Omitting it is
safe: the protocol behaves normally and the panel simply stays empty.
Pass the host `PHPForge\Debug\Capture\CapturePolicy` closures, `$policy->redact(...)` and `$policy->redactUrl(...)`,
to the `InertiaCollector` constructor to apply the module redaction rules. Retain the existing module bootstrap,
routing, access rules, and asset configuration; the public `yii\inertia\Manager::$protocol` property accepts the
instrumented core service.

## Lifecycle, privacy, and errors

Expand Down
2 changes: 1 addition & 1 deletion src/Debug/InertiaCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public function __invoke(ProtocolResultCreated $event): void
/**
* Returns the sanitized diagnostics for the protocol result observed in the active cycle.
*
* @throws InvalidArgumentException If the observed result is not a supported Inertia protocol result.
* @throws InvalidArgumentException if the observed result is not a supported Inertia protocol result.
*
* @return array<string, mixed>|null Sanitized diagnostics, or `null` when no protocol result was observed.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Debug/InertiaPanel.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ final class InertiaPanel extends Panel
*
* @param array<string, mixed> $data Decoded, already-sanitized Inertia diagnostics.
*
* @throws InvalidArgumentException If the capture declares an unsupported result type or malformed values.
* @throws InvalidArgumentException if the capture declares an unsupported result type or malformed values.
*
* @return PanelView Summary, overview, props table, and raw payload disclosure.
*/
Expand Down
4 changes: 4 additions & 0 deletions src/Event/ProtocolResultCreated.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@
*/
final readonly class ProtocolResultCreated
{
/**
* @param RequestContext $request Validated request context the protocol operation was resolved for.
* @param ProtocolResult $result Protocol result produced for the request.
*/
public function __construct(public RequestContext $request, public ProtocolResult $result) {}
}
2 changes: 1 addition & 1 deletion src/PageInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ private static function validateErrors(array $errors): void
* @param mixed $key The candidate key; must be a non-empty string without control characters or empty segments.
* @param string $label Human-readable label used in the error message (for example, `'prop'` or `'shared prop'`).
*
* @throws InvalidPageInputException When `$key` fails validation.
* @throws InvalidPageInputException when `$key` fails validation.
*/
private static function validateKey(mixed $key, string $label): void
{
Expand Down
2 changes: 1 addition & 1 deletion src/Prop/DeferredProp.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public function value(): Closure|PropValue
*
* @param string $group The group name to validate.
*
* @throws InvalidPropException If the group name is invalid.
* @throws InvalidPropException if the group name is invalid.
*/
private static function validateGroup(string $group): void
{
Expand Down
2 changes: 1 addition & 1 deletion src/Prop/MergeProp.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ private static function unique(array $items): array
*
* @param string $path The path string to validate.
*
* @throws InvalidPropException If the path is invalid.
* @throws InvalidPropException if the path is invalid.
*/
private static function validatePath(string $path): void
{
Expand Down
2 changes: 1 addition & 1 deletion src/Prop/OnceProp.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public function value(): Closure|PropValue
/**
* Validates the cache key format.
*
* @throws InvalidPropException When `$key` is empty or contains control characters or commas.
* @throws InvalidPropException when `$key` is empty or contains control characters or commas.
*/
private static function validateKey(string $key): void
{
Expand Down
2 changes: 1 addition & 1 deletion src/Prop/ScrollMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* @param int|string|null $nextPage The next page cursor, or `null` if there is no next page.
* @param int|string|null $currentPage The current page cursor, or `null` if there is no current page.
*
* @throws InvalidPropException When `$pageName` is empty or contains control characters.
* @throws InvalidPropException when `$pageName` is empty or contains control characters.
*/
public function __construct(
public string $pageName,
Expand Down
6 changes: 3 additions & 3 deletions src/Protocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public static function create(
* @param RequestContext $request Validated request context from the framework adapter.
* @param string $absoluteUrl Absolute HTTP/HTTPS URL of the target location.
*
* @throws InvalidArgumentException When `$absoluteUrl` is not a valid absolute HTTP/HTTPS URL.
* @throws InvalidArgumentException when `$absoluteUrl` is not a valid absolute HTTP/HTTPS URL.
*
* @return LocationResult|RedirectResult Returns a {@see LocationResult} for Inertia requests, or a
* {@see RedirectResult} for non-Inertia requests.
Expand Down Expand Up @@ -144,7 +144,7 @@ public function page(
* @param string $url Absolute or root-relative redirect target URL.
* @param int $statusCode HTTP redirect status code (`301`, `302`, `303`, `307`, or `308`).
*
* @throws InvalidArgumentException When `$url` is invalid or `$statusCode` is not an allowed redirect status.
* @throws InvalidArgumentException when `$url` is invalid or `$statusCode` is not an allowed redirect status.
*/
public function redirect(
RequestContext $request,
Expand Down Expand Up @@ -184,7 +184,7 @@ public function redirect(
* @param RequestContext $request Validated request context from the framework adapter.
* @param string $url Absolute or root-relative redirect target URL.
*
* @throws InvalidArgumentException When the request's absolute URL cannot be parsed into scheme and host.
* @throws InvalidArgumentException when the request's absolute URL cannot be parsed into scheme and host.
*
* @return string Returns the absolute redirect URL.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Resolution/PropDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function deferred(): DeferredProp|null
*
* @param mixed $value Raw prop value, which may be a {@see PropValue} wrapper chain or a plain value.
*
* @throws InvalidPropException When the wrapper chain exceeds 64 levels.
* @throws InvalidPropException when the wrapper chain exceeds 64 levels.
*
* @return PropDefinition Flattened definition of the prop value and its semantics.
*/
Expand Down
4 changes: 2 additions & 2 deletions src/Resolution/PropResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ private function collectOnceMetadata(OnceProp|null $once, string $path): void
* @param string $path Dot-notation path of the prop.
* @param mixed $value The resolved value of the prop.
*
* @throws InvalidPropException When scroll metadata is not a {@see ScrollMetadata}.
* @throws InvalidPropException when scroll metadata is not a {@see ScrollMetadata}.
*/
private function collectResolvedMetadata(PropDefinition $definition, string $path, mixed $value): void
{
Expand Down Expand Up @@ -543,7 +543,7 @@ private function pathMatchesPartialRequest(string $path): bool
* @param string $path Dot-notation path used for error messages and metadata registration.
* @param bool $parentWasResolved Whether the parent array was itself resolved, bypassing partial filters.
*
* @throws PropResolutionException When a closure throws and the prop is not deferred.
* @throws PropResolutionException when a closure throws and the prop is not deferred.
*
* @return ResolvedProp The resolved prop value and inclusion status.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/ResolvedPageObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
*/
public function __construct(private Closure $callback) {}

/**
* Forwards the page payload and its shared-prop keys to the configured callback.
*
* @param Page $page Resolved page whose payload and shared-prop keys are forwarded.
*/
public function observe(Page $page): void
{
($this->callback)($page->toArray(), $page->sharedProps());
Expand Down
13 changes: 13 additions & 0 deletions src/Support/DotArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@
*/
final class DotArray
{
/**
* Prevents direct instantiation.
*/
private function __construct() {}

/**
* Expands dot-notated keys in `$values` into a nested associative array.
*
* @param array<string, mixed> $values The flat, dot-notated key-value map to expand.
*
* @throws InvalidPageInputException when a path segment already holds a non-array value.
*
* @return array<string, mixed> The expanded nested associative array.
*/
public static function expand(array $values): array
Expand All @@ -42,6 +47,10 @@ public static function expand(array $values): array
*
* @param array<string, mixed> $values The current result array to write into.
* @param non-empty-list<string> $segments Remaining path segments to traverse.
* @param mixed $value The value to write at the resolved path.
* @param string $path The original dot-notated key, reported in exception messages.
*
* @throws InvalidPageInputException when a segment along the path already holds a non-array value.
*
* @return array<string, mixed> The updated result array with the value written at the given dot-notated path.
*/
Expand Down Expand Up @@ -73,6 +82,10 @@ private static function assign(array $values, array $segments, mixed $value, str
*
* @param array<array-key, mixed> $values The current nested array level.
* @param non-empty-list<string> $segments Remaining path segments below the root.
* @param mixed $value The value to write at the final segment.
* @param string $path The original dot-notated key, reported in exception messages.
*
* @throws InvalidPageInputException when an intermediate segment already holds a non-array value.
*
* @return array<array-key, mixed> The updated nested array with the value inserted at the final segment.
*/
Expand Down
7 changes: 5 additions & 2 deletions src/Support/JsonValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ final class JsonValue
*/
private const int MAX_DEPTH = 512;

/**
* Prevents direct instantiation.
*/
private function __construct() {}

/**
Expand All @@ -36,10 +39,10 @@ private function __construct() {}
* strings, and types that have no JSON representation.
*
* @param mixed $value The prop value to normalize.
* @param string $path Dot-notation path used in exception messages to identify the failing prop.
* @param string $path Dot-notation path used in exception messages to identify the failing prop.
* @param int $depth Current recursion depth; throws when it exceeds 512.
*
* @throws InvalidPropException When the value cannot be represented as JSON.
* @throws InvalidPropException when the value cannot be represented as JSON.
*
* @return mixed Normalized JSON-compatible value.
*/
Expand Down
Loading