1<?php 2/* 3 * This file is part of the Recursion Context package. 4 * 5 * (c) Sebastian Bergmann <sebastian@phpunit.de> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11namespace SebastianBergmann\RecursionContext; 12 13/** 14 * A context containing previously processed arrays and objects 15 * when recursively processing a value. 16 */ 17final class Context 18{ 19 /** 20 * @var array[] 21 */ 22 private $arrays; 23 24 /** 25 * @var \SplObjectStorage 26 */ 27 private $objects; 28 29 /** 30 * Initialises the context 31 */ 32 public function __construct() 33 { 34 $this->arrays = array(); 35 $this->objects = new \SplObjectStorage; 36 } 37 38 /** 39 * Adds a value to the context. 40 * 41 * @param array|object $value The value to add. 42 * 43 * @return int|string The ID of the stored value, either as a string or integer. 44 * 45 * @throws InvalidArgumentException Thrown if $value is not an array or object 46 */ 47 public function add(&$value) 48 { 49 if (is_array($value)) { 50 return $this->addArray($value); 51 } elseif (is_object($value)) { 52 return $this->addObject($value); 53 } 54 55 throw new InvalidArgumentException( 56 'Only arrays and objects are supported' 57 ); 58 } 59 60 /** 61 * Checks if the given value exists within the context. 62 * 63 * @param array|object $value The value to check. 64 * 65 * @return int|string|false The string or integer ID of the stored value if it has already been seen, or false if the value is not stored. 66 * 67 * @throws InvalidArgumentException Thrown if $value is not an array or object 68 */ 69 public function contains(&$value) 70 { 71 if (is_array($value)) { 72 return $this->containsArray($value); 73 } elseif (is_object($value)) { 74 return $this->containsObject($value); 75 } 76 77 throw new InvalidArgumentException( 78 'Only arrays and objects are supported' 79 ); 80 } 81 82 /** 83 * @param array $array 84 * 85 * @return bool|int 86 */ 87 private function addArray(array &$array) 88 { 89 $key = $this->containsArray($array); 90 91 if ($key !== false) { 92 return $key; 93 } 94 95 $key = count($this->arrays); 96 $this->arrays[] = &$array; 97 98 if (!isset($array[PHP_INT_MAX]) && !isset($array[PHP_INT_MAX - 1])) { 99 $array[] = $key; 100 $array[] = $this->objects; 101 } else { /* cover the improbable case too */ 102 do { 103 $key = random_int(PHP_INT_MIN, PHP_INT_MAX); 104 } while (isset($array[$key])); 105 106 $array[$key] = $key; 107 108 do { 109 $key = random_int(PHP_INT_MIN, PHP_INT_MAX); 110 } while (isset($array[$key])); 111 112 $array[$key] = $this->objects; 113 } 114 115 return $key; 116 } 117 118 /** 119 * @param object $object 120 * 121 * @return string 122 */ 123 private function addObject($object) 124 { 125 if (!$this->objects->contains($object)) { 126 $this->objects->attach($object); 127 } 128 129 return spl_object_hash($object); 130 } 131 132 /** 133 * @param array $array 134 * 135 * @return int|false 136 */ 137 private function containsArray(array &$array) 138 { 139 $end = array_slice($array, -2); 140 141 return isset($end[1]) && $end[1] === $this->objects ? $end[0] : false; 142 } 143 144 /** 145 * @param object $value 146 * 147 * @return string|false 148 */ 149 private function containsObject($value) 150 { 151 if ($this->objects->contains($value)) { 152 return spl_object_hash($value); 153 } 154 155 return false; 156 } 157 158 public function __destruct() 159 { 160 foreach ($this->arrays as &$array) { 161 if (is_array($array)) { 162 array_pop($array); 163 array_pop($array); 164 } 165 } 166 } 167} 168