Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.33% covered (success)
93.33%
98 / 105
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZLangRegistry
93.33% covered (success)
93.33%
98 / 105
90.00% covered (success)
90.00%
9 / 10
29.25
0.00% covered (danger)
0.00%
0 / 1
 initialize
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getLanguageCodeFromZid
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getLanguageZidFromCode
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 getLanguageZidsFromCodes
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 isLanguageKnownGivenCode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 fetchLanguageCodeFromZid
81.58% covered (warning)
81.58%
31 / 38
0.00% covered (danger)
0.00%
0 / 1
5.16
 getLanguageCodeFromContent
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isValidLanguageZid
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getLanguageZids
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getListOfFallbackLanguageZids
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * WikiLambda ZLangRegistry
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\WikiLambda\Registry;
12
13use Exception;
14use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
15use MediaWiki\Extension\WikiLambda\ZErrorException;
16use MediaWiki\Extension\WikiLambda\ZErrorFactory;
17use MediaWiki\Extension\WikiLambda\ZObjectContent;
18use MediaWiki\Extension\WikiLambda\ZObjectUtils;
19use MediaWiki\Language\LanguageFallback;
20use MediaWiki\Logger\LoggerFactory;
21use MediaWiki\Title\Title;
22
23/**
24 * A registry service for ZLanguage and language codes
25 */
26class ZLangRegistry extends ZObjectRegistry {
27
28    private const FALLBACK_LANGUAGE_ZID = 'Z1002';
29    private const FALLBACK_LANGUAGE_CODE = 'en';
30    public const MULTILINGUAL_VALUE = 'Z1360';
31
32    /**
33     * Initialize ZLangRegistry
34     */
35    protected function initialize(): void {
36        // Registry for ZObjects of type ZLanguage/Z60
37        $this->type = ZTypeRegistry::Z_LANGUAGE;
38
39        $this->register(
40            self::FALLBACK_LANGUAGE_ZID,
41            self::FALLBACK_LANGUAGE_CODE
42        );
43    }
44
45    /**
46     * Given a ZLanguage Zid, return its language code
47     *
48     * @param string $zid
49     * @return string
50     * @throws ZErrorException
51     */
52    public function getLanguageCodeFromZid( $zid ): string {
53        if ( array_key_exists( $zid, $this->registry ) ) {
54            return $this->registry[ $zid ];
55        }
56
57        $code = $this->fetchLanguageCodeFromZid( $zid );
58        $this->register( $zid, $code );
59        return $code;
60    }
61
62    /**
63     * Given a language code, return its ZLanguage Zid
64     *
65     * @param string $code
66     * @param bool $fallback If true, give the ZLanguage for English
67     * @return string
68     * @throws ZErrorException
69     */
70    public function getLanguageZidFromCode( $code, $fallback = false ): string {
71        $zid = array_search( $code, $this->registry );
72        if ( $zid ) {
73            return $zid;
74        }
75
76        // Not in the registry, but let's check the DB
77        $zObjectStore = WikiLambdaServices::getZObjectStore();
78        $zid = $zObjectStore->findZLanguageFromCode( $code );
79
80        if ( $zid === null ) {
81            if ( $fallback ) {
82                return self::FALLBACK_LANGUAGE_ZID;
83            }
84            throw new ZErrorException(
85                ZErrorFactory::createZErrorInstance(
86                    ZErrorTypeRegistry::Z_ERROR_LANG_NOT_FOUND,
87                    [ 'lang' => $code ]
88                )
89            );
90        }
91
92        // Add it to the register now for faster lookups later
93        $this->register( $zid, $code );
94        return $zid;
95    }
96
97    /**
98     * Given an array of language codes, return a map of code => ZLanguage ZID.
99     * Codes not found in the database map to null.
100     * Checks the in-memory registry first, then issues a single DB query for any
101     * cache misses, and populates the registry with the results for future lookups.
102     *
103     * @param string[] $codes
104     * @return array<string,?string> Map of code => ZID (or null if not found)
105     */
106    public function getLanguageZidsFromCodes( array $codes ): array {
107        $result = [];
108        $missing = [];
109
110        foreach ( $codes as $code ) {
111            $zid = array_search( $code, $this->registry );
112            if ( $zid !== false ) {
113                $result[ $code ] = $zid;
114            } else {
115                $result[ $code ] = null;
116                $missing[] = $code;
117            }
118        }
119
120        if ( !$missing ) {
121            return $result;
122        }
123
124        // Fetch all cache misses in a single DB query
125        $zObjectStore = WikiLambdaServices::getZObjectStore();
126        $found = $zObjectStore->findZLanguagesFromCodes( $missing );
127
128        foreach ( $found as $code => $zid ) {
129            // Populate the in-memory registry so single-code lookups benefit too
130            $this->register( $zid, $code );
131            $result[ $code ] = $zid;
132        }
133
134        return $result;
135    }
136
137    /**
138     * Check if a given language code, is a known ZLanguage Zid
139     *
140     * @param string $code
141     * @return bool
142     */
143    public function isLanguageKnownGivenCode( $code ): bool {
144        try {
145            $this->getLanguageZidFromCode( $code );
146        } catch ( Exception ) {
147            return false;
148        }
149        return true;
150    }
151
152    /**
153     * Fetch zid from the database, parse it and return its language code.
154     *
155     * @param string $zid
156     * @return string The language code of the ZLanguage identified by this Zid
157     * @throws ZErrorException
158     */
159    private function fetchLanguageCodeFromZid( $zid ): string {
160        $zObjectStore = WikiLambdaServices::getZObjectStore();
161
162        // Try the cache table, where it should be available
163        $languages = $zObjectStore->findCodesFromZLanguage( $zid );
164
165        if ( count( $languages ) ) {
166            // Return the first, in case there are aliases
167            return $languages[0];
168        }
169
170        // Fallback to the database just in case it's somehow not cached.
171        $title = Title::newFromText( $zid, NS_MAIN );
172        if ( !$title ) {
173            throw new ZErrorException(
174                ZErrorFactory::createZErrorInstance(
175                    ZErrorTypeRegistry::Z_ERROR_ZID_NOT_FOUND,
176                    [ 'data' => $zid ]
177                )
178            );
179        }
180
181        $content = $zObjectStore->fetchZObjectByTitle( $title );
182        if ( !$content ) {
183            throw new ZErrorException(
184                ZErrorFactory::createZErrorInstance(
185                    ZErrorTypeRegistry::Z_ERROR_ZID_NOT_FOUND,
186                    [ 'data' => $zid ]
187                )
188            );
189        }
190
191        $code = $this->getLanguageCodeFromContent( $content );
192        if ( !$code ) {
193            throw new ZErrorException(
194                ZErrorFactory::createZErrorInstance(
195                    ZErrorTypeRegistry::Z_ERROR_MISSING_KEY,
196                    [
197                        'data' => $content->getObject(),
198                        'keywordArgs' => [ 'missing' => ZTypeRegistry::Z_LANGUAGE_CODE ]
199                    ]
200                )
201            );
202        }
203
204        // This cache miss shouldn't happen, so let's log it in case there's a pattern
205        $logger = LoggerFactory::getInstance( 'WikiLambda' );
206        $logger->warning(
207            'Called fetchLanguageCodeFromZid but not found in cache table: {zid}',
208            [ 'zid' => $zid ]
209        );
210
211        // Re-insert into the languages cache so we don't have this expensive miss again.
212        $zObjectStore->insertZLanguageToLanguagesCache( $zid, $code );
213
214        return $code;
215    }
216
217    /**
218     * Returns the language code from a ZObjectContent wrapping a Z60.
219     *
220     * @param ZObjectContent $content
221     * @return string|bool Language code or false if content object is not valid Z60.
222     */
223    private function getLanguageCodeFromContent( $content ) {
224        $zobject = $content->getObject()->{ ZTypeRegistry::Z_PERSISTENTOBJECT_VALUE };
225        if (
226            ( $zobject->{ZTypeRegistry::Z_OBJECT_TYPE} === ZTypeRegistry::Z_LANGUAGE ) &&
227            ( property_exists( $zobject, ZTypeRegistry::Z_LANGUAGE_CODE ) )
228        ) {
229            return $zobject->{ZTypeRegistry::Z_LANGUAGE_CODE};
230        }
231        return false;
232    }
233
234    /**
235     * Checks if the given Zid is a valid language Zid. For that it first checks whether the
236     * Zid is registered, and if it's not, it fetches it from the database.
237     *
238     * @param string $zid
239     * @return bool Is a valid ZLanguage Zid
240     */
241    public function isValidLanguageZid( $zid ) {
242        if ( !ZObjectUtils::isValidZObjectReference( $zid ) ) {
243            return false;
244        }
245        try {
246            $this->getLanguageCodeFromZid( $zid );
247        } catch ( ZErrorException ) {
248            return false;
249        }
250        return true;
251    }
252
253    /**
254     * Returns an array of language Zids given an array of language codes
255     *
256     * @param string[] $languageCodes
257     * @return string[]
258     */
259    public function getLanguageZids( $languageCodes ) {
260        $languageZids = [];
261        foreach ( $languageCodes as $code ) {
262            try {
263                $languageZids[] = $this->getLanguageZidFromCode( $code );
264            } catch ( ZErrorException ) {
265                // We ignore the language code if it's not available as Zid
266            }
267        }
268        return $languageZids;
269    }
270
271    /**
272     * Return the list of unique language Zids that correspond
273     * to the user's selected language, its fallbacks, and English
274     * if requested.
275     *
276     * @param LanguageFallback $languageFallback
277     * @param string $langCode - Language BCP47 code
278     * @return string[]
279     */
280    public function getListOfFallbackLanguageZids( $languageFallback, $langCode ) {
281        $languages = array_merge(
282            [ $langCode ],
283            $languageFallback->getAll( $langCode, LanguageFallback::MESSAGES )
284        );
285        return $this->getLanguageZids( array_unique( $languages ) );
286    }
287}