MediaWiki master
AutoloadGenerator.php
Go to the documentation of this file.
1<?php
8
9use InvalidArgumentException;
11use RecursiveDirectoryIterator;
12use RecursiveIteratorIterator;
13
34 private const FILETYPE_JSON = 'json';
35 private const FILETYPE_PHP = 'php';
36
40 protected $basepath;
41
45 protected $collector;
46
50 protected $classes = [];
51
55 protected $variableName = 'wgAutoloadClasses';
56
60 protected $overrides = [];
61
67 protected $excludePaths = [];
68
74 protected $psr4Namespaces = [];
75
83 public function __construct( $basepath, $flags = [] ) {
84 if ( !is_array( $flags ) ) {
85 $flags = [ $flags ];
86 }
87 $this->basepath = self::normalizePathSeparator( realpath( $basepath ) );
88 $this->collector = new ClassCollector;
89 if ( in_array( 'local', $flags ) ) {
90 $this->variableName = 'wgAutoloadLocalClasses';
91 }
92 }
93
100 public function setExcludePaths( array $paths ) {
101 foreach ( $paths as $path ) {
102 $this->excludePaths[] = self::normalizePathSeparator( $path );
103 }
104 }
105
114 public function setPsr4Namespaces( array $namespaces ) {
115 wfDeprecated( __METHOD__, '1.40' );
116 foreach ( $namespaces as $ns => $path ) {
117 $ns = rtrim( $ns, '\\' ) . '\\';
118 $this->psr4Namespaces[$ns] = rtrim( self::normalizePathSeparator( $path ), '/' );
119 }
120 }
121
128 private function shouldExclude( $path ) {
129 foreach ( $this->excludePaths as $dir ) {
130 if ( str_starts_with( $path, $dir ) ) {
131 return true;
132 }
133 }
134
135 return false;
136 }
137
146 public function forceClassPath( $fqcn, $inputPath ) {
147 $path = self::normalizePathSeparator( realpath( $inputPath ) );
148 if ( !$path ) {
149 throw new InvalidArgumentException( "Invalid path: $inputPath" );
150 }
151 if ( !str_starts_with( $path, $this->basepath ) ) {
152 throw new InvalidArgumentException( "Path is not within basepath: $inputPath" );
153 }
154 $shortpath = substr( $path, strlen( $this->basepath ) );
155 $this->overrides[$fqcn] = $shortpath;
156 }
157
162 public function readFile( $inputPath ) {
163 // NOTE: do NOT expand $inputPath using realpath(). It is perfectly
164 // reasonable for LocalSettings.php and similar files to be symlinks
165 // to files that are outside of $this->basepath.
166 $inputPath = self::normalizePathSeparator( $inputPath );
167 $len = strlen( $this->basepath );
168 if ( !str_starts_with( $inputPath, $this->basepath ) ) {
169 throw new InvalidArgumentException( "Path is not within basepath: $inputPath" );
170 }
171 if ( $this->shouldExclude( $inputPath ) ) {
172 return;
173 }
174 $fileContents = file_get_contents( $inputPath );
175
176 // Skip files that declare themselves excluded
177 if ( preg_match( '!^// *NO_AUTOLOAD!m', $fileContents ) ) {
178 return;
179 }
180 // Skip files that use CommandLineInc since these execute file-scope
181 // code when included
182 if ( preg_match(
183 '/(require|require_once)[ (].*(CommandLineInc.php|commandLine.inc)/',
184 $fileContents )
185 ) {
186 return;
187 }
188
189 $result = $this->collector->getClasses( $fileContents );
190
191 if ( $result ) {
192 $shortpath = substr( $inputPath, $len );
193 $this->classes[$shortpath] = $result;
194 }
195 }
196
200 public function readDir( $dir ) {
201 $it = new RecursiveDirectoryIterator(
202 self::normalizePathSeparator( realpath( $dir ) ) );
203 $it = new RecursiveIteratorIterator( $it );
204
205 foreach ( $it as $path => $file ) {
206 if ( pathinfo( $path, PATHINFO_EXTENSION ) === 'php' ) {
207 $this->readFile( $path );
208 }
209 }
210 }
211
220 protected function generateJsonAutoload( $filename ) {
221 $key = 'AutoloadClasses';
222 $json = FormatJson::decode( file_get_contents( $filename ), true );
223 unset( $json[$key] );
224 // Inverting the key-value pairs so that they become of the
225 // format class-name : path when they get converted into json.
226 foreach ( $this->classes as $path => $contained ) {
227 foreach ( $contained as $fqcn ) {
228 // Using substr to remove the leading '/'
229 $json[$key][$fqcn] = substr( $path, 1 );
230 }
231 }
232 foreach ( $this->overrides as $path => $fqcn ) {
233 // Using substr to remove the leading '/'
234 $json[$key][$fqcn] = substr( $path, 1 );
235 }
236
237 // Sorting the list of autoload classes.
238 ksort( $json[$key] );
239
240 // Return the whole JSON file
241 return FormatJson::encode( $json, "\t", FormatJson::ALL_OK ) . "\n";
242 }
243
251 protected function generatePHPAutoload( $commandName, $filename ) {
252 // No existing JSON file found; update/generate PHP file
253 $content = [];
254
255 // We need to generate a line each rather than exporting the
256 // full array so __DIR__ can be prepended to all the paths
257 $format = "%s => __DIR__ . %s,";
258 foreach ( $this->classes as $path => $contained ) {
259 $exportedPath = var_export( $path, true );
260 foreach ( $contained as $fqcn ) {
261 $content[$fqcn] = sprintf(
262 $format,
263 var_export( $fqcn, true ),
264 $exportedPath
265 );
266 }
267 }
268
269 foreach ( $this->overrides as $fqcn => $path ) {
270 $content[$fqcn] = sprintf(
271 $format,
272 var_export( $fqcn, true ),
273 var_export( $path, true )
274 );
275 }
276
277 // sort for stable output
278 ksort( $content );
279
280 // extensions using this generator are appending to the existing
281 // autoload.
282 if ( $this->variableName === 'wgAutoloadClasses' ) {
283 $op = '+=';
284 } else {
285 $op = '=';
286 }
287
288 $output = implode( "\n\t", $content );
289 return <<<EOD
290<?php
291// This file is generated by $commandName, do not adjust manually
292// phpcs:disable Generic.Files.LineLength
293global \${$this->variableName};
294
295\${$this->variableName} {$op} [
296 {$output}
297];
298
299EOD;
300 }
301
310 public function getAutoload( $commandName = 'AutoloadGenerator' ) {
311 // We need to check whether an extension.json or skin.json exists or not, and
312 // incase it doesn't, update the autoload.php file.
313
314 $fileinfo = $this->getTargetFileinfo();
315
316 if ( $fileinfo['type'] === self::FILETYPE_JSON ) {
317 return $this->generateJsonAutoload( $fileinfo['filename'] );
318 }
319
320 return $this->generatePHPAutoload( $commandName, $fileinfo['filename'] );
321 }
322
331 public function getTargetFileinfo() {
332 if ( file_exists( $this->basepath . '/extension.json' ) ) {
333 return [
334 'filename' => $this->basepath . '/extension.json',
335 'type' => self::FILETYPE_JSON
336 ];
337 }
338 if ( file_exists( $this->basepath . '/skin.json' ) ) {
339 return [
340 'filename' => $this->basepath . '/skin.json',
341 'type' => self::FILETYPE_JSON
342 ];
343 }
344
345 return [
346 'filename' => $this->basepath . '/autoload.php',
347 'type' => self::FILETYPE_PHP
348 ];
349 }
350
357 protected static function normalizePathSeparator( $path ) {
358 return str_replace( '\\', '/', $path );
359 }
360
370 public function initMediaWikiDefault() {
371 foreach ( [ 'includes', 'languages', 'maintenance', 'mw-config' ] as $dir ) {
372 $this->readDir( $this->basepath . '/' . $dir );
373 }
374 foreach ( glob( $this->basepath . '/*.php' ) as $file ) {
375 $this->readFile( $file );
376 }
377 }
378}
379
381class_alias( AutoloadGenerator::class, 'AutoloadGenerator' );
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Scan given directories and files and create an autoload class map.
string $basepath
Root path of the project being scanned for classes.
static normalizePathSeparator( $path)
Ensure that Unix-style path separators ("/") are used in the path.
string $variableName
The global variable to write output to.
string[] $psr4Namespaces
Configured PSR4 namespaces.
generateJsonAutoload( $filename)
Updates the AutoloadClasses field at the given filename.
string[] $excludePaths
Directories that should be excluded.
array $classes
Map of file shortpath to list of FQCN detected within file.
getTargetFileinfo()
Returns the filename of the extension.json of skin.json, if there's any, or otherwise the path to the...
initMediaWikiDefault()
Initialize the source files and directories which are used for the MediaWiki default autoloader in {m...
setPsr4Namespaces(array $namespaces)
Unlike self::setExcludePaths(), this will only skip outputting the autoloader entry when the namespac...
ClassCollector $collector
Helper class extracts class names from php files.
forceClassPath( $fqcn, $inputPath)
Force a class to be autoloaded from a specific path, regardless of where or if it was detected.
generatePHPAutoload( $commandName, $filename)
Generates a PHP file setting up autoload information.
setExcludePaths(array $paths)
Directories that should be excluded.
array $overrides
Map of FQCN to relative path(from self::$basepath)
getAutoload( $commandName='AutoloadGenerator')
Returns all known classes as a string, which can be used to put into a target file (e....
Read a PHP file and return the FQCN of every class defined within it.
JSON formatter wrapper class.