Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugins - Allow CiviCRM extensions to define plugins/commands for cv #219

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions lib/src/BaseApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +14,8 @@

class BaseApplication extends \Symfony\Component\Console\Application {

protected $stage = 'new';

/**
* Primary entry point for execution of the standalone command.
*/
Expand Down Expand Up @@ -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("<error>WARNING: When using bootstrap --level=%s, some commands may be unavailable.</error>", Cv::input()->getOption('level')));
throw $e;
}
}
}
}

/**
Expand Down
71 changes: 61 additions & 10 deletions lib/src/CvPlugins.php
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
<?php
namespace Civi\Cv;

use Civi\Core\Event\GenericHookEvent;

class CvPlugins {

const PROTOCOL_VERSION = 1;

/**
* @var string[]
*/
private $paths;

private $plugins;

/**
* @var array
* Description the current application environment.
* Ex: ['appName' => 'cv', 'appVersion' => '0.3.50']
*/
private $pluginEnv;

/**
* Load any plugins.
*
Expand All @@ -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'));
}
Expand All @@ -35,29 +49,53 @@ 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) {
$pluginName = preg_replace(';(\d+-)?(.*)(@\w+)?\.php;', '\\2', basename($file));
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,
]);
Expand All @@ -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'];
}

Expand Down
27 changes: 27 additions & 0 deletions lib/src/cvplugin_loader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use Civi\Cv\Cv;

if (defined('CIVICRM_DSN')) {
fprintf(STDERR, "WARNING: Cv plugins initialized after CiviCRM booted. Second stage loading may not work as expected.\n");
}

// Define the hook listener before Civi boots.
$GLOBALS['CIVICRM_FORCE_MODULES'][] = 'cvplugin_loader';

/**
* @param $config
* @param array|NULL $flags
* Only defined on 5.65+.
* @return void
* @see \CRM_Utils_Hook::config()
*/
function cvplugin_loader_civicrm_config($config, $flags = NULL): void {
static $loaded = FALSE;
if (!$loaded) {
if ($flags === NULL || !empty($flags['civicrm'])) {
$loaded = TRUE;
Cv::plugins()->initExtensions();
}
}
}