Completed
Push — master ( 386146...a2d301 )
by John
07:40
created

RefResolver::resolveRecursively()   D

Complexity

Conditions 10
Paths 32

Size

Total Lines 34
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 34
rs 4.8196
cc 10
eloc 23
nc 32
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
2
/*
3
 * This file is part of the KleijnWeb\SwaggerBundle 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\SwaggerBundle\Document;
10
11
use KleijnWeb\SwaggerBundle\Document\Exception\ResourceNotReadableException;
12
use KleijnWeb\SwaggerBundle\Document\Exception\InvalidReferenceException;
13
14
/**
15
 * @author John Kleijn <[email protected]>
16
 */
17
class RefResolver
18
{
19
    /**
20
     * @var object
21
     */
22
    private $document;
23
24
    /**
25
     * @var string
26
     */
27
    private $uri;
28
29
    /**
30
     * @var string
31
     */
32
    private $directory;
33
34
    /**
35
     * @var YamlParser
36
     */
37
    private $yamlParser;
38
39
    /**
40
     * @param object     $document
41
     * @param string     $uri
42
     * @param YamlParser $yamlParser
43
     */
44
    public function __construct($document, $uri, YamlParser $yamlParser = null)
45
    {
46
        if (!is_object($document)) {
47
            throw new \InvalidArgumentException("Document must be object");
48
        }
49
50
        $this->document = $document;
51
        $uriSegs = $this->parseUri($uri);
52
        if (!$uriSegs['proto']) {
53
            $uri = realpath($uri);
54
        }
55
        $this->uri = $uri;
56
        $this->directory = dirname($this->uri);
57
        $this->yamlParser = $yamlParser ?: new YamlParser();
58
    }
59
60
    /**
61
     * @return object
62
     */
63
    public function getDocument()
64
    {
65
        return $this->document;
66
    }
67
68
    /**
69
     * Resolve all references
70
     *
71
     * @return object
72
     */
73
    public function resolve()
74
    {
75
        $this->resolveRecursively($this->document);
76
77
        return $this->document;
78
    }
79
80
    /**
81
     * Revert to original state
82
     */
83
    public function unresolve()
84
    {
85
        $this->unresolveRecursively($this->document, $this->document);
86
    }
87
88
    /**
89
     * @param object|array $composite
90
     * @param object       $document
91
     * @param string       $uri
92
     *
93
     * @throws InvalidReferenceException
94
     * @throws ResourceNotReadableException
95
     */
96
    private function resolveRecursively(&$composite, $document = null, $uri = null)
97
    {
98
        $document = $document ?: $this->document;
99
        $uri = $uri ?: $this->uri;
100
101
        if (is_array($composite)) {
102
            foreach ($composite as &$value) {
103
                if (!is_scalar($value)) {
104
                    $this->resolveRecursively($value, $document, $uri);
105
                }
106
            }
107
        } elseif (is_object($composite)) {
108
            if (property_exists($composite, '$ref')) {
109
                $uri = $composite->{'$ref'};
110
                if ('#' === $uri[0]) {
111
                    $composite = $this->lookup($uri, $document, $uri);
112
                } else {
113
                    $uriSegs = $this->parseUri($uri);
114
                    $normalizedUri = $this->normalizeUri($uriSegs);
115
                    $externalDocument = $this->loadExternal($normalizedUri);
116
                    $composite = $this->lookup($uriSegs['segment'], $externalDocument, $normalizedUri);
117
                    $this->resolveRecursively($composite, $externalDocument, $normalizedUri);
118
                }
119
120
                $composite->id = $uri;
121
                $composite->{'x-ref-id'} = $uri;
122
123
                return;
124
            }
125
            foreach ($composite as $propertyName => &$propertyValue) {
126
                $this->resolveRecursively($propertyValue, $document, $uri);
127
            }
128
        }
129
    }
130
131
    /**
132
     * @param object $current
133
     * @param object $parent
134
     *
135
     * @return void
136
     */
137
    private function unresolveRecursively($current, &$parent = null)
138
    {
139
        foreach ($current as $key => &$value) {
1 ignored issue
show
Bug introduced by
The expression $current of type object|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...
140
            if (is_object($value)) {
141
                $this->unresolveRecursively($value, $current);
142
            }
143
            if ($key === 'x-ref-id') {
144
                $parent = (object)['$ref' => $value];
145
            }
146
        }
147
    }
148
149
    /**
150
     * @param string $path
151
     * @param object $document
152
     * @param string $uri
153
     *
154
     * @return mixed
155
     * @throws InvalidReferenceException
156
     */
157
    private function lookup($path, $document, $uri)
158
    {
159
        $target = $this->lookupRecursively(
160
            explode('/', trim($path, '/#')),
161
            $document
162
        );
163
        if (!$target) {
164
            throw new InvalidReferenceException("Target '$path' does not exist' at '$uri''");
165
        }
166
167
        return $target;
168
    }
169
170
    /**
171
     * @param array  $segments
172
     * @param object $context
173
     *
174
     * @return mixed
175
     */
176
    private function lookupRecursively(array $segments, $context)
177
    {
178
        $segment = array_shift($segments);
179
        if (property_exists($context, $segment)) {
180
            if (!count($segments)) {
181
                return $context->$segment;
182
            }
183
184
            return $this->lookupRecursively($segments, $context->$segment);
185
        }
186
187
        return null;
188
    }
189
190
    /**
191
     * @param string $uri
192
     *
193
     * @return object
194
     * @throws ResourceNotReadableException
195
     */
196
    private function loadExternal($uri)
197
    {
198
        $exception = new ResourceNotReadableException("Failed reading '$uri'");
199
200
        set_error_handler(function () use ($exception) {
201
            throw $exception;
202
        });
203
        $response = file_get_contents($uri);
204
        restore_error_handler();
205
206
        if (false === $response) {
207
            throw $exception;
208
        }
209
        if (preg_match('/\b(yml|yaml)\b/', $uri)) {
210
            return $this->yamlParser->parse($response);
1 ignored issue
show
Bug Best Practice introduced by
The return type of return $this->yamlParser->parse($response); (array|string|stdClass) is incompatible with the return type documented by KleijnWeb\SwaggerBundle\...fResolver::loadExternal of type object.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
211
        }
212
213
        return json_decode($response);
214
    }
215
216
217
    /**
218
     * @param array $uriSegs
219
     *
220
     * @return string
221
     */
222
    private function normalizeUri(array $uriSegs)
223
    {
224
        return
225
            $uriSegs['proto'] . $uriSegs['host']
226
            . rtrim($uriSegs['root'], '/') . '/'
227
            . (!$uriSegs['root'] ? ltrim("$this->directory/", '/') : '')
228
            . $uriSegs['path'];
229
    }
230
231
    /**
232
     * @param string $uri
233
     *
234
     * @return array
235
     */
236
    private function parseUri($uri)
237
    {
238
        $defaults = [
239
            'root'    => '',
240
            'proto'   => '',
241
            'host'    => '',
242
            'path'    => '',
243
            'segment' => ''
244
        ];
245
        $pattern = '@'
246
            . '(?P<proto>[a-z]+\://)?'
247
            . '(?P<host>[0-9a-z\.\@\:]+\.[a-z]+)?'
248
            . '(?P<root>/)?'
249
            . '(?P<path>[^#]*)'
250
            . '(?P<segment>#.*)?'
251
            . '@';
252
253
        preg_match($pattern, $uri, $matches);
254
255
        return array_merge($defaults, array_intersect_key($matches, $defaults));
256
    }
257
}
258