diff --git a/doc/plugins.md b/doc/plugins.md index 0235d803..541d091d 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -7,7 +7,9 @@ Cv plugins are PHP files which register event listeners. ```php // FILE: /etc/cv/plugin/hello-command.php use Civi\Cv\Cv; +use Civi\Cv\Command\CvCommand; use CvDeps\Symfony\Component\Console\Input\InputInterface; +use CvDeps\Symfony\Component\Console\Input\InputArgument; use CvDeps\Symfony\Component\Console\Output\OutputInterface; use CvDeps\Symfony\Component\Console\Command\Command; @@ -16,15 +18,15 @@ if (empty($CV_PLUGIN['protocol']) || $CV_PLUGIN['protocol'] > 1) { } Cv::dispatcher()->addListener('cv.app.commands', function($e) { - $e['commands'][] = new class extends Command { - protected function configure() { - $this->setName('hello')->setDescription('Say a greeting'); - } - protected function execute(InputInterface $input, OutputInterface $output): int { - $output->writeln('Hello there!'); + + $e['commands'][] = (new CvCommand('hello')) + ->setDescription('Say a greeting') + ->addArgument('name', InputArgument::REQUIRED, 'Name of the person to greet') + ->setCode(function($input, $output) { + $output->writeln('Hello, ' . $input->getArgument('name')); return 0; - } - }; + }); + }); ``` diff --git a/lib/README.md b/lib/README.md index 5f79d6ce..3ce8352f 100644 --- a/lib/README.md +++ b/lib/README.md @@ -78,11 +78,13 @@ For more info about `$options`, see the docblocks. ## Experimental API -Other classes are included, but their contracts are subject to change. - -A particularly interesting one is `BootTrait`. This requires `symfony/console`, and it is used by most `cv` subcommands -to achieve common behaviors: - -1. `BootTrait` defines certain CLI options (`--level`, `--user`, `--hostname`, etc). -2. `BootTrait` automatically decides between `Bootstrap.php` and `CmsBootstrap.php`. -3. `BootTrait` passes CLI options through to `Bootstrap.php` or `CmsBootstrap.php`. +Other classes are included, but their contracts are subject to change. These +include higher-level helpers for building Symfony Console apps that incorporate +Civi bootstrap behaviors. + +* `BootTrait` has previously suggested as an experimentally available API + (circa v0.3.44). It changed significantly (circa v0.3.56), where + `configureBootOptions()` was replaced by `$bootOptions`, `mergeDefaultBootDefinition()`, + and `mergeBootDefinition()`. +* As an alternative, consider the classes `BaseApplication` and `CvCommand` if you aim + to build a tool using Symfony Console and Cv Lib. diff --git a/lib/src/BaseApplication.php b/lib/src/BaseApplication.php index 9c4c668f..dcc881e0 100644 --- a/lib/src/BaseApplication.php +++ b/lib/src/BaseApplication.php @@ -2,6 +2,7 @@ namespace Civi\Cv; use Civi\Cv\Util\AliasFilter; +use Civi\Cv\Util\BootTrait; use Civi\Cv\Util\CvArgvInput; use LesserEvil\ShellVerbosityIsEvil; use Symfony\Component\Console\Input\InputInterface; @@ -24,6 +25,8 @@ public static function main(string $name, ?string $binDir, array $argv) { try { $application = new static($name); + Cv::ioStack()->replace('app', $application); + $application->configure(); $argv = AliasFilter::filter($argv); $result = $application->run(new CvArgvInput($argv), Cv::ioStack()->current('output')); } @@ -38,8 +41,7 @@ public static function main(string $name, ?string $binDir, array $argv) { exit($result); } - public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') { - parent::__construct($name, $version); + public function configure() { $this->setCatchExceptions(TRUE); $this->setAutoExit(FALSE); @@ -66,6 +68,12 @@ protected function getDefaultInputDefinition() { $definition = parent::getDefaultInputDefinition(); $definition->addOption(new InputOption('cwd', NULL, InputOption::VALUE_REQUIRED, 'If specified, use the given directory as working directory.')); $definition->addOption(new InputOption('site-alias', NULL, InputOption::VALUE_REQUIRED, 'Load site connection data based on its alias')); + + $c = new class() { + use BootTrait; + }; + $c->mergeDefaultBootDefinition($definition); + return $definition; } diff --git a/lib/src/Command/CvCommand.php b/lib/src/Command/CvCommand.php new file mode 100644 index 00000000..d6bc5616 --- /dev/null +++ b/lib/src/Command/CvCommand.php @@ -0,0 +1,38 @@ +mergeBootDefinition($this->getDefinition()); + } + + /** + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + */ + protected function initialize(InputInterface $input, OutputInterface $output) { + $this->autoboot($input, $output); + parent::initialize($input, $output); + $this->runOptionCallbacks($input, $output); + } + +} diff --git a/lib/src/Cv.php b/lib/src/Cv.php index bb5e4915..26e30db9 100644 --- a/lib/src/Cv.php +++ b/lib/src/Cv.php @@ -79,6 +79,13 @@ public static function ioStack(): IOStack { return static::$instances[__FUNCTION__]; } + /** + * @return \CvDeps\Symfony\Component\Console\Application|\Symfony\Component\Console\Application + */ + public static function app() { + return static::ioStack()->current('app'); + } + /** * @return \CvDeps\Symfony\Component\Console\Input\InputInterface|\Symfony\Component\Console\Input\InputInterface */ diff --git a/lib/src/Util/BootTrait.php b/lib/src/Util/BootTrait.php index c3baf46f..2db48498 100644 --- a/lib/src/Util/BootTrait.php +++ b/lib/src/Util/BootTrait.php @@ -16,13 +16,70 @@ */ trait BootTrait { - public function configureBootOptions($defaultLevel = 'full|cms-full') { - $this->addOption('level', NULL, InputOption::VALUE_REQUIRED, 'Bootstrap level (none,classloader,settings,full,cms-only,cms-full)', $defaultLevel); - $this->addOption('hostname', NULL, InputOption::VALUE_REQUIRED, 'Hostname (for a multisite system)'); - $this->addOption('test', 't', InputOption::VALUE_NONE, 'Bootstrap the test database (CIVICRM_UF=UnitTests)'); - $this->addOption('user', 'U', InputOption::VALUE_REQUIRED, 'CMS user'); + /** + * Describe the expected bootstrap behaviors for this command. + * + * - For most commands, you will want to automatically boot CiviCRM/CMS. + * The default implementation will do this. + * - For some special commands (e.g. core-installer or PHP-script-runner), you may + * want more fine-grained control over when/how the system boots. + * + * @var array + */ + protected $bootOptions = [ + // Whether to automatically boot Civi during `initialize()` phase. + 'auto' => TRUE, + + // Default boot level. + 'default' => 'full|cms-full', + + // List of all boot levels that are allowed in this command. + 'allow' => ['full|cms-full', 'full', 'cms-full', 'settings', 'classloader', 'cms-only', 'none'], + ]; + + /** + * @internal + */ + public function mergeDefaultBootDefinition($definition, $defaultLevel = 'full|cms-full') { + // If we were only dealing with built-in/global commands, then these options could be defined at the command-level. + // However, we also have extension-based commands. The system will boot before we have a chance to discover them. + // By putting these options at the application level, we ensure they will be defined+used. + $definition->addOption(new InputOption('level', NULL, InputOption::VALUE_REQUIRED, 'Bootstrap level (none,classloader,settings,full,cms-only,cms-full)', $defaultLevel)); + $definition->addOption(new InputOption('hostname', NULL, InputOption::VALUE_REQUIRED, 'Hostname (for a multisite system)')); + $definition->addOption(new InputOption('test', 't', InputOption::VALUE_NONE, 'Bootstrap the test database (CIVICRM_UF=UnitTests)')); + $definition->addOption(new InputOption('user', 'U', InputOption::VALUE_REQUIRED, 'CMS user')); + } + + /** + * @internal + */ + public function mergeBootDefinition($definition) { + $bootOptions = $this->getBootOptions(); + $definition->getOption('level')->setDefault($bootOptions['default']); + } + + /** + * Evaluate the $bootOptions. + * + * - If we've already booted, do nothing. + * - If the configuration looks reasonable and if we haven't booted yet, then boot(). + * - If the configuration looks unreasonable, then abort. + */ + protected function autoboot(InputInterface $input, OutputInterface $output): void { + $bootOptions = $this->getBootOptions(); + if (!in_array($input->getOption('level'), $bootOptions['allow'])) { + throw new \LogicException(sprintf("Command called with with level (%s) but only accepts levels (%s)", + $input->getOption('level'), implode(', ', $bootOptions['allow']))); + } + + if (!$this->isBooted() && ($bootOptions['auto'] ?? TRUE)) { + $this->boot($input, $output); + } } + /** + * Start CiviCRM and/or CMS. Respect options like --user and --level. + */ public function boot(InputInterface $input, OutputInterface $output) { $logger = $this->bootLogger($output); $logger->debug('Start'); @@ -290,4 +347,33 @@ private function bootLogger(OutputInterface $output): InternalLogger { return new SymfonyConsoleLogger('BootTrait', $output); } + /** + * @return bool + */ + protected function isBooted() { + return defined('CIVICRM_DSN'); + } + + protected function assertBooted() { + if (!$this->isBooted()) { + throw new \Exception("Error: This command requires bootstrapping, but the system does not appear to be bootstrapped. Perhaps you set --level=none?"); + } + } + + /** + * @return array{auto: bool, default: string, allow: string[]} + */ + public function getBootOptions(): array { + return $this->bootOptions; + } + + /** + * @param array{auto: bool, default: string, allow: string[]} $bootOptions + * @return $this + */ + public function setBootOptions(array $bootOptions) { + $this->bootOptions = array_merge($this->bootOptions, $bootOptions); + return $this; + } + } diff --git a/lib/src/Util/IOStack.php b/lib/src/Util/IOStack.php index 08f85e56..a8e734b1 100644 --- a/lib/src/Util/IOStack.php +++ b/lib/src/Util/IOStack.php @@ -21,16 +21,19 @@ class IOStack { * * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface $output + * @param \Symfony\Component\Console\Application|null $app * @return scalar * Internal identifier for the stack-frame. ID formatting is not guaranteed. */ - public function push(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output) { + public function push(\Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output, ?\Symfony\Component\Console\Application $app = NULL) { ++static::$id; + $app = $app ?: ($this->stack[0]['app'] ?? NULL); array_unshift($this->stack, [ 'id' => static::$id, 'input' => $input, 'output' => $output, 'io' => new SymfonyStyle($input, $output), + 'app' => $app, ]); return static::$id; } @@ -68,6 +71,10 @@ public function get($id, string $property) { return NULL; } + public function replace($property, $value) { + $this->stack[0][$property] = $value; + } + public function reset() { $this->stack = []; } diff --git a/lib/src/Util/OptionalOption.php b/lib/src/Util/OptionalOption.php new file mode 100644 index 00000000..38bf8c92 --- /dev/null +++ b/lib/src/Util/OptionalOption.php @@ -0,0 +1,42 @@ + Means "--refresh=auto"; see $omittedDefault + * cv en -r ==> Means "--refresh=yes"; see $activeDefault + * cv en -r=yes ==> Means "--refresh=yes" + * cv en -r=no ==> Means "--refresh=no" + * + * @param \CvDeps\Symfony\Component\Console\Input\InputInterface|\Symfony\Component\Console\Input\InputInterface $input + * @param array $rawNames + * Ex: array('-r', '--refresh'). + * @param string $omittedDefault + * Value to use if option is completely omitted. + * @param string $activeDefault + * Value to use if option is activated without data. + * @return string + */ + public static function parse($input, $rawNames, $omittedDefault, $activeDefault) { + $value = NULL; + foreach ($rawNames as $rawName) { + if ($input->hasParameterOption($rawName)) { + if (NULL === $input->getParameterOption($rawName)) { + return $activeDefault; + } + else { + return $input->getParameterOption($rawName); + } + } + } + return $omittedDefault; + } + +} diff --git a/src/Command/AngularHtmlListCommand.php b/src/Command/AngularHtmlListCommand.php index b191577e..9ced3f51 100644 --- a/src/Command/AngularHtmlListCommand.php +++ b/src/Command/AngularHtmlListCommand.php @@ -1,15 +1,13 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); if (!$input->getOption('user')) { $output->getErrorOutput()->writeln("For a full list, try passing --user=[username]."); } diff --git a/src/Command/AngularHtmlShowCommand.php b/src/Command/AngularHtmlShowCommand.php index 41801d2c..fed17693 100644 --- a/src/Command/AngularHtmlShowCommand.php +++ b/src/Command/AngularHtmlShowCommand.php @@ -2,15 +2,12 @@ namespace Civi\Cv\Command; use Civi\Cv\Util\Process; -use Civi\Cv\Util\BootTrait; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class AngularHtmlShowCommand extends BaseCommand { - - use BootTrait; +class AngularHtmlShowCommand extends CvCommand { /** * @param string|null $name @@ -38,11 +35,9 @@ protected function configure() { cv ang:html:show crmMailing/BlockMailing.html --diff | colordiff cv ang:html:show "~/crmMailing/BlockMailing.html" '); - $this->configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); if (!$input->getOption('user')) { $output->getErrorOutput()->writeln("For a full list, try passing --user=[username]."); } diff --git a/src/Command/AngularModuleListCommand.php b/src/Command/AngularModuleListCommand.php index 5bc51f57..73dbe08c 100644 --- a/src/Command/AngularModuleListCommand.php +++ b/src/Command/AngularModuleListCommand.php @@ -1,15 +1,13 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); if (!$input->getOption('user')) { $output->getErrorOutput()->writeln("For a full list, try passing --user=[username]."); } diff --git a/src/Command/Api4Command.php b/src/Command/Api4Command.php index 1c796304..40e0b37c 100644 --- a/src/Command/Api4Command.php +++ b/src/Command/Api4Command.php @@ -3,16 +3,14 @@ use Civi\Cv\Encoder; use Civi\Cv\Util\Api4ArgParser; -use Civi\Cv\Util\BootTrait; use Civi\Cv\Util\StructuredOutputTrait; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class Api4Command extends BaseCommand { +class Api4Command extends CvCommand { - use BootTrait; use StructuredOutputTrait; /** @@ -122,7 +120,6 @@ protected function configure() { NOTE: To change the default output format, set CV_OUTPUT. "); - $this->configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -131,8 +128,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $I = ''; $_I = ''; - $this->boot($input, $output); - if (!function_exists('civicrm_api4')) { throw new \RuntimeException("Please enable APIv4 before running APIv4 commands."); } diff --git a/src/Command/ApiBatchCommand.php b/src/Command/ApiBatchCommand.php index 59d9ad9d..8c0bdc71 100644 --- a/src/Command/ApiBatchCommand.php +++ b/src/Command/ApiBatchCommand.php @@ -1,14 +1,11 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -64,7 +60,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Other formats may not work with the fgets() loop. throw new \Exception("api:batch only supports JSON dialog"); } - $this->boot($input, $output); $addDefault = function($v) { $this->defaults = \CRM_Utils_Array::crmArrayMerge($v, $this->defaults); diff --git a/src/Command/ApiCommand.php b/src/Command/ApiCommand.php index 14e15e72..6713afb2 100644 --- a/src/Command/ApiCommand.php +++ b/src/Command/ApiCommand.php @@ -2,16 +2,14 @@ namespace Civi\Cv\Command; use Civi\Cv\Encoder; -use Civi\Cv\Util\BootTrait; use Civi\Cv\Util\StructuredOutputTrait; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class ApiCommand extends BaseCommand { +class ApiCommand extends CvCommand { - use BootTrait; use StructuredOutputTrait; /** @@ -47,7 +45,6 @@ protected function configure() { TIP: To display a full backtrace of any errors, pass "-vv" (very verbose). '); - $this->configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -56,8 +53,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $I = ''; $_I = ''; - $this->boot($input, $output); - list($entity, $action) = explode('.', $input->getArgument('Entity.action')); $params = $this->parseParams($input); diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php deleted file mode 100644 index 2647d191..00000000 --- a/src/Command/BaseCommand.php +++ /dev/null @@ -1,111 +0,0 @@ -runOptionCallbacks($input, $output); - } - - protected function assertBooted() { - if (!$this->isBooted()) { - throw new \Exception("Error: This command requires bootstrapping, but the system does not appear to be bootstrapped. Perhaps you set --level=none?"); - } - } - - /** - * Execute an API call. If it fails, display a formatted error. - * - * Note: If there is an error, we still return it softly so that the - * command can exit gracefully. - * - * @param \Symfony\Component\Console\Input\InputInterface $input - * @param \Symfony\Component\Console\Output\OutputInterface $output - * @param $entity - * @param $action - * @param $params - * @return mixed - */ - protected function callApiSuccess(InputInterface $input, OutputInterface $output, $entity, $action, $params) { - $this->assertBooted(); - $params['debug'] = 1; - if (!isset($params['version'])) { - $params['version'] = 3; - } - $output->writeln("Calling $entity $action API", OutputInterface::VERBOSITY_DEBUG); - $result = \civicrm_api($entity, $action, $params); - if (!empty($result['is_error']) || $output->isDebug()) { - $data = array( - 'entity' => $entity, - 'action' => $action, - 'params' => $params, - 'result' => $result, - ); - if (!empty($result['is_error'])) { - $output->getErrorOutput()->writeln("Error: API Call Failed: " - . Encoder::encode($data, 'pretty')); - } - else { - $output->writeln("API success" . Encoder::encode($data, 'pretty'), - OutputInterface::VERBOSITY_DEBUG); - } - } - return $result; - } - - /** - * Parse an option's data. This is for options where the default behavior - * (of total omission) differs from the activated behavior - * (of an active but unspecified option). - * - * Example, suppose we want these interpretations: - * cv en ==> Means "--refresh=auto"; see $omittedDefault - * cv en -r ==> Means "--refresh=yes"; see $activeDefault - * cv en -r=yes ==> Means "--refresh=yes" - * cv en -r=no ==> Means "--refresh=no" - * - * @param \Symfony\Component\Console\Input\InputInterface $input - * @param array $rawNames - * Ex: array('-r', '--refresh'). - * @param string $omittedDefault - * Value to use if option is completely omitted. - * @param string $activeDefault - * Value to use if option is activated without data. - * @return string - */ - public function parseOptionalOption(InputInterface $input, $rawNames, $omittedDefault, $activeDefault) { - $value = NULL; - foreach ($rawNames as $rawName) { - if ($input->hasParameterOption($rawName)) { - if (NULL === $input->getParameterOption($rawName)) { - return $activeDefault; - } - else { - return $input->getParameterOption($rawName); - } - } - } - return $omittedDefault; - } - - /** - * @return bool - */ - protected function isBooted() { - return defined('CIVICRM_DSN'); - } - -} diff --git a/src/Command/BootCommand.php b/src/Command/BootCommand.php index 11a8b82a..60dc8df1 100644 --- a/src/Command/BootCommand.php +++ b/src/Command/BootCommand.php @@ -1,24 +1,18 @@ setName('php:boot') ->setDescription('Generate PHP bootstrap code'); - $this->configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - switch ($input->getOption('level')) { case 'classloader': $code = sprintf('require_once %s . "/CRM/Core/ClassLoader.php";', var_export(rtrim($GLOBALS["civicrm_root"], '/'), 1)) diff --git a/src/Command/CliCommand.php b/src/Command/CliCommand.php index 9dca5f27..1d24a05d 100644 --- a/src/Command/CliCommand.php +++ b/src/Command/CliCommand.php @@ -6,24 +6,18 @@ // ********************** use Civi\Cv\Application; -use Civi\Cv\Util\BootTrait; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class CliCommand extends BaseCommand { - - use BootTrait; +class CliCommand extends CvCommand { protected function configure() { $this ->setName('cli') ->setDescription('Load interactive command line'); - $this->configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - $cv = new Application(); $sh = new \Psy\Shell(); $sh->addCommands($cv->createCommands()); diff --git a/src/Command/CoreCheckReqCommand.php b/src/Command/CoreCheckReqCommand.php index d9836efc..033db002 100644 --- a/src/Command/CoreCheckReqCommand.php +++ b/src/Command/CoreCheckReqCommand.php @@ -8,7 +8,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class CoreCheckReqCommand extends BaseCommand { +class CoreCheckReqCommand extends CvCommand { use SetupCommandTrait; use DebugDispatcherTrait; @@ -35,7 +35,10 @@ protected function configure() { Example: Show warnings and errors $ cv core:check-req -we '); - $this->configureBootOptions('none'); + } + + public function getBootOptions(): array { + return ['default' => 'none', 'allow' => ['none']]; } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/src/Command/CoreInstallCommand.php b/src/Command/CoreInstallCommand.php index 76166030..96e6bc24 100644 --- a/src/Command/CoreInstallCommand.php +++ b/src/Command/CoreInstallCommand.php @@ -2,6 +2,7 @@ namespace Civi\Cv\Command; use Civi\Cv\Encoder; +use Civi\Cv\Util\OptionalOption; use Civi\Cv\Util\SetupCommandTrait; use Civi\Cv\Util\DebugDispatcherTrait; use Symfony\Component\Console\Input\InputInterface; @@ -9,7 +10,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; -class CoreInstallCommand extends BaseCommand { +class CoreInstallCommand extends CvCommand { use SetupCommandTrait; use DebugDispatcherTrait; @@ -49,7 +50,10 @@ protected function configure() { $ cv core:install --model=extras.opt-in.versionCheck=1 $ cv core:install -m extras.opt-in.versionCheck=1 '); - $this->configureBootOptions('none'); + } + + public function getBootOptions(): array { + return ['default' => 'none', 'allow' => ['none']]; } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -57,7 +61,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $debugMode = FALSE; - $debugEvent = $this->parseOptionalOption($input, ['--debug-event'], NULL, ''); + $debugEvent = OptionalOption::parse($input, ['--debug-event'], NULL, ''); if ($debugEvent !== NULL) { $eventNames = $this->findEventNames($setup->getDispatcher(), $debugEvent); $this->printEventListeners($output, $setup->getDispatcher(), $eventNames); diff --git a/src/Command/CoreUninstallCommand.php b/src/Command/CoreUninstallCommand.php index d6563fca..858b23b7 100644 --- a/src/Command/CoreUninstallCommand.php +++ b/src/Command/CoreUninstallCommand.php @@ -1,6 +1,7 @@ configureBootOptions('none'); + } + + public function getBootOptions(): array { + return ['default' => 'none', 'allow' => ['none']]; } protected function execute(InputInterface $input, OutputInterface $output): int { $setup = $this->bootSetupSubsystem($input, $output); - $debugEvent = $this->parseOptionalOption($input, ['--debug-event'], NULL, ''); + $debugEvent = OptionalOption::parse($input, ['--debug-event'], NULL, ''); if ($debugEvent !== NULL) { $eventNames = $this->findEventNames($setup->getDispatcher(), $debugEvent); $this->printEventListeners($output, $setup->getDispatcher(), $eventNames); diff --git a/src/Command/DebugContainerCommand.php b/src/Command/DebugContainerCommand.php index 2d2fba3b..87673a99 100644 --- a/src/Command/DebugContainerCommand.php +++ b/src/Command/DebugContainerCommand.php @@ -1,7 +1,6 @@ configureBootOptions(); } - protected function execute(InputInterface $input, OutputInterface $output): int { + protected function initialize(InputInterface $input, OutputInterface $output) { define('CIVICRM_CONTAINER_CACHE', 'never'); $output->getErrorOutput()->writeln('The debug command ignores the container cache.'); - $this->boot($input, $output); + parent::initialize($input, $output); + } + protected function execute(InputInterface $input, OutputInterface $output): int { $c = $this->getInspectableContainer($input); $filterPat = $input->getArgument('name'); diff --git a/src/Command/DebugDispatcherCommand.php b/src/Command/DebugDispatcherCommand.php index 1bbb6192..4ca7f609 100644 --- a/src/Command/DebugDispatcherCommand.php +++ b/src/Command/DebugDispatcherCommand.php @@ -1,15 +1,13 @@ configureBootOptions(); } - protected function execute(InputInterface $input, OutputInterface $output): int { + protected function initialize(InputInterface $input, OutputInterface $output) { define('CIVICRM_CONTAINER_CACHE', 'never'); $output->getErrorOutput()->writeln('The debug command ignores the container cache.'); - $this->boot($input, $output); + parent::initialize($input, $output); + } + protected function execute(InputInterface $input, OutputInterface $output): int { $container = \Civi::container(); /* diff --git a/src/Command/EditCommand.php b/src/Command/EditCommand.php index 6e5f3549..9b2375b9 100644 --- a/src/Command/EditCommand.php +++ b/src/Command/EditCommand.php @@ -9,13 +9,10 @@ use Civi\Cv\Config; use Civi\Cv\Encoder; use Civi\Cv\Util\CliEditor; -use Civi\Cv\Util\BootTrait; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class EditCommand extends BaseCommand { - - use BootTrait; +class EditCommand extends CvCommand { /** * @var \Civi\Cv\Util\CliEditor @@ -26,7 +23,6 @@ protected function configure() { $this ->setName('vars:edit') ->setDescription('Edit configuration values for this build'); - $this->configureBootOptions(); } public function __construct($name = NULL) { @@ -47,8 +43,6 @@ public function __construct($name = NULL) { } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - $config = Config::read(); $oldSiteData = empty($config['sites'][CIVICRM_SETTINGS_PATH]) ? array() : $config['sites'][CIVICRM_SETTINGS_PATH]; $oldJson = Encoder::encode($oldSiteData, 'json-pretty'); diff --git a/src/Command/EvalCommand.php b/src/Command/EvalCommand.php index 69e9d0fb..81bdb571 100644 --- a/src/Command/EvalCommand.php +++ b/src/Command/EvalCommand.php @@ -2,15 +2,13 @@ namespace Civi\Cv\Command; use Civi\Cv\Encoder; -use Civi\Cv\Util\BootTrait; use Civi\Cv\Util\StructuredOutputTrait; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class EvalCommand extends BaseCommand { +class EvalCommand extends CvCommand { - use BootTrait; use StructuredOutputTrait; protected function configure() { @@ -37,12 +35,9 @@ protected function configure() { NOTE: To change the default output format, set CV_OUTPUT. '); - $this->configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - if ($input->getOption('out') === 'auto') { $hasReturn = preg_match('/^\s*return[ \t\r\n]/', $input->getArgument('code')) || preg_match('/[;\{]\s*return[ \t\r\n]/', $input->getArgument('code')); diff --git a/src/Command/ExtensionDisableCommand.php b/src/Command/ExtensionDisableCommand.php index abc058a3..8f17c070 100644 --- a/src/Command/ExtensionDisableCommand.php +++ b/src/Command/ExtensionDisableCommand.php @@ -1,11 +1,15 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - list ($foundKeys, $missingKeys) = $this->parseKeys($input, $output); + [$foundKeys, $missingKeys] = $this->parseKeys($input, $output); // Uninstall what's recognized or what looks like an ext key. $disableKeys = array_merge($foundKeys, preg_grep('/\./', $missingKeys)); @@ -52,7 +54,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("Disabling extension \"$key\""); } - $result = $this->callApiSuccess($input, $output, 'Extension', 'disable', array( + $result = VerboseApi::callApi3Success('Extension', 'disable', array( 'keys' => $disableKeys, )); return empty($result['is_error']) ? 0 : 1; diff --git a/src/Command/ExtensionDownloadCommand.php b/src/Command/ExtensionDownloadCommand.php index f5820af6..105f0591 100644 --- a/src/Command/ExtensionDownloadCommand.php +++ b/src/Command/ExtensionDownloadCommand.php @@ -1,15 +1,19 @@ configureBootOptions(); + $this->configureRepoOptions(); } protected function initialize(InputInterface $input, OutputInterface $output) { @@ -65,18 +68,15 @@ protected function initialize(InputInterface $input, OutputInterface $output) { $input->setOption('level', 'none'); $input->setOption('no-install', TRUE); } - parent::initialize($input, $output); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $fs = new Filesystem(); - if ($extRepoUrl = $this->parseRepoUrl($input)) { global $civicrm_setting; $civicrm_setting['Extension Preferences']['ext_repo_url'] = $extRepoUrl; } + parent::initialize($input, $output); + } - $this->boot($input, $output); + protected function execute(InputInterface $input, OutputInterface $output): int { + $fs = new Filesystem(); if ($input->getOption('to') && !$fs->isAbsolutePath($input->getOption('to'))) { throw new \RuntimeException("The --to argument requires an absolute path."); @@ -91,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int while (TRUE) { if ($refresh === 'yes' && $this->isBooted()) { $output->writeln("Refreshing extension cache"); - $result = $this->callApiSuccess($input, $output, 'Extension', 'refresh', array( + $result = VerboseApi::callApi3Success('Extension', 'refresh', array( 'local' => FALSE, 'remote' => TRUE, )); @@ -100,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - list ($downloads, $errors) = $this->parseDownloads($input); + [$downloads, $errors] = $this->parseDownloads($input); if ($refresh == 'auto' && !empty($errors)) { $output->writeln("Extension cache does not contain requested item(s)"); $refresh = 'yes'; @@ -135,7 +135,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int else { $output->writeln("Downloading extension \"$key\" ($url)"); $this->assertBooted(); - $result = $this->callApiSuccess($input, $output, 'Extension', 'download', array( + $result = VerboseApi::callApi3Success('Extension', 'download', array( 'key' => $key, 'url' => $url, 'install' => !$input->getOption('no-install'), @@ -145,7 +145,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int case 'install': $output->writeln("Found extension \"$key\". Enabling."); - $result = $this->callApiSuccess($input, $output, 'Extension', 'enable', array( + $result = VerboseApi::callApi3Success('Extension', 'enable', array( 'key' => $key, )); break; @@ -225,7 +225,7 @@ protected function parseDownloads(InputInterface $input) { $origExpr = $keyOrName; $url = NULL; if (strpos($keyOrName, '@') !== FALSE) { - list ($keyOrName, $url) = explode('@', $keyOrName, 2); + [$keyOrName, $url] = explode('@', $keyOrName, 2); } if (empty($keyOrName) && !empty($url)) { diff --git a/src/Command/ExtensionEnableCommand.php b/src/Command/ExtensionEnableCommand.php index 0de3bba5..73e618e6 100644 --- a/src/Command/ExtensionEnableCommand.php +++ b/src/Command/ExtensionEnableCommand.php @@ -1,12 +1,16 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - // Refresh extensions if (a) ---refresh enabled or (b) there's a cache-miss. $refresh = $input->getOption('refresh') ? 'yes' : 'auto'; - // $refresh = $this->parseOptionalOption($input, array('--refresh', '-r'), 'auto', 'yes'); + // $refresh = OptionalOption::parse(array('--refresh', '-r'), 'auto', 'yes'); while (TRUE) { if ($refresh === 'yes') { $output->writeln("Refreshing extension cache"); - $result = $this->callApiSuccess($input, $output, 'Extension', 'refresh', array( + $result = VerboseApi::callApi3Success('Extension', 'refresh', array( 'local' => TRUE, 'remote' => FALSE, )); @@ -58,7 +59,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - list ($foundKeys, $missingKeys) = $this->parseKeys($input, $output); + [$foundKeys, $missingKeys] = $this->parseKeys($input, $output); if ($refresh == 'auto' && !empty($missingKeys)) { $output->writeln("Extension cache does not contain requested item(s)"); $refresh = 'yes'; @@ -87,7 +88,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("Enabling extension \"$key\""); } - $result = $this->callApiSuccess($input, $output, 'Extension', 'install', array( + $result = VerboseApi::callApi3Success('Extension', 'install', array( 'keys' => $foundKeys, )); return empty($result['is_error']) ? 0 : 1; diff --git a/src/Command/ExtensionListCommand.php b/src/Command/ExtensionListCommand.php index b5577451..09e6a63d 100644 --- a/src/Command/ExtensionListCommand.php +++ b/src/Command/ExtensionListCommand.php @@ -3,15 +3,18 @@ use Civi\Cv\Cv; use Civi\Cv\Util\ArrayUtil; +use Civi\Cv\Util\ExtensionTrait; use Civi\Cv\Util\Relativizer; use Civi\Cv\Util\StructuredOutputTrait; +use Civi\Cv\Util\VerboseApi; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class ExtensionListCommand extends BaseExtensionCommand { +class ExtensionListCommand extends CvCommand { + use ExtensionTrait; use StructuredOutputTrait; /** @@ -56,11 +59,15 @@ protected function configure() { include a unique long name ("org.example.foobar") and a unique short name ("foobar"). However, short names are not strongly guaranteed. '); - parent::configureRepoOptions(); - $this->configureBootOptions(); + $this->configureRepoOptions(); } protected function initialize(InputInterface $input, OutputInterface $output) { + if ($extRepoUrl = $this->parseRepoUrl($input)) { + global $civicrm_setting; + $civicrm_setting['Extension Preferences']['ext_repo_url'] = $extRepoUrl; + } + parent::initialize($input, $output); // We apply different defaults for the 'columns' list depending on the output medium. @@ -82,14 +89,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ? (OutputInterface::OUTPUT_NORMAL | OutputInterface::VERBOSITY_NORMAL) : (OutputInterface::OUTPUT_NORMAL | OutputInterface::VERBOSITY_VERBOSE); - list($local, $remote) = $this->parseLocalRemote($input); - - if ($extRepoUrl = $this->parseRepoUrl($input)) { - global $civicrm_setting; - $civicrm_setting['Extension Preferences']['ext_repo_url'] = $extRepoUrl; - } - - $this->boot($input, $output); + [$local, $remote] = $this->parseLocalRemote($input); if ($remote) { $output->writeln("Using extension feed \"" . \CRM_Extension_System::singleton()->getBrowser()->getRepositoryUrl() . "\"", $wo); @@ -97,7 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->getOption('refresh')) { $output->writeln("Refreshing extensions", $wo); - $result = $this->callApiSuccess($input, $output, 'Extension', 'refresh', array( + $result = VerboseApi::callApi3Success('Extension', 'refresh', array( 'local' => $local, 'remote' => $remote, )); @@ -136,7 +136,7 @@ protected function getRemoteInfos() { */ protected function find($input) { $regex = $input->getArgument('regex'); - list($local, $remote) = $this->parseLocalRemote($input); + [$local, $remote] = $this->parseLocalRemote($input); if ($input->getOption('installed')) { $statusFilter = array('installed'); diff --git a/src/Command/ExtensionUninstallCommand.php b/src/Command/ExtensionUninstallCommand.php index 839ab593..161db433 100644 --- a/src/Command/ExtensionUninstallCommand.php +++ b/src/Command/ExtensionUninstallCommand.php @@ -1,11 +1,15 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - list ($foundKeys, $missingKeys) = $this->parseKeys($input, $output); + [$foundKeys, $missingKeys] = $this->parseKeys($input, $output); // Uninstall what's recognized or what looks like an ext key. $uninstallKeys = array_merge($foundKeys, preg_grep('/\./', $missingKeys)); @@ -52,14 +54,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("Uninstalling extension \"$key\""); } - $result = $this->callApiSuccess($input, $output, 'Extension', 'disable', array( + $result = VerboseApi::callApi3Success('Extension', 'disable', array( 'keys' => $uninstallKeys, )); if (!empty($result['is_error'])) { return 1; } - $result = $this->callApiSuccess($input, $output, 'Extension', 'uninstall', array( + $result = VerboseApi::callApi3Success('Extension', 'uninstall', array( 'keys' => $uninstallKeys, )); return empty($result['is_error']) ? 0 : 1; diff --git a/src/Command/ExtensionUpgradeDbCommand.php b/src/Command/ExtensionUpgradeDbCommand.php index 41f87295..5fbbfd81 100644 --- a/src/Command/ExtensionUpgradeDbCommand.php +++ b/src/Command/ExtensionUpgradeDbCommand.php @@ -1,10 +1,14 @@ configureBootOptions(); } - protected function execute(InputInterface $input, OutputInterface $output): int { + protected function initialize(InputInterface $input, OutputInterface $output) { $output->writeln("WARNING: \"ext:upgrade-db\" is deprecated. Use the main \"updb\" command instead."); - $this->boot($input, $output); + parent::initialize($input, $output); + } + protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln("Applying database upgrades from extensions"); - $result = $this->callApiSuccess($input, $output, 'Extension', 'upgrade', array()); + $result = VerboseApi::callApi3Success('Extension', 'upgrade', array()); if (!empty($result['is_error'])) { return 1; } diff --git a/src/Command/FillCommand.php b/src/Command/FillCommand.php index fb332d27..2199d163 100644 --- a/src/Command/FillCommand.php +++ b/src/Command/FillCommand.php @@ -4,14 +4,11 @@ use Civi\Cv\Config; use Civi\Cv\Encoder; use Civi\Cv\SiteConfigReader; -use Civi\Cv\Util\BootTrait; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class FillCommand extends BaseCommand { - - use BootTrait; +class FillCommand extends CvCommand { protected $fields; @@ -25,7 +22,6 @@ protected function configure() { ->setName('vars:fill') ->setDescription('Generate a configuration file for any missing site data') ->addOption('file', NULL, InputOption::VALUE_REQUIRED, 'Read existing configuration from a file'); - $this->configureBootOptions(); } public function __construct($name = NULL) { @@ -59,7 +55,6 @@ public function __construct($name = NULL) { } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); if (!$input->getOption('file')) { $reader = new SiteConfigReader(CIVICRM_SETTINGS_PATH); $liveData = $reader->compile(array('buildkit', 'home', 'active')); diff --git a/src/Command/FlushCommand.php b/src/Command/FlushCommand.php index b522a90a..d38e1ed1 100644 --- a/src/Command/FlushCommand.php +++ b/src/Command/FlushCommand.php @@ -1,14 +1,12 @@ setHelp(' Flush system caches '); - $this->configureBootOptions(); } - protected function execute(InputInterface $input, OutputInterface $output): int { + protected function initialize(InputInterface $input, OutputInterface $output) { // The main reason we have this as separate command -- so we can ignore // stale class-references that might be retained by the container cache. define('CIVICRM_CONTAINER_CACHE', 'never'); + + parent::initialize($input, $output); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { $this->boot($input, $output); $params = array(); @@ -34,7 +36,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $output->writeln("Flushing system caches"); - $result = $this->callApiSuccess($input, $output, 'System', 'flush', $params); + $result = VerboseApi::callApi3Success('System', 'flush', $params); return empty($result['is_error']) ? 0 : 1; } diff --git a/src/Command/HttpCommand.php b/src/Command/HttpCommand.php index c79b78ce..e7ccc5a5 100644 --- a/src/Command/HttpCommand.php +++ b/src/Command/HttpCommand.php @@ -2,6 +2,7 @@ namespace Civi\Cv\Command; +use Civi\Cv\Util\ExtensionTrait; use Civi\Cv\Util\StructuredOutputTrait; use Civi\Cv\Util\UrlCommandTrait; use GuzzleHttp\Client; @@ -9,8 +10,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class HttpCommand extends BaseExtensionCommand { +class HttpCommand extends CvCommand { + use ExtensionTrait; use StructuredOutputTrait; use UrlCommandTrait; @@ -43,12 +45,9 @@ protected function configure() { enabling the extension. The extra I/O may influence some scripted use-cases. '); - $this->configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - $method = $input->getOption('request'); $data = $this->parseRequestData($input); $headers = $this->parseRequestHeaders($input); diff --git a/src/Command/PathCommand.php b/src/Command/PathCommand.php index 20348d85..1eef6dd9 100644 --- a/src/Command/PathCommand.php +++ b/src/Command/PathCommand.php @@ -1,13 +1,15 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - if (!$input->getOption('ext') && !$input->getOption('config') && !$input->getOption('dynamic')) { $output->getErrorOutput()->writeln("No paths specified. Must use -x, -c, or -d. (See also: cv path -h)"); return 1; diff --git a/src/Command/PipeCommand.php b/src/Command/PipeCommand.php index 25d4140f..6d64eb72 100644 --- a/src/Command/PipeCommand.php +++ b/src/Command/PipeCommand.php @@ -1,14 +1,11 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); if (!is_callable(['Civi', 'pipe'])) { fwrite(STDERR, "This version of CiviCRM does not include Civi::pipe() support.\n"); return 1; diff --git a/src/Command/ScriptCommand.php b/src/Command/ScriptCommand.php index 08e58f01..df20c771 100644 --- a/src/Command/ScriptCommand.php +++ b/src/Command/ScriptCommand.php @@ -2,14 +2,11 @@ namespace Civi\Cv\Command; use Civi\Cv\Util\Filesystem; -use Civi\Cv\Util\BootTrait; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ScriptCommand extends BaseCommand { - - use BootTrait; +class ScriptCommand extends CvCommand { protected function configure() { $this @@ -18,7 +15,10 @@ protected function configure() { ->setDescription('Execute a PHP script') ->addArgument('script', InputArgument::REQUIRED) ->addArgument('scriptArguments', InputArgument::IS_ARRAY, 'Optional arguments to pass to the script as $argv'); - $this->configureBootOptions(); + } + + public function getBootOptions(): array { + return ['auto' => FALSE] + parent::getBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/src/Command/SettingGetCommand.php b/src/Command/SettingGetCommand.php index 36cbe2c7..cf7e0458 100644 --- a/src/Command/SettingGetCommand.php +++ b/src/Command/SettingGetCommand.php @@ -1,7 +1,6 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - $filter = $this->createSettingFilter($input->getArgument('name')); $result = []; diff --git a/src/Command/SettingRevertCommand.php b/src/Command/SettingRevertCommand.php index 489a3e92..722210c9 100644 --- a/src/Command/SettingRevertCommand.php +++ b/src/Command/SettingRevertCommand.php @@ -1,7 +1,6 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); $errorOutput = is_callable([$output, 'getErrorOutput']) ? $output->getErrorOutput() : $output; $filter = $this->createSettingFilter($input->getArgument('name')); diff --git a/src/Command/SettingSetCommand.php b/src/Command/SettingSetCommand.php index 4db84c66..cd717a0c 100644 --- a/src/Command/SettingSetCommand.php +++ b/src/Command/SettingSetCommand.php @@ -1,7 +1,6 @@ configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -91,7 +88,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $I = ''; $_I = ''; - $this->boot($input, $output); $errorOutput = is_callable([$output, 'getErrorOutput']) ? $output->getErrorOutput() : $output; $result = []; diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index a598aa2f..2f5eea5f 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -2,14 +2,12 @@ namespace Civi\Cv\Command; use Civi\Cv\SiteConfigReader; -use Civi\Cv\Util\BootTrait; use Civi\Cv\Util\StructuredOutputTrait; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ShowCommand extends BaseCommand { +class ShowCommand extends CvCommand { - use BootTrait; use StructuredOutputTrait; protected function configure() { @@ -17,11 +15,9 @@ protected function configure() { ->setName('vars:show') ->setDescription('Show the configuration of the local CiviCRM installation') ->configureOutputOptions(); - $this->configureBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); $reader = new SiteConfigReader(CIVICRM_SETTINGS_PATH); $data = $reader->compile(array('buildkit', 'home', 'active')); $this->sendResult($input, $output, $data); diff --git a/src/Command/SqlCliCommand.php b/src/Command/SqlCliCommand.php index f43b259b..367af698 100644 --- a/src/Command/SqlCliCommand.php +++ b/src/Command/SqlCliCommand.php @@ -3,14 +3,11 @@ use Civi\Cv\Util\Datasource; use Civi\Cv\Util\Process; -use Civi\Cv\Util\BootTrait; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class SqlCliCommand extends BaseCommand { - - use BootTrait; +class SqlCliCommand extends CvCommand { protected function configure() { $this @@ -38,7 +35,6 @@ protected function configure() { #ENV[FOO] Produces the numerical value of FOO (or fails) !ENV[FOO] Produces the raw, unescaped string version of FOO "); - $this->configureBootOptions(); } protected function initialize(InputInterface $input, OutputInterface $output) { @@ -48,8 +44,6 @@ protected function initialize(InputInterface $input, OutputInterface $output) { } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->boot($input, $output); - $datasource = new Datasource(); $datasource->loadFromCiviDSN($this->pickDsn($input->getOption('target'))); diff --git a/src/Command/UpgradeCommand.php b/src/Command/UpgradeCommand.php index cbf3e877..ca5b87a9 100644 --- a/src/Command/UpgradeCommand.php +++ b/src/Command/UpgradeCommand.php @@ -6,7 +6,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class UpgradeCommand extends BaseCommand { +class UpgradeCommand extends CvCommand { use StructuredOutputTrait; diff --git a/src/Command/UpgradeDbCommand.php b/src/Command/UpgradeDbCommand.php index c1f98f89..87be5804 100644 --- a/src/Command/UpgradeDbCommand.php +++ b/src/Command/UpgradeDbCommand.php @@ -1,7 +1,6 @@ configureBootOptions(); } /** @@ -52,15 +49,13 @@ protected function configure() { protected function initialize(InputInterface $input, OutputInterface $output) { $this->input = $input; $this->output = $output; - parent::initialize($input, $output); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { if (!defined('CIVICRM_UPGRADE_ACTIVE')) { define('CIVICRM_UPGRADE_ACTIVE', 1); } - $this->boot($input, $output); + parent::initialize($input, $output); + } + protected function execute(InputInterface $input, OutputInterface $output): int { if (!ini_get('safe_mode')) { set_time_limit(0); } diff --git a/src/Command/UpgradeDlCommand.php b/src/Command/UpgradeDlCommand.php index 7b1c7e62..d9482348 100644 --- a/src/Command/UpgradeDlCommand.php +++ b/src/Command/UpgradeDlCommand.php @@ -11,7 +11,7 @@ /** * Command for asking CiviCRM for the appropriate tarball to download. */ -class UpgradeDlCommand extends BaseCommand { +class UpgradeDlCommand extends CvCommand { use StructuredOutputTrait; diff --git a/src/Command/UpgradeGetCommand.php b/src/Command/UpgradeGetCommand.php index 56f358a3..5f4ff0ee 100644 --- a/src/Command/UpgradeGetCommand.php +++ b/src/Command/UpgradeGetCommand.php @@ -1,7 +1,6 @@ configureBootOptions(); + } + + public function getBootOptions(): array { + return ['auto' => FALSE] + parent::getBootOptions(); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/src/Command/UpgradeReportCommand.php b/src/Command/UpgradeReportCommand.php index eaf9b367..366fcc2a 100644 --- a/src/Command/UpgradeReportCommand.php +++ b/src/Command/UpgradeReportCommand.php @@ -9,7 +9,7 @@ /** * Command for asking CiviCRM for the appropriate tarball to download. */ -class UpgradeReportCommand extends BaseCommand { +class UpgradeReportCommand extends CvCommand { const DEFAULT_REPORT_URL = 'https://upgrade.civicrm.org/report'; use StructuredOutputTrait; diff --git a/src/Command/UrlCommand.php b/src/Command/UrlCommand.php index b712836a..a5a74936 100644 --- a/src/Command/UrlCommand.php +++ b/src/Command/UrlCommand.php @@ -2,6 +2,7 @@ namespace Civi\Cv\Command; use Civi\Cv\Encoder; +use Civi\Cv\Util\ExtensionTrait; use Civi\Cv\Util\Process; use Civi\Cv\Util\StructuredOutputTrait; use Civi\Cv\Util\UrlCommandTrait; @@ -9,8 +10,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class UrlCommand extends BaseExtensionCommand { +class UrlCommand extends CvCommand { + use ExtensionTrait; use StructuredOutputTrait; use UrlCommandTrait; @@ -66,24 +68,20 @@ protected function configure() { enabling the extension. The extra I/O may influence some scripted use-cases. '); - $this->configureBootOptions(); } protected function initialize(InputInterface $input, OutputInterface $output) { - parent::initialize($input, $output); if ($input->getFirstArgument() === 'open') { $input->setOption('open', TRUE); } - } - - protected function execute(InputInterface $input, OutputInterface $output): int { if (in_array($input->getOption('out'), Encoder::getTabularFormats()) && !in_array($input->getOption('out'), Encoder::getFormats())) { $input->setOption('tabular', TRUE); } + parent::initialize($input, $output); + } - $this->boot($input, $output); - + protected function execute(InputInterface $input, OutputInterface $output): int { $rows = $this->createUrls($input, $output); if ($input->getOption('open')) { diff --git a/src/Command/BaseExtensionCommand.php b/src/Util/ExtensionTrait.php similarity index 96% rename from src/Command/BaseExtensionCommand.php rename to src/Util/ExtensionTrait.php index efab304d..ead85905 100644 --- a/src/Command/BaseExtensionCommand.php +++ b/src/Util/ExtensionTrait.php @@ -1,14 +1,11 @@ $keys, 1=>$errors). */ diff --git a/src/Util/OptionCallbackTrait.php b/src/Util/OptionCallbackTrait.php index 0b7742e4..7295faf9 100644 --- a/src/Util/OptionCallbackTrait.php +++ b/src/Util/OptionCallbackTrait.php @@ -33,6 +33,7 @@ abstract public function getDefinition(); * @param string $name * The name of the option to * @param callable $callback + * Function(InputInterface $input, OutputInterface, $output, string $optionName) * @return $this */ public function addOptionCallback($name, $callback) { diff --git a/src/Util/SetupCommandTrait.php b/src/Util/SetupCommandTrait.php index 6126be06..e7022d0a 100644 --- a/src/Util/SetupCommandTrait.php +++ b/src/Util/SetupCommandTrait.php @@ -15,7 +15,6 @@ * civicrm-setup framework. */ trait SetupCommandTrait { - use BootTrait; /** * Register any CLI options which affect the initialization of the diff --git a/src/Util/StructuredOutputTrait.php b/src/Util/StructuredOutputTrait.php index f9fd553e..66064542 100644 --- a/src/Util/StructuredOutputTrait.php +++ b/src/Util/StructuredOutputTrait.php @@ -113,7 +113,7 @@ protected function configureOutputOptions($config = []) { * @see Encoder::getFormats */ protected function sendResult(InputInterface $input, OutputInterface $output, $result) { - $flat = $this->parseOptionalOption($input, ['--flat'], FALSE, '.'); + $flat = OptionalOption::parse($input, ['--flat'], FALSE, '.'); if ($flat !== FALSE) { $result = ArrayUtil::implodeTree($flat, $result); } @@ -157,7 +157,7 @@ protected function sendTable(InputInterface $input, OutputInterface $output, $re return; } - $flat = $this->parseOptionalOption($input, ['--flat'], FALSE, '.'); + $flat = OptionalOption::parse($input, ['--flat'], FALSE, '.'); if ($flat !== FALSE) { $filtered = ArrayUtil::filterColumns($records, $columns); $flattened = ArrayUtil::implodeTree($flat, $filtered); diff --git a/src/Util/VerboseApi.php b/src/Util/VerboseApi.php new file mode 100644 index 00000000..475e774b --- /dev/null +++ b/src/Util/VerboseApi.php @@ -0,0 +1,47 @@ +writeln("Calling $entity $action API", OutputInterface::VERBOSITY_DEBUG); + $result = \civicrm_api($entity, $action, $params); + if (!empty($result['is_error']) || $output->isDebug()) { + $data = array( + 'entity' => $entity, + 'action' => $action, + 'params' => $params, + 'result' => $result, + ); + if (!empty($result['is_error'])) { + $output->getErrorOutput()->writeln("Error: API Call Failed: " + . Encoder::encode($data, 'pretty')); + } + else { + $output->writeln("API success" . Encoder::encode($data, 'pretty'), + OutputInterface::VERBOSITY_DEBUG); + } + } + return $result; + } + +} diff --git a/tests/Command/BaseCommandTest.php b/tests/Command/BaseCommandTest.php deleted file mode 100644 index 808b5a0e..00000000 --- a/tests/Command/BaseCommandTest.php +++ /dev/null @@ -1,50 +0,0 @@ -addOption('refresh', array('r'), InputOption::VALUE_OPTIONAL, 'auto'); - - $input = new ArgvInput($inputArgv, $c->getDefinition()); - $this->assertEquals($expectValue, $c->parseOptionalOption($input, array('-r', '--refresh'), 'auto', 'yes')); - } - -} diff --git a/tests/Command/BaseExtensionCommandTest.php b/tests/Command/BaseExtensionCommandTest.php index 83bc2bbc..26f4a81a 100644 --- a/tests/Command/BaseExtensionCommandTest.php +++ b/tests/Command/BaseExtensionCommandTest.php @@ -1,6 +1,7 @@ configureRepoOptions(); $input = new ArgvInput($inputArgv, $c->getDefinition()); diff --git a/tests/Plugin/FluentHelloPluginTest.php b/tests/Plugin/FluentHelloPluginTest.php new file mode 100644 index 00000000..6bc843d6 --- /dev/null +++ b/tests/Plugin/FluentHelloPluginTest.php @@ -0,0 +1,39 @@ +setEnv(['CV_PLUGIN_PATH' => preg_replace(';\.php$;', '', __FILE__)]); + return $process; + } + + public function testRun() { + $output = $this->cvOk('hello:normal'); + $this->assertMatchesRegularExpression('/Hey-yo world via parameter.*Hey-yo world via StyleInterface/s', $output); + } + + public function testRunWithName() { + $output = $this->cvOk('hello:normal Alice'); + $this->assertMatchesRegularExpression('/Hey-yo Alice via parameter.*Hey-yo Alice via StyleInterface/s', $output); + } + + public function testRun_noboot() { + $output = $this->cvOk('hello:noboot'); + $this->assertMatchesRegularExpression('/Hey-yo world via parameter.*Hey-yo world via StyleInterface/s', $output); + } + + public function testRunWithName_noboot() { + $output = $this->cvOk('hello:noboot Bob'); + $this->assertMatchesRegularExpression('/Hey-yo Bob via parameter.*Hey-yo Bob via StyleInterface/s', $output); + } + +} diff --git a/tests/Plugin/FluentHelloPluginTest/hello.php b/tests/Plugin/FluentHelloPluginTest/hello.php new file mode 100644 index 00000000..b4bef8bf --- /dev/null +++ b/tests/Plugin/FluentHelloPluginTest/hello.php @@ -0,0 +1,68 @@ + 1) { + die("Expect CV_PLUGIN API v1"); +} + +if (!preg_match(';^[\w_-]+$;', $CV_PLUGIN['appName'])) { + throw new \RuntimeException("Invalid CV_PLUGIN[appName]" . json_encode($CV_PLUGIN['appName'])); +} + +if (!preg_match(';^([0-9x\.]+(-[\w-]+)?|UNKNOWN)$;', $CV_PLUGIN['appVersion'])) { + throw new \RuntimeException("Invalid CV_PLUGIN[appVersion]: " . json_encode($CV_PLUGIN['appVersion'])); +} + +if ($CV_PLUGIN['name'] !== 'hello') { + throw new \RuntimeException("Invalid CV_PLUGIN[name]"); +} +if (realpath($CV_PLUGIN['file']) !== realpath(__FILE__)) { + throw new \RuntimeException("Invalid CV_PLUGIN[file]"); +} + +Cv::dispatcher()->addListener('*.app.boot', function ($e) { + Cv::io()->writeln("Hey-yo during initial bootstrap!"); +}); + +Cv::dispatcher()->addListener('cv.app.commands', function ($e) { + + $e['commands'][] = (new CvCommand('hello:normal')) + ->setDescription('Say a greeting') + ->addArgument('name') + ->setCode(function($input, $output) { + // ASSERT: With setCode(), it's OK to use un-hinted inputs. + if ($input->getArgument('name') !== Cv::input()->getArgument('name')) { + throw new \RuntimeException("Argument \"name\" is inconsistent!"); + } + if (!Civi\Core\Container::isContainerBooted()) { + throw new \LogicException("Container should have been booted by CvCommand!"); + } + $name = $input->getArgument('name') ?: 'world'; + $output->writeln("Hey-yo $name via parameter!"); + Cv::io()->writeln("Hey-yo $name via StyleInterface!"); + return 0; + }); + + $e['commands'][] = (new CvCommand('hello:noboot')) + ->setDescription('Say a greeting') + ->addArgument('name') + ->setBootOptions(['auto' => FALSE]) + ->setCode(function(InputInterface $input, OutputInterface $output) { + // ASSERT: With setCode(), it's OK to use hinted inputs. + if ($input->getArgument('name') !== Cv::input()->getArgument('name')) { + throw new \RuntimeException("Argument \"name\" is inconsistent!"); + } + if (class_exists('Civi\Core\Container')) { + throw new \LogicException("Container should not have been booted by CvCommand!"); + } + $name = $input->getArgument('name') ?: 'world'; + $output->writeln("Hey-yo $name via parameter!"); + Cv::io()->writeln("Hey-yo $name via StyleInterface!"); + return 0; + }); + +}); diff --git a/tests/Util/OptionalOptionTest.php b/tests/Util/OptionalOptionTest.php new file mode 100644 index 00000000..2f8f9c0b --- /dev/null +++ b/tests/Util/OptionalOptionTest.php @@ -0,0 +1,65 @@ +push(...$this->createInputOutput($inputArgv)); + try { + $this->assertEquals($expectValue, OptionalOption::parse(Cv::input(), ['-r', '--refresh'], 'auto', 'yes')); + } + finally { + Cv::ioStack()->pop(); + } + } + + /** + * @return array + * [0 => InputInterface, 1 => OutputInterface] + */ + protected function createInputOutput(array $argv = NULL): array { + $input = new ArgvInput($argv); + $input->setInteractive(FALSE); + $output = new NullOutput(); + return [$input, $output]; + } + +}