1 <?php
2
3 /**
4 * ArangoDB PHP client: single document
5 *
6 * @package triagens\ArangoDb
7 * @author Jan Steemann
8 * @author Frank Mayer
9 * @copyright Copyright 2012, triagens GmbH, Cologne, Germany
10 */
11
12 namespace triagens\ArangoDb;
13
14 /**
15 * Value object representing a single collection-based document
16 *
17 * <br>
18 *
19 * @package triagens\ArangoDb
20 * @since 0.2
21 */
22 class Document
23 {
24 /**
25 * The document id (might be NULL for new documents)
26 *
27 * @var string - document id
28 */
29 protected $_id;
30
31 /**
32 * The document key (might be NULL for new documents)
33 *
34 * @var string - document id
35 */
36 protected $_key;
37
38 /**
39 * The document revision (might be NULL for new documents)
40 *
41 * @var mixed
42 */
43 protected $_rev;
44
45 /**
46 * The document attributes (names/values)
47 *
48 * @var array
49 */
50 protected $_values = [];
51
52 /**
53 * Flag to indicate whether document was changed locally
54 *
55 * @var bool
56 */
57 protected $_changed = false;
58
59 /**
60 * Flag to indicate whether document is a new document (never been saved to the server)
61 *
62 * @var bool
63 */
64 protected $_isNew = true;
65
66 /**
67 * Flag to indicate whether validation of document values should be performed
68 * This can be turned on, but has a performance penalty
69 *
70 * @var bool
71 */
72 protected $_doValidate = false;
73
74 /**
75 * Flag to indicate whether document was changed locally
76 *
77 * @var bool
78 */
79 protected $_hiddenAttributes = [];
80
81 /**
82 * Flag to indicate whether document was changed locally
83 *
84 * @var bool
85 */
86 protected $_ignoreHiddenAttributes = false;
87
88 /**
89 * Document id index
90 */
91 const ENTRY_ID = '_id';
92
93 /**
94 * Document key index
95 */
96 const ENTRY_KEY = '_key';
97
98 /**
99 * Revision id index
100 */
101 const ENTRY_REV = '_rev';
102
103 /**
104 * isNew id index
105 */
106 const ENTRY_ISNEW = '_isNew';
107
108 /**
109 * hidden attribute index
110 */
111 const ENTRY_HIDDENATTRIBUTES = '_hiddenAttributes';
112
113 /**
114 * hidden attribute index
115 */
116 const ENTRY_IGNOREHIDDENATTRIBUTES = '_ignoreHiddenAttributes';
117
118 /**
119 * waitForSync option index
120 */
121 const OPTION_WAIT_FOR_SYNC = 'waitForSync';
122
123 /**
124 * policy option index
125 */
126 const OPTION_POLICY = 'policy';
127
128 /**
129 * keepNull option index
130 */
131 const OPTION_KEEPNULL = 'keepNull';
132
133 /**
134 * Constructs an empty document
135 *
136 * @param array $options - optional, initial $options for document
137 * <p>Options are :<br>
138 * <li>'_hiddenAttributes' - Set an array of hidden attributes for created documents.
139 * <li>'_ignoreHiddenAttributes' - true to show hidden attributes. Defaults to false</li>
140 * <p>
141 *
142 */
143 public function __construct(array $options = null)
144 {
145 if (is_array($options)) {
146 // keeping the non-underscored version for backwards-compatibility
147 if (isset($options['hiddenAttributes'])) {
148 $this->setHiddenAttributes($options['hiddenAttributes']);
149 }
150 if (isset($options[self::ENTRY_HIDDENATTRIBUTES])) {
151 $this->setHiddenAttributes($options[self::ENTRY_HIDDENATTRIBUTES]);
152 }
153 if (isset($options[self::ENTRY_IGNOREHIDDENATTRIBUTES])) {
154 $this->setIgnoreHiddenAttributes($options[self::ENTRY_IGNOREHIDDENATTRIBUTES]);
155 }
156 if (isset($options[self::ENTRY_ISNEW])) {
157 $this->setIsNew($options[self::ENTRY_ISNEW]);
158 }
159 if (isset($options['_validate'])) {
160 $this->_doValidate = $options['_validate'];
161 }
162 }
163 }
164
165 /**
166 * Factory method to construct a new document using the values passed to populate it
167 *
168 * @throws ClientException
169 *
170 * @param array $values - initial values for document
171 * @param array $options - optional, initial options for document
172 *
173 * @return Document|Edge|Graph
174 */
175 public static function createFromArray($values, array $options = [])
176 {
177 $document = new static($options);
178 foreach ($values as $key => $value) {
179 $document->set($key, $value);
180 }
181
182 $document->setChanged(true);
183
184 return $document;
185 }
186
187 /**
188 * Clone a document
189 *
190 * Returns the clone
191 *
192 * @magic
193 *
194 * @return void
195 */
196 public function __clone()
197 {
198 $this->_id = null;
199 $this->_key = null;
200 $this->_rev = null;
201 // do not change the _changed flag here
202 }
203
204 /**
205 * Get a string representation of the document.
206 *
207 * It will not output hidden attributes.
208 *
209 * Returns the document as JSON-encoded string
210 *
211 * @magic
212 *
213 * @return string - JSON-encoded document
214 */
215 public function __toString()
216 {
217 return $this->toJson();
218 }
219
220 /**
221 * Returns the document as JSON-encoded string
222 *
223 * @param array $options - optional, array of options that will be passed to the getAll function
224 * <p>Options are :
225 * <li>'_includeInternals' - true to include the internal attributes. Defaults to false</li>
226 * <li>'_ignoreHiddenAttributes' - true to show hidden attributes. Defaults to false</li>
227 * </p>
228 *
229 * @return string - JSON-encoded document
230 */
231 public function toJson(array $options = [])
232 {
233 return json_encode($this->getAll($options));
234 }
235
236 /**
237 * Returns the document as a serialized string
238 *
239 * @param array $options - optional, array of options that will be passed to the getAll function
240 * <p>Options are :
241 * <li>'_includeInternals' - true to include the internal attributes. Defaults to false</li>
242 * <li>'_ignoreHiddenAttributes' - true to show hidden attributes. Defaults to false</li>
243 * </p>
244 *
245 * @return string - PHP serialized document
246 */
247 public function toSerialized(array $options = [])
248 {
249 return serialize($this->getAll($options));
250 }
251
252 /**
253 * Returns the attributes with the hidden ones removed
254 *
255 * @param array $attributes - attributes array
256 *
257 * @param array $_hiddenAttributes
258 *
259 * @return array - attributes array
260 */
261 public function filterHiddenAttributes($attributes, array $_hiddenAttributes = [])
262 {
263 $hiddenAttributes = $_hiddenAttributes !== null ? $_hiddenAttributes : $this->getHiddenAttributes();
264
265 if (count($hiddenAttributes) > 0) {
266 foreach ($hiddenAttributes as $hiddenAttributeName) {
267 if (isset($attributes[$hiddenAttributeName])) {
268 unset($attributes[$hiddenAttributeName]);
269 }
270 }
271 }
272
273 unset ($attributes[self::ENTRY_HIDDENATTRIBUTES]);
274
275 return $attributes;
276 }
277
278 /**
279 * Set a document attribute
280 *
281 * The key (attribute name) must be a string.
282 * This will validate the value of the attribute and might throw an
283 * exception if the value is invalid.
284 *
285 * @throws ClientException
286 *
287 * @param string $key - attribute name
288 * @param mixed $value - value for attribute
289 *
290 * @return void
291 */
292 public function set($key, $value)
293 {
294 if ($this->_doValidate) {
295 // validate the value passed
296 ValueValidator::validate($value);
297 }
298
299 if ($key[0] === '_') {
300 if ($key === self::ENTRY_ID) {
301 $this->setInternalId($value);
302
303 return;
304 }
305
306 if ($key === self::ENTRY_KEY) {
307 $this->setInternalKey($value);
308
309 return;
310 }
311
312 if ($key === self::ENTRY_REV) {
313 $this->setRevision($value);
314
315 return;
316 }
317
318 if ($key === self::ENTRY_ISNEW) {
319 $this->setIsNew($value);
320
321 return;
322 }
323 }
324
325 if (!$this->_changed) {
326 if (!isset($this->_values[$key]) || $this->_values[$key] !== $value) {
327 // set changed flag
328 $this->_changed = true;
329 }
330 }
331
332 // and store the value
333 $this->_values[$key] = $value;
334 }
335
336 /**
337 * Set a document attribute, magic method
338 *
339 * This is a magic method that allows the object to be used without
340 * declaring all document attributes first.
341 * This function is mapped to set() internally.
342 *
343 * @throws ClientException
344 *
345 * @magic
346 *
347 * @param string $key - attribute name
348 * @param mixed $value - value for attribute
349 *
350 * @return void
351 */
352 public function __set($key, $value)
353 {
354 $this->set($key, $value);
355 }
356
357 /**
358 * Get a document attribute
359 *
360 * @param string $key - name of attribute
361 *
362 * @return mixed - value of attribute, NULL if attribute is not set
363 */
364 public function get($key)
365 {
366 if (isset($this->_values[$key])) {
367 return $this->_values[$key];
368 }
369
370 return null;
371 }
372
373 /**
374 * Get a document attribute, magic method
375 *
376 * This function is mapped to get() internally.
377 *
378 * @magic
379 *
380 * @param string $key - name of attribute
381 *
382 * @return mixed - value of attribute, NULL if attribute is not set
383 */
384 public function __get($key)
385 {
386 return $this->get($key);
387 }
388
389
390 /**
391 * Is triggered by calling isset() or empty() on inaccessible properties.
392 *
393 * @param string $key - name of attribute
394 *
395 * @return boolean returns true or false (set or not set)
396 */
397 public function __isset($key)
398 {
399 if (isset($this->_values[$key])) {
400 return true;
401 }
402
403 return false;
404 }
405
406 /**
407 * Magic method to unset an attribute.
408 * Caution!!! This works only on the first array level.
409 * The preferred method to unset attributes in the database, is to set those to null and do an update() with the option: 'keepNull' => false.
410 *
411 * @magic
412 *
413 * @param $key
414 */
415 public function __unset($key)
416 {
417 unset($this->_values[$key]);
418 }
419
420 /**
421 * Get all document attributes
422 *
423 * @param mixed $options - optional, array of options for the getAll function, or the boolean value for $includeInternals
424 * <p>Options are :
425 * <li>'_includeInternals' - true to include the internal attributes. Defaults to false</li>
426 * <li>'_ignoreHiddenAttributes' - true to show hidden attributes. Defaults to false</li>
427 * </p>
428 *
429 * @return array - array of all document attributes/values
430 */
431 public function getAll(array $options = [])
432 {
433 // This preserves compatibility for the old includeInternals parameter.
434 $includeInternals = false;
435 $ignoreHiddenAttributes = $this->{self::ENTRY_IGNOREHIDDENATTRIBUTES};
436 $_hiddenAttributes = $this->{self::ENTRY_HIDDENATTRIBUTES};
437
438 if (!is_array($options)) {
439 $includeInternals = $options;
440 } else {
441 // keeping the non-underscored version for backwards-compatibility
442 $includeInternals = array_key_exists(
443 'includeInternals',
444 $options
445 ) ? $options['includeInternals'] : $includeInternals;
446
447 $includeInternals = array_key_exists(
448 '_includeInternals',
449 $options
450 ) ? $options['_includeInternals'] : $includeInternals;
451
452 // keeping the non-underscored version for backwards-compatibility
453 $ignoreHiddenAttributes = array_key_exists(
454 'ignoreHiddenAttributes',
455 $options
456 ) ? $options['ignoreHiddenAttributes'] : $ignoreHiddenAttributes;
457
458 $ignoreHiddenAttributes = array_key_exists(
459 self::ENTRY_IGNOREHIDDENATTRIBUTES,
460 $options
461 ) ? $options[self::ENTRY_IGNOREHIDDENATTRIBUTES] : $ignoreHiddenAttributes;
462
463 $_hiddenAttributes = array_key_exists(
464 self::ENTRY_HIDDENATTRIBUTES,
465 $options
466 ) ? $options[self::ENTRY_HIDDENATTRIBUTES] : $_hiddenAttributes;
467 }
468
469 $data = $this->_values;
470 $nonInternals = ['_changed', '_values', self::ENTRY_HIDDENATTRIBUTES];
471
472 if ($includeInternals === true) {
473 foreach ($this as $key => $value) {
474 if ($key[0] === '_' && 0 !== strpos($key, '__') && !in_array($key, $nonInternals, true)) {
475 $data[$key] = $value;
476 }
477 }
478 }
479
480 if ($ignoreHiddenAttributes === false) {
481 $data = $this->filterHiddenAttributes($data, $_hiddenAttributes);
482 }
483
484 if (null !== $this->_key) {
485 $data['_key'] = $this->_key;
486 }
487
488 return $data;
489 }
490
491 /**
492 * Get all document attributes for insertion/update
493 *
494 * @return mixed - associative array of all document attributes/values
495 */
496 public function getAllForInsertUpdate()
497 {
498 $data = [];
499 foreach ($this->_values as $key => $value) {
500 if ($key === '_id' || $key === '_rev') {
501 continue;
502 } else if ($key === '_key' && $value === null) {
503 // key value not yet set
504 continue;
505 }
506 $data[$key] = $value;
507 }
508 if ($this->_key !== null) {
509 $data['_key'] = $this->_key;
510 }
511
512 return $data;
513 }
514
515
516 /**
517 * Get all document attributes, and return an empty object if the documentapped into a DocumentWrapper class
518 *
519 * @param mixed $options - optional, array of options for the getAll function, or the boolean value for $includeInternals
520 * <p>Options are :
521 * <li>'_includeInternals' - true to include the internal attributes. Defaults to false</li>
522 * <li>'_ignoreHiddenAttributes' - true to show hidden attributes. Defaults to false</li>
523 * </p>
524 *
525 * @return mixed - associative array of all document attributes/values, or an empty StdClass if the document
526 * does not have any
527 */
528 public function getAllAsObject(array $options = [])
529 {
530 $result = $this->getAll($options);
531 if (count($result) === 0) {
532 return new \stdClass();
533 }
534
535 return $result;
536 }
537
538 /**
539 * Set the hidden attributes
540 * $cursor
541 *
542 * @param array $attributes - array of attributes
543 *
544 * @return void
545 */
546 public function setHiddenAttributes(array $attributes)
547 {
548 $this->{self::ENTRY_HIDDENATTRIBUTES} = $attributes;
549 }
550
551 /**
552 * Get the hidden attributes
553 *
554 * @return array $attributes - array of attributes
555 */
556 public function getHiddenAttributes()
557 {
558 return $this->{self::ENTRY_HIDDENATTRIBUTES};
559 }
560
561 /**
562 * @return boolean
563 */
564 public function isIgnoreHiddenAttributes()
565 {
566 return $this->{self::ENTRY_IGNOREHIDDENATTRIBUTES};
567 }
568
569 /**
570 * @param boolean $ignoreHiddenAttributes
571 */
572 public function setIgnoreHiddenAttributes($ignoreHiddenAttributes)
573 {
574 $this->{self::ENTRY_IGNOREHIDDENATTRIBUTES} = (bool) $ignoreHiddenAttributes;
575 }
576
577 /**
578 * Set the changed flag
579 *
580 * @param bool $value - change flag
581 *
582 * @return bool
583 */
584 public function setChanged($value)
585 {
586 return $this->_changed = (bool) $value;
587 }
588
589 /**
590 * Get the changed flag
591 *
592 * @return bool - true if document was changed, false otherwise
593 */
594 public function getChanged()
595 {
596 return $this->_changed;
597 }
598
599 /**
600 * Set the isNew flag
601 *
602 * @param bool $isNew - flags if new or existing doc
603 *
604 * @return void
605 */
606 public function setIsNew($isNew)
607 {
608 $this->_isNew = (bool) $isNew;
609 }
610
611 /**
612 * Get the isNew flag
613 *
614 * @return bool $isNew - flags if new or existing doc
615 */
616 public function getIsNew()
617 {
618 return $this->_isNew;
619 }
620
621 /**
622 * Set the internal document id
623 *
624 * This will throw if the id of an existing document gets updated to some other id
625 *
626 * @throws ClientException
627 *
628 * @param string $id - internal document id
629 *
630 * @return void
631 */
632 public function setInternalId($id)
633 {
634 if ($this->_id !== null && $this->_id !== $id) {
635 throw new ClientException('Should not update the id of an existing document');
636 }
637
638
639 if (!preg_match('/^[a-zA-Z0-9_-]{1,64}\/[a-zA-Z0-9_:\.@\-()+,=;$!*\'%]{1,254}$/', $id)) {
640 throw new ClientException('Invalid format for document id');
641 }
642
643 $this->_id = (string) $id;
644 }
645
646 /**
647 * Set the internal document key
648 *
649 * This will throw if the key of an existing document gets updated to some other key
650 *
651 * @throws ClientException
652 *
653 * @param string $key - internal document key
654 *
655 * @return void
656 */
657 public function setInternalKey($key)
658 {
659 if ($this->_key !== null && $this->_key !== $key) {
660 throw new ClientException('Should not update the key of an existing document');
661 }
662
663 if (!preg_match('/^[a-zA-Z0-9_:\.@\-()+,=;$!*\'%]{1,254}$/', $key)) {
664 throw new ClientException('Invalid format for document key');
665 }
666
667 $this->_key = (string) $key;
668 }
669
670 /**
671 * Get the internal document id (if already known)
672 *
673 * Document ids are generated on the server only. Document ids consist of collection id and
674 * document id, in the format collectionId/documentId
675 *
676 * @return string - internal document id, might be NULL if document does not yet have an id
677 */
678 public function getInternalId()
679 {
680 return $this->_id;
681 }
682
683 /**
684 * Get the internal document key (if already known)
685 *
686 * @return string - internal document key, might be NULL if document does not yet have a key
687 */
688 public function getInternalKey()
689 {
690 return $this->_key;
691 }
692
693 /**
694 * Convenience function to get the document handle (if already known) - is an alias to getInternalId()
695 *
696 * Document handles are generated on the server only. Document handles consist of collection id and
697 * document id, in the format collectionId/documentId
698 *
699 * @return string - internal document id, might be NULL if document does not yet have an id
700 */
701 public function getHandle()
702 {
703 return $this->getInternalId();
704 }
705
706 /**
707 * Get the document id (if already known)
708 *
709 * Document ids are generated on the server only. Document ids are numeric but might be
710 * bigger than PHP_INT_MAX. To reliably store a document id elsewhere, a PHP string should be used
711 *
712 * @return mixed - document id, might be NULL if document does not yet have an id
713 */
714 public function getId()
715 {
716 @list(, $documentId) = explode('/', $this->_id, 2);
717
718 return $documentId;
719 }
720
721 /**
722 * Get the document key (if already known).
723 * Alias function for getInternalKey()
724 *
725 * @return mixed - document key, might be NULL if document does not yet have a key
726 */
727 public function getKey()
728 {
729
730 return $this->getInternalKey();
731 }
732
733 /**
734 * Get the collection id (if already known)
735 *
736 * Collection ids are generated on the server only. Collection ids are numeric but might be
737 * bigger than PHP_INT_MAX. To reliably store a collection id elsewhere, a PHP string should be used
738 *
739 * @return mixed - collection id, might be NULL if document does not yet have an id
740 */
741 public function getCollectionId()
742 {
743 @list($collectionId) = explode('/', $this->_id, 2);
744
745 return $collectionId;
746 }
747
748 /**
749 * Set the document revision
750 *
751 * Revision ids are generated on the server only.
752 *
753 * Document ids are strings, even if they look "numeric"
754 * To reliably store a document id elsewhere, a PHP string must be used
755 *
756 * @param mixed $rev - revision id
757 *
758 * @return void
759 */
760 public function setRevision($rev)
761 {
762 $this->_rev = (string) $rev;
763 }
764
765 /**
766 * Get the document revision (if already known)
767 *
768 * @return mixed - revision id, might be NULL if document does not yet have an id
769 */
770 public function getRevision()
771 {
772 return $this->_rev;
773 }
774 }
775