diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md index b9d38a56e..3db56600d 100644 --- a/doc/articles/plugins.md +++ b/doc/articles/plugins.md @@ -332,6 +332,20 @@ in your composer.json to hint to Composer that the plugin should be activated as as possible to prevent any bad side-effects from Composer assuming packages are installed in another location than they actually are. +### plugin-optional + +Because Composer plugins can be used to perform actions which are necessary for installing +a working application, like modifying which path files get stored in, skipping required +plugins unintentionally can result in broken applications. So, in non-interactive mode, +Composer will fail if a new plugin is not listed in ["allow-plugins"](../06-config.md#allow-plugins) +to force users to decide if they want to execute the plugin, to avoid silent failures. + +As of Composer 2.5.3, you can use the setting `{"extra": {"plugin-optional": true}}` on +your plugin, to tell Composer that skipping the plugin has no catastrophic consequences, +and it can safely be disabled in non-interactive mode if it is not yet listed in +"allow-plugins". The next interactive run of Composer will still prompt users to choose if +they want to enable or disable the plugin. + ## Plugin Autoloading Due to plugins being loaded by Composer at runtime, and to ensure that plugins which diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index dc6dc8d11..c0c86b7b6 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -198,7 +198,8 @@ class PluginManager } } - if (!$this->isPluginAllowed($package->getName(), $isGlobalPlugin)) { + $extra = $package->getExtra(); + if (!$this->isPluginAllowed($package->getName(), $isGlobalPlugin, isset($extra['plugin-optional']) && true === $extra['plugin-optional'])) { $this->io->writeError('Skipped loading "'.$package->getName() . '" '.($isGlobalPlugin ? '(installed globally) ' : '').'as it is not in config.allow-plugins', true, IOInterface::DEBUG); return; @@ -394,9 +395,12 @@ class PluginManager if ($sourcePackage === null) { trigger_error('Calling PluginManager::addPlugin without $sourcePackage is deprecated, if you are using this please get in touch with us to explain the use case', E_USER_DEPRECATED); - } elseif (!$this->isPluginAllowed($sourcePackage->getName(), $isGlobalPlugin)) { - $this->io->writeError('Skipped loading "'.get_class($plugin).' from '.$sourcePackage->getName() . '" '.($isGlobalPlugin ? '(installed globally) ' : '').' as it is not in config.allow-plugins', true, IOInterface::DEBUG); - return; + } else { + $extra = $sourcePackage->getExtra(); + if (!$this->isPluginAllowed($sourcePackage->getName(), $isGlobalPlugin, isset($extra['plugin-optional']) && true === $extra['plugin-optional'])) { + $this->io->writeError('Skipped loading "'.get_class($plugin).' from '.$sourcePackage->getName() . '" '.($isGlobalPlugin ? '(installed globally) ' : '').' as it is not in config.allow-plugins', true, IOInterface::DEBUG); + return; + } } $details = array(); @@ -693,9 +697,10 @@ class PluginManager * * @param string $package * @param bool $isGlobalPlugin + * @param bool $optional * @return bool */ - public function isPluginAllowed($package, $isGlobalPlugin) + public function isPluginAllowed($package, $isGlobalPlugin, $optional = false) { if ($isGlobalPlugin) { $rules = &$this->allowGlobalPluginRules; @@ -763,6 +768,8 @@ class PluginManager break; } } + } elseif ($optional) { + return false; } throw new PluginBlockedException(