diff --git a/lib/src/BaseApplication.php b/lib/src/BaseApplication.php index dcc881e..62a8f3f 100644 --- a/lib/src/BaseApplication.php +++ b/lib/src/BaseApplication.php @@ -5,6 +5,7 @@ use Civi\Cv\Util\BootTrait; use Civi\Cv\Util\CvArgvInput; use LesserEvil\ShellVerbosityIsEvil; +use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutput; @@ -13,6 +14,8 @@ class BaseApplication extends \Symfony\Component\Console\Application { + protected $stage = 'new'; + /** * Primary entry point for execution of the standalone command. */ @@ -52,6 +55,37 @@ public function configure() { 'commands' => $this->createCommands(), ])['commands']; $this->addCommands($commands); + $this->stage = 'configured'; + } + + public function find($name) { + switch ($this->stage) { + case 'new': + case 'extended': + return parent::find($name); + + case 'configured': + try { + return parent::find($name); + } + catch (CommandNotFoundException $e) { + $this->stage = 'extended'; + $c = new class() { + use BootTrait; + }; + + $okLevels = ['full|cms-full', 'full', 'cms-full']; + if (in_array(Cv::input()->getOption('level'), $okLevels)) { + $c->boot(Cv::input(), Cv::output()); + return parent::find($name); + } + else { + $output = is_callable([Cv::output(), 'getErrorOutput']) ? Cv::output()->getErrorOutput() : Cv::output(); + $output->writeln(sprintf("WARNING: When using bootstrap --level=%s, some commands may be unavailable.", Cv::input()->getOption('level'))); + throw $e; + } + } + } } /** diff --git a/lib/src/CvPlugins.php b/lib/src/CvPlugins.php index 0e6ba7c..d769bc1 100644 --- a/lib/src/CvPlugins.php +++ b/lib/src/CvPlugins.php @@ -1,8 +1,12 @@ 'cv', 'appVersion' => '0.3.50'] + */ + private $pluginEnv; + /** * Load any plugins. * @@ -22,6 +33,9 @@ class CvPlugins { * Ex: ['appName' => 'cv', 'appVersion' => '0.3.50'] */ public function init(array $pluginEnv) { + require_once __DIR__ . '/cvplugin_loader.php'; + + $this->pluginEnv = $pluginEnv; if (getenv('CV_PLUGIN_PATH')) { $this->paths = explode(PATH_SEPARATOR, getenv('CV_PLUGIN_PATH')); } @@ -35,7 +49,7 @@ public function init(array $pluginEnv) { // Always load internal plugins $this->paths[] = dirname(__DIR__) . '/plugin'; - $this->plugins = []; + $plugins = []; foreach ($this->paths as $path) { if (file_exists($path) && is_dir($path)) { foreach ((array) glob("$path/*.php") as $file) { @@ -43,21 +57,45 @@ public function init(array $pluginEnv) { if ($pluginName === basename($file)) { throw new \RuntimeException("Malformed plugin name: $file"); } - if (!isset($this->plugins[$pluginName])) { - $this->plugins[$pluginName] = $file; + if (!isset($plugins[$pluginName])) { + $plugins[$pluginName] = $file; } else { - fprintf(STDERR, "WARNING: Plugin %s has multiple definitions (%s, %s)\n", $pluginName, $file, $this->plugins[$pluginName]); + fprintf(STDERR, "WARNING: Plugin %s has multiple definitions (%s, %s)\n", $pluginName, $file, $plugins[$pluginName]); } } } } - ksort($this->plugins); - foreach ($this->plugins as $pluginName => $pluginFile) { - // FIXME: Refactor so that you can add more plugins post-boot `load("/some/glob*.php")` - $this->load($pluginEnv + [ - 'protocol' => 1, + $this->loadAll($plugins); + } + + /** + * Like CvPlugins::init(), this searches for and loads plugins. This is effectively + * the second phase of plugin-loading. It focuses on CiviCRM extensions + * which embed extra plugins. + */ + public function initExtensions(): void { + $plugins = []; + $event = GenericHookEvent::create([ + 'plugins' => &$plugins, + 'pluginEnv' => $this->pluginEnv + ['protocol' => self::PROTOCOL_VERSION], + ]); + \Civi::dispatcher()->dispatch('civi.cv-lib.plugins', $event); + + $this->loadAll($plugins); + } + + /** + * @param array $plugins + * Ex: ['helloworld' => '/etc/cv/plugin/helloworld.php'] + * @internal + */ + public function loadAll(array $plugins): void { + ksort($plugins); + foreach ($plugins as $pluginName => $pluginFile) { + $this->load($this->pluginEnv + [ + 'protocol' => self::PROTOCOL_VERSION, 'name' => $pluginName, 'file' => $pluginFile, ]); @@ -71,9 +109,22 @@ public function init(array $pluginEnv) { * - version: Protocol version (ex: "1") * - name: Basenemae of the plugin (eg `hello.php`) * - file: Logic filename (eg `/etc/cv/plugin/hello.php`) + * * @return void + * @internal */ - protected function load(array $CV_PLUGIN) { + public function load(array $CV_PLUGIN) { + if (isset($this->plugins[$CV_PLUGIN['name']])) { + if ($this->plugins[$CV_PLUGIN['name']] === $CV_PLUGIN['file']) { + return; + } + else { + fprintf(STDERR, "WARNING: Plugin %s has already been loaded from %s. Ignore duplicate %s.\n", + $CV_PLUGIN['name'], $this->plugins[$CV_PLUGIN['name']], $CV_PLUGIN['file']); + return; + } + } + $this->plugins[$CV_PLUGIN['name']] = $CV_PLUGIN['file']; include $CV_PLUGIN['file']; } diff --git a/lib/src/cvplugin_loader.php b/lib/src/cvplugin_loader.php new file mode 100644 index 0000000..4813357 --- /dev/null +++ b/lib/src/cvplugin_loader.php @@ -0,0 +1,27 @@ +initExtensions(); + } + } +}