NFC - Short array syntax - auto-convert Civi dir
[civicrm-core.git] / Civi / API / ExternalBatch.php
CommitLineData
888dab1e
TO
1<?php
2namespace Civi\API;
3
4use Symfony\Component\Process\PhpExecutableFinder;
5use Symfony\Component\Process\Process;
6
7/**
8 * Class ExternalBatch
9 * @package Civi\API
10 *
11 * Perform a series of external, asynchronous, concurrent API call.
12 */
13class ExternalBatch {
14 /**
15 * The time to wait when polling for process status (microseconds).
16 */
17 const POLL_INTERVAL = 10000;
18
19 /**
20 * @var array
21 * Array(int $idx => array $apiCall).
22 */
23 protected $apiCalls;
24
25 protected $defaultParams;
26
27 protected $root;
28
29 protected $settingsPath;
30
2b045ddd 31 protected $env;
888dab1e
TO
32
33 /**
34 * @var array
35 * Array(int $idx => Process $process).
36 */
37 protected $processes;
38
39 /**
40 * @var array
41 * Array(int $idx => array $apiResult).
42 */
43 protected $apiResults;
44
45 /**
46 * @param array $defaultParams
47 * Default values to merge into any API calls.
48 */
c64f69d9 49 public function __construct($defaultParams = []) {
888dab1e
TO
50 global $civicrm_root;
51 $this->root = $civicrm_root;
52 $this->settingsPath = defined('CIVICRM_SETTINGS_PATH') ? CIVICRM_SETTINGS_PATH : NULL;
53 $this->defaultParams = $defaultParams;
2b045ddd 54 $this->env = $_ENV;
40406b56 55 if (empty($_ENV['PATH'])) {
47566c44
TO
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.)');
59 }
888dab1e
TO
60 }
61
62 /**
63 * @param string $entity
64 * @param string $action
65 * @param array $params
4b350175 66 * @return ExternalBatch
888dab1e 67 */
c64f69d9 68 public function addCall($entity, $action, $params = []) {
888dab1e
TO
69 $params = array_merge($this->defaultParams, $params);
70
c64f69d9 71 $this->apiCalls[] = [
888dab1e
TO
72 'entity' => $entity,
73 'action' => $action,
74 'params' => $params,
c64f69d9 75 ];
888dab1e
TO
76 return $this;
77 }
78
79 /**
80 * @param array $env
81 * List of environment variables to add.
82 * @return static
83 */
84 public function addEnv($env) {
85 $this->env = array_merge($this->env, $env);
86 return $this;
87 }
88
89 /**
90 * Run all the API calls concurrently.
91 *
92 * @return static
93 * @throws \CRM_Core_Exception
94 */
95 public function start() {
96 foreach ($this->apiCalls as $idx => $apiCall) {
97 $process = $this->createProcess($apiCall);
98 $process->start();
99 $this->processes[$idx] = $process;
100 }
101 return $this;
102 }
103
104 /**
105 * @return int
106 * The number of running processes.
107 */
108 public function getRunningCount() {
109 $count = 0;
110 foreach ($this->processes as $process) {
111 if ($process->isRunning()) {
112 $count++;
113 }
114 }
115 return $count;
116 }
117
118 public function wait() {
119 while (!empty($this->processes)) {
120 usleep(self::POLL_INTERVAL);
121 foreach (array_keys($this->processes) as $idx) {
122 /** @var Process $process */
123 $process = $this->processes[$idx];
124 if (!$process->isRunning()) {
125 $parsed = json_decode($process->getOutput(), TRUE);
126 if ($process->getExitCode() || $parsed === NULL) {
c64f69d9 127 $this->apiResults[] = [
888dab1e
TO
128 'is_error' => 1,
129 'error_message' => 'External API returned malformed response.',
c64f69d9 130 'trace' => [
888dab1e
TO
131 'code' => $process->getExitCode(),
132 'stdout' => $process->getOutput(),
133 'stderr' => $process->getErrorOutput(),
c64f69d9
CW
134 ],
135 ];
888dab1e
TO
136 }
137 else {
138 $this->apiResults[] = $parsed;
139 }
140 unset($this->processes[$idx]);
141 }
142 }
143 }
144 return $this;
145 }
146
147 /**
148 * @return array
149 */
150 public function getResults() {
151 return $this->apiResults;
152 }
153
154 /**
155 * @param int $idx
156 * @return array
157 */
158 public function getResult($idx = 0) {
159 return $this->apiResults[$idx];
160 }
161
162 /**
163 * Determine if the local environment supports running API calls
164 * externally.
165 *
166 * @return bool
167 */
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')) {
171 return FALSE;
172 }
173 if (!file_exists($this->root . '/bin/cli.php') || !file_exists($this->settingsPath)) {
174 return FALSE;
175 }
176 return TRUE;
177 }
178
179 /**
180 * @param array $apiCall
181 * Array with keys: entity, action, params.
182 * @return Process
183 * @throws \CRM_Core_Exception
184 */
185 public function createProcess($apiCall) {
c64f69d9 186 $parts = [];
888dab1e 187
2b045ddd
TO
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`.
191 $parts[] = 'cv api';
192 $parts[] = escapeshellarg($apiCall['entity'] . '.' . $apiCall['action']);
193 $parts[] = "--out=json-strict";
194 foreach ($apiCall['params'] as $key => $value) {
195 $parts[] = escapeshellarg("$key=$value");
196 }
888dab1e 197 }
2b045ddd
TO
198 else {
199 // But in production, we may not have `cv` installed.
200 $executableFinder = new PhpExecutableFinder();
201 $php = $executableFinder->find();
202 if (!$php) {
203 throw new \CRM_Core_Exception("Failed to locate PHP interpreter.");
204 }
205 $parts[] = $php;
206 $parts[] = escapeshellarg($this->root . '/bin/cli.php');
207 $parts[] = escapeshellarg("-e=" . $apiCall['entity']);
208 $parts[] = escapeshellarg("-a=" . $apiCall['action']);
209 $parts[] = "--json";
210 $parts[] = escapeshellarg("-u=dummyuser");
211 foreach ($apiCall['params'] as $key => $value) {
212 $parts[] = escapeshellarg("--$key=$value");
213 }
888dab1e 214 }
888dab1e 215
2b045ddd 216 $command = implode(" ", $parts);
c64f69d9 217 $env = array_merge($this->env, [
888dab1e 218 'CIVICRM_SETTINGS' => $this->settingsPath,
c64f69d9 219 ]);
888dab1e
TO
220 return new Process($command, $this->root, $env);
221 }
222
223 /**
224 * @return string
225 */
226 public function getRoot() {
227 return $this->root;
228 }
229
230 /**
231 * @param string $root
232 */
233 public function setRoot($root) {
234 $this->root = $root;
235 }
236
237 /**
238 * @return string
239 */
240 public function getSettingsPath() {
241 return $this->settingsPath;
242 }
243
244 /**
245 * @param string $settingsPath
246 */
247 public function setSettingsPath($settingsPath) {
248 $this->settingsPath = $settingsPath;
249 }
250
251}