1 <?php
2 /**
3 * Part of the Joomla Framework Registry Package
4 *
5 * @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE
7 */
8
9 namespace Joomla\Registry;
10
11 use Joomla\Utilities\ArrayHelper;
12
13 /**
14 * Registry class
15 *
16 * @since 1.0
17 */
18 class Registry implements \JsonSerializable, \ArrayAccess, \IteratorAggregate, \Countable
19 {
20 /**
21 * Registry Object
22 *
23 * @var object
24 * @since 1.0
25 */
26 protected $data;
27
28 /**
29 * Flag if the Registry data object has been initialized
30 *
31 * @var boolean
32 * @since 1.5.2
33 */
34 protected $initialized = false;
35
36 /**
37 * Registry instances container.
38 *
39 * @var array
40 * @since 1.0
41 * @deprecated 2.0 Object caching will no longer be supported
42 */
43 protected static $instances = array();
44
45 /**
46 * Path separator
47 *
48 * @var string
49 * @since 1.4.0
50 */
51 public $separator = '.';
52
53 /**
54 * Constructor
55 *
56 * @param mixed $data The data to bind to the new Registry object.
57 *
58 * @since 1.0
59 */
60 public function __construct($data = null)
61 {
62 // Instantiate the internal data object.
63 $this->data = new \stdClass;
64
65 // Optionally load supplied data.
66 if (is_array($data) || is_object($data))
67 {
68 $this->bindData($this->data, $data);
69 }
70 elseif (!empty($data) && is_string($data))
71 {
72 $this->loadString($data);
73 }
74 }
75
76 /**
77 * Magic function to clone the registry object.
78 *
79 * @return Registry
80 *
81 * @since 1.0
82 */
83 public function __clone()
84 {
85 $this->data = unserialize(serialize($this->data));
86 }
87
88 /**
89 * Magic function to render this object as a string using default args of toString method.
90 *
91 * @return string
92 *
93 * @since 1.0
94 */
95 public function __toString()
96 {
97 return $this->toString();
98 }
99
100 /**
101 * Count elements of the data object
102 *
103 * @return integer The custom count as an integer.
104 *
105 * @link http://php.net/manual/en/countable.count.php
106 * @since 1.3.0
107 */
108 public function count()
109 {
110 return count(get_object_vars($this->data));
111 }
112
113 /**
114 * Implementation for the JsonSerializable interface.
115 * Allows us to pass Registry objects to json_encode.
116 *
117 * @return object
118 *
119 * @since 1.0
120 * @note The interface is only present in PHP 5.4 and up.
121 */
122 public function jsonSerialize()
123 {
124 return $this->data;
125 }
126
127 /**
128 * Sets a default value if not already assigned.
129 *
130 * @param string $key The name of the parameter.
131 * @param mixed $default An optional value for the parameter.
132 *
133 * @return mixed The value set, or the default if the value was not previously set (or null).
134 *
135 * @since 1.0
136 */
137 public function def($key, $default = '')
138 {
139 $value = $this->get($key, $default);
140 $this->set($key, $value);
141
142 return $value;
143 }
144
145 /**
146 * Check if a registry path exists.
147 *
148 * @param string $path Registry path (e.g. joomla.content.showauthor)
149 *
150 * @return boolean
151 *
152 * @since 1.0
153 */
154 public function exists($path)
155 {
156 // Return default value if path is empty
157 if (empty($path))
158 {
159 return false;
160 }
161
162 // Explode the registry path into an array
163 $nodes = explode($this->separator, $path);
164
165 // Initialize the current node to be the registry root.
166 $node = $this->data;
167 $found = false;
168
169 // Traverse the registry to find the correct node for the result.
170 foreach ($nodes as $n)
171 {
172 if (is_array($node) && isset($node[$n]))
173 {
174 $node = $node[$n];
175 $found = true;
176 continue;
177 }
178
179 if (!isset($node->$n))
180 {
181 return false;
182 }
183
184 $node = $node->$n;
185 $found = true;
186 }
187
188 return $found;
189 }
190
191 /**
192 * Get a registry value.
193 *
194 * @param string $path Registry path (e.g. joomla.content.showauthor)
195 * @param mixed $default Optional default value, returned if the internal value is null.
196 *
197 * @return mixed Value of entry or null
198 *
199 * @since 1.0
200 */
201 public function get($path, $default = null)
202 {
203 // Return default value if path is empty
204 if (empty($path))
205 {
206 return $default;
207 }
208
209 if (!strpos($path, $this->separator))
210 {
211 return (isset($this->data->$path) && $this->data->$path !== null && $this->data->$path !== '') ? $this->data->$path : $default;
212 }
213
214 // Explode the registry path into an array
215 $nodes = explode($this->separator, trim($path));
216
217 // Initialize the current node to be the registry root.
218 $node = $this->data;
219 $found = false;
220
221 // Traverse the registry to find the correct node for the result.
222 foreach ($nodes as $n)
223 {
224 if (is_array($node) && isset($node[$n]))
225 {
226 $node = $node[$n];
227 $found = true;
228
229 continue;
230 }
231
232 if (!isset($node->$n))
233 {
234 return $default;
235 }
236
237 $node = $node->$n;
238 $found = true;
239 }
240
241 if (!$found || $node === null || $node === '')
242 {
243 return $default;
244 }
245
246 return $node;
247 }
248
249 /**
250 * Returns a reference to a global Registry object, only creating it
251 * if it doesn't already exist.
252 *
253 * This method must be invoked as:
254 * <pre>$registry = Registry::getInstance($id);</pre>
255 *
256 * @param string $id An ID for the registry instance
257 *
258 * @return Registry The Registry object.
259 *
260 * @since 1.0
261 * @deprecated 2.0 Instantiate a new Registry instance instead
262 */
263 public static function getInstance($id)
264 {
265 if (empty(self::$instances[$id]))
266 {
267 self::$instances[$id] = new self;
268 }
269
270 return self::$instances[$id];
271 }
272
273 /**
274 * Gets this object represented as an ArrayIterator.
275 *
276 * This allows the data properties to be accessed via a foreach statement.
277 *
278 * @return \ArrayIterator This object represented as an ArrayIterator.
279 *
280 * @see IteratorAggregate::getIterator()
281 * @since 1.3.0
282 */
283 public function getIterator()
284 {
285 return new \ArrayIterator($this->data);
286 }
287
288 /**
289 * Load an associative array of values into the default namespace
290 *
291 * @param array $array Associative array of value to load
292 * @param boolean $flattened Load from a one-dimensional array
293 * @param string $separator The key separator
294 *
295 * @return Registry Return this object to support chaining.
296 *
297 * @since 1.0
298 */
299 public function loadArray($array, $flattened = false, $separator = null)
300 {
301 if (!$flattened)
302 {
303 $this->bindData($this->data, $array);
304
305 return $this;
306 }
307
308 foreach ($array as $k => $v)
309 {
310 $this->set($k, $v, $separator);
311 }
312
313 return $this;
314 }
315
316 /**
317 * Load the public variables of the object into the default namespace.
318 *
319 * @param object $object The object holding the publics to load
320 *
321 * @return Registry Return this object to support chaining.
322 *
323 * @since 1.0
324 */
325 public function loadObject($object)
326 {
327 $this->bindData($this->data, $object);
328
329 return $this;
330 }
331
332 /**
333 * Load the contents of a file into the registry
334 *
335 * @param string $file Path to file to load
336 * @param string $format Format of the file [optional: defaults to JSON]
337 * @param array $options Options used by the formatter
338 *
339 * @return Registry Return this object to support chaining.
340 *
341 * @since 1.0
342 */
343 public function loadFile($file, $format = 'JSON', $options = array())
344 {
345 $data = file_get_contents($file);
346
347 return $this->loadString($data, $format, $options);
348 }
349
350 /**
351 * Load a string into the registry
352 *
353 * @param string $data String to load into the registry
354 * @param string $format Format of the string
355 * @param array $options Options used by the formatter
356 *
357 * @return Registry Return this object to support chaining.
358 *
359 * @since 1.0
360 */
361 public function loadString($data, $format = 'JSON', $options = array())
362 {
363 // Load a string into the given namespace [or default namespace if not given]
364 $handler = AbstractRegistryFormat::getInstance($format, $options);
365
366 $obj = $handler->stringToObject($data, $options);
367
368 // If the data object has not yet been initialized, direct assign the object
369 if (!$this->initialized)
370 {
371 $this->data = $obj;
372 $this->initialized = true;
373
374 return $this;
375 }
376
377 $this->loadObject($obj);
378
379 return $this;
380 }
381
382 /**
383 * Merge a Registry object into this one
384 *
385 * @param Registry $source Source Registry object to merge.
386 * @param boolean $recursive True to support recursive merge the children values.
387 *
388 * @return Registry Return this object to support chaining.
389 *
390 * @since 1.0
391 */
392 public function merge($source, $recursive = false)
393 {
394 if (!$source instanceof Registry)
395 {
396 return false;
397 }
398
399 $this->bindData($this->data, $source->toArray(), $recursive, false);
400
401 return $this;
402 }
403
404 /**
405 * Method to extract a sub-registry from path
406 *
407 * @param string $path Registry path (e.g. joomla.content.showauthor)
408 *
409 * @return Registry|null Registry object if data is present
410 *
411 * @since 1.2.0
412 */
413 public function extract($path)
414 {
415 $data = $this->get($path);
416
417 if (is_null($data))
418 {
419 return null;
420 }
421
422 return new Registry($data);
423 }
424
425 /**
426 * Checks whether an offset exists in the iterator.
427 *
428 * @param mixed $offset The array offset.
429 *
430 * @return boolean True if the offset exists, false otherwise.
431 *
432 * @since 1.0
433 */
434 public function offsetExists($offset)
435 {
436 return (boolean) ($this->get($offset) !== null);
437 }
438
439 /**
440 * Gets an offset in the iterator.
441 *
442 * @param mixed $offset The array offset.
443 *
444 * @return mixed The array value if it exists, null otherwise.
445 *
446 * @since 1.0
447 */
448 public function offsetGet($offset)
449 {
450 return $this->get($offset);
451 }
452
453 /**
454 * Sets an offset in the iterator.
455 *
456 * @param mixed $offset The array offset.
457 * @param mixed $value The array value.
458 *
459 * @return void
460 *
461 * @since 1.0
462 */
463 public function offsetSet($offset, $value)
464 {
465 $this->set($offset, $value);
466 }
467
468 /**
469 * Unsets an offset in the iterator.
470 *
471 * @param mixed $offset The array offset.
472 *
473 * @return void
474 *
475 * @since 1.0
476 */
477 public function offsetUnset($offset)
478 {
479 $this->set($offset, null);
480 }
481
482 /**
483 * Set a registry value.
484 *
485 * @param string $path Registry Path (e.g. joomla.content.showauthor)
486 * @param mixed $value Value of entry
487 * @param string $separator The key separator
488 *
489 * @return mixed The value of the that has been set.
490 *
491 * @since 1.0
492 */
493 public function set($path, $value, $separator = null)
494 {
495 if (empty($separator))
496 {
497 $separator = $this->separator;
498 }
499
500 /**
501 * Explode the registry path into an array and remove empty
502 * nodes that occur as a result of a double separator. ex: joomla..test
503 * Finally, re-key the array so they are sequential.
504 */
505 $nodes = array_values(array_filter(explode($separator, $path), 'strlen'));
506
507 if (!$nodes)
508 {
509 return null;
510 }
511
512 // Initialize the current node to be the registry root.
513 $node = $this->data;
514
515 // Traverse the registry to find the correct node for the result.
516 for ($i = 0, $n = count($nodes) - 1; $i < $n; $i++)
517 {
518 if (is_object($node))
519 {
520 if (!isset($node->{$nodes[$i]}) && ($i != $n))
521 {
522 $node->{$nodes[$i]} = new \stdClass;
523 }
524
525 // Pass the child as pointer in case it is an object
526 $node = &$node->{$nodes[$i]};
527
528 continue;
529 }
530
531 if (is_array($node))
532 {
533 if (!isset($node[$nodes[$i]]) && ($i != $n))
534 {
535 $node[$nodes[$i]] = new \stdClass;
536 }
537
538 // Pass the child as pointer in case it is an array
539 $node = &$node[$nodes[$i]];
540 }
541 }
542
543 // Get the old value if exists so we can return it
544 switch (true)
545 {
546 case (is_object($node)):
547 $result = $node->{$nodes[$i]} = $value;
548 break;
549
550 case (is_array($node)):
551 $result = $node[$nodes[$i]] = $value;
552 break;
553
554 default:
555 $result = null;
556 break;
557 }
558
559 return $result;
560 }
561
562 /**
563 * Append value to a path in registry
564 *
565 * @param string $path Parent registry Path (e.g. joomla.content.showauthor)
566 * @param mixed $value Value of entry
567 *
568 * @return mixed The value of the that has been set.
569 *
570 * @since 1.4.0
571 */
572 public function append($path, $value)
573 {
574 $result = null;
575
576 /**
577 * Explode the registry path into an array and remove empty
578 * nodes that occur as a result of a double dot. ex: joomla..test
579 * Finally, re-key the array so they are sequential.
580 */
581 $nodes = array_values(array_filter(explode('.', $path), 'strlen'));
582
583 if ($nodes)
584 {
585 // Initialize the current node to be the registry root.
586 $node = $this->data;
587
588 // Traverse the registry to find the correct node for the result.
589 // TODO Create a new private method from part of code below, as it is almost equal to 'set' method
590 for ($i = 0, $n = count($nodes) - 1; $i <= $n; $i++)
591 {
592 if (is_object($node))
593 {
594 if (!isset($node->{$nodes[$i]}) && ($i != $n))
595 {
596 $node->{$nodes[$i]} = new \stdClass;
597 }
598
599 // Pass the child as pointer in case it is an array
600 $node = &$node->{$nodes[$i]};
601 }
602 elseif (is_array($node))
603 {
604 if (!isset($node[$nodes[$i]]) && ($i != $n))
605 {
606 $node[$nodes[$i]] = new \stdClass;
607 }
608
609 // Pass the child as pointer in case it is an array
610 $node = &$node[$nodes[$i]];
611 }
612 }
613
614 if (!is_array($node))
615 // Convert the node to array to make append possible
616 {
617 $node = get_object_vars($node);
618 }
619
620 array_push($node, $value);
621 $result = $value;
622 }
623
624 return $result;
625 }
626
627 /**
628 * Transforms a namespace to an array
629 *
630 * @return array An associative array holding the namespace data
631 *
632 * @since 1.0
633 */
634 public function toArray()
635 {
636 return (array) $this->asArray($this->data);
637 }
638
639 /**
640 * Transforms a namespace to an object
641 *
642 * @return object An an object holding the namespace data
643 *
644 * @since 1.0
645 */
646 public function toObject()
647 {
648 return $this->data;
649 }
650
651 /**
652 * Get a namespace in a given string format
653 *
654 * @param string $format Format to return the string in
655 * @param mixed $options Parameters used by the formatter, see formatters for more info
656 *
657 * @return string Namespace in string format
658 *
659 * @since 1.0
660 */
661 public function toString($format = 'JSON', $options = array())
662 {
663 // Return a namespace in a given format
664 $handler = AbstractRegistryFormat::getInstance($format, $options);
665
666 return $handler->objectToString($this->data, $options);
667 }
668
669 /**
670 * Method to recursively bind data to a parent object.
671 *
672 * @param object $parent The parent object on which to attach the data values.
673 * @param mixed $data An array or object of data to bind to the parent object.
674 * @param boolean $recursive True to support recursive bindData.
675 * @param boolean $allowNull True to allow null values.
676 *
677 * @return void
678 *
679 * @since 1.0
680 */
681 protected function bindData($parent, $data, $recursive = true, $allowNull = true)
682 {
683 // The data object is now initialized
684 $this->initialized = true;
685
686 // Ensure the input data is an array.
687 $data = is_object($data)
688 ? get_object_vars($data)
689 : (array) $data;
690
691 foreach ($data as $k => $v)
692 {
693 if (!$allowNull && !(($v !== null) && ($v !== '')))
694 {
695 continue;
696 }
697
698 if ($recursive && ((is_array($v) && ArrayHelper::isAssociative($v)) || is_object($v)))
699 {
700 if (!isset($parent->$k))
701 {
702 $parent->$k = new \stdClass;
703 }
704
705 $this->bindData($parent->$k, $v);
706
707 continue;
708 }
709
710 $parent->$k = $v;
711 }
712 }
713
714 /**
715 * Method to recursively convert an object of data to an array.
716 *
717 * @param object $data An object of data to return as an array.
718 *
719 * @return array Array representation of the input object.
720 *
721 * @since 1.0
722 */
723 protected function asArray($data)
724 {
725 $array = array();
726
727 if (is_object($data))
728 {
729 $data = get_object_vars($data);
730 }
731
732 foreach ($data as $k => $v)
733 {
734 if (is_object($v) || is_array($v))
735 {
736 $array[$k] = $this->asArray($v);
737
738 continue;
739 }
740
741 $array[$k] = $v;
742 }
743
744 return $array;
745 }
746
747 /**
748 * Dump to one dimension array.
749 *
750 * @param string $separator The key separator.
751 *
752 * @return string[] Dumped array.
753 *
754 * @since 1.3.0
755 */
756 public function flatten($separator = null)
757 {
758 $array = array();
759
760 if (empty($separator))
761 {
762 $separator = $this->separator;
763 }
764
765 $this->toFlatten($separator, $this->data, $array);
766
767 return $array;
768 }
769
770 /**
771 * Method to recursively convert data to one dimension array.
772 *
773 * @param string $separator The key separator.
774 * @param array|object $data Data source of this scope.
775 * @param array &$array The result array, it is pass by reference.
776 * @param string $prefix Last level key prefix.
777 *
778 * @return void
779 *
780 * @since 1.3.0
781 */
782 protected function toFlatten($separator = null, $data = null, &$array = array(), $prefix = '')
783 {
784 $data = (array) $data;
785
786 if (empty($separator))
787 {
788 $separator = $this->separator;
789 }
790
791 foreach ($data as $k => $v)
792 {
793 $key = $prefix ? $prefix . $separator . $k : $k;
794
795 if (is_object($v) || is_array($v))
796 {
797 $this->toFlatten($separator, $v, $array, $key);
798
799 continue;
800 }
801
802 $array[$key] = $v;
803 }
804 }
805 }
806