= 1; default 1. * - `per_page` — integer 1..MAX_PER_PAGE; clamped to the max. * * @package Affilun\Presentation\Admin\Page */ declare(strict_types=1); namespace Affilun\Presentation\Admin\Page; use Affilun\Infrastructure\Persistence\SyncJobListQuery; use InvalidArgumentException; /** * Pure-function helper that converts the `$_GET` array of the §6 Sync * Jobs admin page into a normalized {@see SyncJobListQuery}. * * See the file-level docblock for the per-axis parsing rules, the * wire ⇔ DB status translation, and the graceful-fallback contract. */ final class SyncJobsListQueryParser { /** * `$_GET` key carrying the wire-vocabulary status filter. */ public const KEY_STATUS = 'status'; /** * `$_GET` key carrying the job-type filter. */ public const KEY_JOB_TYPE = 'job_type'; /** * `$_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'; /** * Wire-vocabulary status corresponding to DB `pending` per §6.0. */ private const WIRE_STATUS_QUEUED = 'queued'; /** * DB-vocabulary status that maps to wire `queued` per §6.0. */ private const DB_STATUS_PENDING = 'pending'; /** * Convert `$_GET`-shaped input into a {@see SyncJobListQuery}. * * Construction failures fall back to * {@see SyncJobListQuery::defaults()} so a malformed URL cannot * break the admin page rendering. * * @param array $rawGet `$_GET`-shaped input (already unslashed). */ public function parse( array $rawGet ): SyncJobListQuery { $status = self::parseNonEmptyString( $rawGet, self::KEY_STATUS ); if ( $status !== null ) { if ( $status === self::WIRE_STATUS_QUEUED ) { $status = self::DB_STATUS_PENDING; } if ( ! in_array( $status, SyncJobListQuery::VALID_STATUSES, true ) ) { $status = null; } } $jobType = self::parseNonEmptyString( $rawGet, self::KEY_JOB_TYPE ); if ( $jobType !== null && ! in_array( $jobType, SyncJobListQuery::VALID_JOB_TYPES, true ) ) { $jobType = null; } $page = self::parsePositiveInt( $rawGet, self::KEY_PAGE, 1 ); $perPage = self::parsePositiveInt( $rawGet, self::KEY_PER_PAGE, SyncJobListQuery::DEFAULT_PER_PAGE ); if ( $perPage > SyncJobListQuery::MAX_PER_PAGE ) { $perPage = SyncJobListQuery::MAX_PER_PAGE; } try { return new SyncJobListQuery( $status, $jobType, $page, $perPage ); } catch ( InvalidArgumentException $e ) { unset( $e ); return SyncJobListQuery::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 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; } }