Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParamValidator
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 4
156
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 normalizeSettings
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 validateValue
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 getValue
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare( strict_types = 1 );
3
4/**
5 * ParamValidator.php
6 *
7 * This file is part of the Codex design system, providing advanced validation
8 * for web request parameters. The `ParamValidator` class integrates with
9 * `ParamValidatorCallbacks` to ensure type safety, multi-value support, and constraint enforcement.
10 *
11 * This class is inspired by and borrows concepts from MediaWiki's `ParamValidator`.
12 * While it has been adapted to meet the requirements of the Codex system, it maintains
13 * a similar approach to parameter validation. Any direct code or conceptual borrowing
14 * has been done with due acknowledgment of MediaWiki's contributors.
15 *
16 * @category ParamValidator
17 * @package  Codex\ParamValidator
18 * @since    0.3.0
19 * @author   Doğu Abaris <abaris@null.net>
20 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
21 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
22 */
23
24namespace Wikimedia\Codex\ParamValidator;
25
26use DomainException;
27use UnexpectedValueException;
28use Wikimedia\Codex\Contract\ParamValidator\IParamValidatorCallbacks;
29
30/**
31 * ParamValidator provides advanced validation for web request parameters.
32 *
33 * The `ParamValidator` class validates and normalizes parameters fetched via
34 * the `IParamValidatorCallbacks` interface. It supports multi-value parameters,
35 * custom types, and a variety of constraints to standardize parameter validation.
36 *
37 * This implementation is inspired by MediaWiki's `ParamValidator`, with notable
38 * adaptations for Codex-specific needs. The approach and structure of this class
39 * owe much to the original design, and credit is extended to MediaWiki's contributors.
40 *
41 * @category ParamValidator
42 * @package  Codex\ParamValidator
43 * @since    0.3.0
44 * @author   Doğu Abaris <abaris@null.net>
45 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
46 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
47 * TODO: Once MediaWiki's ParamValidator is split into a standalone library,this class will no longer be necessary.
48 */
49class ParamValidator {
50    /**
51     * Provides access to web request parameters through callbacks.
52     */
53    private IParamValidatorCallbacks $callbacks;
54    /**
55     * Holds type definitions for parameter validation.
56     *
57     * @var array<string, callable|array>
58     */
59    private array $typeDefs;
60    /**
61     * Default limit for the number of values allowed for multi-value parameters.
62     */
63    private int $ismultiLimit1;
64    /**
65     * High limit for the number of values allowed for multi-value parameters when
66     * high limits are enabled.
67     */
68    private int $ismultiLimit2;
69    /**
70     * Default value for the parameter. If omitted, null is the default.
71     */
72    public const PARAM_DEFAULT = 'param-default';
73    /**
74     * Type of the parameter, defined as a string or an array of enumerated values.
75     */
76    public const PARAM_TYPE = 'param-type';
77    /**
78     * Indicates that the parameter is required.
79     */
80    public const PARAM_REQUIRED = 'param-required';
81    /**
82     * Indicates that the parameter accepts multiple values.
83     */
84    public const PARAM_ISMULTI = 'param-ismulti';
85
86    /**
87     * Constructor for ParamValidator.
88     *
89     * @since 0.3.0
90     *
91     * @param IParamValidatorCallbacks $callbacks Provides access to web request parameters.
92     * @param array<string, mixed> $options Configuration for the validator, including:
93     *                                      - `typeDefs`: Array of type definitions.
94     *                                      - `ismultiLimits`: Two integers for multi-value limits.
95     */
96    public function __construct( IParamValidatorCallbacks $callbacks, array $options = [] ) {
97        $this->callbacks = $callbacks;
98
99        $this->typeDefs = array_merge(
100            TypeDefinitions::getDefaultTypeDefs(),
101            $options['typeDefs'] ?? []
102        );
103
104        $this->ismultiLimit1 = $options['ismultiLimits'][0] ?? 50;
105        $this->ismultiLimit2 = $options['ismultiLimits'][1] ?? 500;
106    }
107
108    /**
109     * Normalizes parameter settings by ensuring they have consistent structure and defaults.
110     *
111     * @since 0.3.0
112     * @param mixed $settings Parameter settings or default value.
113     * @return array<string, mixed> Normalized settings.
114     */
115    public function normalizeSettings( $settings ): array {
116        if ( !is_array( $settings ) ) {
117            $settings = [ self::PARAM_DEFAULT => $settings ];
118        }
119
120        $settings[self::PARAM_TYPE] ??= gettype( $settings[self::PARAM_DEFAULT] ?? null );
121
122        return $settings;
123    }
124
125    /**
126     * Validates a parameter value against its settings.
127     *
128     * @since 0.3.0
129     *
130     * @param string $name Parameter name.
131     * @param mixed $value Parameter value.
132     * @param array<string, mixed> $settings Parameter settings.
133     * @param array<string, mixed> $options Additional options for validation.
134     * @return mixed The validated value or values.
135     * @throws DomainException If the type is unknown.
136     * @throws UnexpectedValueException If validation fails.
137     */
138    public function validateValue( string $name, $value, array $settings, array $options = [] ) {
139        $settings = $this->normalizeSettings( $settings );
140
141        if ( !isset( $this->typeDefs[$settings[self::PARAM_TYPE]] ) ) {
142            throw new DomainException( "Unknown type: {$settings[self::PARAM_TYPE]}" );
143        }
144
145        $typeDef = $this->typeDefs[$settings[self::PARAM_TYPE]];
146
147        if ( $settings[self::PARAM_ISMULTI] ?? false ) {
148            if ( !is_array( $value ) ) {
149                throw new UnexpectedValueException( "Multi-value parameter '$name' must be an array." );
150            }
151
152            $limit = ( $options['useHighLimits'] ?? false ) ? $this->ismultiLimit2 : $this->ismultiLimit1;
153
154            if ( count( $value ) > $limit ) {
155                throw new UnexpectedValueException( "Too many values for parameter '$name'. Limit: $limit" );
156            }
157
158            return array_map( static fn ( $v ) => $typeDef( $name, $v, $settings, $options ), $value );
159        }
160
161        return $typeDef( $name, $value, $settings, $options );
162    }
163
164    /**
165     * Validates and fetches a parameter value.
166     *
167     * @since 0.3.0
168     *
169     * @param string $name Parameter name.
170     * @param array<string, mixed> $settings Parameter settings.
171     * @param array<string, mixed> $options Additional options for validation.
172     * @return array<string, mixed> Validated value.
173     * @throws UnexpectedValueException If the parameter is missing and required.
174     */
175    public function getValue( string $name, array $settings, array $options = [] ): array {
176        $value = $this->callbacks->getValue( $name, $settings[self::PARAM_DEFAULT] ?? null, $options );
177
178        if ( $value === null && ( $settings[self::PARAM_REQUIRED] ?? false ) ) {
179            throw new UnexpectedValueException( "Missing required parameter: $name" );
180        }
181
182        return $this->validateValue( $name, $value, $settings, $options );
183    }
184}