= 1; default 1. * - `per_page` — integer 1..MAX_PER_PAGE; clamped to the max, * out-of-range below 1 falls back to the default. * * @package Affilun\Presentation\Admin\Page */ declare(strict_types=1); namespace Affilun\Presentation\Admin\Page; use Affilun\Infrastructure\Persistence\LogListQuery; use InvalidArgumentException; /** * Pure-function helper that converts the `$_GET` array of the §6 Logs * admin page into a normalized {@see LogListQuery}. * * See the file-level docblock for the per-axis parsing rules and the * graceful-fallback contract that catches construction errors from a * tampered or stale URL. */ final class LogsListQueryParser { /** * `$_GET` key carrying the PSR-3 level filter. */ public const KEY_LEVEL = 'level'; /** * `$_GET` key carrying the channel exact-match filter. */ public const KEY_CHANNEL = 'channel'; /** * `$_GET` key carrying the related-product-id filter. */ public const KEY_RELATED_PRODUCT_ID = 'related_product_id'; /** * `$_GET` key carrying the related-job-id filter. */ public const KEY_RELATED_JOB_ID = 'related_job_id'; /** * `$_GET` key carrying the 1-based page number. * * Mirrors WordPress's own `paged` query var so the admin * pagination links interoperate with core conventions. */ public const KEY_PAGE = 'paged'; /** * `$_GET` key carrying the records-per-page count. */ public const KEY_PER_PAGE = 'per_page'; /** * Convert `$_GET`-shaped input into a {@see LogListQuery}. * * Construction failures fall back to {@see LogListQuery::defaults()} * so a malformed URL cannot break the admin page rendering. * * @param array $rawGet `$_GET`-shaped input (already unslashed). */ public function parse( array $rawGet ): LogListQuery { $level = self::parseNonEmptyString( $rawGet, self::KEY_LEVEL ); if ( $level !== null && ! in_array( $level, LogListQuery::VALID_LEVELS, true ) ) { $level = null; } $channel = self::parseNonEmptyString( $rawGet, self::KEY_CHANNEL ); $relatedProductId = self::parsePositiveIntOrNull( $rawGet, self::KEY_RELATED_PRODUCT_ID ); $relatedJobId = self::parsePositiveIntOrNull( $rawGet, self::KEY_RELATED_JOB_ID ); $page = self::parsePositiveInt( $rawGet, self::KEY_PAGE, 1 ); $perPage = self::parsePositiveInt( $rawGet, self::KEY_PER_PAGE, LogListQuery::DEFAULT_PER_PAGE ); if ( $perPage > LogListQuery::MAX_PER_PAGE ) { $perPage = LogListQuery::MAX_PER_PAGE; } try { return new LogListQuery( $level, $channel, $relatedProductId, $relatedJobId, $page, $perPage ); } catch ( InvalidArgumentException $e ) { unset( $e ); return LogListQuery::defaults(); } } /** * Read a positive-integer axis from `$rawGet`, falling back to * `$fallback` when the key is missing, non-scalar, non-numeric, * or below 1. * * @param array $rawGet $_GET-shaped input. * @param string $key Axis key. * @param int $fallback Default value when absent. */ private static function parsePositiveInt( array $rawGet, string $key, int $fallback ): int { if ( ! array_key_exists( $key, $rawGet ) ) { return $fallback; } $raw = $rawGet[ $key ]; if ( ! is_scalar( $raw ) ) { return $fallback; } $asString = trim( (string) $raw ); if ( $asString === '' || preg_match( '/^-?\d+$/', $asString ) !== 1 ) { return $fallback; } $value = (int) $asString; if ( $value < 1 ) { return $fallback; } return $value; } /** * Read an optional positive-integer axis from `$rawGet`, returning * null when the key is missing, non-scalar, non-numeric, or below 1. * * @param array $rawGet $_GET-shaped input. * @param string $key Axis key. */ private static function parsePositiveIntOrNull( array $rawGet, string $key ): ?int { if ( ! array_key_exists( $key, $rawGet ) ) { return null; } $raw = $rawGet[ $key ]; if ( ! is_scalar( $raw ) ) { return null; } $asString = trim( (string) $raw ); if ( $asString === '' || preg_match( '/^-?\d+$/', $asString ) !== 1 ) { return null; } $value = (int) $asString; if ( $value < 1 ) { return null; } return $value; } /** * Read a non-empty (post-trim) string axis from `$rawGet`, * returning null when the key is missing, non-scalar, or * whitespace-only. * * Routes through `sanitize_text_field()` when available so a * malicious URL parameter cannot smuggle markup into the rendered * filter form. * * @param array $rawGet $_GET-shaped input. * @param string $key Axis key. */ private static function parseNonEmptyString( array $rawGet, string $key ): ?string { if ( ! array_key_exists( $key, $rawGet ) ) { return null; } $raw = $rawGet[ $key ]; if ( ! is_scalar( $raw ) ) { return null; } $asString = (string) $raw; if ( function_exists( 'sanitize_text_field' ) ) { $asString = (string) sanitize_text_field( $asString ); } $trimmed = trim( $asString ); if ( $trimmed === '' ) { return null; } return $trimmed; } }