This commit is contained in:
KhaiNguyen
2020-02-13 10:39:37 +07:00
commit 59401cb805
12867 changed files with 4646216 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
A custom autoloader for Composer
=====================================
This is a custom autoloader generator that uses a classmap to always load the latest version of a class.
The problem this autoloader is trying to solve is conflicts that arise when two or more plugins use the same package, but one of the plugins uses an older version of said package.
This is solved by keeping an in memory map of all the different classes that can be loaded, and updating the map with the path to the latest version of the package for the autoloader to find when we instantiate the class.
This only works if we instantiate the class after all the plugins have loaded. That is why the class produces an error if the plugin calls a class but has not loaded all the plugins yet.
It diverges from the default Composer autoloader setup in the following ways:
* It creates an `autoload_classmap_package.php` file in the `vendor/composer` directory.
* This file includes the version numbers from each package that is used.
* The autoloader will only load the latest version of the library no matter what plugin loads the library.
* Only call the library classes after all the plugins have loaded and the `plugins_loaded` action has fired.
Usage
-----
In your project's `composer.json`, add the following lines:
```json
{
"require-dev": {
"automattic/jetpack-autoloader": "^1"
}
}
```
After the next update/install, you will have a `vendor/autoload_packages.php` file.
Load the file in your plugin via main plugin file.
In the main plugin you will also need to include the files like this.
```php
require_once . plugin_dir_path( __FILE__ ) . '/vendor/autoload_packages.php';
```
Current Limitations
-----
We currently only support packages that autoload via psr-4 definition in their package.

View File

@@ -0,0 +1,28 @@
{
"name": "automattic/jetpack-autoloader",
"description": "Creates a custom autoloader for a plugin or theme.",
"type": "composer-plugin",
"license": "GPL-2.0-or-later",
"require": {
"composer-plugin-api": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5"
},
"autoload": {
"psr-4": {
"Automattic\\Jetpack\\Autoloader\\": "src"
}
},
"extra": {
"class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin"
},
"scripts": {
"phpunit": [
"@composer install",
"./vendor/phpunit/phpunit/phpunit --colors=always"
]
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,286 @@
<?php
/**
* Autoloader Generator.
*
* @package automattic/jetpack-autoloader
*/
// phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_useFound
// phpcs:disable PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound
// phpcs:disable PHPCompatibility.FunctionDeclarations.NewClosure.Found
// phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_namespaceFound
// phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_dirFound
// phpcs:disable WordPress.Files.FileName.InvalidClassFileName
// phpcs:disable WordPress.Files.FileName.NotHyphenatedLowercase
// phpcs:disable WordPress.Files.FileName.InvalidClassFileName
// phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_var_export
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fopen
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_fwrite
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// phpcs:disable WordPress.NamingConventions.ValidVariableName.InterpolatedVariableNotSnakeCase
// phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
// phpcs:disable WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase
namespace Automattic\Jetpack\Autoloader;
use Composer\Autoload\AutoloadGenerator as BaseGenerator;
use Composer\Autoload\ClassMapGenerator;
use Composer\Config;
use Composer\Installer\InstallationManager;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use Composer\Repository\InstalledRepositoryInterface;
use Composer\Util\Filesystem;
/**
* Class AutoloadGenerator.
*/
class AutoloadGenerator extends BaseGenerator {
/**
* Instantiate an AutoloadGenerator object.
*
* @param IOInterface $io IO object.
*/
public function __construct( IOInterface $io = null ) {
$this->io = $io;
}
/**
* Dump the autoloader.
*
* @param Config $config Config object.
* @param InstalledRepositoryInterface $localRepo Installed Reposetories object.
* @param PackageInterface $mainPackage Main Package object.
* @param InstallationManager $installationManager Manager for installing packages.
* @param string $targetDir Path to the current target directory.
* @param bool $scanPsr0Packages Whether to search for packages. Currently hard coded to always be false.
* @param string $suffix The autoloader suffix, ignored since we want our autoloader to only be included once.
*/
public function dump(
Config $config,
InstalledRepositoryInterface $localRepo,
PackageInterface $mainPackage,
InstallationManager $installationManager,
$targetDir,
$scanPsr0Packages = null, // Not used we always optimize.
$suffix = null
) {
$filesystem = new Filesystem();
$filesystem->ensureDirectoryExists( $config->get( 'vendor-dir' ) );
$basePath = $filesystem->normalizePath( realpath( getcwd() ) );
$vendorPath = $filesystem->normalizePath( realpath( $config->get( 'vendor-dir' ) ) );
$targetDir = $vendorPath . '/' . $targetDir;
$filesystem->ensureDirectoryExists( $targetDir );
$packageMap = $this->buildPackageMap( $installationManager, $mainPackage, $localRepo->getCanonicalPackages() );
$autoloads = $this->parseAutoloads( $packageMap, $mainPackage );
$classMap = $this->getClassMap( $autoloads, $filesystem, $vendorPath, $basePath );
// Generate the files.
file_put_contents( $targetDir . '/autoload_classmap_package.php', $this->getAutoloadClassmapPackagesFile( $classMap ) );
$this->io->writeError( '<info>Generated ' . $targetDir . '/autoload_classmap_package.php</info>', true );
file_put_contents( $vendorPath . '/autoload_packages.php', $this->getAutoloadPackageFile( $suffix ) );
$this->io->writeError( '<info>Generated ' . $vendorPath . '/autoload_packages.php</info>', true );
}
/**
* This function differs from the composer parseAutoloadsType in that beside returning the path.
* It also return the path and the version of a package.
*
* Currently supports only psr-4 and clasmap parsing.
*
* @param array $packageMap Map of all the packages.
* @param string $type Type of autoloader to use, currently not used, since we only support psr-4.
* @param PackageInterface $mainPackage Instance of the Package Object.
*
* @return array
*/
protected function parseAutoloadsType( array $packageMap, $type, PackageInterface $mainPackage ) {
$autoloads = array();
if ( 'psr-4' !== $type && 'classmap' !== $type ) {
return parent::parseAutoloadsType( $packageMap, $type, $mainPackage );
}
foreach ( $packageMap as $item ) {
list($package, $installPath) = $item;
$autoload = $package->getAutoload();
if ( $package === $mainPackage ) {
$autoload = array_merge_recursive( $autoload, $package->getDevAutoload() );
}
if ( null !== $package->getTargetDir() && $package !== $mainPackage ) {
$installPath = substr( $installPath, 0, -strlen( '/' . $package->getTargetDir() ) );
}
if ( 'psr-4' === $type && isset( $autoload['psr-4'] ) && is_array( $autoload['psr-4'] ) ) {
foreach ( $autoload['psr-4'] as $namespace => $paths ) {
$paths = is_array( $paths ) ? $paths : array( $paths );
foreach ( $paths as $path ) {
$relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path;
$autoloads[ $namespace ][] = array(
'path' => $relativePath,
'version' => $package->getVersion(), // Version of the class comes from the package - should we try to parse it?
);
}
}
}
if ( 'classmap' === $type && isset( $autoload['classmap'] ) && is_array( $autoload['classmap'] ) ) {
foreach ( $autoload['classmap'] as $paths ) {
$paths = is_array( $paths ) ? $paths : array( $paths );
foreach ( $paths as $path ) {
$relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path;
$autoloads[] = array(
'path' => $relativePath,
'version' => $package->getVersion(), // Version of the class comes from the package - should we try to parse it?
);
}
}
}
}
return $autoloads;
}
/**
* Take the autoloads array and return the classMap that contains the path and the version for each namespace.
*
* @param array $autoloads Array of autoload settings defined defined by the packages.
* @param Filesystem $filesystem Filesystem class instance.
* @param string $vendorPath Path to the vendor directory.
* @param string $basePath Base Path.
*
* @return array $classMap
*/
private function getClassMap( array $autoloads, Filesystem $filesystem, $vendorPath, $basePath ) {
$blacklist = null;
if ( ! empty( $autoloads['exclude-from-classmap'] ) ) {
$blacklist = '{(' . implode( '|', $autoloads['exclude-from-classmap'] ) . ')}';
}
$classmapString = '';
// Scan the PSR-4 and classmap directories for class files, and add them to the class map.
foreach ( $autoloads['psr-4'] as $namespace => $packages_info ) {
foreach ( $packages_info as $package ) {
$dir = $filesystem->normalizePath(
$filesystem->isAbsolutePath( $package['path'] )
? $package['path']
: $basePath . '/' . $package['path']
);
$namespace = empty( $namespace ) ? null : $namespace;
$map = ClassMapGenerator::createMap( $dir, $blacklist, $this->io, $namespace );
foreach ( $map as $class => $path ) {
$classCode = var_export( $class, true );
$pathCode = $this->getPathCode( $filesystem, $basePath, $vendorPath, $path );
$versionCode = var_export( $package['version'], true );
$classmapString .= <<<CLASS_CODE
$classCode => array(
'version' => $versionCode,
'path' => $pathCode
),
CLASS_CODE;
$classmapString .= PHP_EOL;
}
}
}
foreach ( $autoloads['classmap'] as $package ) {
$dir = $filesystem->normalizePath(
$filesystem->isAbsolutePath( $package['path'] )
? $package['path']
: $basePath . '/' . $package['path']
);
$map = ClassMapGenerator::createMap( $dir, $blacklist, $this->io, null );
foreach ( $map as $class => $path ) {
$classCode = var_export( $class, true );
$pathCode = $this->getPathCode( $filesystem, $basePath, $vendorPath, $path );
$versionCode = var_export( $package['version'], true );
$classmapString .= <<<CLASS_CODE
$classCode => array(
'version' => $versionCode,
'path' => $pathCode
),
CLASS_CODE;
$classmapString .= PHP_EOL;
}
}
return 'array( ' . PHP_EOL . $classmapString . ');' . PHP_EOL;
}
/**
* Generate the PHP that will be used in the autoload_classmap_package.php files.
*
* @param srting $classMap class map array string that is to be written out to the file.
*
* @return string
*/
private function getAutoloadClassmapPackagesFile( $classMap ) {
return <<<INCLUDE_CLASSMAP
<?php
// This file `autoload_classmap_packages.php` was auto generated by automattic/jetpack-autoloader.
\$vendorDir = dirname(__DIR__);
\$baseDir = dirname(\$vendorDir);
return $classMap
INCLUDE_CLASSMAP;
}
/**
* Generate the PHP that will be used in the autoload_packages.php files.
*
* @param string $suffix Unique suffix added to the jetpack_enqueue_packages function.
*
* @return string
*/
private function getAutoloadPackageFile( $suffix ) {
$sourceLoader = fopen( __DIR__ . '/autoload.php', 'r' );
$file_contents = stream_get_contents( $sourceLoader );
$file_contents .= <<<INCLUDE_FILES
/**
* Prepare all the classes for autoloading.
*/
function enqueue_packages_$suffix() {
\$class_map = require_once dirname( __FILE__ ) . '/composer/autoload_classmap_package.php';
foreach ( \$class_map as \$class_name => \$class_info ) {
enqueue_package_class( \$class_name, \$class_info['version'], \$class_info['path'] );
}
\$autoload_file = __DIR__ . '/composer/autoload_files.php';
\$includeFiles = file_exists( \$autoload_file )
? require \$autoload_file
: array();
foreach ( \$includeFiles as \$fileIdentifier => \$file ) {
if ( empty( \$GLOBALS['__composer_autoload_files'][ \$fileIdentifier ] ) ) {
require \$file;
\$GLOBALS['__composer_autoload_files'][ \$fileIdentifier ] = true;
}
}
}
enqueue_packages_$suffix();
INCLUDE_FILES;
return $file_contents;
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* Custom Autoloader Composer Plugin, hooks into composer events to generate the custom autoloader.
*
* @package automattic/jetpack-autoloader
*/
// phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_useFound
// phpcs:disable PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound
// phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_namespaceFound
// phpcs:disable WordPress.Files.FileName.NotHyphenatedLowercase
// phpcs:disable WordPress.Files.FileName.InvalidClassFileName
// phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
namespace Automattic\Jetpack\Autoloader;
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Plugin\PluginInterface;
use Composer\EventDispatcher\EventSubscriberInterface;
/**
* Class CustomAutoloaderPlugin.
*
* @package automattic/jetpack-autoloader
*/
class CustomAutoloaderPlugin implements PluginInterface, EventSubscriberInterface {
/**
* IO object.
*
* @var IOInterface IO object.
*/
private $io;
/**
* Composer object.
*
* @var Composer Composer object.
*/
private $composer;
/**
* Do nothing.
*
* @param Composer $composer Composer object.
* @param IOInterface $io IO object.
*/
public function activate( Composer $composer, IOInterface $io ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$this->composer = $composer;
$this->io = $io;
}
/**
* Tell composer to listen for events and do something with them.
*
* @return array List of succribed events.
*/
public static function getSubscribedEvents() {
return array(
ScriptEvents::POST_AUTOLOAD_DUMP => 'postAutoloadDump',
);
}
/**
* Generate the custom autolaoder.
*
* @param Event $event Script event object.
*/
public function postAutoloadDump( Event $event ) {
$installationManager = $this->composer->getInstallationManager();
$repoManager = $this->composer->getRepositoryManager();
$localRepo = $repoManager->getLocalRepository();
$package = $this->composer->getPackage();
$config = $this->composer->getConfig();
$optimize = true;
$suffix = $config->get( 'autoloader-suffix' )
? $config->get( 'autoloader-suffix' )
: md5( uniqid( '', true ) );
$generator = new AutoloadGenerator( $this->io );
$generator->dump( $config, $localRepo, $package, $installationManager, 'composer', $optimize, $suffix );
$this->generated = true;
}
}

View File

@@ -0,0 +1,102 @@
<?php
/**
* This file `autoload_packages.php`was generated by automattic/jetpack-autoloader.
*
* From your plugin include this file with:
* require_once . plugin_dir_path( __FILE__ ) . '/vendor/autoload_packages.php';
*
* @package automattic/jetpack-autoloader
*/
// phpcs:disable PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound
// phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_namespaceFound
// phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_ns_cFound
namespace Automattic\Jetpack\Autoloader;
if ( ! function_exists( __NAMESPACE__ . '\enqueue_package_class' ) ) {
global $jetpack_packages_classes;
if ( ! is_array( $jetpack_packages_classes ) ) {
$jetpack_packages_classes = array();
}
/**
* Adds the version of a package to the $jetpack_packages global array so that
* the autoloader is able to find it.
*
* @param string $class_name Name of the class that you want to autoload.
* @param string $version Version of the class.
* @param string $path Absolute path to the class so that we can load it.
*/
function enqueue_package_class( $class_name, $version, $path ) {
global $jetpack_packages_classes;
if ( ! isset( $jetpack_packages_classes[ $class_name ] ) ) {
$jetpack_packages_classes[ $class_name ] = array(
'version' => $version,
'path' => $path,
);
}
// If we have a @dev version set always use that one!
if ( 'dev-' === substr( $jetpack_packages_classes[ $class_name ]['version'], 0, 4 ) ) {
return;
}
// Always favour the @dev version. Since that version is the same as bleeding edge.
// We need to make sure that we don't do this in production!
if ( 'dev-' === substr( $version, 0, 4 ) ) {
$jetpack_packages_classes[ $class_name ] = array(
'version' => $version,
'path' => $path,
);
return;
}
// Set the latest version!
if ( version_compare( $jetpack_packages_classes[ $class_name ]['version'], $version, '<' ) ) {
$jetpack_packages_classes[ $class_name ] = array(
'version' => $version,
'path' => $path,
);
}
}
}
if ( ! function_exists( __NAMESPACE__ . '\autoloader' ) ) {
/**
* Used for autoloading jetpack packages.
*
* @param string $class_name Class Name to load.
*/
function autoloader( $class_name ) {
global $jetpack_packages_classes;
if ( isset( $jetpack_packages_classes[ $class_name ] ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
if ( function_exists( 'did_action' ) && ! did_action( 'plugins_loaded' ) ) {
_doing_it_wrong(
esc_html( $class_name ),
sprintf(
/* translators: %s Name of a PHP Class */
esc_html__( 'Not all plugins have loaded yet but we requested the class %s', 'jetpack' ),
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$class_name
),
esc_html( $jetpack_packages_classes[ $class_name ]['version'] )
);
}
}
if ( file_exists( $jetpack_packages_classes[ $class_name ]['path'] ) ) {
require_once $jetpack_packages_classes[ $class_name ]['path'];
return true;
}
}
return false;
}
// Add the jetpack autoloader.
spl_autoload_register( __NAMESPACE__ . '\autoloader' );
}