--- /dev/null
+<?php
+namespace Civi\API;
+
+use Symfony\Component\Process\PhpExecutableFinder;
+use Symfony\Component\Process\Process;
+
+/**
+ * Class ExternalBatch
+ * @package Civi\API
+ *
+ * Perform a series of external, asynchronous, concurrent API call.
+ */
+class ExternalBatch {
+ /**
+ * The time to wait when polling for process status (microseconds).
+ */
+ const POLL_INTERVAL = 10000;
+
+ /**
+ * @var array
+ * Array(int $idx => array $apiCall).
+ */
+ protected $apiCalls;
+
+ protected $defaultParams;
+
+ protected $root;
+
+ protected $settingsPath;
+
+ protected $env = array();
+
+ /**
+ * @var array
+ * Array(int $idx => Process $process).
+ */
+ protected $processes;
+
+ /**
+ * @var array
+ * Array(int $idx => array $apiResult).
+ */
+ protected $apiResults;
+
+ /**
+ * @param array $defaultParams
+ * Default values to merge into any API calls.
+ */
+ public function __construct($defaultParams = array()) {
+ global $civicrm_root;
+ $this->root = $civicrm_root;
+ $this->settingsPath = defined('CIVICRM_SETTINGS_PATH') ? CIVICRM_SETTINGS_PATH : NULL;
+ $this->defaultParams = $defaultParams;
+ }
+
+ /**
+ * @param string $entity
+ * @param string $action
+ * @param array $params
+ * @return $this
+ */
+ public function addCall($entity, $action, $params = array()) {
+ $params = array_merge($this->defaultParams, $params);
+
+ $this->apiCalls[] = array(
+ 'entity' => $entity,
+ 'action' => $action,
+ 'params' => $params,
+ );
+ return $this;
+ }
+
+ /**
+ * @param array $env
+ * List of environment variables to add.
+ * @return static
+ */
+ public function addEnv($env) {
+ $this->env = array_merge($this->env, $env);
+ return $this;
+ }
+
+ /**
+ * Run all the API calls concurrently.
+ *
+ * @return static
+ * @throws \CRM_Core_Exception
+ */
+ public function start() {
+ foreach ($this->apiCalls as $idx => $apiCall) {
+ $process = $this->createProcess($apiCall);
+ $process->start();
+ $this->processes[$idx] = $process;
+ }
+ return $this;
+ }
+
+ /**
+ * @return int
+ * The number of running processes.
+ */
+ public function getRunningCount() {
+ $count = 0;
+ foreach ($this->processes as $process) {
+ if ($process->isRunning()) {
+ $count++;
+ }
+ }
+ return $count;
+ }
+
+ public function wait() {
+ while (!empty($this->processes)) {
+ usleep(self::POLL_INTERVAL);
+ foreach (array_keys($this->processes) as $idx) {
+ /** @var Process $process */
+ $process = $this->processes[$idx];
+ if (!$process->isRunning()) {
+ $parsed = json_decode($process->getOutput(), TRUE);
+ if ($process->getExitCode() || $parsed === NULL) {
+ $this->apiResults[] = array(
+ 'is_error' => 1,
+ 'error_message' => 'External API returned malformed response.',
+ 'trace' => array(
+ 'code' => $process->getExitCode(),
+ 'stdout' => $process->getOutput(),
+ 'stderr' => $process->getErrorOutput(),
+ ),
+ );
+ }
+ else {
+ $this->apiResults[] = $parsed;
+ }
+ unset($this->processes[$idx]);
+ }
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getResults() {
+ return $this->apiResults;
+ }
+
+ /**
+ * @param int $idx
+ * @return array
+ */
+ public function getResult($idx = 0) {
+ return $this->apiResults[$idx];
+ }
+
+ /**
+ * Determine if the local environment supports running API calls
+ * externally.
+ *
+ * @return bool
+ */
+ public function isSupported() {
+ // If you try in Windows, feel free to change this...
+ if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' || !function_exists('proc_open')) {
+ return FALSE;
+ }
+ if (!file_exists($this->root . '/bin/cli.php') || !file_exists($this->settingsPath)) {
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ /**
+ * @param array $apiCall
+ * Array with keys: entity, action, params.
+ * @return Process
+ * @throws \CRM_Core_Exception
+ */
+ public function createProcess($apiCall) {
+ $parts = array();
+
+ $executableFinder = new PhpExecutableFinder();
+ $php = $executableFinder->find();
+ if (!$php) {
+ throw new \CRM_Core_Exception("Failed to locate PHP interpreter.");
+ }
+ $parts[] = $php;
+
+ $parts[] = escapeshellarg($this->root . '/bin/cli.php');
+ $parts[] = escapeshellarg("-e=" . $apiCall['entity']);
+ $parts[] = escapeshellarg("-a=" . $apiCall['action']);
+ $parts[] = "--json";
+ $parts[] = escapeshellarg("-u=dummyuser");
+ foreach ($apiCall['params'] as $key => $value) {
+ $parts[] = escapeshellarg("--$key=$value");
+ }
+ $command = implode(" ", $parts);
+
+ $env = array_merge($this->env, array(
+ 'CIVICRM_SETTINGS' => $this->settingsPath,
+ ));
+ return new Process($command, $this->root, $env);
+ }
+
+ /**
+ * @return string
+ */
+ public function getRoot() {
+ return $this->root;
+ }
+
+ /**
+ * @param string $root
+ */
+ public function setRoot($root) {
+ $this->root = $root;
+ }
+
+ /**
+ * @return string
+ */
+ public function getSettingsPath() {
+ return $this->settingsPath;
+ }
+
+ /**
+ * @param string $settingsPath
+ */
+ public function setSettingsPath($settingsPath) {
+ $this->settingsPath = $settingsPath;
+ }
+
+}
return civicrm_api($entity, $action, $params);
}
+ /**
+ * Create a batch of external API calls which can
+ * be executed concurrently.
+ *
+ * @code
+ * $calls = $this->createExternalAPI()
+ * ->addCall('Contact', 'get', ...)
+ * ->addCall('Contact', 'get', ...)
+ * ...
+ * ->run()
+ * ->getResults();
+ * @endcode
+ *
+ * @return \Civi\API\ExternalBatch
+ * @throws PHPUnit_Framework_SkippedTestError
+ */
+ public function createExternalAPI() {
+ global $civicrm_root;
+ $defaultParams = array(
+ 'version' => $this->_apiversion,
+ 'debug' => 1,
+ );
+
+ $calls = new \Civi\API\ExternalBatch($defaultParams);
+ $calls->setSettingsPath("$civicrm_root/tests/phpunit/CiviTest/civicrm.settings.cli.php");
+
+ if (!$calls->isSupported()) {
+ $this->markTestSkipped('The test relies on Civi\API\ExternalBatch. This is unsupported in the local environment.');
+ }
+
+ return $calls;
+ }
+
/**
* wrap api functions.
* so we can ensure they succeed & throw exceptions without litterering the test with checks