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