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
22 changes: 22 additions & 0 deletions src/Exception/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,28 @@ enum Message: string
*/
case PANEL_TITLE_EMPTY = 'Debug panel title must not be empty.';

/**
* Indicates that a registration entry declares a non-boolean `enabled` option.
*
* Format: "Debug registration '%s' option 'enabled' must be a boolean."
*/
case REGISTRATION_ENABLED_INVALID = 'Debug registration \'%s\' option \'enabled\' must be a boolean.';

/**
* Indicates that a registration entry is neither a class string nor an array declaring a `class` string.
*
* Format: "Debug registration '%s' must be a class string or an array declaring a 'class' string."
*/
case REGISTRATION_ENTRY_INVALID
= 'Debug registration \'%s\' must be a class string or an array declaring a \'class\' string.';

/**
* Indicates that a registration key differs from the ID the registered collector or panel declares.
*
* Format: "Debug %s registered as '%s' must match its ID '%s'."
*/
case REGISTRATION_ID_MISMATCH = 'Debug %s registered as \'%s\' must match its ID \'%s\'.';

/**
* Indicates that a route definition field does not satisfy its expected shape.
*
Expand Down
99 changes: 99 additions & 0 deletions src/Registration/EntryParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\Registration;

use InvalidArgumentException;
use PHPForge\Debug\Exception\Message;

use function is_array;
use function is_bool;
use function is_string;

/**
* Reads the collector and panel entries an application declares in its debug configuration.
*
* The three checks are separate calls, so each host keeps its own validation order and wraps every
* {@see InvalidArgumentException} in the message its users already know.
*/
final class EntryParser
{
/**
* Rejects a string registration key that differs from the ID the registered object declares.
*
* An integer key comes from a list entry, which names no ID and is accepted as is.
*
* @param int|string $key Key the configuration registers the entry under.
* @param string $id ID the registered collector or panel declares.
* @param string $kind Entry kind named in the failure, such as `'collector'` or `'panel'`.
*
* @throws InvalidArgumentException when a string key differs from the declared ID.
*/
public static function assertKeyMatchesId(int|string $key, string $id, string $kind): void
{
if (is_string($key) && $key !== $id) {
throw new InvalidArgumentException(
Message::REGISTRATION_ID_MISMATCH->getMessage($kind, $key, $id),
);
}
}

/**
* Returns the effective `enabled` flag of an entry.
*
* A `null` value counts as absent, as it does for {@see PanelOverride::fromArray()}.
*
* @param array<array-key, mixed> $options Entry options, or a whole configuration array declaring `enabled`.
* @param string $id Configuration ID naming the entry in a failure.
*
* @throws InvalidArgumentException when `enabled` is neither a `bool` nor `null`.
*
* @return bool Declared flag, or `true` when the entry omits it.
*/
public static function enabled(array $options, string $id): bool
{
$enabled = $options['enabled'] ?? true;

if (is_bool($enabled) === false) {
throw new InvalidArgumentException(
Message::REGISTRATION_ENABLED_INVALID->getMessage($id),
);
}

return $enabled;
}

/**
* Splits one entry into the class to resolve and the options declared beside it.
*
* The class is not checked for existence, so a disabled entry naming an uninstalled optional package never
* reaches the autoloader.
*
* @param mixed $entry Class string, or an array declaring a `class` string plus options.
* @param string $id Configuration ID naming the entry in a failure.
*
* @throws InvalidArgumentException when the entry declares no class string.
*
* @return ParsedEntry Class and remaining options, `enabled` included.
*/
public static function parse(mixed $entry, string $id): ParsedEntry
{
if (is_string($entry)) {
return new ParsedEntry($entry);
}

if (is_array($entry) === false || is_string($entry['class'] ?? null) === false) {
throw new InvalidArgumentException(
Message::REGISTRATION_ENTRY_INVALID->getMessage($id),
);
}

/** @var string $class */
$class = $entry['class'];

unset($entry['class']);

return new ParsedEntry($class, $entry);
}
}
21 changes: 21 additions & 0 deletions src/Registration/ParsedEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\Registration;

/**
* Represents one collector or panel entry of the application configuration, split into its class and its options.
*
* The options keep `enabled` next to every other key, so each host validates them with its own rules: a collector
* reads the flag through {@see EntryParser::enabled()}, and a panel hands the whole map to
* {@see PanelOverride::fromArray()}.
*/
final readonly class ParsedEntry
{
/**
* @param string $class Class name or container identifier the host resolves.
* @param array<array-key, mixed> $options Keys the entry declares beside `class`, in declaration order.
*/
public function __construct(public string $class, public array $options = []) {}
}
5 changes: 3 additions & 2 deletions src/Storage/ExceptionSnapshot.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Throwable;

use function array_map;
use function array_replace;
use function is_int;
use function is_string;

Expand Down Expand Up @@ -256,7 +257,7 @@ public function getPrevious(): self|null
public function getTrace(): array
{
return array_map(
static fn(array $frame): array => [...$frame, 'args' => $frame['args']->values()],
static fn(array $frame): array => array_replace($frame, ['args' => $frame['args']->values()]),
$this->trace,
);
}
Expand All @@ -275,7 +276,7 @@ public function jsonSerialize(): array
'file' => $this->file,
'line' => $this->line,
'trace' => array_map(
static fn(array $frame): array => [...$frame, 'args' => $frame['args']->jsonSerialize()],
static fn(array $frame): array => array_replace($frame, ['args' => $frame['args']->jsonSerialize()]),
$this->trace,
),
'toString' => $this->toString,
Expand Down
76 changes: 76 additions & 0 deletions src/View/Grid/FilterInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\View\Grid;

use UIAwesome\Html\Form\{InputText, Option, Select};

/**
* Builds the labelled filter controls shown in the filter row of a debug grid.
*/
final class FilterInput
{
/**
* Returns the filter dropdown offering the allowed values of the attribute, preceded by an empty option.
*
* @param string $prefix Filter group the attribute belongs to.
* @param string $attribute Attribute the control filters on.
* @param string $label Attribute name announced to assistive technology.
* @param array<string, string> $filters Active filters of the group, keyed by attribute.
* @param array<array-key, string> $options Selectable values mapped to the label shown for each of them.
* @param string $class Class list applied to the control.
*
* @return Select Rendered dropdown.
*/
public static function select(
string $prefix,
string $attribute,
string $label,
array $filters,
array $options,
string $class = 'yii-debug-select',
): Select {
$select = Select::tag()
->class($class)
->addAriaAttribute('label', "Filter by {$label}")
->name("{$prefix}[{$attribute}]")
->value($filters[$attribute] ?? '')
->option(Option::tag()->value('')->content(''));

foreach ($options as $value => $optionLabel) {
$select = $select->option(
Option::tag()
->value((string) $value)
->content($optionLabel),
);
}

return $select;
}

/**
* Returns the free-text filter box carrying the value the attribute is currently filtered by.
*
* @param string $prefix Filter group the attribute belongs to.
* @param string $attribute Attribute the control filters on.
* @param string $label Attribute name announced to assistive technology.
* @param array<string, string> $filters Active filters of the group, keyed by attribute.
* @param string $class Class list applied to the control.
*
* @return InputText Rendered text box.
*/
public static function text(
string $prefix,
string $attribute,
string $label,
array $filters,
string $class = 'yii-debug-input',
): InputText {
return InputText::tag()
->class($class)
->addAriaAttribute('label', "Filter by {$label}")
->name("{$prefix}[{$attribute}]")
->value($filters[$attribute] ?? '');
}
}
29 changes: 29 additions & 0 deletions src/View/Grid/PanelHeading.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\View\Grid;

use PHPForge\Debug\Panel\PanelTitle;
use UIAwesome\Html\Heading\H1;

/**
* Renders the visually hidden page heading that names a panel for assistive technology.
*/
final class PanelHeading
{
/**
* Returns the screen-reader-only heading carrying the panel title.
*
* @param PanelTitle $title Title announced as the page heading.
*
* @return string Rendered heading.
*/
public static function render(PanelTitle $title): string
{
return H1::tag()
->class('yii-debug-sr-only')
->content($title)
->render();
}
}
30 changes: 30 additions & 0 deletions src/View/Grid/SortHeader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace PHPForge\Debug\View\Grid;

use Stringable;

/**
* Represents the header cell of a sortable column, pairing the rendered sort link with the announced order.
*/
final readonly class SortHeader implements Stringable
{
/**
* @param string $link Rendered link requesting the order of the next click.
* @param array<string, string> $attributes Header cell attributes announcing the active order, empty while the
* rows are ordered by another attribute.
*/
public function __construct(public string $link, public array $attributes = []) {}

/**
* Returns the rendered sort link of the header cell.
*
* @return string Rendered link.
*/
public function __toString(): string
{
return $this->link;
}
}
Loading
Loading