Passed
Push — master ( 026574...008c1f )
by Chris
02:50
created

Document::__clone()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 9
c 0
b 0
f 0
ccs 0
cts 4
cp 0
rs 9.6666
cc 3
eloc 6
nc 3
nop 0
crap 12
1
<?php declare(strict_types=1);
2
3
namespace DaveRandom\Jom;
4
5
use DaveRandom\Jom\Exceptions\DocumentTreeCreationFailedException;
6
use DaveRandom\Jom\Exceptions\InvalidNodeValueException;
7
use DaveRandom\Jom\Exceptions\InvalidPointerException;
8
use DaveRandom\Jom\Exceptions\InvalidSubjectNodeException;
9
use DaveRandom\Jom\Exceptions\ParseFailureException;
10
use DaveRandom\Jom\Exceptions\PointerReferenceNotFoundException;
11
use DaveRandom\Jom\Exceptions\WriteOperationForbiddenException;
12
use ExceptionalJSON\DecodeErrorException;
13
14
final class Document implements \JsonSerializable
15
{
16
    /** @var Node */
17
    private $rootNode;
18
19
    /**
20
     * @throws PointerReferenceNotFoundException
21
     */
22
    private function evaluatePointerPath(Pointer $pointer, Node $current): Node
23
    {
24
        foreach ($pointer->getPath() as $component) {
25
            if (!($current instanceof VectorNode)) {
26
                throw new PointerReferenceNotFoundException(
27
                    "Pointer '{$pointer}' does not indicate a valid path in the document"
28
                );
29
            }
30
31
            if (!$current->offsetExists($component)) {
32
                throw new PointerReferenceNotFoundException("The referenced property or index '{$component}' does not exist");
33
            }
34
35
            $current = $current->offsetGet($component);
36
        }
37
38
        return $current;
39
    }
40
41
    /**
42
     * @throws PointerReferenceNotFoundException
43
     * @throws InvalidSubjectNodeException
44
     */
45
    private function evaluateRelativePointer(Pointer $pointer, Node $current): Node
46
    {
47
        if ($current->getOwnerDocument() !== $this) {
48
            throw new InvalidSubjectNodeException('Base node belongs to a different document');
49
        }
50
51
        for ($i = $pointer->getRelativeLevels(); $i > 0; $i--) {
52
            $current = $current->getParent();
53
54
            if ($current === null) {
55
                throw new PointerReferenceNotFoundException(
56
                    "Pointer '{$pointer}' does not indicate a valid path in the document relative to the supplied node"
57
                );
58
            }
59
        }
60
61
        return $this->evaluatePointerPath($pointer, $current);
62
    }
63
64
    /**
65
     * @throws InvalidNodeValueException
66
     * @throws InvalidSubjectNodeException
67
     * @throws WriteOperationForbiddenException
68
     */
69
    private function importVectorNode(VectorNode $node): VectorNode
70
    {
71
        if (!($node instanceof ArrayNode || $node instanceof ObjectNode)) {
72
            throw new InvalidSubjectNodeException('Source node is of unknown type ' . \get_class($node));
73
        }
74
75
        $newNode = new $node(null, $this);
76
77
        foreach ($node as $key => $value) {
78
            $newNode[$key] = $this->import($value);
79
        }
80
81
        return $newNode;
82
    }
83
84
    /**
85
     * @throws InvalidSubjectNodeException
86
     */
87
    private function importScalarNode(Node $node): Node
88
    {
89
        if (!($node instanceof BooleanNode || $node instanceof NumberNode || $node instanceof StringNode)) {
90
            throw new InvalidSubjectNodeException('Source node is of unknown type ' . \get_class($node));
91
        }
92
93
        try {
94
            return Node::createFromValue($node->getValue(), $this);
95
        //@codeCoverageIgnoreStart
96
        } catch (\Exception $e) {
97
            throw new \Error('Unexpected ' . \get_class($e) . ": {$e->getMessage()}", 0, $e);
98
        }
99
        //@codeCoverageIgnoreEnd
100
    }
101
102
    private function __construct() { }
103
104
    /**
105
     * @throws DocumentTreeCreationFailedException
106
     */
107
    public function __clone()
108
    {
109
        try {
110
            $this->rootNode = $this->import($this->rootNode);
111
        } catch (InvalidNodeValueException $e) {
112
            throw new DocumentTreeCreationFailedException("Creating document tree failed: {$e->getMessage()}", $e);
113
        //@codeCoverageIgnoreStart
114
        } catch (\Exception $e) {
115
            throw new \Error('Unexpected ' . \get_class($e) . ": {$e->getMessage()}", 0, $e);
116
        }
117
        //@codeCoverageIgnoreEnd
118
    }
119
120
    /**
121
     * @throws DocumentTreeCreationFailedException
122
     * @throws ParseFailureException
123
     */
124
    public static function parse(string $json, int $depthLimit = 512, int $options = 0): Document
125
    {
126
        static $nodeFactory;
127
128
        try {
129
            $data = \ExceptionalJSON\decode($json, false, $depthLimit, $options & ~\JSON_OBJECT_AS_ARRAY);
130
131
            $doc = new self();
132
            $doc->rootNode = ($nodeFactory ?? $nodeFactory = new SafeNodeFactory)
133
                ->createNodeFromValue($data, $doc);
134
135
            return $doc;
136
        } catch (DecodeErrorException $e) {
137
            throw new ParseFailureException("Decoding JSON string failed: {$e->getMessage()}", $e);
138
        } catch (InvalidNodeValueException $e) {
139
            throw new DocumentTreeCreationFailedException("Creating document tree failed: {$e->getMessage()}", $e);
140
        //@codeCoverageIgnoreStart
141
        } catch (\Exception $e) {
142
            throw new \Error('Unexpected ' . \get_class($e) . ": {$e->getMessage()}", 0, $e);
143
        }
144
        //@codeCoverageIgnoreEnd
145
    }
146
147
    /**
148
     * @throws DocumentTreeCreationFailedException
149
     */
150 21
    public static function createFromValue($value): Document
151
    {
152
        try {
153 21
            $doc = new self();
154 21
            $doc->rootNode = Node::createFromValue($value, $doc);
155
156 21
            return $doc;
157
        } catch (InvalidNodeValueException $e) {
158
            throw new DocumentTreeCreationFailedException("Creating document tree failed: {$e->getMessage()}", $e);
159
        //@codeCoverageIgnoreStart
160
        } catch (\Exception $e) {
161
            throw new \Error('Unexpected ' . \get_class($e) . ": {$e->getMessage()}", 0, $e);
162
        }
163
        //@codeCoverageIgnoreEnd
164
    }
165
166
    /**
167
     * @throws DocumentTreeCreationFailedException
168
     */
169
    public static function createFromNode(Node $node): Document
170
    {
171
        try {
172
            $doc = new self();
173
            $doc->rootNode = $doc->import($node);
174
175
            return $doc;
176
        } catch (InvalidNodeValueException $e) {
177
            throw new DocumentTreeCreationFailedException("Creating document tree failed: {$e->getMessage()}", $e);
178
        //@codeCoverageIgnoreStart
179
        } catch (\Exception $e) {
180
            throw new \Error('Unexpected ' . \get_class($e) . ": {$e->getMessage()}", 0, $e);
181
        }
182
        //@codeCoverageIgnoreEnd
183
    }
184
185 20
    public function getRootNode(): ?Node
186
    {
187 20
        return $this->rootNode;
188
    }
189
190
    /**
191
     * @throws InvalidSubjectNodeException
192
     * @throws WriteOperationForbiddenException
193
     * @throws InvalidNodeValueException
194
     * @throws InvalidSubjectNodeException
195
     */
196
    public function import(Node $node): Node
197
    {
198
        if ($node->getOwnerDocument() === $this) {
199
            throw new InvalidSubjectNodeException('Cannot import tne supplied node, already owned by this document');
200
        }
201
202
        if ($node instanceof NullNode) {
203
            return new NullNode($this);
204
        }
205
206
        return $node instanceof VectorNode
207
            ? $this->importVectorNode($node)
208
            : $this->importScalarNode($node);
209
    }
210
211
    /**
212
     * @param Pointer|string $pointer
213
     * @return Node|int|string
214
     * @throws InvalidPointerException
215
     * @throws PointerReferenceNotFoundException
216
     * @throws InvalidSubjectNodeException
217
     */
218
    public function evaluatePointer($pointer, Node $base = null)
219
    {
220
        if (!($pointer instanceof Pointer)) {
221
            $pointer = Pointer::createFromString((string)$pointer);
222
        }
223
224
        if (!$pointer->isRelative()) {
225
            return $this->evaluatePointerPath($pointer, $this->rootNode);
226
        }
227
228
        $target = $this->evaluateRelativePointer($pointer, $base ?? $this->rootNode);
229
230
        return $pointer->isKeyLookup()
231
            ? $target->getKey()
232
            : $target;
233
    }
234
235
    public function jsonSerialize()
236
    {
237
        return $this->rootNode !== null
238
            ? $this->rootNode->jsonSerialize()
239
            : null;
240
    }
241
}
242