Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
30.77% covered (danger)
30.77%
20 / 65
37.50% covered (danger)
37.50%
9 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
SiteList
31.25% covered (danger)
31.25%
20 / 64
37.50% covered (danger)
37.50%
9 / 24
433.07
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 append
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 offsetSet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasValidType
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getObjectType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNewOffset
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setElement
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 offsetUnset
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getGlobalIdentifiers
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSite
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSite
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeSite
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasInternalId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSiteByInternalId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 removeSiteByInternalId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasNavigationId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSiteByNavigationId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeSiteByNavigationId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSite
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroup
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getSerialVersionId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __serialize
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 __unserialize
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
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 */
20
21namespace MediaWiki\Site;
22
23use ArrayObject;
24use InvalidArgumentException;
25
26/**
27 * Array-like collection of Site objects.
28 *
29 * This uses ArrayObject to intercept additions and deletions for purposes
30 * such as additional indexing, and to enforce that values are restricted
31 * to Site objects only.
32 *
33 * @since 1.21
34 * @ingroup Site
35 * @author Jeroen De Dauw < jeroendedauw@gmail.com >
36 */
37class SiteList extends ArrayObject {
38    /**
39     * @see SiteList::getNewOffset()
40     * @since 1.20
41     * @var int
42     */
43    protected $indexOffset = 0;
44
45    /**
46     * Internal site identifiers pointing to their sites offset value.
47     *
48     * @since 1.21
49     *
50     * @var array Maps int identifiers to local ArrayObject keys
51     */
52    protected $byInternalId = [];
53
54    /**
55     * Global site identifiers pointing to their sites offset value.
56     *
57     * @since 1.21
58     *
59     * @var array Maps string identifiers to local ArrayObject keys
60     */
61    protected $byGlobalId = [];
62
63    /**
64     * Navigational site identifiers alias inter-language prefixes
65     * pointing to their sites offset value.
66     *
67     * @since 1.23
68     *
69     * @var array Maps string identifiers to local ArrayObject keys
70     */
71    protected $byNavigationId = [];
72
73    /**
74     * @see Overrides ArrayObject::__construct https://www.php.net/manual/en/arrayobject.construct.php
75     * @since 1.20
76     * @param null|array $input
77     * @param int $flags
78     * @param string $iterator_class
79     */
80    public function __construct( $input = null, $flags = 0, $iterator_class = 'ArrayIterator' ) {
81        parent::__construct( [], $flags, $iterator_class );
82
83        if ( $input !== null ) {
84            foreach ( $input as $offset => $value ) {
85                $this->offsetSet( $offset, $value );
86            }
87        }
88    }
89
90    /**
91     * @see Overrides ArrayObject::append
92     * @since 1.20
93     * @param mixed $value
94     */
95    public function append( $value ): void {
96        $this->setElement( null, $value );
97    }
98
99    /**
100     * @since 1.20
101     * @see Overrides ArrayObject::offsetSet()
102     * @param mixed $index
103     * @param mixed $value
104     */
105    public function offsetSet( $index, $value ): void {
106        $this->setElement( $index, $value );
107    }
108
109    /**
110     * Returns if the provided value has the same type as the elements
111     * that can be added to this ArrayObject.
112     *
113     * @since 1.20
114     * @param mixed $value
115     * @return bool
116     */
117    protected function hasValidType( $value ) {
118        $class = $this->getObjectType();
119        return $value instanceof $class;
120    }
121
122    /**
123     * The class or interface type that array elements must match.
124     *
125     * @since 1.21
126     * @return string
127     */
128    public function getObjectType() {
129        return Site::class;
130    }
131
132    /**
133     * Find a new offset for when appending an element.
134     *
135     * @since 1.20
136     * @return int
137     */
138    protected function getNewOffset() {
139        while ( $this->offsetExists( $this->indexOffset ) ) {
140            $this->indexOffset++;
141        }
142
143        return $this->indexOffset;
144    }
145
146    /**
147     * Actually set the element and enforce type checking and offset resolving.
148     *
149     * @since 1.20
150     * @param int|string|null $index
151     * @param Site $site
152     */
153    protected function setElement( $index, $site ) {
154        if ( !$this->hasValidType( $site ) ) {
155            throw new InvalidArgumentException(
156                'Can only add ' . $this->getObjectType() . ' implementing objects to ' . static::class . '.'
157            );
158        }
159
160        $index ??= $this->getNewOffset();
161
162        if ( $this->hasSite( $site->getGlobalId() ) ) {
163            $this->removeSite( $site->getGlobalId() );
164        }
165
166        $this->byGlobalId[$site->getGlobalId()] = $index;
167        $this->byInternalId[$site->getInternalId()] = $index;
168
169        $ids = $site->getNavigationIds();
170        foreach ( $ids as $navId ) {
171            $this->byNavigationId[$navId] = $index;
172        }
173
174        parent::offsetSet( $index, $site );
175    }
176
177    /**
178     * @see ArrayObject::offsetUnset()
179     *
180     * @since 1.21
181     *
182     * @param mixed $index
183     */
184    public function offsetUnset( $index ): void {
185        if ( $this->offsetExists( $index ) ) {
186            /**
187             * @var Site $site
188             */
189            $site = $this->offsetGet( $index );
190
191            unset( $this->byGlobalId[$site->getGlobalId()] );
192            unset( $this->byInternalId[$site->getInternalId()] );
193
194            $ids = $site->getNavigationIds();
195            foreach ( $ids as $navId ) {
196                unset( $this->byNavigationId[$navId] );
197            }
198        }
199
200        parent::offsetUnset( $index );
201    }
202
203    /**
204     * Returns all the global site identifiers.
205     * Optionally only those belonging to the specified group.
206     *
207     * @since 1.21
208     *
209     * @return array
210     */
211    public function getGlobalIdentifiers() {
212        return array_keys( $this->byGlobalId );
213    }
214
215    /**
216     * Returns if the list contains the site with the provided global site identifier.
217     *
218     * @param string $globalSiteId
219     *
220     * @return bool
221     */
222    public function hasSite( $globalSiteId ) {
223        return array_key_exists( $globalSiteId, $this->byGlobalId );
224    }
225
226    /**
227     * Returns the Site with the provided global site identifier.
228     * The site needs to exist, so if not sure, call hasGlobalId first.
229     *
230     * @since 1.21
231     *
232     * @param string $globalSiteId
233     *
234     * @return Site
235     */
236    public function getSite( $globalSiteId ) {
237        return $this->offsetGet( $this->byGlobalId[$globalSiteId] );
238    }
239
240    /**
241     * Removes the site with the specified global site identifier.
242     * The site needs to exist, so if not sure, call hasGlobalId first.
243     *
244     * @since 1.21
245     *
246     * @param string $globalSiteId
247     */
248    public function removeSite( $globalSiteId ) {
249        $this->offsetUnset( $this->byGlobalId[$globalSiteId] );
250    }
251
252    /**
253     * Whether the list contains no sites.
254     *
255     * @since 1.21
256     * @return bool
257     */
258    public function isEmpty() {
259        return $this->byGlobalId === [];
260    }
261
262    /**
263     * Returns if the list contains the site with the provided site id.
264     *
265     * @param int $id
266     *
267     * @return bool
268     */
269    public function hasInternalId( $id ) {
270        return array_key_exists( $id, $this->byInternalId );
271    }
272
273    /**
274     * Returns the Site with the provided site id.
275     * The site needs to exist, so if not sure, call has first.
276     *
277     * @since 1.21
278     *
279     * @param int $id
280     *
281     * @return Site
282     */
283    public function getSiteByInternalId( $id ) {
284        return $this->offsetGet( $this->byInternalId[$id] );
285    }
286
287    /**
288     * Removes the site with the specified site id.
289     * The site needs to exist, so if not sure, call has first.
290     *
291     * @since 1.21
292     *
293     * @param int $id
294     */
295    public function removeSiteByInternalId( $id ) {
296        $this->offsetUnset( $this->byInternalId[$id] );
297    }
298
299    /**
300     * Returns if the list contains the site with the provided navigational site id.
301     *
302     * @param string $id
303     *
304     * @return bool
305     */
306    public function hasNavigationId( $id ) {
307        return array_key_exists( $id, $this->byNavigationId );
308    }
309
310    /**
311     * Returns the Site with the provided navigational site id.
312     * The site needs to exist, so if not sure, call has first.
313     *
314     * @since 1.23
315     *
316     * @param string $id
317     *
318     * @return Site
319     */
320    public function getSiteByNavigationId( $id ) {
321        return $this->offsetGet( $this->byNavigationId[$id] );
322    }
323
324    /**
325     * Removes the site with the specified navigational site id.
326     * The site needs to exist, so if not sure, call has first.
327     *
328     * @since 1.23
329     *
330     * @param string $id
331     */
332    public function removeSiteByNavigationId( $id ) {
333        $this->offsetUnset( $this->byNavigationId[$id] );
334    }
335
336    /**
337     * Sets a site in the list. If the site was not there,
338     * it will be added. If it was, it will be updated.
339     *
340     * @since 1.21
341     *
342     * @param Site $site
343     */
344    public function setSite( Site $site ) {
345        $this[] = $site;
346    }
347
348    /**
349     * Returns the sites that are in the provided group.
350     *
351     * @since 1.21
352     *
353     * @param string $groupName
354     *
355     * @return SiteList
356     */
357    public function getGroup( $groupName ) {
358        $group = new self();
359
360        /**
361         * @var Site $site
362         */
363        foreach ( $this as $site ) {
364            if ( $site->getGroup() === $groupName ) {
365                $group[] = $site;
366            }
367        }
368
369        return $group;
370    }
371
372    /**
373     * A version ID that identifies the serialization structure used by __serialize()
374     * and unserialize(). This is useful for constructing cache keys in cases where the cache relies
375     * on serialization for storing the SiteList.
376     *
377     * @var string A string uniquely identifying the version of the serialization structure,
378     *             not including any sub-structures.
379     */
380    private const SERIAL_VERSION_ID = '2014-03-17';
381
382    /**
383     * Returns the version ID that identifies the serialization structure used by
384     * __serialize() and unserialize(), including the structure of any nested structures.
385     * This is useful for constructing cache keys in cases where the cache relies
386     * on serialization for storing the SiteList.
387     *
388     * @return string A string uniquely identifying the version of the serialization structure,
389     *                including any sub-structures.
390     */
391    public static function getSerialVersionId() {
392        return self::SERIAL_VERSION_ID . '+Site:' . Site::SERIAL_VERSION_ID;
393    }
394
395    /**
396     * @see Overrides Serializable::serialize
397     * @since 1.38
398     * @return array
399     */
400    public function __serialize(): array {
401        // Data that should go into serialization calls.
402        //
403        // NOTE: When changing the structure, either implement unserialize() to handle the
404        //      old structure too, or update SERIAL_VERSION_ID to kill any caches.
405        return [
406            'data' => $this->getArrayCopy(),
407            'index' => $this->indexOffset,
408            'internalIds' => $this->byInternalId,
409            'globalIds' => $this->byGlobalId,
410            'navigationIds' => $this->byNavigationId,
411        ];
412    }
413
414    /**
415     * @see Overrides Serializable::unserialize
416     * @since 1.38
417     * @param array $serializationData
418     */
419    public function __unserialize( $serializationData ): void {
420        foreach ( $serializationData['data'] as $offset => $value ) {
421            // Just set the element, bypassing checks and offset resolving,
422            // as these elements have already gone through this.
423            parent::offsetSet( $offset, $value );
424        }
425
426        $this->indexOffset = $serializationData['index'];
427
428        $this->byInternalId = $serializationData['internalIds'];
429        $this->byGlobalId = $serializationData['globalIds'];
430        $this->byNavigationId = $serializationData['navigationIds'];
431    }
432}
433
434/** @deprecated class alias since 1.41 */
435class_alias( SiteList::class, 'SiteList' );