drupal-civicrm/sites/all/modules/civicrm/Civi/API/ExternalBatch.php
2018-01-14 13:10:16 +00:00

252 lines
6.1 KiB
PHP

<?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;
/**
* @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;
$this->env = $_ENV;
if (empty($_ENV['PATH'])) {
// FIXME: If we upgrade to newer Symfony\Process and use the newer
// inheritEnv feature, then this becomes unnecessary.
throw new \CRM_Core_Exception('ExternalBatch cannot detect environment: $_ENV is missing. (Tip: Set variables_order=EGPCS in php.ini.)');
}
}
/**
* @param string $entity
* @param string $action
* @param array $params
* @return ExternalBatch
*/
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();
if (defined('CIVICRM_TEST') && CIVICRM_TEST) {
// When testing, civicrm.settings.php may rely on $_CV, which is only
// populated/propagated if we execute through `cv`.
$parts[] = 'cv api';
$parts[] = escapeshellarg($apiCall['entity'] . '.' . $apiCall['action']);
$parts[] = "--out=json-strict";
foreach ($apiCall['params'] as $key => $value) {
$parts[] = escapeshellarg("$key=$value");
}
}
else {
// But in production, we may not have `cv` installed.
$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;
}
}