Completed
Push — master ( c762b0...684d44 )
by John
02:56
created

RefResolver::resolveRecursively()   D

Complexity

Conditions 10
Paths 36

Size

Total Lines 33
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 4.8196
c 0
b 0
f 0
cc 10
eloc 23
nc 36
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types = 1);
2
/*
3
 * This file is part of the KleijnWeb\ApiDescriptions package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace KleijnWeb\ApiDescriptions\Description\Document\Definition\RefResolver;
10
11
use KleijnWeb\ApiDescriptions\Description\Document\Definition\Loader\DefinitionLoader;
12
13
/**
14
 * @author John Kleijn <[email protected]>
15
 */
16
class RefResolver
17
{
18
    /**
19
     * @var object
20
     */
21
    private $definition;
22
23
    /**
24
     * @var string
25
     */
26
    private $uri;
27
28
    /**
29
     * @var string
30
     */
31
    private $directory;
32
33
    /**
34
     * @var DefinitionLoader
35
     */
36
    private $loader;
37
38
    /**
39
     * @param \stdClass        $definition
40
     * @param string           $uri
41
     * @param DefinitionLoader $loader
42
     */
43
    public function __construct(\stdClass $definition, $uri, DefinitionLoader $loader = null)
44
    {
45
        $this->definition = $definition;
46
        $this->uri        = $uri;
47
        $this->directory  = dirname($this->uri);
48
        $this->loader     = $loader ?: new DefinitionLoader();
49
    }
50
51
    /**
52
     * @return \stdClass
53
     */
54
    public function getDefinition(): \stdClass
55
    {
56
        return $this->definition;
57
    }
58
59
    /**
60
     * Resolve all references
61
     *
62
     * @return mixed The whole definition can be a reference to a scalar value
63
     */
64
    public function resolve()
65
    {
66
        $this->resolveRecursively($this->definition);
67
68
        return $this->definition;
69
    }
70
71
    /**
72
     * Revert to original state
73
     *
74
     * @return \stdClass
75
     */
76
    public function unresolve(): \stdClass
77
    {
78
        $this->unresolveRecursively($this->definition);
79
80
        return $this->definition;
81
    }
82
83
    /**
84
     * @param object|array $current
85
     * @param \stdClass    $document
86
     * @param string       $uri
87
     */
88
    private function resolveRecursively(&$current, \stdClass $document = null, string $uri = null)
89
    {
90
        $document = $document ?: $this->definition;
91
        $uri      = $uri ?: $this->uri;
92
93
        if (is_array($current)) {
94
            foreach ($current as &$value) {
95
                $this->resolveRecursively($value, $document, $uri);
96
            }
97
        } elseif (is_object($current)) {
98
            if (property_exists($current, '$ref')) {
99
                $uri = $current->{'$ref'};
100
                if ('#' === $uri[0]) {
101
                    $current = $this->lookup($uri, $document);
102
                    $this->resolveRecursively($current, $document, $uri);
103
                } else {
104
                    $uriSegs          = $this->parseUri($uri);
105
                    $normalizedUri    = $this->normalizeFileUri($uriSegs);
106
                    $externalDocument = $this->loadExternal($normalizedUri);
107
                    $current          = $this->lookup($uriSegs['fragment'], $externalDocument, $normalizedUri);
108
                    $this->resolveRecursively($current, $externalDocument, $normalizedUri);
109
                }
110
                if (is_object($current)) {
111
                    $current->{'x-ref-id'} = $uri;
112
                }
113
114
                return;
115
            }
116
            foreach ($current as $propertyName => &$propertyValue) {
117
                $this->resolveRecursively($propertyValue, $document, $uri);
118
            }
119
        }
120
    }
121
122
    /**
123
     * @param object|array $current
124
     * @param object|array $parent
125
     *
126
     * @return void
127
     */
128
    private function unresolveRecursively(&$current, &$parent = null)
129
    {
130
        foreach ($current as $key => &$value) {
0 ignored issues
show
Bug introduced by
The expression $current of type object|array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
131
            if ($value !== null && !is_scalar($value)) {
132
                $this->unresolveRecursively($value, $current);
133
            }
134
            if ($key === 'x-ref-id') {
135
                $parent = (object)['$ref' => $value];
136
            }
137
        }
138
    }
139
140
    /**
141
     * @param string    $path
142
     * @param \stdClass $document
143
     * @param string    $uri
144
     *
145
     * @return mixed
146
     * @throws InvalidReferenceException
147
     */
148
    private function lookup($path, \stdClass $document, string $uri = null)
149
    {
150
        $target = $this->lookupRecursively(
151
            explode('/', trim($path, '/#')),
152
            $document
153
        );
154
        if (!$target) {
155
            throw new InvalidReferenceException("Target '$path' does not exist'" . ($uri ? " at '$uri''" : ''));
156
        }
157
158
        return $target;
159
    }
160
161
    /**
162
     * @param array     $segments
163
     * @param \stdClass $context
164
     *
165
     * @return mixed
166
     */
167
    private function lookupRecursively(array $segments, \stdClass $context)
168
    {
169
        $segment = str_replace(['~0', '~1'], ['~', '/'], array_shift($segments));
170
        if (property_exists($context, $segment)) {
171
            if (!count($segments)) {
172
                return $context->$segment;
173
            }
174
175
            return $this->lookupRecursively($segments, $context->$segment);
176
        }
177
178
        return null;
179
    }
180
181
    /**
182
     * @param string $fileUrl
183
     *
184
     * @return \stdClass
185
     */
186
    private function loadExternal(string $fileUrl): \stdClass
187
    {
188
        return $this->loader->load($fileUrl);
189
    }
190
191
    /**
192
     * @param array $uriSegments
193
     *
194
     * @return string
195
     */
196
    private function normalizeFileUri(array $uriSegments): string
197
    {
198
        $path  = $uriSegments['path'];
199
        $auth  = !$uriSegments['user'] ? '' : "{$uriSegments['user']}:{$uriSegments['pass']}@";
200
        $query = !$uriSegments['query'] ? '' : "?{$uriSegments['query']}";
201
        $port  = !$uriSegments['port'] ? '' : ":{$uriSegments['port']}";
202
        $host  = !$uriSegments['host'] ? '' : "{$uriSegments['scheme']}://$auth{$uriSegments['host']}{$port}";
203
204
        if (substr($path, 0, 1) !== '/') {
205
            $path = "$this->directory/$path";
206
        }
207
208
        return "{$host}{$path}{$query}";
209
    }
210
211
    /**
212
     * @param string $uri
213
     *
214
     * @return array
215
     */
216
    private function parseUri(string $uri): array
217
    {
218
        $defaults = [
219
            'scheme'   => '',
220
            'host'     => '',
221
            'port'     => '',
222
            'user'     => '',
223
            'pass'     => '',
224
            'path'     => '',
225
            'query'    => '',
226
            'fragment' => ''
227
        ];
228
229
        if (0 === strpos($uri, 'file://')) {
230
            // parse_url botches this up
231
            preg_match('@file://(?P<path>[^#]*)(?P<fragment>#.*)?@', $uri, $matches);
232
233
            return array_merge($defaults, array_intersect_key($matches, $defaults));
234
        }
235
236
        return array_merge($defaults, array_intersect_key(parse_url($uri), $defaults));
237
    }
238
}
239