Context.php 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  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 random_int;
  19. use function spl_object_hash;
  20. use SplObjectStorage;
  21. final class Context
  22. {
  23. private array $arrays = [];
  24. private SplObjectStorage $objects;
  25. public function __construct()
  26. {
  27. $this->objects = new SplObjectStorage;
  28. }
  29. /**
  30. * @codeCoverageIgnore
  31. */
  32. public function __destruct()
  33. {
  34. foreach ($this->arrays as &$array) {
  35. if (is_array($array)) {
  36. array_pop($array);
  37. array_pop($array);
  38. }
  39. }
  40. }
  41. /**
  42. * @psalm-template T
  43. *
  44. * @psalm-param T $value
  45. *
  46. * @param-out T $value
  47. */
  48. public function add(object|array &$value): int|string|false
  49. {
  50. if (is_array($value)) {
  51. return $this->addArray($value);
  52. }
  53. return $this->addObject($value);
  54. }
  55. /**
  56. * @psalm-template T
  57. *
  58. * @psalm-param T $value
  59. *
  60. * @param-out T $value
  61. */
  62. public function contains(object|array &$value): int|string|false
  63. {
  64. if (is_array($value)) {
  65. return $this->containsArray($value);
  66. }
  67. return $this->containsObject($value);
  68. }
  69. private function addArray(array &$array): int
  70. {
  71. $key = $this->containsArray($array);
  72. if ($key !== false) {
  73. return $key;
  74. }
  75. $key = count($this->arrays);
  76. $this->arrays[] = &$array;
  77. if (!array_key_exists(PHP_INT_MAX, $array) && !array_key_exists(PHP_INT_MAX - 1, $array)) {
  78. $array[] = $key;
  79. $array[] = $this->objects;
  80. } else {
  81. /* Cover the improbable case, too.
  82. *
  83. * Note that array_slice() (used in containsArray()) will return the
  84. * last two values added, *not necessarily* the highest integer keys
  85. * in the array. Therefore, the order of these writes to $array is
  86. * important, but the actual keys used is not. */
  87. do {
  88. /** @noinspection PhpUnhandledExceptionInspection */
  89. $key = random_int(PHP_INT_MIN, PHP_INT_MAX);
  90. } while (array_key_exists($key, $array));
  91. $array[$key] = $key;
  92. do {
  93. /** @noinspection PhpUnhandledExceptionInspection */
  94. $key = random_int(PHP_INT_MIN, PHP_INT_MAX);
  95. } while (array_key_exists($key, $array));
  96. $array[$key] = $this->objects;
  97. }
  98. return $key;
  99. }
  100. private function addObject(object $object): string
  101. {
  102. if (!$this->objects->contains($object)) {
  103. $this->objects->attach($object);
  104. }
  105. return spl_object_hash($object);
  106. }
  107. private function containsArray(array $array): int|false
  108. {
  109. $end = array_slice($array, -2);
  110. return isset($end[1]) && $end[1] === $this->objects ? $end[0] : false;
  111. }
  112. private function containsObject(object $value): string|false
  113. {
  114. if ($this->objects->contains($value)) {
  115. return spl_object_hash($value);
  116. }
  117. return false;
  118. }
  119. }