1 <?php
2
3 /**
4 * ArangoDB PHP client: batch
5 *
6 * @package triagens\ArangoDb
7 * @author Frank Mayer
8 * @since 1.1
9 *
10 */
11
12 namespace triagens\ArangoDb;
13
14 /**
15 * Provides batching functionality
16 *
17 * <br>
18 *
19 * @package triagens\ArangoDb
20 * @since 1.1
21 */
22 class Batch
23 {
24 /**
25 * Batch Response Object
26 *
27 * @var HttpResponse $_batchResponse
28 */
29 public $_batchResponse;
30
31
32 /**
33 * Flag that signals if this batch was processed or not. Processed => true ,or not processed => false
34 *
35 * @var boolean $_processed
36 */
37 private $_processed = false;
38
39
40 /**
41 * The array of BatchPart objects
42 *
43 * @var array $_batchParts
44 */
45 private $_batchParts = [];
46
47
48 /**
49 * The next batch part id
50 *
51 * @var integer|string $_nextBatchPartId
52 */
53 private $_nextBatchPartId;
54
55
56 /**
57 * An array of BatchPartCursor options
58 *
59 * @var array $_batchParts
60 */
61 private $_batchPartCursorOptions = [];
62
63
64 /**
65 * The connection object
66 *
67 * @var Connection $_connection
68 */
69 private $_connection;
70
71 /**
72 * The sanitize default value
73 *
74 * @var bool $_sanitize
75 */
76 private $_sanitize = false;
77
78 /**
79 * The Batch NextId
80 *
81 * @var integer|string $_nextId
82 */
83 private $_nextId = 0;
84
85
86 /**
87 * Constructor for Batch instance. Batch instance by default starts capturing request after initiated.
88 * To disable this, pass startCapture=>false inside the options array parameter
89 *
90 * @param Connection $connection that this batch class will monitor for requests in order to batch them. Connection parameter is mandatory.
91 * @param array $options An array of options for Batch construction. See below for options:
92 *
93 * <p>Options are :
94 * <li>'_sanitize' - True to remove _id and _rev attributes from result documents returned from this batch. Defaults to false.</li>
95 * <li>'startCapture' - Start batch capturing immediately after batch instantiation. Defaults to true.</li>
96 * <li>'batchSize' - Defines a fixed array size for holding the batch parts. The id's of the batch parts can only be integers.
97 * When this option is defined, the batch mechanism will use an SplFixedArray instead of the normal PHP arrays.
98 * In most cases, this will result in increased performance of about 5% to 15%, depending on batch size and data.</li>
99 * </p>
100 */
101 public function __construct(Connection $connection, array $options = [])
102 {
103 $startCapture = true;
104 $sanitize = false;
105 $batchSize = 0;
106 $options = array_merge($options, $this->getCursorOptions());
107 extract($options, EXTR_IF_EXISTS);
108 $this->_sanitize = $sanitize;
109 $this->batchSize = $batchSize;
110
111 if ($this->batchSize > 0) {
112 $this->_batchParts = new \SplFixedArray($this->batchSize);
113 }
114
115 $this->setConnection($connection);
116
117 // set default cursor options. Sanitize is currently the only local one.
118 $this->_batchPartCursorOptions = [Cursor::ENTRY_SANITIZE => (bool) $this->_sanitize];
119
120 if ($startCapture === true) {
121 $this->startCapture();
122 }
123 }
124
125
126 /**
127 * Sets the connection for he current batch. (mostly internal function)
128 *
129 * @param Connection $connection
130 *
131 * @return Batch
132 */
133 public function setConnection($connection)
134 {
135 $this->_connection = $connection;
136
137 return $this;
138 }
139
140
141 /**
142 * Start capturing requests. To stop capturing, use stopCapture()
143 *
144 * see triagens\ArangoDb\Batch::stopCapture()
145 *
146 *
147 * @return Batch
148 *
149 */
150 public function startCapture()
151 {
152 $this->activate();
153
154 return $this;
155 }
156
157
158 /**
159 * Stop capturing requests. If the batch has not been processed yet, more requests can be appended by calling startCapture() again.
160 *
161 * see Batch::startCapture()
162 *
163 * @throws ClientException
164 * @return Batch
165 */
166 public function stopCapture()
167 {
168 // check if this batch is the active one... and capturing. Ignore, if we're not capturing...
169 if ($this->isActive() && $this->isCapturing()) {
170 $this->setCapture(false);
171
172 return $this;
173 } else {
174 throw new ClientException('Cannot stop capturing with this batch. Batch is not active...');
175 }
176 }
177
178
179 /**
180 * Returns true, if this batch is active in its associated connection.
181 *
182 * @return bool
183 */
184 public function isActive()
185 {
186 $activeBatch = $this->getActive($this->_connection);
187
188 return $activeBatch === $this;
189 }
190
191
192 /**
193 * Returns true, if this batch is capturing requests.
194 *
195 * @return bool
196 */
197 public function isCapturing()
198 {
199 return $this->getConnectionCaptureMode($this->_connection);
200 }
201
202
203 /**
204 * Activates the batch. This sets the batch active in its associated connection and also starts capturing.
205 *
206 * @return Batch $this
207 */
208 public function activate()
209 {
210 $this->setActive();
211 $this->setCapture(true);
212
213 return $this;
214 }
215
216
217 /**
218 * Sets the batch active in its associated connection.
219 *
220 * @return Batch $this
221 */
222 public function setActive()
223 {
224 $this->_connection->setActiveBatch($this);
225
226 return $this;
227 }
228
229
230 /**
231 * Sets the batch's associated connection into capture mode.
232 *
233 * @param boolean $state
234 *
235 * @return Batch $this
236 */
237 public function setCapture($state)
238 {
239 $this->_connection->setCaptureBatch($state);
240
241 return $this;
242 }
243
244
245 /**
246 * Gets active batch in given connection.
247 *
248 * @param Connection $connection
249 *
250 * @return $this
251 */
252 public function getActive($connection)
253 {
254 $connection->getActiveBatch();
255
256 return $this;
257 }
258
259
260 /**
261 * Returns true, if given connection is in batch-capture mode.
262 *
263 * @param Connection $connection
264 *
265 * @return bool
266 */
267 public function getConnectionCaptureMode($connection)
268 {
269 return $connection->isInBatchCaptureMode();
270 }
271
272
273 /**
274 * Sets connection into Batch-Request mode. This is necessary to distinguish between normal and the batch request.
275 *
276 * @param boolean $state
277 *
278 * @return $this
279 */
280 private function setBatchRequest($state)
281 {
282 $this->_connection->setBatchRequest($state);
283 $this->_processed = true;
284
285 return $this;
286 }
287
288
289 /**
290 * Sets the id of the next batch-part. The id can later be used to retrieve the batch-part.
291 *
292 * @param mixed $batchPartId
293 *
294 * @return Batch
295 */
296 public function nextBatchPartId($batchPartId)
297 {
298 $this->_nextBatchPartId = $batchPartId;
299
300 return $this;
301 }
302
303
304 /**
305 * Set client side cursor options (for example: sanitize) for the next batch part.
306 *
307 * @param mixed $batchPartCursorOptions
308 *
309 * @return Batch
310 */
311 public function nextBatchPartCursorOptions($batchPartCursorOptions)
312 {
313 $this->_batchPartCursorOptions = $batchPartCursorOptions;
314
315 return $this;
316 }
317
318
319 /**
320 * Append the request to the batch-part
321 *
322 * @param mixed $method - The method of the request (GET, POST...)
323 * @param mixed $request - The request that will get appended to the batch
324 *
325 * @return HttpResponse
326 *
327 * @throws \triagens\ArangoDb\ClientException
328 */
329 public function append($method, $request)
330 {
331 preg_match('%/_api/simple/(?P<simple>\w*)|/_api/(?P<direct>\w*)%ix', $request, $regs);
332
333 if (!isset($regs['direct'])) {
334 $regs['direct'] = '';
335 }
336 $type = $regs['direct'] !== '' ? $regs['direct'] : $regs['simple'];
337
338 if ($method === 'GET' && $type === $regs['direct']) {
339 $type = 'get' . $type;
340 }
341
342 if (null === $this->_nextBatchPartId) {
343 if (is_a($this->_batchParts, 'SplFixedArray')) {
344 $nextNumeric = $this->_nextId;
345 $this->_nextId++;
346 } else {
347 $nextNumeric = count($this->_batchParts);
348 }
349 $batchPartId = $nextNumeric;
350 } else {
351 $batchPartId = $this->_nextBatchPartId;
352 $this->_nextBatchPartId = null;
353 }
354
355 $eol = HttpHelper::EOL;
356
357 $result = 'HTTP/1.1 202 Accepted' . $eol;
358 $result .= 'location: /_db/_system/_api/document/0/0' . $eol;
359 $result .= 'content-type: application/json; charset=utf-8' . $eol;
360 $result .= 'etag: "0"' . $eol;
361 $result .= 'connection: Close' . $eol . $eol;
362 $result .= '{"error":false,"_id":"0/0","id":"0","_rev":0,"hasMore":1, "result":[{}], "documents":[{}]}' . $eol . $eol;
363
364 $response = new HttpResponse($result);
365 $batchPart = new BatchPart($this, $batchPartId, $type, $request, $response, ['cursorOptions' => $this->_batchPartCursorOptions]);
366
367 $this->_batchParts[$batchPartId] = $batchPart;
368
369 $response->setBatchPart($batchPart);
370
371 return $response;
372 }
373
374
375 /**
376 * Split batch request and use ContentId as array key
377 *
378 * @param mixed $pattern
379 * @param mixed $string
380 *
381 * @return array $array - Array of batch-parts
382 *
383 * @throws \triagens\ArangoDb\ClientException
384 */
385 public function splitWithContentIdKey($pattern, $string)
386 {
387 $array = [];
388 $exploded = explode($pattern, $string);
389 foreach ($exploded as $key => $value) {
390 $response = new HttpResponse($value);
391 $contentId = $response->getHeader('Content-Id');
392
393 if (null !== $contentId) {
394 $array[$contentId] = $value;
395 } else {
396 $array[$key] = $value;
397 }
398 }
399
400 return $array;
401 }
402
403
404 /**
405 * Processes this batch. This sends the captured requests to the server as one batch.
406 *
407 * @return HttpResponse|Batch - Batch if processing of the batch was successful or the HttpResponse object in case of a failure. A successful process just means that tha parts were processed. Each part has it's own response though and should be checked on its own.
408 *
409 * @throws ClientException
410 * @throws \triagens\ArangoDb\Exception
411 */
412 public function process()
413 {
414 if ($this->isCapturing()) {
415 $this->stopCapture();
416 }
417 $this->setBatchRequest(true);
418 $data = '';
419 $batchParts = $this->getBatchParts();
420
421 if (count($batchParts) === 0) {
422 throw new ClientException('Can\'t process empty batch.');
423 }
424
425 /** @var $partValue BatchPart */
426 foreach ($batchParts as $partValue) {
427 if (null !== $partValue) {
428 $data .= '--' . HttpHelper::MIME_BOUNDARY . HttpHelper::EOL;
429 $data .= 'Content-Type: application/x-arango-batchpart' . HttpHelper::EOL;
430
431 if (null !== $partValue->getId()) {
432 $data .= 'Content-Id: ' . (string) $partValue->getId() . HttpHelper::EOL . HttpHelper::EOL;
433 } else {
434 $data .= HttpHelper::EOL;
435 }
436
437 $data .= (string) $partValue->getRequest() . HttpHelper::EOL;
438 }
439 }
440 $data .= '--' . HttpHelper::MIME_BOUNDARY . '--' . HttpHelper::EOL . HttpHelper::EOL;
441
442 $params = [];
443 $url = UrlHelper::appendParamsUrl(Urls::URL_BATCH, $params);
444 $this->_batchResponse = $this->_connection->post($url, $data);
445 if ($this->_batchResponse->getHttpCode() !== 200) {
446 return $this->_batchResponse;
447 }
448 $body = $this->_batchResponse->getBody();
449 $body = trim($body, '--' . HttpHelper::MIME_BOUNDARY . '--');
450 $batchParts = $this->splitWithContentIdKey('--' . HttpHelper::MIME_BOUNDARY . HttpHelper::EOL, $body);
451
452 foreach ($batchParts as $partKey => $partValue) {
453 $response = new HttpResponse($partValue);
454 $body = $response->getBody();
455 $response = new HttpResponse($body);
456 $batchPartResponses[$partKey] = $response;
457 $this->getPart($partKey)->setResponse($batchPartResponses[$partKey]);
458 }
459
460 return $this;
461 }
462
463
464 /**
465 * Get the total count of the batch parts
466 *
467 * @return integer $count
468 */
469 public function countParts()
470 {
471 return count($this->_batchParts);
472 }
473
474
475 /**
476 * Get the batch part identified by the array key (0...n) or its id (if it was set with nextBatchPartId($id) )
477 *
478 * @param mixed $partId the batch part id. Either it's numeric key or a given name.
479 *
480 * @return mixed $batchPart
481 *
482 * @throws ClientException
483 */
484 public function getPart($partId)
485 {
486 if (!isset($this->_batchParts[$partId])) {
487 throw new ClientException('Request batch part does not exist.');
488 }
489
490 return $this->_batchParts[$partId];
491 }
492
493
494 /**
495 * Get the batch part identified by the array key (0...n) or its id (if it was set with nextBatchPartId($id) )
496 *
497 * @param mixed $partId the batch part id. Either it's numeric key or a given name.
498 *
499 * @return mixed $partId
500 *
501 * @throws \triagens\ArangoDb\ClientException
502 */
503 public function getPartResponse($partId)
504 {
505 return $this->getPart($partId)->getResponse();
506 }
507
508
509 /**
510 * Get the batch part identified by the array key (0...n) or its id (if it was set with nextBatchPartId($id) )
511 *
512 * @param mixed $partId the batch part id. Either it's numeric key or a given name.
513 *
514 * @return mixed $partId
515 *
516 * @throws \triagens\ArangoDb\ClientException
517 */
518 public function getProcessedPartResponse($partId)
519 {
520 return $this->getPart($partId)->getProcessedResponse();
521 }
522
523
524 /**
525 * Returns the array of batch-parts
526 *
527 * @return array $_batchParts
528 */
529 public function getBatchParts()
530 {
531 return $this->_batchParts;
532 }
533
534
535 /**
536 * Return an array of cursor options
537 *
538 * @return array - array of options
539 */
540 private function getCursorOptions()
541 {
542 return $this->_batchPartCursorOptions;
543 }
544
545
546 /**
547 * Return this batch's connection
548 *
549 * @return Connection
550 */
551 public function getConnection()
552 {
553 return $this->_connection;
554 }
555 }
556