bedrock-autoloader.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <?php
  2. /**
  3. * Plugin Name: Bedrock Autoloader
  4. * Plugin URI: https://github.com/roots/bedrock/
  5. * Description: An autoloader that enables standard plugins to be required just like must-use plugins. The autoloaded plugins are included during mu-plugin loading. An asterisk (*) next to the name of the plugin designates the plugins that have been autoloaded.
  6. * Version: 1.0.1
  7. * Author: Roots
  8. * Author URI: https://roots.io/
  9. * License: MIT License
  10. */
  11. namespace Roots\Bedrock;
  12. if (!is_blog_installed()) {
  13. return;
  14. }
  15. /**
  16. * Class Autoloader
  17. * @package Roots\Bedrock
  18. * @author Roots
  19. * @link https://roots.io/
  20. */
  21. class Autoloader
  22. {
  23. /**
  24. * Singleton instance.
  25. *
  26. * @var static
  27. */
  28. private static $instance;
  29. /**
  30. * Store Autoloader cache and site option.
  31. *
  32. * @var array
  33. */
  34. private $cache;
  35. /**
  36. * Autoloaded plugins.
  37. *
  38. * @var array
  39. */
  40. private $autoPlugins;
  41. /**
  42. * Autoloaded mu-plugins.
  43. *
  44. * @var array
  45. */
  46. private $muPlugins;
  47. /**
  48. * Number of plugins.
  49. *
  50. * @var int
  51. */
  52. private $count;
  53. /**
  54. * Newly activated plugins.
  55. *
  56. * @var array
  57. */
  58. private $activated;
  59. /**
  60. * Relative path to the mu-plugins directory.
  61. *
  62. * @var string
  63. */
  64. private $relativePath;
  65. /**
  66. * Create an instance of Autoloader.
  67. *
  68. * @return void
  69. */
  70. public function __construct()
  71. {
  72. if (isset(self::$instance)) {
  73. return;
  74. }
  75. self::$instance = $this;
  76. $this->relativePath = '/../' . basename(__DIR__);
  77. if (is_admin()) {
  78. add_filter('show_advanced_plugins', [$this, 'showInAdmin'], 0, 2);
  79. }
  80. $this->loadPlugins();
  81. }
  82. /**
  83. * Run some checks then autoload our plugins.
  84. *
  85. * @return void
  86. */
  87. public function loadPlugins()
  88. {
  89. $this->checkCache();
  90. $this->validatePlugins();
  91. $this->countPlugins();
  92. array_map(static function () {
  93. include_once WPMU_PLUGIN_DIR . '/' . func_get_args()[0];
  94. }, array_keys($this->cache['plugins']));
  95. $this->pluginHooks();
  96. }
  97. /**
  98. * Filter show_advanced_plugins to display the autoloaded plugins.
  99. *
  100. * @param bool $show Whether to show the advanced plugins for the specified plugin type.
  101. * @param string $type The plugin type, i.e., `mustuse` or `dropins`
  102. * @return bool We return `false` to prevent WordPress from overriding our work
  103. */
  104. public function showInAdmin($show, $type)
  105. {
  106. $screen = get_current_screen();
  107. $current = is_multisite() ? 'plugins-network' : 'plugins';
  108. if ($screen->base !== $current || $type !== 'mustuse' || !current_user_can('activate_plugins')) {
  109. return $show;
  110. }
  111. $this->updateCache();
  112. $this->autoPlugins = array_map(function ($auto_plugin) {
  113. $auto_plugin['Name'] .= ' *';
  114. return $auto_plugin;
  115. }, $this->autoPlugins);
  116. $GLOBALS['plugins']['mustuse'] = array_unique(array_merge($this->autoPlugins, $this->muPlugins), SORT_REGULAR);
  117. return false;
  118. }
  119. /**
  120. * This sets the cache or calls for an update
  121. *
  122. * @return void
  123. */
  124. private function checkCache()
  125. {
  126. $cache = get_site_option('bedrock_autoloader');
  127. if ($cache === false || (isset($cache['plugins'], $cache['count']) && count($cache['plugins']) !== $cache['count'])) {
  128. $this->updateCache();
  129. return;
  130. }
  131. $this->cache = $cache;
  132. }
  133. /**
  134. * Update mu-plugin cache.
  135. *
  136. * Get the plugins and mu-plugins from the mu-plugin path and remove duplicates.
  137. * Check cache against current plugins for newly activated plugins.
  138. * After that, we can update the cache.
  139. *
  140. * @return void
  141. */
  142. private function updateCache()
  143. {
  144. require_once ABSPATH . 'wp-admin/includes/plugin.php';
  145. $this->autoPlugins = get_plugins($this->relativePath);
  146. $this->muPlugins = get_mu_plugins();
  147. $plugins = array_diff_key($this->autoPlugins, $this->muPlugins);
  148. $rebuild = !(isset($this->cache['plugins']) && is_array($this->cache['plugins']));
  149. $this->activated = $rebuild ? $plugins : array_diff_key($plugins, $this->cache['plugins']);
  150. $this->cache = ['plugins' => $plugins, 'count' => $this->countPlugins()];
  151. update_site_option('bedrock_autoloader', $this->cache);
  152. }
  153. /**
  154. * Activate plugin hooks.
  155. *
  156. * This accounts for the plugin hooks that would run if the plugins were
  157. * loaded as usual. Plugins are removed by deletion, so there's no way
  158. * to deactivate or uninstall.
  159. *
  160. * @return void
  161. */
  162. private function pluginHooks()
  163. {
  164. if (!is_array($this->activated)) {
  165. return;
  166. }
  167. foreach ($this->activated as $plugin_file => $plugin_info) {
  168. do_action('activate_' . $plugin_file);
  169. }
  170. }
  171. /**
  172. * Check that the plugin file exists, if it doesn't update the cache.
  173. *
  174. * @return void
  175. */
  176. private function validatePlugins()
  177. {
  178. foreach ($this->cache['plugins'] as $plugin_file => $plugin_info) {
  179. if (!file_exists(WPMU_PLUGIN_DIR . '/' . $plugin_file)) {
  180. $this->updateCache();
  181. break;
  182. }
  183. }
  184. }
  185. /**
  186. * Count the number of autoloaded plugins.
  187. *
  188. * Count our plugins (but only once) by counting the top level folders in the
  189. * mu-plugins dir. If it's more or less than last time, update the cache.
  190. *
  191. * @return int Number of autoloaded plugins.
  192. */
  193. private function countPlugins()
  194. {
  195. if (isset($this->count)) {
  196. return $this->count;
  197. }
  198. $count = count(glob(WPMU_PLUGIN_DIR . '/*/', GLOB_ONLYDIR | GLOB_NOSORT));
  199. if (!isset($this->cache['count']) || $count !== $this->cache['count']) {
  200. $this->count = $count;
  201. $this->updateCache();
  202. }
  203. return $this->count;
  204. }
  205. }
  206. new Autoloader();