1 <?php
2
3 /**
4 * ArangoDB PHP client: graph handler
5 *
6 * @package triagens\ArangoDb
7 * @author Jan Steemann
8 * @author Frank Mayer
9 * @author Florian Bartels
10 * @copyright Copyright 2014, triagens GmbH, Cologne, Germany
11 *
12 * @since 1.2
13 */
14
15 namespace triagens\ArangoDb;
16
17 /**
18 * A handler that manages graphs.
19 *
20 * @package triagens\ArangoDb
21 * @since 1.2
22 */
23 class GraphHandler extends
24 Handler
25 {
26 /**
27 * documents array index
28 */
29 const ENTRY_GRAPH = 'graph';
30
31 /**
32 * conditional update of edges or vertices
33 */
34 const OPTION_REVISION = 'revision';
35
36 /**
37 * vertex parameter
38 */
39 const OPTION_VERTICES = 'vertices';
40
41 /**
42 * direction parameter
43 */
44 const OPTION_EDGES = 'edges';
45
46 /**
47 * direction parameter
48 */
49 const OPTION_KEY = '_key';
50
51 /**
52 * collection parameter
53 */
54 const OPTION_COLLECTION = 'collection';
55
56 /**
57 * collections parameter
58 */
59 const OPTION_COLLECTIONS = 'collections';
60
61 /**
62 * example parameter
63 */
64 const KEY_FROM = '_from';
65
66 /**
67 * example parameter
68 */
69 const KEY_TO = '_to';
70
71 /**
72 * name parameter
73 */
74 const OPTION_NAME = 'name';
75
76 /**
77 * edge definition parameter
78 */
79 const OPTION_EDGE_DEFINITION = 'edgeDefinition';
80
81 /**
82 * edge definitions parameter
83 */
84 const OPTION_EDGE_DEFINITIONS = 'edgeDefinitions';
85
86 /**
87 * GraphHandler cache store
88 */
89 protected $cache;
90
91 /**
92 * @var $cacheEnabled boolean GraphHandler use cache store
93 */
94 protected $cacheEnabled = false;
95
96 /**
97 * Create a graph
98 *
99 * This will create a graph using the given graph object and return an array of the created graph object's attributes.<br><br>
100 *
101 * @throws Exception
102 *
103 * @param Graph $graph - The graph object which holds the information of the graph to be created
104 *
105 * @return array
106 * @since 1.2
107 */
108 public function createGraph(Graph $graph)
109 {
110 $edgeDefinitions = [];
111 foreach ($graph->getEdgeDefinitions() as $ed) {
112 $edgeDefinitions[] = $ed->transformToArray();
113 }
114
115 $params = [
116 self::OPTION_NAME => $graph->getKey(),
117 self::OPTION_EDGE_DEFINITIONS => $edgeDefinitions,
118 self::OPTION_ORPHAN_COLLECTIONS => $graph->getOrphanCollections()
119 ];
120 $url = Urls::URL_GRAPH;
121 $response = $this->getConnection()->post($url, $this->json_encode_wrapper($params));
122 $json = $response->getJson();
123 $graph->setInternalId($json['graph'][Graph::ENTRY_ID]);
124 $graph->set(Graph::ENTRY_KEY, $json['graph'][self::OPTION_NAME]);
125 $graph->setRevision($json['graph'][Graph::ENTRY_REV]);
126
127 return $graph->getAll();
128 }
129
130 /**
131 * orphan collection parameter
132 */
133 const OPTION_ORPHAN_COLLECTIONS = 'orphanCollections';
134
135 /**
136 * drop collection
137 */
138 const OPTION_DROP_COLLECTION = 'dropCollection';
139
140 /**
141 * batchsize
142 */
143 private $batchsize;
144
145 /**
146 * count
147 */
148 private $count;
149
150
151 /**
152 * limit
153 */
154 private $limit;
155
156 /**
157 * Get a graph
158 *
159 * This will get a graph.<br><br>
160 *
161 * @param String $graph - The name of the graph
162 * @param array $options - Options to pass to the method
163 *
164 * @return Graph|false
165 * @throws \triagens\ArangoDb\ClientException
166 * @since 1.2
167 */
168 public function getGraph($graph, array $options = [])
169 {
170 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph]);
171
172 try {
173 $response = $this->getConnection()->get($url);
174 } catch (Exception $e) {
175 return false;
176 }
177
178 $data = $response->getJson();
179
180 $options['_isNew'] = false;
181
182 $result = Graph::createFromArray($data['graph'], $options);
183 $result->set(Graph::ENTRY_KEY, $data['graph'][self::OPTION_NAME]);
184
185 return $result;
186 }
187
188 /**
189 * Sets the batchsize for any method creating a cursor.
190 * Will be reset after the cursor has been created.
191 *
192 * @param int $batchsize
193 */
194 public function setBatchsize($batchsize)
195 {
196 $this->batchsize = $batchsize;
197 }
198
199
200 /**
201 * Sets the count for any method creating a cursor.
202 * Will be reset after the cursor has been created.
203 *
204 * @param int $count
205 */
206 public function setCount($count)
207 {
208 $this->count = $count;
209 }
210
211
212 /**
213 * Sets the limit for any method creating a cursor.
214 * Will be reset after the cursor has been created.
215 *
216 * @param int $limit
217 */
218 public function setLimit($limit)
219 {
220 $this->limit = $limit;
221 }
222
223
224 /**
225 * Get a graph's properties<br><br>
226 *
227 * @throws Exception
228 *
229 * @param mixed $graph - graph name as a string or instance of Graph
230 *
231 * @return bool - Returns an array of attributes. Will throw if there is an error
232 * @since 1.2
233 */
234 public function properties($graph)
235 {
236 if ($graph instanceof Graph) {
237 $graph = $graph->getKey();
238 }
239
240 $url = UrlHelper::buildUrl(Urls::URL_DOCUMENT . '/_graphs', [$graph]);
241
242 $result = $this->getConnection()->get($url);
243
244 return $result->getJson();
245 }
246
247
248 /**
249 * Drop a graph and remove all its vertices and edges, also drops vertex and edge collections<br><br>
250 *
251 * @throws Exception
252 *
253 * @param mixed $graph - graph name as a string or instance of Graph
254 * @param bool $dropCollections - if set to false the graphs collections will not be droped.
255 *
256 * @return bool - always true, will throw if there is an error
257 * @since 1.2
258 */
259 public function dropGraph($graph, $dropCollections = true)
260 {
261 if ($graph instanceof Graph) {
262 $graph = $graph->getKey();
263 }
264
265
266 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph]);
267 $url = UrlHelper::appendParamsUrl($url, ['dropCollections' => $dropCollections]);
268 $this->getConnection()->delete($url);
269
270 return true;
271 }
272
273 /**
274 * add an orphan collection to the graph.
275 *
276 * This will add a further orphan collection to the graph.<br><br>
277 *
278 *
279 * @throws Exception
280 *
281 * @param mixed $graph - graph name as a string or instance of Graph
282 * @param string $orphanCollection - the orphan collection to be added as string.
283 *
284 * @return Graph
285 * @since 2.2
286 */
287 public function addOrphanCollection($graph, $orphanCollection)
288 {
289 if ($graph instanceof Graph) {
290 $graph = $graph->getKey();
291 }
292
293 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_VERTEX]);
294 $data = [
295 self::OPTION_COLLECTION => $orphanCollection
296 ];
297
298 try {
299 $response = $this->getConnection()->post($url, $this->json_encode_wrapper($data));
300 } catch (Exception $e) {
301 throw new ClientException($e->getMessage());
302 }
303
304 $data = $response->getJson();
305
306 $options['_isNew'] = false;
307
308 $result = Graph::createFromArray($data['graph'], $options);
309 $result->set(Graph::ENTRY_KEY, $data['graph'][self::OPTION_NAME]);
310
311 return $result;
312 }
313
314 /**
315 * deletes an orphan collection from the graph.
316 *
317 * This will delete an orphan collection from the graph.<br><br>
318 *
319 *
320 * @throws Exception
321 *
322 * @param mixed $graph - graph name as a string or instance of Graph
323 * @param string $orphanCollection - the orphan collection to be removed as string.
324 * @param boolean $dropCollection - if set to true the collection is deleted, not just removed from the graph.
325 *
326 * @return Graph
327 * @since 2.2
328 */
329 public function deleteOrphanCollection($graph, $orphanCollection, $dropCollection = false)
330 {
331 if ($graph instanceof Graph) {
332 $graph = $graph->getKey();
333 }
334
335 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_VERTEX, $orphanCollection]);
336 $data = [
337 self::OPTION_DROP_COLLECTION => $dropCollection
338 ];
339 $url = UrlHelper::appendParamsUrl($url, $data);
340
341 try {
342 $response = $this->getConnection()->delete($url);
343 } catch (Exception $e) {
344 throw new ClientException($e->getMessage());
345 }
346
347 $data = $response->getJson();
348
349 $options['_isNew'] = false;
350
351 $result = Graph::createFromArray($data['graph'], $options);
352 $result->set(Graph::ENTRY_KEY, $data['graph'][self::OPTION_NAME]);
353
354 return $result;
355 }
356
357 /**
358 * gets all vertex collection from the graph.
359 *
360 * This will get all vertex collection (orphans and used in edge definitions) from the graph.<br><br>
361 *
362 * If this method or any method that calls this method is used in batch mode and caching is off,<br>
363 * then for each call, this method will make an out of batch API call to the db in order to get the appropriate collections.<br><br>
364 *
365 * If caching is on, then the GraphHandler will only call the DB API once for the chosen graph, and return data from cache for the following calls.<br>
366 *
367 * @param mixed $graph - graph name as a string or instance of Graph
368 * @param array $options - optional, an array of options
369 * <p>Options are :<br>
370 * <li>'excludeOrphans' - boolean value: true to exclude the orphans or false to include orphans in the result.<br>
371 * Defaults to false</li>
372 *
373 * @return array
374 * @throws ClientException@since 2.2
375 */
376 public function getVertexCollections($graph, array $options = [])
377 {
378 if ($graph instanceof Graph) {
379 $graph = $graph->getKey();
380 }
381
382 $excludeOrphans = false;
383 $_useCache = $this->cacheEnabled;
384
385 if ((bool) $options && isset($options['excludeOrphans']) && !is_bool($options['excludeOrphans'])) {
386 $excludeOrphans = UrlHelper::getBoolString($options['excludeOrphans']);
387 }
388
389 if ($_useCache === true) {
390 if ($excludeOrphans === true && !empty($this->cache[$graph]['excludeOrphans']['result'])) {
391 return $this->cache[$graph]['excludeOrphans']['vertexCollections'];
392 } else if (!empty($this->cache[$graph]['vertexCollections'])) {
393 return $this->cache[$graph]['vertexCollections'];
394 }
395 }
396 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_VERTEX]);
397
398 if ($excludeOrphans === true) {
399 $url = UrlHelper::appendParamsUrl($url, ['excludeOrphans' => $excludeOrphans]);
400 }
401
402 $connection = $this->getConnection();
403 $batchCaptureMode = $connection->isInBatchCaptureMode();
404
405 if ($batchCaptureMode === true) {
406 $this->getConnection()->setBatchRequest(false);
407 }
408
409 try {
410 $response = $this->getConnection()->get($url);
411 } catch (Exception $e) {
412 throw new ClientException($e->getMessage());
413 }
414
415 if ($batchCaptureMode === true) {
416 $this->getConnection()->setBatchRequest(true);
417 }
418
419 $data = $response->getJson();
420 $data = $data[self::OPTION_COLLECTIONS];
421
422 sort($data);
423
424 if ($_useCache === true) {
425 if ($excludeOrphans === true && !empty($this->cache[$graph]['excludeOrphans']['vertexCollections'])) {
426 $this->cache[$graph]['excludeOrphans']['vertexCollections'] = $data;
427 } else {
428 $this->cache[$graph]['vertexCollections'] = $data;
429 }
430 }
431
432 return $data;
433 }
434
435 /**
436 * adds an edge definition to the graph.
437 *
438 * This will add a further edge definition to the graph.<br><br>
439 *
440 *
441 * @throws Exception
442 *
443 * @param mixed $graph - graph name as a string or instance of Graph
444 * @param EdgeDefinition $edgeDefinition - the new edge definition.
445 *
446 * @return Graph
447 * @since 2.2
448 */
449 public function addEdgeDefinition($graph, $edgeDefinition)
450 {
451 if ($graph instanceof Graph) {
452 $graph = $graph->getKey();
453 }
454
455 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_EDGE]);
456 $data = $edgeDefinition->transformToArray();
457
458 try {
459 $response = $this->getConnection()->post($url, $this->json_encode_wrapper($data));
460
461 } catch (Exception $e) {
462 throw new ClientException($e->getMessage());
463 }
464
465 $data = $response->getJson();
466
467 $options['_isNew'] = false;
468
469 $result = Graph::createFromArray($data['graph'], $options);
470 $result->set(Graph::ENTRY_KEY, $data['graph'][self::OPTION_NAME]);
471
472 return $result;
473 }
474
475 /**
476 * deletes an edge definition from the graph.
477 *
478 * This will delete an edge definition from the graph.<br><br>
479 *
480 *
481 * @throws Exception
482 *
483 * @param mixed $graph - graph name as a string or instance of Graph
484 * @param string $edgeDefinition - the name of the edge definitions relation.
485 * @param boolean $dropCollection - if set to true the edge definitions collections are deleted.
486 *
487 * @return Graph
488 * @since 2.2
489 */
490 public function deleteEdgeDefinition($graph, $edgeDefinition, $dropCollection = false)
491 {
492 if ($graph instanceof Graph) {
493 $graph = $graph->getKey();
494 }
495
496 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_EDGE, $edgeDefinition]);
497 $data = [
498 self::OPTION_DROP_COLLECTION => $dropCollection
499 ];
500 $url = UrlHelper::appendParamsUrl($url, $data);
501 try {
502 $response = $this->getConnection()->delete($url);
503 } catch (Exception $e) {
504 throw new ClientException($e->getMessage());
505 }
506
507 $data = $response->getJson();
508
509 $options['_isNew'] = false;
510
511 $result = Graph::createFromArray($data['graph'], $options);
512 $result->set(Graph::ENTRY_KEY, $data['graph'][self::OPTION_NAME]);
513
514 return $result;
515 }
516
517 /**
518 * gets all edge collections from the graph.
519 *
520 * This will get all edge collections from the graph.<br><br>
521 *
522 * If this method or any method that calls this method is used in batch mode and caching is off,<br>
523 * then for each call, this method will make an out of batch API call to the db in order to get the appropriate collections.<br><br>
524 *
525 * If caching is on, then the GraphHandler will only call the DB API once for the chosen graph, and return data from cache for the following calls.<br>
526 *
527 * @throws Exception
528 *
529 * @param mixed $graph - graph name as a string or instance of Graph
530 *
531 * @return array
532 *
533 * @since 2.2
534 */
535 public function getEdgeCollections($graph)
536 {
537 if ($graph instanceof Graph) {
538 $graph = $graph->getKey();
539 }
540
541 $_useCache = $this->cacheEnabled;
542
543 if ($_useCache === true && !empty($this->cache[$graph]['edgeCollections'])) {
544 return $this->cache[$graph]['edgeCollections'];
545 }
546
547 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_EDGE]);
548
549 $connection = $this->getConnection();
550 $batchCaptureMode = $connection->isInBatchCaptureMode();
551
552 if ($batchCaptureMode === true) {
553 $this->getConnection()->setBatchRequest(false);
554 }
555
556 try {
557 $response = $this->getConnection()->get($url);
558 } catch (Exception $e) {
559 throw new ClientException($e->getMessage());
560 }
561
562 if ($batchCaptureMode === true) {
563 $this->getConnection()->setBatchRequest(true);
564 }
565
566 $data = $response->getJson();
567 $data = $data[self::OPTION_COLLECTIONS];
568
569 sort($data);
570
571 if ($_useCache === true && !empty($this->cache[$graph]['edgeCollections'])) {
572 $this->cache[$graph]['edgeCollections'] = $data;
573 }
574
575 return $data;
576 }
577
578
579 /**
580 * replaces an edge definition of the graph.
581 *
582 * This will replace an edge definition in the graph.<br><br>
583 *
584 *
585 * @throws Exception
586 *
587 * @param mixed $graph - graph name as a string or instance of Graph
588 * @param EdgeDefinition $edgeDefinition - the edge definition.
589 *
590 * @return Graph
591 * @since 2.2
592 */
593 public function replaceEdgeDefinition($graph, $edgeDefinition)
594 {
595 if ($graph instanceof Graph) {
596 $graph = $graph->getKey();
597 }
598
599 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_EDGE, $edgeDefinition->getRelation()]);
600 $data = $edgeDefinition->transformToArray();
601
602 try {
603 $response = $this->getConnection()->put($url, $this->json_encode_wrapper($data));
604 } catch (Exception $e) {
605 throw new ClientException($e->getMessage());
606 }
607
608 $data = $response->getJson();
609
610 $options['_isNew'] = false;
611
612 $result = Graph::createFromArray($data['graph'], $options);
613 $result->set(Graph::ENTRY_KEY, $data['graph'][self::OPTION_NAME]);
614
615 return $result;
616 }
617
618 /**
619 * save a vertex to a graph
620 *
621 * This will add the vertex-document to the graph and return the vertex id
622 *
623 * This will throw if the vertex cannot be saved<br><br>
624 *
625 * @throws Exception
626 *
627 * @param mixed $graph - graph name as a string or instance of Graph
628 * @param mixed $document - the vertex to be added, can be passed as a vertex object or an array
629 * @param string $collection - if one uses a graph with more than one vertex collection one must provide
630 * the collection to store the vertex.
631 *
632 * @return string - id of vertex created
633 * @since 1.2
634 */
635 public function saveVertex($graph, $document, $collection = null)
636 {
637 if ($graph instanceof Graph) {
638 $graph = $graph->getKey();
639 }
640
641 if (is_array($document)) {
642 $document = Vertex::createFromArray($document);
643 }
644
645 if ($collection === null) {
646 $vertexCollections = $this->getVertexCollections($graph);
647 $vertexCollectionsCount = count($vertexCollections);
648 if ($vertexCollectionsCount !== 1) {
649 throw new ClientException('A collection must be provided.');
650 } else if ($vertexCollectionsCount === 1) {
651 $collection = $vertexCollections[0];
652 }
653 }
654
655 $data = $document->getAll();
656 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_VERTEX, $collection]);
657
658 $response = $this->getConnection()->post($url, $this->json_encode_wrapper($data));
659
660 // This makes sure that if we're in batch mode, it will not go further and choke on the checks below.
661 // Caution: Instead of a document ID, we are returning the batchpart object.
662 // The Id of the BatchPart can be retrieved by calling getId() on it.
663 // We're basically returning an object here, in order not to accidentally use the batch part id as the document id
664 if ($batchPart = $response->getBatchPart()) {
665 return $batchPart;
666 }
667
668 $jsonArray = $response->getJson();
669 $vertex = $jsonArray['vertex'];
670
671 $document->setInternalId($vertex[Vertex::ENTRY_ID]);
672 $document->setRevision($vertex[Vertex::ENTRY_REV]);
673
674 $document->setIsNew(false);
675
676 return $document->getInternalId();
677 }
678
679
680 /**
681 * Get a single vertex from a graph
682 *
683 * This will throw if the vertex cannot be fetched from the server<br><br>
684 *
685 * @throws Exception
686 *
687 * @param mixed $graph - graph name as a string or instance of Graph
688 * @param mixed $vertexId - the vertex identifier
689 * @param array $options optional, an array of options:
690 * <p>
691 * <li><b>_includeInternals</b> - true to include the internal attributes. Defaults to false</li>
692 * <li><b>_ignoreHiddenAttributes</b> - true to show hidden attributes. Defaults to false</li>
693 * </p>
694 * @param string $collection - if one uses a graph with more than one vertex collection one must provide the collection
695 * to load the vertex.
696 *
697 * @return Document
698 * @since 1.2
699 */
700 public function getVertex($graph, $vertexId, array $options = [], $collection = null)
701 {
702 if ($graph instanceof Graph) {
703 $graph = $graph->getKey();
704 }
705 $parts = explode('/', $vertexId);
706 if (count($parts) === 2) {
707 list($collection, $vertexId) = $parts;
708 }
709
710 if ($collection === null) {
711 $vertexCollections = $this->getVertexCollections($graph);
712 $vertexCollectionsCount = count($vertexCollections);
713 if ($vertexCollectionsCount !== 1) {
714 throw new ClientException('A collection must be provided.');
715 } else if ($vertexCollectionsCount === 1) {
716 $collection = $vertexCollections[0];
717 }
718 }
719
720 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_VERTEX, $collection, $vertexId]);
721 $response = $this->getConnection()->get($url);
722
723 $jsonArray = $response->getJson();
724 $vertex = $jsonArray['vertex'];
725
726 $options['_isNew'] = false;
727
728 return Vertex::createFromArray($vertex, $options);
729 }
730
731
732 /**
733 * Check if a vertex exists
734 *
735 * This will call self::getVertex() internally and checks if there
736 * was an exception thrown which represents an 404 request.
737 *
738 * @throws Exception When any other error than a 404 occurs
739 *
740 * @param mixed $graph - graph name as a string or instance of Graph
741 * @param mixed $vertexId - the vertex identifier
742 *
743 * @return boolean
744 */
745 public function hasVertex($graph, $vertexId)
746 {
747 try {
748 // will throw ServerException if entry could not be retrieved
749 $this->getVertex($graph, $vertexId);
750
751 return true;
752 } catch (ServerException $e) {
753 // we are expecting a 404 to return boolean false
754 if ($e->getCode() === 404) {
755 return false;
756 }
757
758 // just rethrow
759 throw $e;
760 }
761 }
762
763
764 /**
765 * Replace an existing vertex in a graph, identified graph name and vertex id
766 *
767 * This will update the vertex on the server
768 *
769 * If policy is set to error (locally or globally through the ConnectionOptions)
770 * and the passed document has a _rev value set, the database will check
771 * that the revision of the to-be-replaced vertex is the same as the one given.<br><br>
772 *
773 * @throws Exception
774 *
775 * @param mixed $graph - graph name as a string or instance of Graph
776 * @param mixed $vertexId - the vertex id as string or number
777 * @param Document $document - the vertex-document to be updated
778 * @param mixed $options optional, an array of options (see below) or the boolean value for $policy (for compatibility prior to version 1.1 of this method):
779 * <p>
780 * <li><b>revision</b> - revision for conditional updates ('some-revision-id' [use the passed in revision id], false or true [use document's revision])</li>
781 * <li><b>policy</b> - update policy to be used in case of conflict ('error', 'last' or NULL [use default])</li>
782 * <li><b>waitForSync</b> - can be used to force synchronisation of the document replacement operation to disk even in case that the waitForSync flag had been disabled for the entire collection</li>
783 * </p>
784 * @param string $collection - if one uses a graph with more than one vertex collection one must provide the collection
785 *
786 * @return bool - always true, will throw if there is an error
787 * @since 1.2
788 */
789 public function replaceVertex($graph, $vertexId, Document $document, array $options = [], $collection = null)
790 {
791 if ($graph instanceof Graph) {
792 $graph = $graph->getKey();
793 }
794
795 $parts = explode('/', $vertexId);
796 if (count($parts) === 2) {
797 list($collection, $vertexId) = $parts;
798 }
799
800 if ($collection === null) {
801 $vertexCollections = $this->getVertexCollections($graph);
802 $vertexCollectionsCount = count($vertexCollections);
803 if ($vertexCollectionsCount !== 1) {
804 throw new ClientException('A collection must be provided.');
805 } else if ($vertexCollectionsCount === 1) {
806 $collection = $vertexCollections[0];
807 }
808 }
809
810 $options = array_merge([self::OPTION_REVISION => false], $options);
811
812 $params = $this->includeOptionsInParams(
813 $options, [
814 'waitForSync' => $this->getConnectionOption(ConnectionOptions::OPTION_WAIT_SYNC),
815 'policy' => $this->getConnectionOption(ConnectionOptions::OPTION_REPLACE_POLICY)
816
817 ]
818 );
819
820 //Include the revision for conditional updates if required
821 if ($options[self::OPTION_REVISION] === true) {
822
823 $revision = $document->getRevision();
824
825 if (null !== $revision) {
826 $params[ConnectionOptions::OPTION_REVISION] = $revision;
827 }
828 } elseif ($options[self::OPTION_REVISION]) {
829 $params[ConnectionOptions::OPTION_REVISION] = $options[self::OPTION_REVISION];
830 }
831
832 $data = $document->getAll();
833 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_VERTEX, $collection, $vertexId]);
834 $url = UrlHelper::appendParamsUrl($url, $params);
835
836 $response = $this->getConnection()->put($url, $this->json_encode_wrapper($data));
837
838 $jsonArray = $response->getJson();
839 $vertex = $jsonArray['vertex'];
840
841 $document->setInternalId($vertex[Vertex::ENTRY_ID]);
842 $document->setRevision($vertex[Vertex::ENTRY_REV]);
843
844
845 return true;
846 }
847
848
849 /**
850 * Update an existing vertex in a graph, identified by graph name and vertex id
851 *
852 * This will update the vertex on the server
853 *
854 * This will throw if the vertex cannot be updated
855 *
856 * If policy is set to error (locally or globally through the ConnectionOptions)
857 * and the passed vertex-document has a _rev value set, the database will check
858 * that the revision of the to-be-replaced document is the same as the one given.<br><br>
859 *
860 * @throws Exception
861 *
862 * @param mixed $graph - graph name as a string or instance of Graph
863 * @param mixed $vertexId - the vertex id as string or number
864 * @param Document $document - the patch vertex-document which contains the attributes and values to be updated
865 * @param mixed $options optional, an array of options (see below)
866 * <p>
867 * <li><b>policy</b> - update policy to be used in case of conflict ('error', 'last' or NULL [use default])</li>
868 * <li><b>keepNull</b> - can be used to instruct ArangoDB to delete existing attributes instead setting their values to null. Defaults to true (keep attributes when set to null)</li>
869 * <li><b>waitForSync</b> - can be used to force synchronisation of the document update operation to disk even in case that the waitForSync flag had been disabled for the entire collection</li>
870 * </p>
871 * @param string $collection - if one uses a graph with more than one vertex collection one must provide the collection
872 *
873 * @return bool - always true, will throw if there is an error
874 * @since 1.2
875 */
876 public function updateVertex($graph, $vertexId, Document $document, array $options = [], $collection = null)
877 {
878 if ($graph instanceof Graph) {
879 $graph = $graph->getKey();
880 }
881 $parts = explode('/', $vertexId);
882 if (count($parts) === 2) {
883 list($collection, $vertexId) = $parts;
884 }
885
886 if ($collection === null) {
887 $vertexCollections = $this->getVertexCollections($graph);
888 $vertexCollectionsCount = count($vertexCollections);
889 if ($vertexCollectionsCount !== 1) {
890 throw new ClientException('A collection must be provided.');
891 } else if ($vertexCollectionsCount === 1) {
892 $collection = $vertexCollections[0];
893 }
894 }
895
896 $options = array_merge([self::OPTION_REVISION => false], $options);
897
898 $params = $this->includeOptionsInParams(
899 $options, [
900 'waitForSync' => $this->getConnectionOption(ConnectionOptions::OPTION_WAIT_SYNC),
901 'keepNull' => true,
902 'policy' => $this->getConnectionOption(ConnectionOptions::OPTION_UPDATE_POLICY)
903
904 ]
905 );
906
907 //Include the revision for conditional updates if required
908 if ($options[self::OPTION_REVISION] === true) {
909
910 $revision = $document->getRevision();
911
912 if (null !== $revision) {
913 $params[ConnectionOptions::OPTION_REVISION] = $revision;
914 }
915 } elseif ($options[self::OPTION_REVISION]) {
916 $params[ConnectionOptions::OPTION_REVISION] = $options[self::OPTION_REVISION];
917 }
918
919 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_VERTEX, $collection, $vertexId]);
920 $url = UrlHelper::appendParamsUrl($url, $params);
921 $result = $this->getConnection()->patch($url, $this->json_encode_wrapper($document->getAll()));
922 $json = $result->getJson();
923 $vertex = $json['vertex'];
924 $document->setRevision($vertex[Vertex::ENTRY_REV]);
925
926 return true;
927 }
928
929
930 /**
931 * Remove a vertex from a graph, identified by the graph name and vertex id<br><br>
932 *
933 * @throws Exception
934 *
935 * @param mixed $graph - graph name as a string or instance of Graph
936 * @param mixed $vertexId - the vertex id as string or number
937 * @param mixed $revision - optional, the revision of the vertex to be deleted
938 * @param mixed $options optional, an array of options (see below) or the boolean value for $policy (for compatibility prior to version 1.1 of this method)
939 * <p>
940 * <li><b>policy</b> - update policy to be used in case of conflict ('error', 'last' or NULL [use default])</li>
941 * <li><b>waitForSync</b> - can be used to force synchronisation of the document removal operation to disk even in case that the waitForSync flag had been disabled for the entire collection</li>
942 * </p>
943 * @param string $collection - if one uses a graph with more than one vertex collection one must provide the collection
944 *
945 * @return bool - always true, will throw if there is an error
946 * @since 1.2
947 */
948 public function removeVertex($graph, $vertexId, $revision = null, array $options = [], $collection = null)
949 {
950 if ($graph instanceof Graph) {
951 $graph = $graph->getKey();
952 }
953 $parts = explode('/', $vertexId);
954 if (count($parts) === 2) {
955 list($collection, $vertexId) = $parts;
956 }
957
958 if ($collection === null) {
959 $vertexCollections = $this->getVertexCollections($graph);
960 $vertexCollectionsCount = count($vertexCollections);
961 if ($vertexCollectionsCount !== 1) {
962 throw new ClientException('A collection must be provided.');
963 } else if ($vertexCollectionsCount === 1) {
964 $collection = $vertexCollections[0];
965 }
966 }
967
968
969 $params = $this->includeOptionsInParams(
970 $options, [
971 'waitForSync' => $this->getConnectionOption(ConnectionOptions::OPTION_WAIT_SYNC),
972 'keepNull' => true,
973 'policy' => $this->getConnectionOption(ConnectionOptions::OPTION_DELETE_POLICY)
974
975 ]
976 );
977
978 if (null !== $revision) {
979 $params[ConnectionOptions::OPTION_REVISION] = $revision;
980 }
981
982 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_VERTEX, $collection, $vertexId]);
983 $url = UrlHelper::appendParamsUrl($url, $params);
984 $this->getConnection()->delete($url);
985
986 return true;
987 }
988
989
990 /**
991 * save an edge to a graph
992 *
993 * This will save the edge to the graph and return the edges-document's id
994 *
995 * This will throw if the edge cannot be saved<br><br>
996 *
997 * @throws Exception
998 *
999 * @param mixed $graph - graph name as a string or instance of Graph
1000 * @param mixed $from - the 'from' vertex
1001 * @param mixed $to - the 'to' vertex
1002 * @param mixed $label - (optional) a label for the edge
1003 * @param mixed $document - the edge-document to be added, can be passed as an edge object or an array
1004 * @param string $collection - if one uses a graph with more than one vertex collection one must provide the collection
1005 *
1006 * @return mixed - id of edge created
1007 * @since 1.2
1008 */
1009 public function saveEdge($graph, $from, $to, $label = null, $document, $collection = null)
1010 {
1011 if ($graph instanceof Graph) {
1012 $graph = $graph->getKey();
1013 }
1014
1015 if ($collection === null) {
1016 $edgeCollections = $this->getEdgeCollections($graph);
1017 $edgeCollectionsCount = count($edgeCollections);
1018 if ($edgeCollectionsCount !== 1) {
1019 throw new ClientException('A collection must be provided.');
1020 } else if ($edgeCollectionsCount === 1) {
1021 $collection = $edgeCollections[0];
1022 }
1023 }
1024
1025 if (is_array($document)) {
1026 $document = Edge::createFromArray($document);
1027 }
1028 if (null !== $label) {
1029 $document->set('$label', $label);
1030 }
1031 $document->setFrom($from);
1032 $document->setTo($to);
1033 $data = $document->getAll();
1034 $data[self::KEY_FROM] = $from;
1035 $data[self::KEY_TO] = $to;
1036
1037 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_EDGE, $collection]);
1038
1039 $response = $this->getConnection()->post($url, $this->json_encode_wrapper($data));
1040
1041 $jsonArray = $response->getJson();
1042 $edge = $jsonArray['edge'];
1043
1044 $document->setInternalId($edge[Edge::ENTRY_ID]);
1045 $document->setRevision($edge[Edge::ENTRY_REV]);
1046
1047 $document->setIsNew(false);
1048
1049 return $document->getInternalId();
1050 }
1051
1052
1053 /**
1054 * Get a single edge from a graph
1055 *
1056 * This will throw if the edge cannot be fetched from the server<br><br>
1057 *
1058 * @throws Exception
1059 *
1060 * @param mixed $graph - graph name as a string or instance of Graph
1061 * @param mixed $edgeId - edge identifier
1062 * @param array $options optional, array of options
1063 * <p>
1064 * <li><b>_includeInternals</b> - true to include the internal attributes. Defaults to false</li>
1065 * <li><b>_ignoreHiddenAttributes</b> - true to show hidden attributes. Defaults to false</li>
1066 * </p>
1067 * @param string $collection - if one uses a graph with more than one vertex collection one must provide the collection
1068 *
1069 * @return Document - the edge document fetched from the server
1070 * @since 1.2
1071 */
1072 public function getEdge($graph, $edgeId, array $options = [], $collection = null)
1073 {
1074 if ($graph instanceof Graph) {
1075 $graph = $graph->getKey();
1076 }
1077 $parts = explode('/', $edgeId);
1078 if (count($parts) === 2) {
1079 list($collection, $edgeId) = $parts;
1080 }
1081
1082 if ($collection === null) {
1083 $edgeCollections = $this->getEdgeCollections($graph);
1084 $edgeCollectionsCount = count($edgeCollections);
1085 if ($edgeCollectionsCount !== 1) {
1086 throw new ClientException('A collection must be provided.');
1087 } else if ($edgeCollectionsCount === 1) {
1088 $collection = $edgeCollections[0];
1089 }
1090 }
1091
1092 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_EDGE, $collection, $edgeId]);
1093 $response = $this->getConnection()->get($url);
1094
1095 $jsonArray = $response->getJson();
1096 $edge = $jsonArray['edge'];
1097
1098 $options['_isNew'] = false;
1099
1100 return Edge::createFromArray($edge, $options);
1101 }
1102
1103
1104 /**
1105 * Check if an edge exists
1106 *
1107 * This will call self::getEdge() internally and checks if there
1108 * was an exception thrown which represents an 404 request.
1109 *
1110 * @throws Exception When any other error than a 404 occurs
1111 *
1112 * @param mixed $graph - graph name as a string or instance of Graph
1113 * @param mixed $edgeId - the vertex identifier
1114 *
1115 * @return boolean
1116 */
1117 public function hasEdge($graph, $edgeId)
1118 {
1119 try {
1120 // will throw ServerException if entry could not be retrieved
1121 $this->getEdge($graph, $edgeId);
1122
1123 return true;
1124 } catch (ServerException $e) {
1125 // we are expecting a 404 to return boolean false
1126 if ($e->getCode() === 404) {
1127 return false;
1128 }
1129
1130 // just rethrow
1131 throw $e;
1132 }
1133 }
1134
1135
1136 /**
1137 * Replace an existing edge in a graph, identified graph name and edge id
1138 *
1139 * This will replace the edge on the server
1140 *
1141 * This will throw if the edge cannot be Replaced
1142 *
1143 * If policy is set to error (locally or globally through the ConnectionOptions)
1144 * and the passed document has a _rev value set, the database will check
1145 * that the revision of the to-be-replaced edge is the same as the one given.<br><br>
1146 *
1147 * @throws Exception
1148 *
1149 * @param mixed $graph - graph name as a string or instance of Graph
1150 * @param mixed $edgeId - edge id as string or number
1151 * @param mixed $label - label for the edge or ''
1152 * @param Edge $document - edge document to be updated
1153 * @param mixed $options optional, array of options (see below) or the boolean value for $policy (for compatibility prior to version 1.1 of this method)
1154 * <p>
1155 * <li><b>policy</b> - update policy to be used in case of conflict ('error', 'last' or NULL [use default])</li>
1156 * <li><b>waitForSync</b> - can be used to force synchronisation of the document replacement operation to disk even in case that the waitForSync flag had been disabled for the entire collection</li>
1157 * </p>
1158 * @param string $collection - if one uses a graph with more than one vertex collection one must provide the collection
1159 *
1160 * @return bool - always true, will throw if there is an error
1161 * @since 1.2
1162 */
1163 public function replaceEdge($graph, $edgeId, $label, Edge $document, array $options = [], $collection = null)
1164 {
1165 if ($graph instanceof Graph) {
1166 $graph = $graph->getKey();
1167 }
1168 $parts = explode('/', $edgeId);
1169 if (count($parts) === 2) {
1170 list($collection, $edgeId) = $parts;
1171 }
1172
1173 if ($collection === null) {
1174 $edgeCollections = $this->getEdgeCollections($graph);
1175 $edgeCollectionsCount = count($edgeCollections);
1176 if ($edgeCollectionsCount !== 1) {
1177 throw new ClientException('A collection must be provided.');
1178 } else if ($edgeCollectionsCount === 1) {
1179 $collection = $edgeCollections[0];
1180 }
1181 }
1182
1183 $options = array_merge([self::OPTION_REVISION => false], $options);
1184
1185 $params = $this->includeOptionsInParams(
1186 $options, [
1187 'waitForSync' => $this->getConnectionOption(ConnectionOptions::OPTION_WAIT_SYNC),
1188 'silent' => false,
1189 'ignoreRevs' => true,
1190 'policy' => $this->getConnectionOption(ConnectionOptions::OPTION_REPLACE_POLICY),
1191 ]
1192 );
1193
1194 //Include the revision for conditional updates if required
1195 $headers = [];
1196 if ($options[self::OPTION_REVISION] === true) {
1197 $revision = $document->getRevision();
1198
1199 if (null !== $revision) {
1200 $params['ignoreRevs'] = false;
1201 $headers['if-match'] = '"' . $revision . '"';
1202 }
1203 } elseif ($options[self::OPTION_REVISION]) {
1204 $revision = $options[self::OPTION_REVISION];
1205 $params['ignoreRevs'] = false;
1206 $headers['if-match'] = '"' . $revision . '"';
1207 }
1208
1209 $data = $document->getAllForInsertUpdate();
1210 if (null !== $label) {
1211 $document->set('$label', $label);
1212 }
1213
1214 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_EDGE, $collection, $edgeId]);
1215 $url = UrlHelper::appendParamsUrl($url, $params);
1216
1217 $response = $this->getConnection()->put($url, $this->json_encode_wrapper($data), $headers);
1218
1219 $jsonArray = $response->getJson();
1220 $edge = $jsonArray['edge'];
1221
1222 $document->setInternalId($edge[Edge::ENTRY_ID]);
1223 $document->setRevision($edge[Edge::ENTRY_REV]);
1224
1225 return true;
1226 }
1227
1228
1229 /**
1230 * Update an existing edge in a graph, identified by graph name and edge id
1231 *
1232 * This will update the edge on the server
1233 *
1234 * This will throw if the edge cannot be updated
1235 *
1236 * If policy is set to error (locally or globally through the ConnectionOptions)
1237 * and the passed edge-document has a _rev value set, the database will check
1238 * that the revision of the to-be-replaced document is the same as the one given.<br><br>
1239 *
1240 * @throws Exception
1241 *
1242 * @param mixed $graph - graph name as a string or instance of Graph
1243 * @param mixed $edgeId - edge id as string or number
1244 * @param mixed $label - label for the edge or ''
1245 * @param Edge $document - patch edge-document which contains the attributes and values to be updated
1246 * @param mixed $options optional, array of options (see below)
1247 * <p>
1248 * <li><b>policy</b> - update policy to be used in case of conflict ('error', 'last' or NULL [use default])</li>
1249 * <li><b>keepNull</b> - can be used to instruct ArangoDB to delete existing attributes instead setting their values to null. Defaults to true (keep attributes when set to null)</li>
1250 * <li><b>waitForSync</b> - can be used to force synchronisation of the document update operation to disk even in case that the waitForSync flag had been disabled for the entire collection</li>
1251 * </p>
1252 * @param string $collection - if one uses a graph with more than one vertex collection one must provide the collection
1253 *
1254 * @return bool - always true, will throw if there is an error
1255 * @since 1.2
1256 */
1257 public function updateEdge($graph, $edgeId, $label, Edge $document, array $options = [], $collection = null)
1258 {
1259 if ($graph instanceof Graph) {
1260 $graph = $graph->getKey();
1261 }
1262 $parts = explode('/', $edgeId);
1263 if (count($parts) === 2) {
1264 list($collection, $edgeId) = $parts;
1265 }
1266
1267 if ($collection === null) {
1268 $edgeCollections = $this->getEdgeCollections($graph);
1269 $edgeCollectionsCount = count($edgeCollections);
1270 if ($edgeCollectionsCount !== 1) {
1271 throw new ClientException('A collection must be provided.');
1272 } else if ($edgeCollectionsCount === 1) {
1273 $collection = $edgeCollections[0];
1274 }
1275 }
1276
1277 $options = array_merge([self::OPTION_REVISION => false], $options);
1278
1279 $params = $this->includeOptionsInParams(
1280 $options, [
1281 'waitForSync' => $this->getConnectionOption(ConnectionOptions::OPTION_WAIT_SYNC),
1282 'keepNull' => true,
1283 'policy' => $this->getConnectionOption(ConnectionOptions::OPTION_UPDATE_POLICY),
1284 ]
1285 );
1286
1287 //Include the revision for conditional updates if required
1288 if ($options[self::OPTION_REVISION] === true) {
1289
1290 $revision = $document->getRevision();
1291
1292 if (null !== $revision) {
1293 $params[ConnectionOptions::OPTION_REVISION] = $revision;
1294 }
1295 } elseif ($options[self::OPTION_REVISION]) {
1296 $params[ConnectionOptions::OPTION_REVISION] = $options[self::OPTION_REVISION];
1297 }
1298
1299 if (null !== $label) {
1300 $document->set('$label', $label);
1301 }
1302
1303 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_EDGE, $collection, $edgeId]);
1304 $url = UrlHelper::appendParamsUrl($url, $params);
1305 $result = $this->getConnection()->patch($url, $this->json_encode_wrapper($document->getAll()));
1306 $json = $result->getJson();
1307 $edge = $json['edge'];
1308 $document->setRevision($edge[Edge::ENTRY_REV]);
1309
1310 return true;
1311 }
1312
1313
1314 /**
1315 * Remove a edge from a graph, identified by the graph name and edge id<br><br>
1316 *
1317 * @throws Exception
1318 *
1319 * @param mixed $graph - graph name as a string or instance of Graph
1320 * @param mixed $edgeId - edge id as string or number
1321 * @param mixed $revision - optional revision of the edge to be deleted
1322 * @param mixed $options optional, array of options (see below) or the boolean value for $policy (for compatibility prior to version 1.1 of this method)
1323 * <p>
1324 * <li><b>policy</b> - update policy to be used in case of conflict ('error', 'last' or NULL [use default])</li>
1325 * <li><b>waitForSync</b> - can be used to force synchronisation of the document removal operation to disk even in case that the waitForSync flag had been disabled for the entire collection</li>
1326 * </p>
1327 * @param string $collection - if one uses a graph with more than one vertex collection one must provide the collection
1328 *
1329 * @return bool - always true, will throw if there is an error
1330 * @since 1.2
1331 */
1332 public function removeEdge($graph, $edgeId, $revision = null, array $options = [], $collection = null)
1333 {
1334 if ($graph instanceof Graph) {
1335 $graph = $graph->getKey();
1336 }
1337 $parts = explode('/', $edgeId);
1338 if (count($parts) === 2) {
1339 list($collection, $edgeId) = $parts;
1340 }
1341
1342 if ($collection === null) {
1343 $edgeCollections = $this->getEdgeCollections($graph);
1344 $edgeCollectionsCount = count($edgeCollections);
1345 if ($edgeCollectionsCount !== 1) {
1346 throw new ClientException('A collection must be provided.');
1347 } else if ($edgeCollectionsCount === 1) {
1348 $collection = $edgeCollections[0];
1349 }
1350 }
1351
1352 $params = $this->includeOptionsInParams(
1353 $options, [
1354 'waitForSync' => $this->getConnectionOption(ConnectionOptions::OPTION_WAIT_SYNC),
1355 'keepNull' => true,
1356 'policy' => $this->getConnectionOption(ConnectionOptions::OPTION_DELETE_POLICY),
1357 ]
1358 );
1359 if (null !== $revision) {
1360 $params[ConnectionOptions::OPTION_REVISION] = $revision;
1361 }
1362
1363 $url = UrlHelper::buildUrl(Urls::URL_GRAPH, [$graph, Urls::URLPART_EDGE, $collection, $edgeId]);
1364 $url = UrlHelper::appendParamsUrl($url, $params);
1365 $this->getConnection()->delete($url);
1366
1367 return true;
1368 }
1369
1370 /**
1371 * Clears the GraphHandler's cache
1372 *
1373 * @return $this
1374 */
1375 public function clearCache()
1376 {
1377 $this->cache = null;
1378
1379 return $this;
1380 }
1381
1382 /**
1383 * Checks if caching in enabled
1384 *
1385 * @return boolean
1386 */
1387 public function getCacheEnabled()
1388 {
1389 return $this->cacheEnabled;
1390 }
1391
1392 /**
1393 * @param boolean $useCache
1394 *
1395 * @return $this
1396 */
1397 public function setCacheEnabled($useCache)
1398 {
1399 $this->cacheEnabled = $useCache;
1400
1401 return $this;
1402 }
1403 }
1404