-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add shared UI parity contracts, Asset composition, and cross-adapter acceptance documentation. #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add shared UI parity contracts, Asset composition, and cross-adapter acceptance documentation. #10
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace PHPForge\Debug\Data; | ||
|
|
||
| use PHPForge\Debug\Helper\Dump; | ||
|
|
||
| use function array_filter; | ||
| use function array_key_exists; | ||
| use function array_values; | ||
| use function get_object_vars; | ||
| use function is_float; | ||
| use function is_int; | ||
| use function is_numeric; | ||
| use function is_object; | ||
| use function is_scalar; | ||
| use function is_string; | ||
| use function mb_stripos; | ||
| use function mb_strtolower; | ||
| use function preg_match; | ||
|
|
||
| /** | ||
| * Filters typed rows against exact, partial, and leading numeric comparison conditions. | ||
| */ | ||
| final class FilterEngine | ||
| { | ||
| private const string CHARSET = 'UTF-8'; | ||
|
|
||
| /** | ||
| * @var list< | ||
| * array{attribute: string, operator: '>'|'<'|'>=', value: float} | ||
| * |array{attribute: string, operator: 'contains'|'same', value: string} | ||
| * > | ||
| */ | ||
| private array $conditions = []; | ||
|
|
||
| /** | ||
| * Registers an exact, partial, or leading numeric comparison condition. | ||
| * | ||
| * Empty and non-scalar raw values register nothing, so unfiltered attributes can be passed through unconditionally. | ||
| * | ||
| * @param string $attribute Row attribute (public property or array key) the condition applies to. | ||
| * @param mixed $rawValue Raw filter value as read from the request; scalars are compared as strings. | ||
| * @param bool $partial Whether a non-numeric value matches as a case-insensitive substring instead of whole-value. | ||
| */ | ||
| public function addCondition(string $attribute, mixed $rawValue, bool $partial = false): void | ||
| { | ||
| $value = is_scalar($rawValue) ? (string) $rawValue : ''; | ||
|
|
||
| if ($value === '') { | ||
| return; | ||
| } | ||
|
|
||
| if (preg_match('/^\s*([<>])\s*(-?(?:\d+(?:\.\d+)?|\.\d+))\s*$/D', $value, $matches) === 1) { | ||
|
Check warning on line 55 in src/Data/FilterEngine.php
|
||
| $this->conditions[] = [ | ||
| 'attribute' => $attribute, | ||
| 'operator' => $matches[1], | ||
| 'value' => (float) $matches[2], | ||
| ]; | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $this->conditions[] = [ | ||
| 'attribute' => $attribute, | ||
| 'operator' => $partial ? 'contains' : 'same', | ||
| 'value' => $value, | ||
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * Registers a numeric greater-than-or-equal condition. | ||
| * | ||
| * @param string $attribute Row attribute (public property or array key) the condition applies to. | ||
| * @param float $value Inclusive lower bound the attribute value must reach. | ||
| */ | ||
| public function addMinimumCondition(string $attribute, float $value): void | ||
| { | ||
| $this->conditions[] = ['attribute' => $attribute, 'operator' => '>=', 'value' => $value]; | ||
| } | ||
|
|
||
| /** | ||
| * Applies all registered conditions and resets the engine for the next run. | ||
| * | ||
| * @template TRow of array<string, mixed>|object | ||
| * | ||
| * @param array<int, TRow> $rows Rows to filter; typed row objects or string-keyed arrays. | ||
| * | ||
| * @return list<TRow> Rows matching every registered condition, reindexed. | ||
| */ | ||
| public function filter(array $rows): array | ||
| { | ||
| $filtered = array_values(array_filter($rows, $this->matches(...))); | ||
|
|
||
| $this->conditions = []; | ||
|
|
||
| return $filtered; | ||
| } | ||
|
|
||
| /** | ||
| * @param array<string, mixed>|object $row Typed row object or string-keyed array. | ||
| */ | ||
| private function matches(array|object $row): bool | ||
| { | ||
| foreach ($this->conditions as $condition) { | ||
| $attribute = $condition['attribute']; | ||
|
|
||
| $values = is_object($row) ? get_object_vars($row) : $row; | ||
|
|
||
| if (!array_key_exists($attribute, $values)) { | ||
| return false; | ||
| } | ||
|
|
||
| $candidate = $values[$attribute]; | ||
| $operator = $condition['operator']; | ||
|
|
||
| if ($operator === '>' || $operator === '<' || $operator === '>=') { | ||
| $expected = $condition['value']; | ||
|
|
||
| if ( | ||
| !is_float($expected) | ||
| || !is_int($candidate) && !is_float($candidate) && !is_string($candidate) | ||
| || !is_numeric($candidate) | ||
| ) { | ||
| return false; | ||
| } | ||
|
|
||
| $candidate = (float) $candidate; | ||
|
Check warning on line 129 in src/Data/FilterEngine.php
|
||
|
|
||
| $matched = match ($operator) { | ||
|
Check warning on line 131 in src/Data/FilterEngine.php
|
||
| '>' => $candidate > $expected, | ||
|
Check warning on line 132 in src/Data/FilterEngine.php
|
||
| '<' => $candidate < $expected, | ||
|
Check warning on line 133 in src/Data/FilterEngine.php
|
||
| default => $candidate >= $expected, | ||
| }; | ||
|
|
||
| if (!$matched) { | ||
| return false; | ||
| } | ||
|
|
||
| continue; | ||
|
Check warning on line 141 in src/Data/FilterEngine.php
|
||
| } | ||
|
|
||
| $candidate = is_scalar($candidate) | ||
| ? (string) $candidate | ||
| : Dump::export($candidate); | ||
|
|
||
| $expected = $condition['value']; | ||
|
|
||
| if (!is_string($expected)) { | ||
| return false; | ||
| } | ||
|
|
||
| if ($operator === 'contains') { | ||
| if (mb_stripos($candidate, $expected, 0, self::CHARSET) === false) { | ||
|
Check warning on line 155 in src/Data/FilterEngine.php
|
||
| return false; | ||
| } | ||
|
|
||
| continue; | ||
|
Check warning on line 159 in src/Data/FilterEngine.php
|
||
| } | ||
|
|
||
| if ( | ||
| mb_strtolower($candidate, self::CHARSET) | ||
| !== mb_strtolower($expected, self::CHARSET) | ||
| ) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace PHPForge\Debug\Data; | ||
|
|
||
| /** | ||
| * Freezes the `Prefix[attribute]` query-parameter vocabulary shared by every debug-panel filter form. | ||
| */ | ||
| final class FilterPrefix | ||
| { | ||
| public const string ASSET = 'Asset'; | ||
|
|
||
| public const string DB = 'Db'; | ||
|
|
||
| public const string DEBUG = 'Debug'; | ||
|
|
||
| public const string EVENT = 'Event'; | ||
|
|
||
| public const string LOG = 'Log'; | ||
|
|
||
| public const string MAIL = 'Mail'; | ||
|
|
||
| public const string PROFILE = 'Profile'; | ||
|
|
||
| public const string QUEUE = 'Queue'; | ||
|
|
||
| public const string ROUTER = 'Router'; | ||
|
|
||
| public const string TIMELINE = 'Timeline'; | ||
|
|
||
| public const string USER = 'User'; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace PHPForge\Debug\Data; | ||
|
|
||
| use UIAwesome\Html\Form\{Option, Select}; | ||
| use UIAwesome\Html\Phrasing\{Label, Span}; | ||
|
|
||
| use function is_numeric; | ||
| use function min; | ||
| use function strcasecmp; | ||
|
|
||
| /** | ||
| * Resolves the `per-page` grid page size and renders the shared page-size selector. | ||
| */ | ||
| final class PageSize | ||
| { | ||
| /** | ||
| * Default page size applied when no `per-page` parameter is supplied or the value is invalid. | ||
| */ | ||
| public const int DEFAULT = 50; | ||
|
|
||
| /** | ||
| * Hard cap on the number of rows per page. | ||
| */ | ||
| public const int MAX = 1000; | ||
|
|
||
| /** | ||
| * Selector options in display order; the literal `all` disables pagination. | ||
| */ | ||
| public const array OPTIONS = ['10', '25', '50', '100', 'all']; | ||
|
|
||
| /** | ||
| * Returns the `per-page` selector state, canonicalizing `all` and falling back to the default. | ||
| * | ||
| * @param string|null $raw Raw `per-page` query-parameter value, or `null` when absent. | ||
| * @param int $default Page size used when no value is supplied. | ||
| */ | ||
| public static function current(string|null $raw, int $default = self::DEFAULT): string | ||
| { | ||
| if ($raw === null) { | ||
| return (string) $default; | ||
| } | ||
|
|
||
| return strcasecmp($raw, 'all') === 0 ? 'all' : $raw; | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the raw `per-page` value into an effective page size. | ||
| * | ||
| * @param string|null $raw Raw `per-page` query-parameter value, or `null` when absent. | ||
| * @param positive-int $default Page size used when no value is supplied or the value is invalid. | ||
| * | ||
| * @return positive-int|null Effective page size capped at {@see MAX}, or `null` when `all` disables pagination. | ||
| */ | ||
| public static function resolve(string|null $raw, int $default = self::DEFAULT): int|null | ||
| { | ||
| if ($raw !== null && strcasecmp($raw, 'all') === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| $size = $raw !== null && is_numeric($raw) ? (int) $raw : $default; | ||
|
|
||
| if ($size <= 0) { | ||
| $size = $default; | ||
| } | ||
|
|
||
| return min($size, self::MAX); | ||
| } | ||
|
|
||
| /** | ||
| * Renders the inline page-size selector shown in the grid summary header. | ||
| * | ||
| * Usage example: | ||
| * ```php | ||
| * $html = \PHPForge\Debug\Data\PageSize::selectorHtml('50'); | ||
| * ``` | ||
| * | ||
| * @param string $current Currently selected raw value (one of {@see OPTIONS} for a highlighted option). | ||
| */ | ||
| public static function selectorHtml(string $current): string | ||
| { | ||
| $select = Select::tag() | ||
| ->addDataAttribute('yii-debug-pagesize', true) | ||
| ->class('yii-debug-grid-pagesize-select') | ||
| ->name('per-page'); | ||
|
|
||
| foreach (self::OPTIONS as $row) { | ||
| $select = $select->option( | ||
| Option::tag() | ||
| ->value($row) | ||
| ->content($row === 'all' ? 'All' : $row) | ||
| ->selected($row === $current), | ||
| ); | ||
| } | ||
|
|
||
| return Label::tag() | ||
| ->class('yii-debug-grid-pagesize') | ||
| ->html( | ||
| Span::tag() | ||
| ->class('yii-debug-grid-pagesize-label') | ||
| ->content('Rows'), | ||
| $select, | ||
| ) | ||
| ->render(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.