diff --git a/lib/Pico.php b/lib/Pico.php index f1ddfb9..89e799b 100644 --- a/lib/Pico.php +++ b/lib/Pico.php @@ -444,9 +444,13 @@ class Pico /** * Loads plugins from vendor/pico-plugin.php and Pico::$pluginsDir * - * See {@see Pico::loadLocalPlugins()} for details about plugins installed - * to {@see Pico::$pluginsDir}, and {@see Pico::loadComposerPlugins()} for - * details about plugins installed using `composer`. + * See {@see Pico::loadComposerPlugins()} for details about plugins loaded + * from `vendor/pico-plugin.php` (i.e. plugins that were installed using + * `composer`), and {@see Pico::loadLocalPlugins()} for details about + * plugins installed to {@see Pico::$pluginsDir}. + * + * Pico always loads plugins from `vendor/pico-plugin.php` first and + * ignores conflicting plugins in {@see Pico::$pluginsDir}. * * Please note that Pico will change the processing order when needed to * incorporate plugin dependencies. See {@see Pico::sortPlugins()} for @@ -460,8 +464,55 @@ class Pico */ protected function loadPlugins() { - $this->loadLocalPlugins(); $this->loadComposerPlugins(); + $this->loadLocalPlugins(); + } + + /** + * Loads plugins from vendor/pico-plugin.php + * + * This method loads all plugins installed using `composer` and Pico's + * `picocms/pico-installer` installer by reading the `pico-plugin.php` in + * composer's `vendor` dir. + * + * @see Pico::loadPlugins() + * @see Pico::loadLocalPlugins() + * @return void + */ + protected function loadComposerPlugins() + { + $composerPlugins = array(); + if (file_exists($this->getVendorDir() . 'vendor/pico-plugin.php')) { + // composer root package + $composerPlugins = require($this->getVendorDir() . 'vendor/pico-plugin.php') ?: array(); + } elseif (file_exists($this->getVendorDir() . '../../../vendor/pico-plugin.php')) { + // composer dependency package + $composerPlugins = require($this->getVendorDir() . '../../../vendor/pico-plugin.php') ?: array(); + } + + foreach ($composerPlugins as $package => $classNames) { + foreach ($classNames as $className) { + $plugin = new $className($this); + $className = get_class($plugin); + + if (isset($this->plugins[$className])) { + continue; + } + + if (!($plugin instanceof PicoPluginInterface)) { + throw new RuntimeException( + "Unable to load plugin '" . $className . "' via 'vendor/pico-plugin.php': " + . "Plugins installed by composer must implement 'PicoPluginInterface'" + ); + } + + $this->plugins[$className] = $plugin; + + if (defined($className . '::API_VERSION') && ($className::API_VERSION >= static::API_VERSION)) { + $this->nativePlugins[$className] = $plugin; + } + } + } } /** @@ -471,7 +522,7 @@ class Pico * `.php` only. Plugin names are treated case insensitive. * Pico will throw a RuntimeException if it can't load a plugin. * - * Plugin files MAY be prefixed by a number (e.g. 00-PicoDeprecated.php) + * Plugin files MAY be prefixed by a number (e.g. `00-PicoDeprecated.php`) * to indicate their processing order. Plugins without a prefix will be * loaded last. If you want to use a prefix, you MUST NOT use the reserved * prefixes `00` to `09`. Prefixes are completely optional, however, you @@ -489,35 +540,53 @@ class Pico */ protected function loadLocalPlugins() { + $pluginsLowered = array_change_key_case($this->plugins, CASE_LOWER); + $pluginFiles = array(); - $files = scandir($this->getPluginsDir()); - if ($files !== false) { - foreach ($files as $file) { - if ($file[0] === '.') { + $files = scandir($this->getPluginsDir()) ?: array(); + foreach ($files as $file) { + if ($file[0] === '.') { + continue; + } + + if (is_dir($this->getPluginsDir() . $file)) { + $className = preg_replace('/^[0-9]+-/', '', $file); + $classNameLowered = strtolower($className); + + if (isset($pluginsLowered[$classNameLowered])) { continue; } - if (is_dir($this->getPluginsDir() . $file)) { - $className = preg_replace('/^[0-9]+-/', '', $file); - + if (file_exists($this->getPluginsDir() . $file . '/' . $className . '.php')) { + $pluginFiles[$className] = $file . '/' . $className . '.php'; + } else { $subdirFiles = $this->getFilesGlob($this->getPluginsDir() . $file . '/?*.php', self::SORT_NONE); foreach ($subdirFiles as $subdirFile) { $subdirFile = basename($subdirFile, '.php'); - if (strcasecmp($className, $subdirFile) === 0) { + if ($classNameLowered === strtolower($subdirFile)) { $pluginFiles[$className] = $file . '/' . $subdirFile . '.php'; + break; } } + } - if (!isset($pluginFiles[$className])) { - throw new RuntimeException( - "Unable to load plugin '" . $className . "' from " - . "'" . $file . "/" . $className . ".php': File not found" - ); - } - } elseif (substr($file, -4) === '.php') { - $className = preg_replace('/^[0-9]+-/', '', substr($file, 0, -4)); - $pluginFiles[$className] = $file; + if (!isset($pluginFiles[$className])) { + throw new RuntimeException( + "Unable to load plugin '" . $className . "' from " + . "'" . $file . "/" . $className . ".php': File not found" + ); + } + } elseif (substr($file, -4) === '.php') { + $className = preg_replace('/^[0-9]+-/', '', substr($file, 0, -4)); + $classNameLowered = strtolower($className); + + if (isset($pluginsLowered[$classNameLowered])) { + continue; } + + $pluginFiles[$className] = $file; + } else { + throw new RuntimeException("Unable to load plugin from '" . $file . "': Not a valid plugin file"); } } @@ -537,10 +606,6 @@ class Pico $plugin = new $className($this); $className = get_class($plugin); - if (isset($this->plugins[$className])) { - continue; - } - $this->plugins[$className] = $plugin; if ($plugin instanceof PicoPluginInterface) { @@ -554,44 +619,6 @@ class Pico } } - /** - * Loads plugins from vendor/pico-plugin.php - * - * This method loads all plugins installed using `composer` and Pico's - * `picocms/pico-composer` installer by reading the `pico-plugin.php` in - * composer's `vendor` dir. Using composer enables plugin developers to - * load multiple plugins and their dependencies using a single composer - * package. - * - * @see Pico::loadPlugins() - * @see Pico::loadLocalPlugins() - * @return void - */ - protected function loadComposerPlugins() - { - $composerPlugins = array(); - if (file_exists($this->getVendorDir() . 'vendor/pico-plugin.php')) { - // composer root package - $composerPlugins = require($this->getVendorDir() . 'vendor/pico-plugin.php') ?: array(); - } elseif (file_exists($this->getVendorDir() . '../../../vendor/pico-plugin.php')) { - // composer dependency package - $composerPlugins = require($this->getVendorDir() . '../../../vendor/pico-plugin.php') ?: array(); - } - - foreach ($composerPlugins as $package => $classNames) { - foreach ($classNames as $className) { - $plugin = new $className($this); - $this->plugins[$className] = $plugin; - - if ($plugin instanceof PicoPluginInterface) { - if (defined($className . '::API_VERSION') && ($className::API_VERSION >= static::API_VERSION)) { - $this->nativePlugins[$className] = $plugin; - } - } - } - } - } - /** * Manually loads a plugin *