Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.24% covered (success)
93.24%
69 / 74
90.32% covered (success)
90.32%
28 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
MessageValue
93.24% covered (success)
93.24%
69 / 74
90.32% covered (success)
90.32%
28 / 31
45.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 new
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromSpecifier
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 params
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 textParamsOfType
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 listParamsOfType
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 textParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 numParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 longDurationParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shortDurationParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expiryParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dateTimeParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dateParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 timeParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 userGroupParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sizeParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 bitrateParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rawParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 plaintextParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 commaListParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 semicolonListParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pipeListParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 textListParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dump
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isSameAs
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 toJsonArray
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 jsonClassHintFor
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 newFromJsonArray
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 hint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Wikimedia\Message;
4
5use Wikimedia\Assert\Assert;
6use Wikimedia\JsonCodec\Hint;
7use Wikimedia\JsonCodec\JsonCodecable;
8use Wikimedia\JsonCodec\JsonCodecableTrait;
9
10/**
11 * Value object representing a message for i18n.
12 *
13 * A MessageValue holds a key and an array of parameters. It can be converted
14 * to a string in a particular language using formatters obtained from an
15 * IMessageFormatterFactory.
16 *
17 * MessageValues are pure value objects and are newable and (de)serializable.
18 *
19 * @newable
20 */
21class MessageValue implements MessageSpecifier, JsonCodecable {
22    use JsonCodecableTrait;
23
24    private readonly string $key;
25
26    /** @var list<MessageParam> */
27    private array $params = [];
28
29    /**
30     * @stable to call
31     *
32     * @param string $key
33     * @param (MessageParam|MessageSpecifier|string|int|float)[] $params Values that are not instances
34     *  of MessageParam are wrapped using ParamType::TEXT.
35     */
36    public function __construct( string $key, array $params = [] ) {
37        $this->key = $key;
38        $this->params( ...$params );
39        Assert::invariant( array_is_list( $this->params ), "should be list" );
40    }
41
42    /**
43     * Static constructor for easier chaining of `->params()` methods
44     * @param string $key
45     * @param (MessageParam|MessageSpecifier|string|int|float)[] $params
46     * @return MessageValue
47     */
48    public static function new( string $key, array $params = [] ): MessageValue {
49        return new MessageValue( $key, $params );
50    }
51
52    /**
53     * Convert from any MessageSpecifier to a MessageValue.
54     *
55     * When the given object is an instance of MessageValue, the same object is returned.
56     *
57     * @since 1.43
58     * @param MessageSpecifier $spec
59     * @return MessageValue
60     */
61    public static function newFromSpecifier( MessageSpecifier $spec ): MessageValue {
62        if ( $spec instanceof MessageValue ) {
63            return $spec;
64        }
65        return new MessageValue( $spec->getKey(), $spec->getParams() );
66    }
67
68    /**
69     * Get the message key
70     */
71    public function getKey(): string {
72        return $this->key;
73    }
74
75    /**
76     * Get the parameter array
77     *
78     * @return MessageParam[]
79     */
80    public function getParams(): array {
81        return $this->params;
82    }
83
84    /**
85     * Chainable mutator which adds text parameters and MessageParam parameters
86     *
87     * @param MessageParam|MessageSpecifier|string|int|float ...$values
88     * @return $this
89     */
90    public function params( ...$values ): MessageValue {
91        foreach ( $values as $value ) {
92            if ( $value instanceof MessageParam ) {
93                $this->params[] = $value;
94            } else {
95                $this->params[] = new ScalarParam( ParamType::TEXT, $value );
96            }
97        }
98        return $this;
99    }
100
101    /**
102     * Chainable mutator which adds text parameters with a common type
103     *
104     * @param string|ParamType $type One of the ParamType constants
105     * @param MessageSpecifier|string|int|float ...$values Scalar values
106     * @return $this
107     */
108    public function textParamsOfType( string|ParamType $type, ...$values ): MessageValue {
109        if ( is_string( $type ) ) {
110            wfDeprecated( __METHOD__ . ' with string type', '1.45' );
111            $type = ParamType::from( $type );
112        }
113        foreach ( $values as $value ) {
114            $this->params[] = new ScalarParam( $type, $value );
115        }
116        return $this;
117    }
118
119    /**
120     * Chainable mutator which adds list parameters with a common type
121     *
122     * @param string|ListType $listType One of the ListType constants
123     * @param (MessageParam|MessageSpecifier|string|int|float)[] ...$values Each value
124     *  is an array of items suitable to pass as $params to ListParam::__construct()
125     * @return $this
126     */
127    public function listParamsOfType( string|ListType $listType, ...$values ): MessageValue {
128        if ( is_string( $listType ) ) {
129            wfDeprecated( __METHOD__ . ' with string listType', '1.45' );
130            $listType = ListType::from( $listType );
131        }
132        foreach ( $values as $value ) {
133            $this->params[] = new ListParam( $listType, $value );
134        }
135        return $this;
136    }
137
138    /**
139     * Chainable mutator which adds parameters of type text (ParamType::TEXT).
140     *
141     * @param MessageSpecifier|string|int|float ...$values
142     * @return $this
143     */
144    public function textParams( ...$values ): MessageValue {
145        return $this->textParamsOfType( ParamType::TEXT, ...$values );
146    }
147
148    /**
149     * Chainable mutator which adds numeric parameters (ParamType::NUM).
150     *
151     * @param int|float ...$values
152     * @return $this
153     */
154    public function numParams( ...$values ): MessageValue {
155        return $this->textParamsOfType( ParamType::NUM, ...$values );
156    }
157
158    /**
159     * Chainable mutator which adds parameters which are a duration specified
160     * in seconds (ParamType::DURATION_LONG).
161     *
162     * This is similar to shorDurationParams() except that the result will be
163     * more verbose.
164     *
165     * @param int|float ...$values
166     * @return $this
167     */
168    public function longDurationParams( ...$values ): MessageValue {
169        return $this->textParamsOfType( ParamType::DURATION_LONG, ...$values );
170    }
171
172    /**
173     * Chainable mutator which adds parameters which are a duration specified
174     * in seconds (ParamType::DURATION_SHORT).
175     *
176     * This is similar to longDurationParams() except that the result will be more
177     * compact.
178     *
179     * @param int|float ...$values
180     * @return $this
181     */
182    public function shortDurationParams( ...$values ): MessageValue {
183        return $this->textParamsOfType( ParamType::DURATION_SHORT, ...$values );
184    }
185
186    /**
187     * Chainable mutator which adds parameters which are an expiry timestamp (ParamType::EXPIRY).
188     *
189     * @param string ...$values Timestamp as accepted by the Wikimedia\Timestamp library,
190     *  or "infinity"
191     * @return $this
192     */
193    public function expiryParams( ...$values ): MessageValue {
194        return $this->textParamsOfType( ParamType::EXPIRY, ...$values );
195    }
196
197    /**
198     * Chainable mutator which adds parameters which are a date-time timestamp (ParamType::DATETIME).
199     *
200     * @since 1.36
201     * @param string ...$values Timestamp as accepted by the Wikimedia\Timestamp library.
202     * @return $this
203     */
204    public function dateTimeParams( ...$values ): MessageValue {
205        return $this->textParamsOfType( ParamType::DATETIME, ...$values );
206    }
207
208    /**
209     * Chainable mutator which adds parameters which are a date timestamp (ParamType::DATE).
210     *
211     * @since 1.36
212     * @param string ...$values Timestamp as accepted by the Wikimedia\Timestamp library.
213     * @return $this
214     */
215    public function dateParams( ...$values ): MessageValue {
216        return $this->textParamsOfType( ParamType::DATE, ...$values );
217    }
218
219    /**
220     * Chainable mutator which adds parameters which are a time timestamp (ParamType::TIME).
221     *
222     * @since 1.36
223     * @param string ...$values Timestamp as accepted by the Wikimedia\Timestamp library.
224     * @return $this
225     */
226    public function timeParams( ...$values ): MessageValue {
227        return $this->textParamsOfType( ParamType::TIME, ...$values );
228    }
229
230    /**
231     * Chainable mutator which adds parameters which are a user group (ParamType::GROUP).
232     *
233     * @since 1.38
234     * @param string ...$values User Groups
235     * @return $this
236     */
237    public function userGroupParams( ...$values ): MessageValue {
238        return $this->textParamsOfType( ParamType::GROUP, ...$values );
239    }
240
241    /**
242     * Chainable mutator which adds parameters which are a number of bytes (ParamType::SIZE).
243     *
244     * @param int ...$values
245     * @return $this
246     */
247    public function sizeParams( ...$values ): MessageValue {
248        return $this->textParamsOfType( ParamType::SIZE, ...$values );
249    }
250
251    /**
252     * Chainable mutator which adds parameters which are a number of bits per
253     * second (ParamType::BITRATE).
254     *
255     * @param int|float ...$values
256     * @return $this
257     */
258    public function bitrateParams( ...$values ): MessageValue {
259        return $this->textParamsOfType( ParamType::BITRATE, ...$values );
260    }
261
262    /**
263     * Chainable mutator which adds "raw" parameters (ParamType::RAW).
264     *
265     * Raw parameters are substituted after formatter processing. The caller is responsible
266     * for ensuring that the value will be safe for the intended output format, and
267     * documenting what that intended output format is.
268     *
269     * @param string ...$values
270     * @return $this
271     */
272    public function rawParams( ...$values ): MessageValue {
273        return $this->textParamsOfType( ParamType::RAW, ...$values );
274    }
275
276    /**
277     * Chainable mutator which adds plaintext parameters (ParamType::PLAINTEXT).
278     *
279     * Plaintext parameters are substituted after formatter processing. The value
280     * will be escaped by the formatter as appropriate for the target output format
281     * so as to be represented as plain text rather than as any sort of markup.
282     *
283     * @param string ...$values
284     * @return $this
285     */
286    public function plaintextParams( ...$values ): MessageValue {
287        return $this->textParamsOfType( ParamType::PLAINTEXT, ...$values );
288    }
289
290    /**
291     * Chainable mutator which adds comma lists (ListType::COMMA).
292     *
293     * The list parameters thus created are formatted as a comma-separated list,
294     * or some local equivalent.
295     *
296     * @param (MessageParam|MessageSpecifier|string|int|float)[] ...$values Each value
297     *  is an array of items suitable to pass as $params to ListParam::__construct()
298     * @return $this
299     */
300    public function commaListParams( ...$values ): MessageValue {
301        return $this->listParamsOfType( ListType::COMMA, ...$values );
302    }
303
304    /**
305     * Chainable mutator which adds semicolon lists (ListType::SEMICOLON).
306     *
307     * The list parameters thus created are formatted as a semicolon-separated
308     * list, or some local equivalent.
309     *
310     * @param (MessageParam|MessageSpecifier|string|int|float)[] ...$values Each value
311     *  is an array of items suitable to pass as $params to ListParam::__construct()
312     * @return $this
313     */
314    public function semicolonListParams( ...$values ): MessageValue {
315        return $this->listParamsOfType( ListType::SEMICOLON, ...$values );
316    }
317
318    /**
319     * Chainable mutator which adds pipe lists (ListType::PIPE).
320     *
321     * The list parameters thus created are formatted as a pipe ("|") -separated
322     * list, or some local equivalent.
323     *
324     * @param (MessageParam|MessageSpecifier|string|int|float)[] ...$values Each value
325     *  is an array of items suitable to pass as $params to ListParam::__construct()
326     * @return $this
327     */
328    public function pipeListParams( ...$values ): MessageValue {
329        return $this->listParamsOfType( ListType::PIPE, ...$values );
330    }
331
332    /**
333     * Chainable mutator which adds natural-language lists (ListType::AND).
334     *
335     * The list parameters thus created, when formatted, are joined as in natural
336     * language. In English, this means a comma-separated list, with the last
337     * two elements joined with "and".
338     *
339     * @param (MessageParam|string)[] ...$values
340     * @return $this
341     */
342    public function textListParams( ...$values ): MessageValue {
343        return $this->listParamsOfType( ListType::AND, ...$values );
344    }
345
346    /**
347     * Dump the object for testing/debugging
348     *
349     * @return string
350     */
351    public function dump(): string {
352        $contents = '';
353        foreach ( $this->params as $param ) {
354            $contents .= $param->dump();
355        }
356        return '<message key="' . htmlspecialchars( $this->key ) . '">' .
357            $contents . '</message>';
358    }
359
360    public function isSameAs( MessageValue $mv ): bool {
361        return $this->key === $mv->key &&
362            count( $this->params ) === count( $mv->params ) &&
363            array_all(
364                $this->params,
365                static fn ( $v, $k ) => $v->isSameAs( $mv->params[$k] )
366            );
367    }
368
369    public function toJsonArray(): array {
370        // WARNING: When changing how this class is serialized, follow the instructions
371        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
372        return [
373            'key' => $this->key,
374            'params' => array_map(
375                /**
376                 * Serialize trivial parameters as scalar values to minimize the footprint. Full
377                 * round-trip compatibility is guaranteed via the constructor and {@see params}.
378                 */
379                static fn ( $p ) => (
380                    $p->getType() === ParamType::TEXT &&
381                    is_scalar( $p->getValue() )
382                ) ? $p->getValue() : $p,
383                $this->params
384            ),
385        ];
386    }
387
388    /** @inheritDoc */
389    public static function jsonClassHintFor( string $keyName ) {
390        // Reduce serialization overhead by eliminating the type information
391        // when 'params' consists of MessageParam instances
392        if ( $keyName === 'params' ) {
393            return Hint::build(
394                MessageParam::class, Hint::INHERITED,
395                Hint::LIST, Hint::USE_SQUARE
396            );
397        }
398        return null;
399    }
400
401    public static function newFromJsonArray( array $json ): MessageValue {
402        // WARNING: When changing how this class is serialized, follow the instructions
403        // at <https://www.mediawiki.org/wiki/Manual:Parser_cache/Serialization_compatibility>!
404        // Support use of [MessageValue::class, Hint::INHERITED] for
405        // DataMessageValue as well:
406        if ( isset( $json['code'] ) ) {
407            return DataMessageValue::newFromJsonArray( $json );
408        }
409        return new self( $json['key'], $json['params'] );
410    }
411
412    /**
413     * If you are serializing a MessageValue (or a DataMessageValue), use
414     * this JsonCodec hint to suppress unnecessary type information.
415     */
416    public static function hint(): Hint {
417        return Hint::build( self::class, Hint::INHERITED );
418    }
419}