Skip to content

Commit

Permalink
Merge pull request #2 from alleyinteractive/apcu
Browse files Browse the repository at this point in the history
Add APCu Caching
  • Loading branch information
srtfisher authored Jul 24, 2023
2 parents 0bef718 + 77ddeb8 commit 4adff86
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 31 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to `WP Plugin Loader` will be documented in this file.

## 0.1.1 - 2023-07-24

- Added APCu caching for plugin folder lookups.

## 0.1.0 - 2023-07-22

- Initial release
40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WP Plugin Loader

Code-enabled WordPress plugin loading.
Code-enabled WordPress plugin loading package.

## Installation

Expand All @@ -25,16 +25,50 @@ new WP_Plugin_Loader( [

The plugin loader will load the specified plugins, be it files or folders under
`plugins`/`client-mu-plugins`, and mark them as activated on the plugins screen.
You can pass files or plugin folders that the package will attempt to determine
the main plugin file from and load.

Also supports preventing activations of plugins via the plugins screen (useful
to fully lock down the plugins enabled on site);
See [APCu Caching](#apcu-caching) for more information on caching.

### Plugin Directories

Out of the box, the package will attempt to load your plugin from
`wp-content/plugins`. When it is found, the package will attempt to load your
plugin from `wp-content/client-mu-plugins`. For non-WordPress VIP sites, the
plugin will also load plugins from `wp-content/mu-plugins`.

### Preventing Activations

The package supports preventing activations of plugins via the plugins screen
(useful to fully lock down the plugins enabled on site):

```php
use Alley\WP\WP_Plugin_Loader;

( new WP_Plugin_Loader( [ ... ] )->prevent_activations();
```

Plugin activations will be prevented on the plugin screen as well as with a
capability check.

### APCu Caching

When a plugin is loaded by a directory name the package will attempt to
determine the main plugin file from the directory. This can be a semi-expensive
operation that can be cached with APCu. To enable caching, call
`enable_caching()` or `set_cache_prefix( $prefix )` to specify a custom cache
prefix.

```php
use Alley\WP\WP_Plugin_Loader;

( new WP_Plugin_Loader( [ ... ] ) )->enable_caching();

( new WP_Plugin_Loader( [ ... ] ) )->set_cache_prefix( 'my-prefix' );
```

Note: caching will only be enabled if APCu is available.

## Testing

Run `composer test` to run tests against PHPUnit and the PHP code in the plugin.
Expand Down
150 changes: 122 additions & 28 deletions src/class-wp-plugin-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
* WordPress Plugin Loader
*/
class WP_Plugin_Loader {
/**
* Cache prefix for APCu caching.
*
* @var string|null
*/
protected ?string $cache_prefix = null;

/**
* Array of loaded plugins.
*
Expand Down Expand Up @@ -49,34 +56,103 @@ public function __construct( public array $plugins = [] ) {
/**
* Prevent any plugin activations for non-code activated plugins.
*
* @todo Harden with a capability check.
*
* @param bool $prevent Whether to prevent activations.
* @return static
*/
public function prevent_activations( bool $prevent = true ): void {
public function prevent_activations( bool $prevent = true ): static {
$this->prevent_activations = $prevent;

return $this;
}

/**
* Enable APCu caching for plugin paths.
*
* @return static
*/
public function enable_caching(): static {
return $this->set_cache_prefix( 'wp-plugin-loader-' );
}

/**
* Set the cache prefix for APCu caching.
*
* @param string|null $prefix The cache prefix.
* @return static
*/
public function set_cache_prefix( ?string $prefix ): static {
$this->cache_prefix = function_exists( 'apcu_fetch' ) && filter_var( ini_get( 'apc.enabled' ), FILTER_VALIDATE_BOOLEAN )
? $prefix
: null;

return $this;
}

/**
* Load the requested plugins.
*/
protected function load_plugins(): void {
$client_mu_plugins = is_dir( WP_CONTENT_DIR . '/client-mu-plugins' );
$folders = [
WP_CONTENT_DIR . '/plugins',
];

$client_mu_plugins_dir = defined( 'WPCOM_VIP_CLIENT_MU_PLUGIN_DIR' )
? WPCOM_VIP_CLIENT_MU_PLUGIN_DIR
: ( is_dir( WP_CONTENT_DIR . '/client-mu-plugins' ) ? WP_CONTENT_DIR . '/client-mu-plugins' : null );

/**
* The client-mu-plugins directory should always be used if the
* directory exists. If the WPCOM_VIP_CLIENT_MU_PLUGIN_DIR constant is
* defined, then we won't add the mu-plugins directory to the list of
* folders. Otherwise, we will add the mu-plugins directory to the list
* of folders.
*/
if ( defined( 'WPCOM_VIP_CLIENT_MU_PLUGIN_DIR' ) ) {
$folders[] = WPCOM_VIP_CLIENT_MU_PLUGIN_DIR;
} else {
if ( $client_mu_plugins_dir ) {
$folders[] = WP_CONTENT_DIR . '/client-mu-plugins';
}

$folders[] = WP_CONTENT_DIR . '/mu-plugins';
}

// Loop through each plugin and attempt to load it.
foreach ( $this->plugins as $plugin ) {
if ( file_exists( WP_PLUGIN_DIR . "/$plugin" ) && ! is_dir( WP_PLUGIN_DIR . "/$plugin" ) ) {
require_once WP_PLUGIN_DIR . "/$plugin";
// Loop through each possible folder and attempt to load the plugin
// from it.
foreach ( $folders as $folder ) {
if ( file_exists( $folder . "/$plugin" ) && ! is_dir( $folder . "/$plugin" ) ) {
require_once $folder . "/$plugin"; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable

// Mark the plugin as loaded if it is in the /plugins directory.
if ( WP_CONTENT_DIR . '/plugins' === $folder ) {
$this->loaded_plugins[] = trim( $plugin, '/' );
}

continue 2;
}
}

// Attempt to locate the plugin by name if it isn't a file.
if ( false === strpos( $plugin, '.php' ) ) {
// Check the APCu cache if we have a prefix set.
if ( $this->cache_prefix ) {
$cached_plugin_path = apcu_fetch( $this->cache_prefix . $plugin );

$this->loaded_plugins[] = trim( $plugin, '/' );
if ( false !== $cached_plugin_path ) {
// Check if the plugin path is valid. If it is, require
// it. Continue either way if the cache was not false.
if ( is_string( $cached_plugin_path ) && ! empty( $cached_plugin_path ) ) {
require_once $cached_plugin_path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable

continue;
} elseif ( $client_mu_plugins && file_exists( WP_CONTENT_DIR . "/client-mu-plugins/$plugin" ) && ! is_dir( WP_CONTENT_DIR . "/client-mu-plugins/$plugin" ) ) {
$plugin = ltrim( $plugin, '/' );
$this->loaded_plugins[] = trim( substr( $cached_plugin_path, strlen( WP_PLUGIN_DIR ) + 1 ), '/' );
}

require_once WP_CONTENT_DIR . "/client-mu-plugins/$plugin";
continue;
}
}

continue;
} elseif ( false === strpos( $plugin, '.php' ) ) {
// Attempt to locate the plugin by name if it isn't a file.
$sanitized_plugin = $this->sanitize_plugin_name( $plugin );

Expand All @@ -86,36 +162,54 @@ protected function load_plugins(): void {
WP_PLUGIN_DIR . "/$sanitized_plugin.php",
];

$match = false;
// Include the client mu-plugins directory if it exists.
if ( $client_mu_plugins_dir ) {
$paths[] = "$client_mu_plugins_dir/$sanitized_plugin/$sanitized_plugin.php";
$paths[] = "$client_mu_plugins_dir/$sanitized_plugin/plugin.php";
$paths[] = "$client_mu_plugins_dir/$sanitized_plugin.php";
}

foreach ( $paths as $path ) {
if ( file_exists( $path ) ) {
require_once $path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable

$match = true;
// Cache the plugin path in APCu if we have a prefix set.
if ( $this->cache_prefix ) {
apcu_store( $this->cache_prefix . $plugin, $path );
}


// Mark the plugin as loaded if it is in the /plugins directory.
$this->loaded_plugins[] = trim( substr( $path, strlen( WP_PLUGIN_DIR ) + 1 ), '/' );
break;
}
}

// Bail if we found a match.
if ( $match ) {
continue;
continue 2;
}
}
}

$error_message = sprintf( 'WP Plugin Loader: Plugin %s not found.', $plugin );
$this->handle_missing_plugin( $plugin );
}
}

trigger_error( esc_html( $error_message ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
/**
* Handle a missing plugin.
*
* @todo Change return type to never when 8.1 is required.
*
* @param string $plugin The plugin name passed to the loader.
* @return void
*/
protected function handle_missing_plugin( string $plugin ): void {
$error_message = sprintf( 'WP Plugin Loader: Plugin %s not found.', $plugin );

if ( extension_loaded( 'newrelic' ) && function_exists( 'newrelic_notice_error' ) ) {
newrelic_notice_error( $error_message );
}
trigger_error( esc_html( $error_message ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error

// Bye bye!
die( esc_html( $error_message ) );
if ( extension_loaded( 'newrelic' ) && function_exists( 'newrelic_notice_error' ) ) {
newrelic_notice_error( $error_message );
}

// Bye bye!
die( esc_html( $error_message ) );
}

/**
Expand Down
1 change: 1 addition & 0 deletions stubs/constants.stub
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<?php
define( 'WP_CONTENT_DIR', 'wp-content' );
define( 'WP_PLUGIN_DIR', 'wp-content/plugins' );
define( 'WPCOM_VIP_CLIENT_MU_PLUGIN_DIR', 'wp-content/client-mu-plugins' );

0 comments on commit 4adff86

Please sign in to comment.