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