Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
44.44% covered (danger)
44.44%
24 / 54
27.27% covered (danger)
27.27%
3 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
AutoLoader
50.00% covered (danger)
50.00%
24 / 48
27.27% covered (danger)
27.27%
3 / 11
82.50
0.00% covered (danger)
0.00%
0 / 1
 registerNamespaces
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 registerClasses
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadFiles
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 find
86.36% covered (warning)
86.36%
19 / 22
0.00% covered (danger)
0.00%
0 / 1
9.21
 autoload
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 assertTesting
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getClassFiles
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getNamespaceDirectories
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getState
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 restoreState
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7// NO_AUTOLOAD -- file scope code, can't load self
8
9/**
10 * Locations of core classes
11 * Extension classes are specified with $wgAutoloadClasses
12 */
13require_once __DIR__ . '/../autoload.php';
14
15/**
16 * @defgroup Autoload Autoload
17 */
18
19/**
20 * This initializes autoloading for MediaWiki core, extensions, and vendored
21 * libraries. Namespaces must follow the PSR-4 standard for autoloading.
22 *
23 * MediaWiki core does not use PSR-4 autoloading due to performance issues
24 * but enforces the mapping to be maintained for future use. Instead of using
25 * PSR-0, a class map stored in autoload.php generated via a script:
26 * php maintenance/run.php generateAutoload
27 *
28 * @see <https://www.php-fig.org/psr/psr-4/>
29 * @see <https://techblog.wikimedia.org/2024/01/16/web-perf-hero-mate-szabo/>
30 *
31 * NOTE: This file sets up the PHP autoloader and so its stable contract is not this
32 * class, but the act of initializing spl_autoload_register and vendor.
33 * This file is widely referenced (akin to includes/Defines.php) and is therefore
34 * not renamed or moved to /includes/Autoload.
35 *
36 * @since 1.7
37 * @ingroup Autoload
38 */
39class AutoLoader {
40
41    /**
42     * @var string[] Namespace (ends with \) => Path (ends with /)
43     */
44    private static $psr4Namespaces = [];
45
46    /**
47     * @var string[] Class => File
48     */
49    private static $classFiles = [];
50
51    /**
52     * Register a directory to load the classes of a given namespace from,
53     * per PSR4.
54     *
55     * @see <https://www.php-fig.org/psr/psr-4/>
56     * @since 1.39
57     * @param string[] $dirs a map of namespace (ends with \) to path (ends with /)
58     */
59    public static function registerNamespaces( array $dirs ): void {
60        self::$psr4Namespaces += $dirs;
61    }
62
63    /**
64     * Register a file to load the given class from.
65     * @since 1.39
66     *
67     * @param string[] $files a map of qualified class names to file names
68     */
69    public static function registerClasses( array $files ): void {
70        self::$classFiles += $files;
71    }
72
73    /**
74     * Load a file that declares classes, functions, or constants.
75     * The file will be loaded immediately using require_once in function scope.
76     *
77     * @note The file to be loaded MUST NOT set global variables or otherwise
78     * affect the global state. It MAY however use conditionals to determine
79     * what to declare and how, e.g. to provide polyfills.
80     *
81     * @note The file to be loaded MUST NOT assume that MediaWiki has been
82     * initialized. In particular, it MUST NOT access configuration variables
83     * or MediaWikiServices.
84     *
85     * @since 1.39
86     *
87     * @param string $file the path of the file to load.
88     */
89    public static function loadFile( string $file ): void {
90        require_once $file;
91    }
92
93    /**
94     * Batch version of loadFile()
95     *
96     * @see loadFile()
97     *
98     * @since 1.39
99     *
100     * @param string[] $files the paths of the files to load.
101     */
102    public static function loadFiles( array $files ): void {
103        foreach ( $files as $f ) {
104            self::loadFile( $f );
105        }
106    }
107
108    /**
109     * Find the file containing the given class.
110     *
111     * @param class-string $className Name of class we're looking for.
112     * @return string|null The path containing the class, not null if not found
113     */
114    public static function find( $className ): ?string {
115        global $wgAutoloadLocalClasses, $wgAutoloadClasses;
116
117        // NOTE: $wgAutoloadClasses is supported for compatibility with old-style extension
118        //       registration files.
119
120        $filename = $wgAutoloadLocalClasses[$className] ??
121            self::$classFiles[$className] ??
122            $wgAutoloadClasses[$className] ??
123            false;
124
125        if ( !$filename && str_contains( $className, '\\' ) ) {
126            // This class is namespaced, so look in the namespace map
127            $prefix = $className;
128            // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
129            while ( ( $pos = strrpos( $prefix, '\\' ) ) !== false ) {
130                // Check to see if this namespace prefix is in the map
131                $prefix = substr( $className, 0, $pos + 1 );
132                if ( isset( self::$psr4Namespaces[$prefix] ) ) {
133                    $relativeClass = substr( $className, $pos + 1 );
134                    // Build the expected filename, and see if it exists
135                    $file = self::$psr4Namespaces[$prefix] .
136                        strtr( $relativeClass, '\\', '/' ) .
137                        '.php';
138                    if ( is_file( $file ) ) {
139                        $filename = $file;
140                        break;
141                    }
142                }
143
144                // Remove trailing separator for next iteration
145                $prefix = rtrim( $prefix, '\\' );
146            }
147        }
148
149        if ( !$filename ) {
150            // Class not found; let the next autoloader try to find it
151            return null;
152        }
153
154        // Make an absolute path, this improves performance by avoiding some stat calls
155        // Optimisation: use string offset access instead of substr
156        if ( $filename[0] !== '/' && $filename[1] !== ':' ) {
157            $filename = __DIR__ . '/../' . $filename;
158        }
159
160        return $filename;
161    }
162
163    /**
164     * autoload - take a class name and attempt to load it
165     *
166     * @param class-string $className Name of class we're looking for.
167     */
168    public static function autoload( $className ) {
169        $filename = self::find( $className );
170
171        if ( $filename !== null ) {
172            require_once $filename;
173        }
174    }
175
176    ///// Methods used during testing //////////////////////////////////////////////
177    private static function assertTesting( string $method ): void {
178        if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
179            throw new LogicException( "$method is not supported outside phpunit tests!" );
180        }
181    }
182
183    /**
184     * Returns a map of class names to file paths for testing.
185     * @note Will throw if called outside of phpunit tests!
186     * @return string[]
187     */
188    public static function getClassFiles(): array {
189        global $wgAutoloadLocalClasses, $wgAutoloadClasses;
190
191        self::assertTesting( __METHOD__ );
192
193        // NOTE: ensure the order of preference is the same as used by find().
194        return array_merge(
195            $wgAutoloadClasses,
196            self::$classFiles,
197            $wgAutoloadLocalClasses
198        );
199    }
200
201    /**
202     * Returns a map of namespace names to directories, per PSR4.
203     * @note Will throw if called outside of phpunit tests!
204     * @return string[]
205     */
206    public static function getNamespaceDirectories(): array {
207        self::assertTesting( __METHOD__ );
208        return self::$psr4Namespaces;
209    }
210
211    /**
212     * Returns an array representing the internal state of Autoloader,
213     * so it can be remembered and later restored during testing.
214     * @internal
215     * @note Will throw if called outside of phpunit tests!
216     * @return array
217     */
218    public static function getState(): array {
219        self::assertTesting( __METHOD__ );
220        return [
221            'classFiles' => self::$classFiles,
222            'psr4Namespaces' => self::$psr4Namespaces,
223        ];
224    }
225
226    /**
227     * Returns an array representing the internal state of Autoloader,
228     * so it can be remembered and later restored during testing.
229     * @internal
230     * @note Will throw if called outside of phpunit tests!
231     *
232     * @param array $state A state array returned by getState().
233     */
234    public static function restoreState( $state ): void {
235        self::assertTesting( __METHOD__ );
236
237        self::$classFiles = $state['classFiles'];
238        self::$psr4Namespaces = $state['psr4Namespaces'];
239    }
240
241}
242
243spl_autoload_register( AutoLoader::autoload( ... ) );
244
245// Load composer's autoloader if present
246if ( is_readable( __DIR__ . '/../vendor/autoload.php' ) ) {
247    require_once __DIR__ . '/../vendor/autoload.php';
248} elseif ( file_exists( __DIR__ . '/../vendor/autoload.php' ) ) {
249    die( __DIR__ . '/../vendor/autoload.php exists but is not readable' );
250}