Context.php 4.6 KB

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