Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 155
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
AutoloadGenerator
0.00% covered (danger)
0.00%
0 / 155
0.00% covered (danger)
0.00%
0 / 11
3660
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 dump
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 parseAutoloads
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 sortPackageMap
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 getFileIdentifier
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPathCode
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 parseAutoloadsType
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
930
 processAutoloads
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
56
 removeLegacyFiles
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 writeAutoloaderFiles
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 writeManifests
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * Autoloader Generator.
4 *
5 * @package automattic/jetpack-autoloader
6 */
7
8namespace Automattic\Jetpack\Autoloader;
9
10use Composer\Composer;
11use Composer\Config;
12use Composer\Installer\InstallationManager;
13use Composer\IO\IOInterface;
14use Composer\Package\PackageInterface;
15use Composer\Repository\InstalledRepositoryInterface;
16use Composer\Util\Filesystem;
17use Composer\Util\PackageSorter;
18
19/**
20 * Class AutoloadGenerator.
21 */
22class AutoloadGenerator {
23
24    const VERSION = '6.0.1';
25
26    /**
27     * IO object.
28     *
29     * @var IOInterface IO object.
30     */
31    private $io;
32
33    /**
34     * The filesystem utility.
35     *
36     * @var Filesystem
37     */
38    private $filesystem;
39
40    /**
41     * Instantiate an AutoloadGenerator object.
42     *
43     * @param IOInterface $io IO object.
44     */
45    public function __construct( IOInterface $io ) {
46        $this->io         = $io;
47        $this->filesystem = new Filesystem();
48    }
49
50    /**
51     * Dump the Jetpack autoloader files.
52     *
53     * @param Composer                     $composer The Composer object.
54     * @param Config                       $config Config object.
55     * @param InstalledRepositoryInterface $localRepo Installed Repository object.
56     * @param PackageInterface             $mainPackage Main Package object.
57     * @param InstallationManager          $installationManager Manager for installing packages.
58     * @param string                       $targetDir Path to the current target directory.
59     * @param bool                         $scanPsrPackages Whether or not PSR packages should be converted to a classmap.
60     * @param string                       $suffix The autoloader suffix.
61     */
62    public function dump(
63        Composer $composer,
64        Config $config,
65        InstalledRepositoryInterface $localRepo,
66        PackageInterface $mainPackage,
67        InstallationManager $installationManager,
68        $targetDir,
69        $scanPsrPackages = false,
70        $suffix = null
71    ) {
72        $this->filesystem->ensureDirectoryExists( $config->get( 'vendor-dir' ) );
73
74        $packageMap = $composer->getAutoloadGenerator()->buildPackageMap( $installationManager, $mainPackage, $localRepo->getCanonicalPackages() );
75        $autoloads  = $this->parseAutoloads( $packageMap, $mainPackage );
76
77        // Convert the autoloads into a format that the manifest generator can consume more easily.
78        $basePath           = $this->filesystem->normalizePath( realpath( getcwd() ) );
79        $vendorPath         = $this->filesystem->normalizePath( realpath( $config->get( 'vendor-dir' ) ) );
80        $processedAutoloads = $this->processAutoloads( $autoloads, $scanPsrPackages, $vendorPath, $basePath );
81        unset( $packageMap, $autoloads );
82
83        // Make sure none of the legacy files remain that can lead to problems with the autoloader.
84        $this->removeLegacyFiles( $vendorPath );
85
86        // Write all of the files now that we're done.
87        $this->writeAutoloaderFiles( $vendorPath . '/jetpack-autoloader/', $suffix );
88        $this->writeManifests( $vendorPath . '/' . $targetDir, $processedAutoloads );
89
90        if ( ! $scanPsrPackages ) {
91            $this->io->writeError( '<warning>You are generating an unoptimized autoloader. If this is a production build, consider using the -o option.</warning>' );
92        }
93    }
94
95    /**
96     * Compiles an ordered list of namespace => path mappings
97     *
98     * @param  array            $packageMap  Array of array(package, installDir-relative-to-composer.json).
99     * @param  PackageInterface $mainPackage Main package instance.
100     *
101     * @return array The list of path mappings.
102     */
103    public function parseAutoloads( array $packageMap, PackageInterface $mainPackage ) {
104        $rootPackageMap = array_shift( $packageMap );
105
106        $sortedPackageMap   = $this->sortPackageMap( $packageMap );
107        $sortedPackageMap[] = $rootPackageMap;
108        array_unshift( $packageMap, $rootPackageMap );
109
110        $psr0     = $this->parseAutoloadsType( $packageMap, 'psr-0', $mainPackage );
111        $psr4     = $this->parseAutoloadsType( $packageMap, 'psr-4', $mainPackage );
112        $classmap = $this->parseAutoloadsType( array_reverse( $sortedPackageMap ), 'classmap', $mainPackage );
113        $files    = $this->parseAutoloadsType( $sortedPackageMap, 'files', $mainPackage );
114
115        krsort( $psr0 );
116        krsort( $psr4 );
117
118        return array(
119            'psr-0'    => $psr0,
120            'psr-4'    => $psr4,
121            'classmap' => $classmap,
122            'files'    => $files,
123        );
124    }
125
126    /**
127     * Sorts packages by dependency weight
128     *
129     * Packages of equal weight retain the original order
130     *
131     * @param  array $packageMap The package map.
132     *
133     * @return array
134     */
135    protected function sortPackageMap( array $packageMap ) {
136        $packages = array();
137        $paths    = array();
138
139        foreach ( $packageMap as $item ) {
140            list( $package, $path ) = $item;
141            $name                   = $package->getName();
142            $packages[ $name ]      = $package;
143            $paths[ $name ]         = $path;
144        }
145
146        $sortedPackages = PackageSorter::sortPackages( $packages );
147
148        $sortedPackageMap = array();
149
150        foreach ( $sortedPackages as $package ) {
151            $name               = $package->getName();
152            $sortedPackageMap[] = array( $packages[ $name ], $paths[ $name ] );
153        }
154
155        return $sortedPackageMap;
156    }
157
158    /**
159     * Returns the file identifier.
160     *
161     * @param PackageInterface $package The package instance.
162     * @param string           $path The path.
163     */
164    protected function getFileIdentifier( PackageInterface $package, $path ) {
165        return md5( $package->getName() . ':' . $path );
166    }
167
168    /**
169     * Returns the path code for the given path.
170     *
171     * @param Filesystem $filesystem The filesystem instance.
172     * @param string     $basePath The base path.
173     * @param string     $vendorPath The vendor path.
174     * @param string     $path The path.
175     *
176     * @return string The path code.
177     */
178    protected function getPathCode( Filesystem $filesystem, $basePath, $vendorPath, $path ) {
179        if ( ! $filesystem->isAbsolutePath( $path ) ) {
180            $path = $basePath . '/' . $path;
181        }
182        $path = $filesystem->normalizePath( $path );
183
184        $baseDir = '';
185        if ( 0 === strpos( $path . '/', $vendorPath . '/' ) ) {
186            $path    = substr( $path, strlen( $vendorPath ) );
187            $baseDir = '$vendorDir';
188
189            if ( false !== $path ) {
190                $baseDir .= ' . ';
191            }
192        } else {
193            $path = $filesystem->normalizePath( $filesystem->findShortestPath( $basePath, $path, true ) );
194            if ( ! $filesystem->isAbsolutePath( $path ) ) {
195                $baseDir = '$baseDir . ';
196                $path    = '/' . $path;
197            }
198        }
199
200        if ( strpos( $path, '.phar' ) !== false ) {
201            $baseDir = "'phar://' . " . $baseDir;
202        }
203
204        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
205        return $baseDir . ( ( false !== $path ) ? var_export( $path, true ) : '' );
206    }
207
208    /**
209     * This function differs from the composer parseAutoloadsType in that beside returning the path.
210     * It also return the path and the version of a package.
211     *
212     * Supports PSR-4, PSR-0, and classmap parsing.
213     *
214     * @param array            $packageMap Map of all the packages.
215     * @param string           $type Type of autoloader to use.
216     * @param PackageInterface $mainPackage Instance of the Package Object.
217     *
218     * @return array
219     */
220    protected function parseAutoloadsType( array $packageMap, $type, PackageInterface $mainPackage ) {
221        $autoloads = array();
222
223        foreach ( $packageMap as $item ) {
224            list($package, $installPath) = $item;
225            $autoload                    = $package->getAutoload();
226            $version                     = $package->getVersion(); // Version of the class comes from the package - should we try to parse it?
227
228            // Store our own actual package version, not "dev-trunk" or whatever.
229            if ( $package->getName() === 'automattic/jetpack-autoloader' ) {
230                $version = self::VERSION;
231            }
232
233            if ( $package === $mainPackage ) {
234                $autoload = array_merge_recursive( $autoload, $package->getDevAutoload() );
235            }
236
237            if ( null !== $package->getTargetDir() && $package !== $mainPackage ) {
238                $installPath = substr( $installPath, 0, -strlen( '/' . $package->getTargetDir() ) );
239            }
240
241            if ( in_array( $type, array( 'psr-4', 'psr-0' ), true ) && isset( $autoload[ $type ] ) && is_array( $autoload[ $type ] ) ) {
242                foreach ( $autoload[ $type ] as $namespace => $paths ) {
243                    $paths = is_array( $paths ) ? $paths : array( $paths );
244                    foreach ( $paths as $path ) {
245                        $relativePath              = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path;
246                        $autoloads[ $namespace ][] = array(
247                            'path'    => $relativePath,
248                            'version' => $version,
249                        );
250                    }
251                }
252            }
253
254            if ( 'classmap' === $type && isset( $autoload['classmap'] ) && is_array( $autoload['classmap'] ) ) {
255                foreach ( $autoload['classmap'] as $paths ) {
256                    $paths = is_array( $paths ) ? $paths : array( $paths );
257                    foreach ( $paths as $path ) {
258                        $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path;
259                        $autoloads[]  = array(
260                            'path'    => $relativePath,
261                            'version' => $version,
262                        );
263                    }
264                }
265            }
266            if ( 'files' === $type && isset( $autoload['files'] ) && is_array( $autoload['files'] ) ) {
267                foreach ( $autoload['files'] as $paths ) {
268                    $paths = is_array( $paths ) ? $paths : array( $paths );
269                    foreach ( $paths as $path ) {
270                        $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path;
271                        $autoloads[ $this->getFileIdentifier( $package, $path ) ] = array(
272                            'path'    => $relativePath,
273                            'version' => $version,
274                        );
275                    }
276                }
277            }
278        }
279
280        return $autoloads;
281    }
282
283    /**
284     * Given Composer's autoloads this will convert them to a version that we can use to generate the manifests.
285     *
286     * When the $scanPsrPackages argument is true, PSR-4 namespaces are converted to classmaps. When $scanPsrPackages
287     * is false, PSR-4 namespaces are not converted to classmaps.
288     *
289     * PSR-0 namespaces are always converted to classmaps.
290     *
291     * @param array  $autoloads The autoloads we want to process.
292     * @param bool   $scanPsrPackages Whether or not PSR-4 packages should be converted to a classmap.
293     * @param string $vendorPath The path to the vendor directory.
294     * @param string $basePath The path to the current directory.
295     *
296     * @return array $processedAutoloads
297     */
298    private function processAutoloads( $autoloads, $scanPsrPackages, $vendorPath, $basePath ) {
299        $processor = new AutoloadProcessor(
300            function ( $path, $excludedClasses, $namespace ) use ( $basePath ) {
301                $dir = $this->filesystem->normalizePath(
302                    $this->filesystem->isAbsolutePath( $path ) ? $path : $basePath . '/' . $path
303                );
304
305                // Composer 2.4 changed the name of the class.
306                if ( class_exists( \Composer\ClassMapGenerator\ClassMapGenerator::class ) ) {
307                    if ( ! is_dir( $dir ) && ! is_file( $dir ) ) {
308                        return array();
309                    }
310                    $generator = new \Composer\ClassMapGenerator\ClassMapGenerator();
311                    $generator->scanPaths( $dir, $excludedClasses, 'classmap', empty( $namespace ) ? null : $namespace );
312                    return $generator->getClassMap()->getMap();
313                }
314
315                return \Composer\Autoload\ClassMapGenerator::createMap(
316                    $dir,
317                    $excludedClasses,
318                    null, // Don't pass the IOInterface since the normal autoload generation will have reported already.
319                    empty( $namespace ) ? null : $namespace
320                );
321            },
322            function ( $path ) use ( $basePath, $vendorPath ) {
323                return $this->getPathCode( $this->filesystem, $basePath, $vendorPath, $path );
324            }
325        );
326
327        return array(
328            'psr-4'    => $processor->processPsr4Packages( $autoloads, $scanPsrPackages ),
329            'classmap' => $processor->processClassmap( $autoloads, $scanPsrPackages ),
330            'files'    => $processor->processFiles( $autoloads ),
331        );
332    }
333
334    /**
335     * Removes all of the legacy autoloader files so they don't cause any problems.
336     *
337     * @param string $outDir The directory legacy files are written to.
338     */
339    private function removeLegacyFiles( $outDir ) {
340        $files = array(
341            'autoload_functions.php',
342            'class-autoloader-handler.php',
343            'class-classes-handler.php',
344            'class-files-handler.php',
345            'class-plugins-handler.php',
346            'class-version-selector.php',
347        );
348        foreach ( $files as $file ) {
349            $this->filesystem->remove( $outDir . '/' . $file );
350        }
351    }
352
353    /**
354     * Writes all of the autoloader files to disk.
355     *
356     * @param string $outDir The directory to write to.
357     * @param string $suffix The unique autoloader suffix.
358     */
359    private function writeAutoloaderFiles( $outDir, $suffix ) {
360        $this->io->writeError( "<info>Generating jetpack autoloader ($outDir)</info>" );
361
362        // We will remove all autoloader files to generate this again.
363        $this->filesystem->emptyDirectory( $outDir );
364
365        // Write the autoloader files.
366        AutoloadFileWriter::copyAutoloaderFiles( $this->io, $outDir, $suffix );
367    }
368
369    /**
370     * Writes all of the manifest files to disk.
371     *
372     * @param string $outDir The directory to write to.
373     * @param array  $processedAutoloads The processed autoloads.
374     */
375    private function writeManifests( $outDir, $processedAutoloads ) {
376        $this->io->writeError( "<info>Generating jetpack autoloader manifests ($outDir)</info>" );
377
378        $manifestFiles = array(
379            'classmap' => 'jetpack_autoload_classmap.php',
380            'psr-4'    => 'jetpack_autoload_psr4.php',
381            'files'    => 'jetpack_autoload_filemap.php',
382        );
383
384        foreach ( $manifestFiles as $key => $file ) {
385            // Make sure the file doesn't exist so it isn't there if we don't write it.
386            $this->filesystem->remove( $outDir . '/' . $file );
387            if ( empty( $processedAutoloads[ $key ] ) ) {
388                continue;
389            }
390
391            $content = ManifestGenerator::buildManifest( $key, $file, $processedAutoloads[ $key ] );
392            if ( empty( $content ) ) {
393                continue;
394            }
395
396            if ( file_put_contents( $outDir . '/' . $file, $content ) ) {
397                $this->io->writeError( "  <info>Generated: $file</info>" );
398            } else {
399                $this->io->writeError( "  <error>Error: $file</error>" );
400            }
401        }
402    }
403}