Merge pull request #8633 from totten/master-fts-empty-to
[civicrm-core.git] / Civi / API / ExternalBatch.php
1 <?php
2 namespace Civi\API;
3
4 use Symfony\Component\Process\PhpExecutableFinder;
5 use 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 */
13 class 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
31 protected $env;
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 */
49 public function __construct($defaultParams = array()) {
50 global $civicrm_root;
51 $this->root = $civicrm_root;
52 $this->settingsPath = defined('CIVICRM_SETTINGS_PATH') ? CIVICRM_SETTINGS_PATH : NULL;
53 $this->defaultParams = $defaultParams;
54 $this->env = $_ENV;
55 }
56
57 /**
58 * @param string $entity
59 * @param string $action
60 * @param array $params
61 * @return ExternalBatch
62 */
63 public function addCall($entity, $action, $params = array()) {
64 $params = array_merge($this->defaultParams, $params);
65
66 $this->apiCalls[] = array(
67 'entity' => $entity,
68 'action' => $action,
69 'params' => $params,
70 );
71 return $this;
72 }
73
74 /**
75 * @param array $env
76 * List of environment variables to add.
77 * @return static
78 */
79 public function addEnv($env) {
80 $this->env = array_merge($this->env, $env);
81 return $this;
82 }
83
84 /**
85 * Run all the API calls concurrently.
86 *
87 * @return static
88 * @throws \CRM_Core_Exception
89 */
90 public function start() {
91 foreach ($this->apiCalls as $idx => $apiCall) {
92 $process = $this->createProcess($apiCall);
93 $process->start();
94 $this->processes[$idx] = $process;
95 }
96 return $this;
97 }
98
99 /**
100 * @return int
101 * The number of running processes.
102 */
103 public function getRunningCount() {
104 $count = 0;
105 foreach ($this->processes as $process) {
106 if ($process->isRunning()) {
107 $count++;
108 }
109 }
110 return $count;
111 }
112
113 public function wait() {
114 while (!empty($this->processes)) {
115 usleep(self::POLL_INTERVAL);
116 foreach (array_keys($this->processes) as $idx) {
117 /** @var Process $process */
118 $process = $this->processes[$idx];
119 if (!$process->isRunning()) {
120 $parsed = json_decode($process->getOutput(), TRUE);
121 if ($process->getExitCode() || $parsed === NULL) {
122 $this->apiResults[] = array(
123 'is_error' => 1,
124 'error_message' => 'External API returned malformed response.',
125 'trace' => array(
126 'code' => $process->getExitCode(),
127 'stdout' => $process->getOutput(),
128 'stderr' => $process->getErrorOutput(),
129 ),
130 );
131 }
132 else {
133 $this->apiResults[] = $parsed;
134 }
135 unset($this->processes[$idx]);
136 }
137 }
138 }
139 return $this;
140 }
141
142 /**
143 * @return array
144 */
145 public function getResults() {
146 return $this->apiResults;
147 }
148
149 /**
150 * @param int $idx
151 * @return array
152 */
153 public function getResult($idx = 0) {
154 return $this->apiResults[$idx];
155 }
156
157 /**
158 * Determine if the local environment supports running API calls
159 * externally.
160 *
161 * @return bool
162 */
163 public function isSupported() {
164 // If you try in Windows, feel free to change this...
165 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' || !function_exists('proc_open')) {
166 return FALSE;
167 }
168 if (!file_exists($this->root . '/bin/cli.php') || !file_exists($this->settingsPath)) {
169 return FALSE;
170 }
171 return TRUE;
172 }
173
174 /**
175 * @param array $apiCall
176 * Array with keys: entity, action, params.
177 * @return Process
178 * @throws \CRM_Core_Exception
179 */
180 public function createProcess($apiCall) {
181 $parts = array();
182
183 if (defined('CIVICRM_TEST') && CIVICRM_TEST) {
184 // When testing, civicrm.settings.php may rely on $_CV, which is only
185 // populated/propagated if we execute through `cv`.
186 $parts[] = 'cv api';
187 $parts[] = escapeshellarg($apiCall['entity'] . '.' . $apiCall['action']);
188 $parts[] = "--out=json-strict";
189 foreach ($apiCall['params'] as $key => $value) {
190 $parts[] = escapeshellarg("$key=$value");
191 }
192 }
193 else {
194 // But in production, we may not have `cv` installed.
195 $executableFinder = new PhpExecutableFinder();
196 $php = $executableFinder->find();
197 if (!$php) {
198 throw new \CRM_Core_Exception("Failed to locate PHP interpreter.");
199 }
200 $parts[] = $php;
201 $parts[] = escapeshellarg($this->root . '/bin/cli.php');
202 $parts[] = escapeshellarg("-e=" . $apiCall['entity']);
203 $parts[] = escapeshellarg("-a=" . $apiCall['action']);
204 $parts[] = "--json";
205 $parts[] = escapeshellarg("-u=dummyuser");
206 foreach ($apiCall['params'] as $key => $value) {
207 $parts[] = escapeshellarg("--$key=$value");
208 }
209 }
210
211 $command = implode(" ", $parts);
212 $env = array_merge($this->env, array(
213 'CIVICRM_SETTINGS' => $this->settingsPath,
214 ));
215 return new Process($command, $this->root, $env);
216 }
217
218 /**
219 * @return string
220 */
221 public function getRoot() {
222 return $this->root;
223 }
224
225 /**
226 * @param string $root
227 */
228 public function setRoot($root) {
229 $this->root = $root;
230 }
231
232 /**
233 * @return string
234 */
235 public function getSettingsPath() {
236 return $this->settingsPath;
237 }
238
239 /**
240 * @param string $settingsPath
241 */
242 public function setSettingsPath($settingsPath) {
243 $this->settingsPath = $settingsPath;
244 }
245
246 }