1<?php
2/*
3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 */
15
16namespace Alcaeus\MongoDbAdapter;
17
18use MongoDB\BSON;
19use MongoDB\Model;
20
21/**
22 * @internal
23 */
24class TypeConverter
25{
26    /**
27     * Converts a legacy type to the new BSON type
28     *
29     * This method handles type conversion from ext-mongo to ext-mongodb:
30     *  - For all types (MongoId, MongoDate, etc.) it returns the correct BSON
31     *    object instance
32     *  - For arrays and objects it iterates over properties and converts each
33     *    item individually
34     *  - For other types it returns the value unconverted
35     *
36     * @param mixed $value
37     * @return mixed
38     */
39    public static function fromLegacy($value)
40    {
41        switch (true) {
42            case $value instanceof TypeInterface:
43                return $value->toBSONType();
44            case is_array($value):
45            case is_object($value);
46                $result = [];
47
48                foreach ($value as $key => $item) {
49                    $result[$key] = self::fromLegacy($item);
50                }
51
52                return self::ensureCorrectType($result, is_object($value));
53            default:
54                return $value;
55        }
56    }
57
58    /**
59     * Converts a BSON type to the legacy types
60     *
61     * This method handles type conversion from ext-mongodb to ext-mongo:
62     *  - For all instances of BSON\Type it returns an object of the
63     *    corresponding legacy type (MongoId, MongoDate, etc.)
64     *  - For arrays and objects it iterates over properties and converts each
65     *    item individually
66     *  - For other types it returns the value unconverted
67     *
68     * @param mixed $value
69     * @return mixed
70     */
71    public static function toLegacy($value)
72    {
73        switch (true) {
74            case $value instanceof BSON\Type:
75                return self::convertBSONObjectToLegacy($value);
76            case is_array($value):
77            case is_object($value):
78                $result = [];
79
80                foreach ($value as $key => $item) {
81                    $result[$key] = self::toLegacy($item);
82                }
83
84                return $result;
85            default:
86                return $value;
87        }
88    }
89
90    /**
91     * Helper method to find out if an array has numerical indexes
92     *
93     * For performance reason, this method checks the first array index only.
94     * More thorough inspection of the array might be needed.
95     * Note: Returns true for empty arrays to preserve compatibility with empty
96     * lists.
97     *
98     * @param array $array
99     * @return bool
100     */
101    public static function isNumericArray(array $array)
102    {
103        return $array === [] || is_numeric(array_keys($array)[0]);
104    }
105
106    /**
107     * Converter method to convert a BSON object to its legacy type
108     *
109     * @param BSON\Type $value
110     * @return mixed
111     */
112    private static function convertBSONObjectToLegacy(BSON\Type $value)
113    {
114        switch (true) {
115            case $value instanceof BSON\ObjectID:
116                return new \MongoId($value);
117            case $value instanceof BSON\Binary:
118                return new \MongoBinData($value);
119            case $value instanceof BSON\Javascript:
120                return new \MongoCode($value);
121            case $value instanceof BSON\MaxKey:
122                return new \MongoMaxKey();
123            case $value instanceof BSON\MinKey:
124                return new \MongoMinKey();
125            case $value instanceof BSON\Regex:
126                return new \MongoRegex($value);
127            case $value instanceof BSON\Timestamp:
128                return new \MongoTimestamp($value);
129            case $value instanceof BSON\UTCDatetime:
130                return new \MongoDate($value);
131            case $value instanceof Model\BSONDocument:
132            case $value instanceof Model\BSONArray:
133                return array_map(
134                    ['self', 'toLegacy'],
135                    $value->getArrayCopy()
136                );
137            default:
138                return $value;
139        }
140    }
141
142    /**
143     * Converts all arrays with non-numeric keys to stdClass
144     *
145     * @param array $array
146     * @param bool $wasObject
147     * @return array|Model\BSONArray|Model\BSONDocument
148     */
149    private static function ensureCorrectType(array $array, $wasObject = false)
150    {
151        if ($wasObject || ! static::isNumericArray($array)) {
152            return new Model\BSONDocument($array);
153        }
154
155        return $array;
156    }
157}
158