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
76 public function __construct( $basepath, $flags = [] ) {
77 if ( !is_array( $flags ) ) {
78 $flags = [ $flags ];
79 }
80 $this->basepath = self::normalizePathSeparator( realpath( $basepath ) );
81 $this->collector = new ClassCollector;
82 if ( in_array( 'local', $flags ) ) {
83 $this->variableName = 'wgAutoloadLocalClasses';
84 }
85 }
86
93 public function setExcludePaths( array $paths ) {
94 foreach ( $paths as $path ) {
95 $this->excludePaths[] = self::normalizePathSeparator( $path );
96 }
97 }
98
105 private function shouldExclude( $path ) {
106 foreach ( $this->excludePaths as $dir ) {
107 if ( str_starts_with( $path, $dir ) ) {
108 return true;
109 }
110 }
111
112 return false;
113 }
114
123 public function forceClassPath( $fqcn, $inputPath ) {
124 $path = self::normalizePathSeparator( realpath( $inputPath ) );
125 if ( !$path ) {
126 throw new InvalidArgumentException( "Invalid path: $inputPath" );
127 }
128 if ( !str_starts_with( $path, $this->basepath ) ) {
129 throw new InvalidArgumentException( "Path is not within basepath: $inputPath" );
130 }
131 $shortpath = substr( $path, strlen( $this->basepath ) );
132 $this->overrides[$fqcn] = $shortpath;
133 }
134
139 public function readFile( $inputPath ) {
140 // NOTE: do NOT expand $inputPath using realpath(). It is perfectly
141 // reasonable for LocalSettings.php and similar files to be symlinks
142 // to files that are outside of $this->basepath.
143 $inputPath = self::normalizePathSeparator( $inputPath );
144 $len = strlen( $this->basepath );
145 if ( !str_starts_with( $inputPath, $this->basepath ) ) {
146 throw new InvalidArgumentException( "Path is not within basepath: $inputPath" );
147 }
148 if ( $this->shouldExclude( $inputPath ) ) {
149 return;
150 }
151 $fileContents = file_get_contents( $inputPath );
152
153 // Skip files that declare themselves excluded
154 if ( preg_match( '!^// *NO_AUTOLOAD!m', $fileContents ) ) {
155 return;
156 }
157 // Skip files that use CommandLineInc since these execute file-scope
158 // code when included
159 if ( preg_match(
160 '/(require|require_once)[ (].*(CommandLineInc.php|commandLine.inc)/',
161 $fileContents )
162 ) {
163 return;
164 }
165
166 $result = $this->collector->getClasses( $fileContents );
167
168 if ( $result ) {
169 $shortpath = substr( $inputPath, $len );
170 $this->classes[$shortpath] = $result;
171 }
172 }
173
177 public function readDir( $dir ) {
178 $it = new RecursiveDirectoryIterator(
179 self::normalizePathSeparator( realpath( $dir ) ) );
180 $it = new RecursiveIteratorIterator( $it );
181
182 foreach ( $it as $path => $file ) {
183 if ( pathinfo( $path, PATHINFO_EXTENSION ) === 'php' ) {
184 $this->readFile( $path );
185 }
186 }
187 }
188
197 protected function generateJsonAutoload( $filename ) {
198 $key = 'AutoloadClasses';
199 $json = FormatJson::decode( file_get_contents( $filename ), true );
200 unset( $json[$key] );
201 // Inverting the key-value pairs so that they become of the
202 // format class-name : path when they get converted into json.
203 foreach ( $this->classes as $path => $contained ) {
204 foreach ( $contained as $fqcn ) {
205 // Using substr to remove the leading '/'
206 $json[$key][$fqcn] = substr( $path, 1 );
207 }
208 }
209 foreach ( $this->overrides as $path => $fqcn ) {
210 // Using substr to remove the leading '/'
211 $json[$key][$fqcn] = substr( $path, 1 );
212 }
213
214 // Sorting the list of autoload classes.
215 ksort( $json[$key] );
216
217 // Return the whole JSON file
218 return FormatJson::encode( $json, "\t", FormatJson::ALL_OK ) . "\n";
219 }
220
228 protected function generatePHPAutoload( $commandName, $filename ) {
229 // No existing JSON file found; update/generate PHP file
230 $content = [];
231
232 // We need to generate a line each rather than exporting the
233 // full array so __DIR__ can be prepended to all the paths
234 $format = "%s => __DIR__ . %s,";
235 foreach ( $this->classes as $path => $contained ) {
236 $exportedPath = var_export( $path, true );
237 foreach ( $contained as $fqcn ) {
238 $content[$fqcn] = sprintf(
239 $format,
240 var_export( $fqcn, true ),
241 $exportedPath
242 );
243 }
244 }
245
246 foreach ( $this->overrides as $fqcn => $path ) {
247 $content[$fqcn] = sprintf(
248 $format,
249 var_export( $fqcn, true ),
250 var_export( $path, true )
251 );
252 }
253
254 // sort for stable output
255 ksort( $content );
256
257 // extensions using this generator are appending to the existing
258 // autoload.
259 if ( $this->variableName === 'wgAutoloadClasses' ) {
260 $op = '+=';
261 } else {
262 $op = '=';
263 }
264
265 $output = implode( "\n\t", $content );
266 return <<<EOD
267<?php
268// This file is generated by $commandName, do not adjust manually
269// phpcs:disable Generic.Files.LineLength
270global \${$this->variableName};
271
272\${$this->variableName} {$op} [
273 {$output}
274];
275
276EOD;
277 }
278
287 public function getAutoload( $commandName = 'AutoloadGenerator' ) {
288 // We need to check whether an extension.json or skin.json exists or not, and
289 // incase it doesn't, update the autoload.php file.
290
291 $fileinfo = $this->getTargetFileinfo();
292
293 if ( $fileinfo['type'] === self::FILETYPE_JSON ) {
294 return $this->generateJsonAutoload( $fileinfo['filename'] );
295 }
296
297 return $this->generatePHPAutoload( $commandName, $fileinfo['filename'] );
298 }
299
308 public function getTargetFileinfo() {
309 if ( file_exists( $this->basepath . '/extension.json' ) ) {
310 return [
311 'filename' => $this->basepath . '/extension.json',
312 'type' => self::FILETYPE_JSON
313 ];
314 }
315 if ( file_exists( $this->basepath . '/skin.json' ) ) {
316 return [
317 'filename' => $this->basepath . '/skin.json',
318 'type' => self::FILETYPE_JSON
319 ];
320 }
321
322 return [
323 'filename' => $this->basepath . '/autoload.php',
324 'type' => self::FILETYPE_PHP
325 ];
326 }
327
334 protected static function normalizePathSeparator( $path ) {
335 return str_replace( '\\', '/', $path );
336 }
337
347 public function initMediaWikiDefault() {
348 foreach ( [ 'includes', 'languages', 'maintenance', 'mw-config' ] as $dir ) {
349 $this->readDir( $this->basepath . '/' . $dir );
350 }
351 foreach ( glob( $this->basepath . '/*.php' ) as $file ) {
352 $this->readFile( $file );
353 }
354 }
355}
356
358class_alias( AutoloadGenerator::class, 'AutoloadGenerator' );
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.
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...
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.