Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.73% covered (success)
98.73%
78 / 79
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
MultiListBuilder
98.73% covered (success)
98.73%
78 / 79
66.67% covered (warning)
66.67%
2 / 3
17
0.00% covered (danger)
0.00%
0 / 1
 buildWeightedTagsFromLegacyParameters
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 buildWeightedTags
97.06% covered (success)
97.06%
33 / 34
0.00% covered (danger)
0.00%
0 / 1
5
 buildTagWeightsFromLegacyParameters
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
11
1<?php
2
3namespace CirrusSearch\Extra\MultiList;
4
5use Wikimedia\Assert\Assert;
6
7/**
8 * Utility class for encoding weighted tags.
9 *
10 * @see https://wikitech.wikimedia.org/wiki/Search/WeightedTags
11 */
12class MultiListBuilder {
13
14    private const WEIGHTED_TAG_DEFAULT_NAME = 'exists';
15
16    /**
17     * @param string $tagPrefix A prefix shared by all `$tagNames`
18     * @param string|string[]|null $tagNames Optional tag name or list of tag names.
19     *   Each tag will be set for each target ID. Omit for tags which are fully defined by their prefix.
20     * @param int|int[]|null $tagWeights Optional tag weight(s).
21     *   * If `$tagNames` is null, an integer.
22     *   * Otherwise, a `[ tagName => weight ]` map. This may only use a subset of `$tagNames` as keys.
23     *       However, only valid keys (which exist in `$tagNames`) will result in returned tags.
24     *
25     * A single weight ranges between 1-1000.
26     *
27     * @return MultiListWeightedTag[]
28     * @deprecated use {@link buildWeightedTags} instead use {@link buildTagWeightsFromLegacyParameters} to migrate
29     */
30    public static function buildWeightedTagsFromLegacyParameters(
31        string $tagPrefix,
32        $tagNames = null,
33        $tagWeights = null
34    ): array {
35        $tagWeights = self::buildTagWeightsFromLegacyParameters( $tagNames, $tagWeights );
36
37        return self::buildWeightedTags( $tagPrefix, $tagWeights );
38    }
39
40    /**
41     * @param string $tagPrefix A prefix shared by all `$tagNames`
42     * @param null|null[]|int[] $tagWeightsByName Optional tag weights. A map of optional weights, keyed by tag name.
43     *   Omit for tags which are fully defined by their prefix.
44     *   A single weight ranges between 1-1000.
45     *
46     * @return MultiListWeightedTag[]
47     */
48    public static function buildWeightedTags(
49        string $tagPrefix,
50        ?array $tagWeightsByName = null
51    ): array {
52        Assert::precondition(
53            strpos( $tagPrefix, MultiListItem::DELIMITER ) === false,
54            "invalid tag prefix $tagPrefix: must not contain " . MultiListItem::DELIMITER
55        );
56
57        Assert::parameterType(
58            [
59                'array',
60                'null'
61            ],
62            $tagWeightsByName,
63            '$tagWeightsByName'
64        );
65
66        if ( $tagWeightsByName === null ) {
67            $tagWeightsByName = [ self::WEIGHTED_TAG_DEFAULT_NAME => null ];
68        }
69
70        foreach ( $tagWeightsByName as $tagName => $tagWeight ) {
71            Assert::precondition(
72                strpos( $tagName, MultiListWeightedTag::WEIGHT_DELIMITER ) === false,
73                "invalid tag name $tagName: must not contain " . MultiListWeightedTag::WEIGHT_DELIMITER
74            );
75            if ( $tagWeight !== null ) {
76                Assert::precondition(
77                    is_int( $tagWeight ),
78                    "weights must be integers but $tagWeight is " . get_debug_type( $tagWeight )
79                );
80                Assert::precondition(
81                    $tagWeight >= 1 && $tagWeight <= 1000,
82                    "weights must be between 1 and 1000 (found: $tagWeight)"
83                );
84            }
85        }
86
87        return array_map(
88            static fn ( $tagName ) => new MultiListWeightedTag(
89                $tagPrefix, $tagName, $tagWeightsByName[$tagName]
90            ),
91            array_keys( $tagWeightsByName )
92        );
93    }
94
95    /**
96     * Merges `$tagNames` and `$tagWeights` into a single map that can be passed to {@link buildWeightedTags}.
97     *
98     * @param null|string|string[] $tagNames Optional tag name or list of tag names.
99     * Each tag will be set for each target ID. Omit for tags which are fully defined by their prefix.
100     * @param null|int|int[] $tagWeights Optional tag weight(s).
101     *   * If `$tagNames` is null, an integer.
102     *   * Otherwise, a `[ tagName => weight ]` map. This may only use a subset of `$tagNames` as keys.
103     *   * However, only valid keys (which exist in `$tagNames`) will result in returned tags.
104     *
105     * A single weight ranges between 1-1000.
106     *
107     * @return null[]|int[] A map of tag weights keyed by tag name
108     * @see buildWeightedTags
109     * @see buildWeightedTagsFromLegacyParameters
110     */
111    public static function buildTagWeightsFromLegacyParameters( $tagNames = null, $tagWeights = null ) {
112        Assert::parameterType(
113            [
114                'string',
115                'array',
116                'null'
117            ],
118            $tagNames,
119            '$tagNames'
120        );
121        if ( $tagNames === null ) {
122            $tagNames = [ self::WEIGHTED_TAG_DEFAULT_NAME ];
123            if ( $tagWeights !== null ) {
124                Assert::parameterType( 'integer', $tagWeights, '$tagWeights' );
125                $tagWeights = [ self::WEIGHTED_TAG_DEFAULT_NAME => $tagWeights ];
126            } else {
127                $tagWeights = [ self::WEIGHTED_TAG_DEFAULT_NAME => null ];
128            }
129        } elseif ( is_string( $tagNames ) ) {
130            if ( $tagWeights === null ) {
131                $tagWeights = [ $tagNames => null ];
132            }
133
134            $tagNames = [ $tagNames ];
135        } elseif ( is_array( $tagNames ) ) {
136            Assert::parameterElementType( 'string', $tagNames, '$tagNames' );
137            if ( $tagWeights === null ) {
138                $tagWeights = array_fill_keys( $tagNames, null );
139            }
140        }
141
142        if ( $tagWeights ) {
143            foreach ( $tagWeights as $tagName => $tagWeight ) {
144                Assert::precondition(
145                    strpos( $tagName, MultiListWeightedTag::WEIGHT_DELIMITER ) === false,
146                    "invalid tag name $tagName: must not contain " . MultiListWeightedTag::WEIGHT_DELIMITER
147                );
148                Assert::precondition(
149                    in_array( $tagName, $tagNames, true ),
150                    "tag name $tagName used in \$tagWeights but not found in \$tagNames"
151                );
152
153                if ( $tagWeight !== null ) {
154                    Assert::precondition(
155                        is_int( $tagWeight ),
156                        "weights must be integers but $tagWeight is " . get_debug_type( $tagWeight )
157                    );
158                    Assert::precondition(
159                        $tagWeight >= 1 && $tagWeight <= 1000,
160                        "weights must be between 1 and 1000 (found: $tagWeight)"
161                    );
162                }
163            }
164        }
165
166        return $tagWeights;
167    }
168
169}