4 use Symfony\Component\Process\PhpExecutableFinder
;
5 use Symfony\Component\Process\Process
;
11 * Perform a series of external, asynchronous, concurrent API call.
15 * The time to wait when polling for process status (microseconds).
17 const POLL_INTERVAL
= 10000;
21 * Array(int $idx => array $apiCall).
25 protected $defaultParams;
29 protected $settingsPath;
35 * Array(int $idx => Process $process).
41 * Array(int $idx => array $apiResult).
43 protected $apiResults;
46 * @param array $defaultParams
47 * Default values to merge into any API calls.
49 public function __construct($defaultParams = []) {
51 $this->root
= $civicrm_root;
52 $this->settingsPath
= defined('CIVICRM_SETTINGS_PATH') ? CIVICRM_SETTINGS_PATH
: NULL;
53 $this->defaultParams
= $defaultParams;
55 if (empty($_ENV['PATH'])) {
56 // FIXME: If we upgrade to newer Symfony\Process and use the newer
57 // inheritEnv feature, then this becomes unnecessary.
58 throw new \
CRM_Core_Exception('ExternalBatch cannot detect environment: $_ENV is missing. (Tip: Set variables_order=EGPCS in php.ini.)');
63 * @param string $entity
64 * @param string $action
65 * @param array $params
66 * @return ExternalBatch
68 public function addCall($entity, $action, $params = []) {
69 $params = array_merge($this->defaultParams
, $params);
81 * List of environment variables to add.
84 public function addEnv($env) {
85 $this->env
= array_merge($this->env
, $env);
90 * Run all the API calls concurrently.
93 * @throws \CRM_Core_Exception
95 public function start() {
96 foreach ($this->apiCalls
as $idx => $apiCall) {
97 $process = $this->createProcess($apiCall);
99 $this->processes
[$idx] = $process;
106 * The number of running processes.
108 public function getRunningCount() {
110 foreach ($this->processes
as $process) {
111 if ($process->isRunning()) {
118 public function wait() {
119 while (!empty($this->processes
)) {
120 usleep(self
::POLL_INTERVAL
);
121 foreach (array_keys($this->processes
) as $idx) {
122 /** @var \Symfony\Component\Process\Process $process */
123 $process = $this->processes
[$idx];
124 if (!$process->isRunning()) {
125 $parsed = json_decode($process->getOutput(), TRUE);
126 if ($process->getExitCode() ||
$parsed === NULL) {
127 $this->apiResults
[] = [
129 'error_message' => 'External API returned malformed response.',
131 'code' => $process->getExitCode(),
132 'stdout' => $process->getOutput(),
133 'stderr' => $process->getErrorOutput(),
138 $this->apiResults
[] = $parsed;
140 unset($this->processes
[$idx]);
150 public function getResults() {
151 return $this->apiResults
;
158 public function getResult($idx = 0) {
159 return $this->apiResults
[$idx];
163 * Determine if the local environment supports running API calls
168 public function isSupported() {
169 // If you try in Windows, feel free to change this...
170 if (strtoupper(substr(PHP_OS
, 0, 3)) === 'WIN' ||
!function_exists('proc_open')) {
173 if (!file_exists($this->root
. '/bin/cli.php') ||
!file_exists($this->settingsPath
)) {
180 * @param array $apiCall
181 * Array with keys: entity, action, params.
182 * @return \Symfony\Component\Process\Process
183 * @throws \CRM_Core_Exception
185 public function createProcess($apiCall) {
188 if (defined('CIVICRM_TEST') && CIVICRM_TEST
) {
189 // When testing, civicrm.settings.php may rely on $_CV, which is only
190 // populated/propagated if we execute through `cv`.
192 $parts[] = escapeshellarg($apiCall['entity'] . '.' . $apiCall['action']);
193 $parts[] = "--out=json-strict";
194 foreach ($apiCall['params'] as $key => $value) {
195 $parts[] = escapeshellarg("$key=$value");
199 // But in production, we may not have `cv` installed.
200 $executableFinder = new PhpExecutableFinder();
201 $php = $executableFinder->find();
203 throw new \
CRM_Core_Exception("Failed to locate PHP interpreter.");
206 $parts[] = escapeshellarg($this->root
. '/bin/cli.php');
207 $parts[] = escapeshellarg("-e=" . $apiCall['entity']);
208 $parts[] = escapeshellarg("-a=" . $apiCall['action']);
210 $parts[] = escapeshellarg("-u=dummyuser");
211 foreach ($apiCall['params'] as $key => $value) {
212 $parts[] = escapeshellarg("--$key=$value");
216 $command = implode(" ", $parts);
217 $env = array_merge($this->env
, [
218 'CIVICRM_SETTINGS' => $this->settingsPath
,
220 return new Process($command, $this->root
, $env);
226 public function getRoot() {
231 * @param string $root
233 public function setRoot($root) {
240 public function getSettingsPath() {
241 return $this->settingsPath
;
245 * @param string $settingsPath
247 public function setSettingsPath($settingsPath) {
248 $this->settingsPath
= $settingsPath;