Skip to content

Commit

Permalink
Feature Branch: Update the Autoloader (#15106)
Browse files Browse the repository at this point in the history
* Autoloader: remove ignore list and notice (#14938)

We're planning to remove the plugins_loaded restriction, so let's
remove the ignore list and the PHP notice.

* Autoloader: Change autoloader function name to autoload (#14937)

Change the function name from 'autoloader' to 'autoload'. This change will
allow us to use a new version of the autoloader function when an old
version has been loaded before the new version.

* Moves autoloader code into a separate file and renames class/file maps. (#14944)

* Added autoloader meta file generation.

To be used to determine the package version that has generated the autoloader, and figuring out what autoloader should take precedence.

* Removed the meta file, instead moved the autoloader function to a separate file for easier development.

* Renamed the classmap files to avoid confusing them with old autoloader class and file maps.

* Removed unneeded changes.

* Renamed class and file map files to a jetpack specific name to avoid conflicts.

* Fixed an invalid rename, thank you @kbrown9!

* Removed a redundant composer.json file declaration.

* Using suffix in the namespace to allow for repeating names.

* Wrapped the code that runs on file load in a function to make unit tests pass.

* Find the latest autoloader on first load. (#14992)

* [not verified] Autoloader: add the set_up_autoloader function.

* [not verified] Autoloader: update global array names and remove spl_autoload_register

Update the global classmap and filemap array names so the old autoloaders
can't overwrite the new autoloader's version selections.

Remove the call to spl_autoload_register(), because that it now handled
in set_up_autoloader().

* [not verified] Moved the autoloader functions into a separate include.

* Autoloader: remove setup() from test_file_loader.php

The spl_autoload_register() call isn't needed in setup() and the
setup() method doesn't do anything else, so remove the entire setup()
method.

Co-authored-by: Kim Brown <50059399+kbrown9@users.noreply.github.com>

* Autoloader: use all of the map files to populate the global maps (#15002)

* Autoloader: loop through all map files in enqueue_files()

The latest autoloader needs to loop through all of the classmap and
filemap files created by the new autoloader and populate the globals.
Add a loop through the active plugins to the enqueue_files() function.

* Autoloader: improve enqueue_files

Improve enqueue_files() by:
 - checking the map files with is_readable instead of file_exists
 - checking that the local map variables are arrays before using them
 - move the file_loader() block out of the active plugins loop

* Autoloader: remove plugins_loaded action conditional around file_loader

We no longer need to wait until the plugins_loaded action to call file_loader(),
so remove the conditionals and the plugins_loaded hook.

* Autoloader: Handle an activating plugin

When a plugin is in the process of activating, it won't be in the active
plugins list yet. We need to make sure that its classmap and filemap files
are processed and the globals are updated so that the autoloader can use
them with the newly activated plugin.

 - In enqueue_files(), process the current plugin's map file and the map files
   for all active plugins.
 - In set_up_autoloader(), detect if a new autoloader has been loaded by an
   activating plugin. If it has, reset the global classmap.

* Autoloader: handle an activating plugin

When a plugin is in the process of activating, it's not in the active
plugins list. We need to add the activating plugin's packages to the
autoloader before the plugin loads.

* Added a check against bulk activation.

* Added network activated plugins to the active plugin list.

* Added a nonce 'check'.

Co-authored-by: Igor Zinovyev <zinigor@gmail.com>

* Autoloader: refactor functions.php (#15111)

* Autoloader: refactor functions.php into classes

* Autoloader: update unit tests

Update the unit tests to use the new Classes_Handler and Files_Handler classes.

* Autoloader: update is_current_plugin_active()

Update is_current_plugin_active() to check both active and activating
plugins when determining whether the current plugin is active. The
active and activating plugins can be obtained using the
Plugins_Handler::get_active_plugins() method.

* Added newly generated files to gitignore.

* Autoloader: improve Plugins_Handler

Improve the Plugins_Handler class by:
 - Giving some methods more descriptive names.
 - Adding a $jetpack_autoloader_activating_plugins global variable that
   contains the plugins that are activating via a non-request method.
 - Refactoring a few methods to improve clarity.

* Autoloader: update the autoloader chain after autoloader reset

Add a new function, Autoloader_Handler::update_autoloader_chain(), which
handles updates of the autoloader chain as follows:
 - Registers the latest autoloader function.
 - Moves the v1 autoloader function to the end of the chain.
 - Removes any other v2 autoloader functions. This is needed for situations where
   a plugin activates using a non-request method, so the activating plugin was not
   known when the autoloader first ran. If the activating plugin has an autoloader
   with a later version, that autoloader will be registered and the previously
   registered autoloader function should be removed.

Finally, use the Autoloader_Handler::update_autoloader_chain() method in
set_up_autoloader().

* Autoloader: update update_autoloader_chain()

In Autoloader_Handler::update_autoloader_chain(), check whether the autoloader
object is a string. If not, just continue. We're only looking for Jetpack
autoloader functions, and they're registered as strings.

Co-authored-by: Igor Zinovyev <zinigor@gmail.com>

* Autoloader: Skip single-file plugins (#15232)

Some plugins don't have a dedicated directory and only consist of a single file (e.g. 'Hello Dolly').
This means they also don't use Composer, so we can skip them entirely.

* Autoloader: add Version_Selector class (#15135)

Add a new class, Version_Selector, which is used by the Autoloader_Handler,
Classes_Handler, and Files_Handler classes to select package versions.

Remove Plugins_Handler::create_map_path_array() and Plugins_Handler::get_active_plugins_paths()
because they're no longer used.

* Autoloader: display an error message if a non-default vendor directory is used (#15364)

The autoloader package only supports the default composer vendor directory. If
a consuming plugin uses a non-default vendor directory, generate an error during
composer dump-autoload.

* Autoloader: remove file_exists() before loading the file (#15375)

* Autoloader: remove unneeded methods (#15480)

Remove Plugins_Handler::create_map_path_array() and
Plugins_Handler::get_active_plugins_paths() because they are
not used.

* Autoloader: refactor Plugins_Handler and add unit tests (#15516)

* Autoloader: refactor a few methods in Plugins_Handler

Refactor a few methods in Plugins_Handler to make the class easier
to test.

* Autoloader: add unit tests for Plugins_Handler

* Autoloader: add unit tests for Version_Selector (#15592)

* Autoloader: improve performance by using directories to identify plugins

Performance profiling showed that the get_plugins() call in
Plugins_Handler::get_current_plugin() is the longest running call in the
autoloader setup process.

We use get_plugins() to obtain the main file for the current plugin. Instead
of identifying plugins by their directory and main file, let's identify them
with their directory. This will allow us to eliminate the get_plugins() call
and improve autoloader performance.

* Autoloader: add a new method for converting plugin dir/file strings to dirs

* Autoloader: refactor Plugins_Handler and unit tests

Refactor Plugin_Handler so that units tests will cover the
Plugins_Handler::convert_plugins_to_dirs() method. Also fix the
Plugins_Handler unit tests.

* Autoloader: fix Plugins_Handler unit tests

PHPUnit5.7.27 doesn't support the assertEqualsCanonicalizing() method,
so just sort the output arrays before calling assertEquals().

* Autoloader: handle an updating plugin

The Autoloader generates the in-memory classmap when the first autoloader package
is loaded. When a plugin is updated, the path to a package class could have changed,
making the in-memory classmap point to an invalid file location.

Reset the autoloader when a plugin with the v2.x autoloader is updated so that the classmap
is kept up to date.

* Autoloader: remove the unnecessary 'convert_plugins_to_dirs()' calls

The plugin slugs are converted to the directory names in the get_all_active_plugins()
method. So remove the convert_plugins_to_dirs() calls from get_multisite_plugins()
and get_active_plugins().

* Autoloader: move the upgrader_post_install hook callback to Plugins_Handler

* Autoloader: fix is_directory_plugin() call

* Update test

Co-authored-by: Igor Zinovyev <zinigor@gmail.com>
Co-authored-by: Sergey Mitroshin <sergeymitr@gmail.com>
Co-authored-by: Brandon Kraft <public@brandonkraft.com>
  • Loading branch information
4 people authored Jun 23, 2020
1 parent bc6d22b commit fb2c6f1
Show file tree
Hide file tree
Showing 17 changed files with 1,370 additions and 324 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@ _inc/jetpack-strings.php
/vendor/composer/
/vendor/autoload.php
/vendor/autoload_packages.php
/vendor/autoload_functions.php
/vendor/class-autoloader-handler.php
/vendor/class-classes-handler.php
/vendor/class-files-handler.php
/vendor/class-plugins-handler.php
/vendor/class-version-selector.php
2 changes: 2 additions & 0 deletions packages/autoloader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ Current Limitations
-----

We currently only support packages that autoload via psr-4 definition in their package.

Your project must use the default composer vendor directory, `vendor`.
2 changes: 1 addition & 1 deletion packages/autoloader/phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<phpunit bootstrap="tests/php/bootstrap.php" backupGlobals="false" colors="true">
<phpunit bootstrap="tests/php/bootstrap.php" backupGlobals="true" colors="true">
<testsuites>
<testsuite name="main">
<directory prefix="test" suffix=".php">tests/php</directory>
Expand Down
91 changes: 47 additions & 44 deletions packages/autoloader/src/AutoloadGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
// 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
Expand Down Expand Up @@ -39,6 +38,15 @@
*/
class AutoloadGenerator extends BaseGenerator {

const COMMENT = <<<AUTOLOADER_COMMENT
/**
* This file was automatically generated by automattic/jetpack-autoloader.
*
* @package automattic/jetpack-autoloader
*/
AUTOLOADER_COMMENT;

/**
* Instantiate an AutoloadGenerator object.
*
Expand All @@ -57,7 +65,7 @@ public function __construct( IOInterface $io = null ) {
* @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.
* @param string $suffix The autoloader suffix.
*/
public function dump(
Config $config,
Expand All @@ -84,15 +92,32 @@ public function dump(
$fileMap = $this->getFileMap( $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( $targetDir . '/jetpack_autoload_classmap.php', $this->getAutoloadClassmapPackagesFile( $classMap ) );
$this->io->writeError( '<info>Generated ' . $targetDir . '/jetpack_autoload_classmap.php</info>', true );

file_put_contents( $targetDir . '/autoload_files_package.php', $this->getAutoloadFilesPackagesFile( $fileMap ) );
$this->io->writeError( '<info>Generated ' . $targetDir . '/autoload_files_package.php</info>', true );
file_put_contents( $targetDir . '/jetpack_autoload_filemap.php', $this->getAutoloadFilesPackagesFile( $fileMap ) );
$this->io->writeError( '<info>Generated ' . $targetDir . '/jetpack_autoload_filemap.php</info>', true );

file_put_contents( $vendorPath . '/autoload_packages.php', $this->getAutoloadPackageFile( $suffix ) );
file_put_contents( $vendorPath . '/autoload_packages.php', $this->getAutoloadPackageFile( 'autoload.php', $suffix ) );
$this->io->writeError( '<info>Generated ' . $vendorPath . '/autoload_packages.php</info>', true );

file_put_contents( $vendorPath . '/autoload_functions.php', $this->getAutoloadPackageFile( 'functions.php', $suffix ) );
$this->io->writeError( '<info>Generated ' . $vendorPath . '/autoload_functions.php</info>', true );

file_put_contents( $vendorPath . '/class-autoloader-handler.php', $this->getAutoloadPackageFile( 'class-autoloader-handler.php', $suffix ) );
$this->io->writeError( '<info>Generated ' . $vendorPath . '/class-autoloader-handler.php</info>', true );

file_put_contents( $vendorPath . '/class-classes-handler.php', $this->getAutoloadPackageFile( 'class-classes-handler.php', $suffix ) );
$this->io->writeError( '<info>Generated ' . $vendorPath . '/class-classes-handler.php</info>', true );

file_put_contents( $vendorPath . '/class-files-handler.php', $this->getAutoloadPackageFile( 'class-files-handler.php', $suffix ) );
$this->io->writeError( '<info>Generated ' . $vendorPath . '/class-files-handler.php</info>', true );

file_put_contents( $vendorPath . '/class-plugins-handler.php', $this->getAutoloadPackageFile( 'class-plugins-handler.php', $suffix ) );
$this->io->writeError( '<info>Generated ' . $vendorPath . '/class-plugins-handler.php</info>', true );

file_put_contents( $vendorPath . '/class-version-selector.php', $this->getAutoloadPackageFile( 'class-version-selector.php', $suffix ) );
$this->io->writeError( '<info>Generated ' . $vendorPath . '/class-version-selector.php</info>', true );
}

/**
Expand Down Expand Up @@ -313,45 +338,23 @@ private function getAutoloadFilesPackagesFile( $filesMap ) {
/**
* 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.
* @param String $filename a file to prepare.
* @param String $suffix Unique suffix used in the namespace.
*
* @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_package.php';
\$includeFiles = file_exists( \$autoload_file )
? require \$autoload_file
: array();
foreach ( \$includeFiles as \$fileIdentifier => \$file_data ) {
enqueue_package_file( \$fileIdentifier, \$file_data[ 'version' ], \$file_data[ 'path' ] );
}
if ( function_exists( 'has_action') && function_exists( 'did_action' ) && ! did_action( 'plugins_loaded' ) && false === has_action( 'plugins_loaded', __NAMESPACE__ . '\\file_loader' ) ) {
// Add action if it has not been added and has not happened yet.
// Priority -10 to load files as early as possible in case plugins try to use them during `plugins_loaded`.
add_action( 'plugins_loaded', __NAMESPACE__ . '\\file_loader', 0, -10 );
} elseif( ! function_exists( 'did_action' ) || did_action( 'plugins_loaded' ) ) {
file_loader(); // Either WordPress is not loaded or plugin is doing it wrong. Either way we'll load the files so nothing breaks.
}
}
enqueue_packages_$suffix();
INCLUDE_FILES;

return $file_contents;
private function getAutoloadPackageFile( $filename, $suffix ) {
$header = self::COMMENT;
$header .= PHP_EOL;
$header .= 'namespace Automattic\Jetpack\Autoloader\jp' . $suffix . ';';
$header .= PHP_EOL . PHP_EOL;

$sourceLoader = fopen( __DIR__ . '/' . $filename, 'r' );
$file_contents = stream_get_contents( $sourceLoader );
return str_replace(
'/* HEADER */',
$header,
$file_contents
);
}
}
12 changes: 10 additions & 2 deletions packages/autoloader/src/CustomAutoloaderPlugin.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?php // phpcs:ignore WordPress.Files.FileName
<?php //phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase
/**
* Custom Autoloader Composer Plugin, hooks into composer events to generate the custom autoloader.
*
Expand Down Expand Up @@ -71,11 +71,19 @@ public static function getSubscribedEvents() {
*/
public function postAutoloadDump( Event $event ) {

$config = $this->composer->getConfig();

if ( 'vendor' !== $config->raw()['config']['vendor-dir'] ) {
$this->io->writeError( "\n<error>An error occurred while generating the autoloader files:", true );
$this->io->writeError( 'The project\'s composer.json or composer environment set a non-default vendor directory.', true );
$this->io->writeError( 'The default composer vendor directory must be used.</error>', true );
exit();
}

$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' )
Expand Down
178 changes: 3 additions & 175 deletions packages/autoloader/src/autoload.php
Original file line number Diff line number Diff line change
@@ -1,178 +1,6 @@
<?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
*/
/* HEADER */ // phpcs:ignore

// phpcs:disable PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound
// phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_namespaceFound
// phpcs:disable PHPCompatibility.Keywords.NewKeywords.t_ns_cFound
require_once trailingslashit( dirname( __FILE__ ) ) . '/autoload_functions.php';

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,
);

return;
}
// 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__ . '\enqueue_package_file' ) ) {
global $jetpack_packages_files;

if ( ! is_array( $jetpack_packages_files ) ) {
$jetpack_packages_files = array();
}
/**
* Adds the version of a package file to the $jetpack_packages_files global array so that
* we can load the most recent version after 'plugins_loaded'.
*
* @param string $file_identifier Unique id to file assigned by composer based on package name and filename.
* @param string $version Version of the file.
* @param string $path Absolute path to the file so that we can load it.
*/
function enqueue_package_file( $file_identifier, $version, $path ) {
global $jetpack_packages_files;

if ( ! isset( $jetpack_packages_files[ $file_identifier ] ) ) {
$jetpack_packages_files[ $file_identifier ] = array(
'version' => $version,
'path' => $path,
);

return;
}
// If we have a @dev version set always use that one!
if ( 'dev-' === substr( $jetpack_packages_files[ $file_identifier ]['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_files[ $file_identifier ] = array(
'version' => $version,
'path' => $path,
);

return;
}
// Set the latest version!
if ( version_compare( $jetpack_packages_files[ $file_identifier ]['version'], $version, '<' ) ) {
$jetpack_packages_files[ $file_identifier ] = array(
'version' => $version,
'path' => $path,
);
}
}
}

if ( ! function_exists( __NAMESPACE__ . '\file_loader' ) ) {
/**
* Include latest version of all enqueued files. Should be called after all plugins are loaded.
*/
function file_loader() {
global $jetpack_packages_files;
foreach ( $jetpack_packages_files as $file_identifier => $file_data ) {
if ( empty( $GLOBALS['__composer_autoload_files'][ $file_identifier ] ) ) {
require $file_data['path'];

$GLOBALS['__composer_autoload_files'][ $file_identifier ] = true;
}
}
}
}

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 ) {
// TODO ideally we shouldn't skip any of these, see: https://github.com/Automattic/jetpack/pull/12646.
$ignore = in_array(
$class_name,
array(
'Automattic\Jetpack\Connection\Manager',
'Jetpack_Options',
),
true
);

if ( ! $ignore && 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'] )
);
}
}

require_once $jetpack_packages_classes[ $class_name ]['path'];

return true;
}

return false;
}

// Add the jetpack autoloader.
spl_autoload_register( __NAMESPACE__ . '\autoloader' );
}
set_up_autoloader();
Loading

0 comments on commit fb2c6f1

Please sign in to comment.