Completed
Pull Request — master (#52)
by John
03:28 queued 45s
created

RefResolver::parseUri()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 21
rs 9.3142
cc 1
eloc 16
nc 1
nop 1
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 $definition;
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     $definition
41
     * @param string     $uri
42
     * @param YamlParser $yamlParser
43
     */
44
    public function __construct($definition, $uri, YamlParser $yamlParser = null)
45
    {
46
        $this->definition = $definition;
47
        $uriSegs = $this->parseUri($uri);
48
        if (!$uriSegs['proto']) {
49
            $uri = realpath($uri);
50
        }
51
        $this->uri = $uri;
52
        $this->directory = dirname($this->uri);
53
        $this->yamlParser = $yamlParser ?: new YamlParser();
54
    }
55
56
    /**
57
     * @return object
58
     */
59
    public function getDefinition()
60
    {
61
        return $this->definition;
62
    }
63
64
    /**
65
     * Resolve all references
66
     *
67
     * @return object
68
     */
69
    public function resolve()
70
    {
71
        $this->resolveRecursively($this->definition);
72
73
        return $this->definition;
74
    }
75
76
    /**
77
     * Revert to original state
78
     *
79
     * @return object
80
     */
81
    public function unresolve()
82
    {
83
        $this->unresolveRecursively($this->definition);
84
85
        return $this->definition;
86
    }
87
88
    /**
89
     * @param object|array $current
90
     * @param object       $document
91
     * @param string       $uri
92
     *
93
     * @throws InvalidReferenceException
94
     * @throws ResourceNotReadableException
95
     */
96
    private function resolveRecursively(&$current, $document = null, $uri = null)
97
    {
98
        $document = $document ?: $this->definition;
99
        $uri = $uri ?: $this->uri;
100
101
        if (is_array($current)) {
102
            foreach ($current as &$value) {
103
                if ($value !== null && !is_scalar($value)) {
104
                    $this->resolveRecursively($value, $document, $uri);
105
                }
106
            }
107
        } elseif (is_object($current)) {
108
            if (property_exists($current, '$ref')) {
109
                $uri = $current->{'$ref'};
110
                if ('#' === $uri[0]) {
111
                    $current = $this->lookup($uri, $document);
112
                } else {
113
                    $uriSegs = $this->parseUri($uri);
114
                    $normalizedUri = $this->normalizeUri($uriSegs);
115
                    $externalDocument = $this->loadExternal($normalizedUri);
116
                    $current = $this->lookup($uriSegs['segment'], $externalDocument, $normalizedUri);
117
                    $this->resolveRecursively($current, $externalDocument, $normalizedUri);
118
                }
119
                if (is_object($current)) {
120
                    $current->id = $uri;
121
                    $current->{'x-ref-id'} = $uri;
122
                }
123
124
                return;
125
            }
126
            foreach ($current as $propertyName => &$propertyValue) {
127
                $this->resolveRecursively($propertyValue, $document, $uri);
128
            }
129
        }
130
    }
131
132
    /**
133
     * @param object|array $current
134
     * @param object|array $parent
135
     *
136
     * @return void
137
     */
138
    private function unresolveRecursively(&$current, &$parent = null)
139
    {
140
        foreach ($current as $key => &$value) {
141
            if ($value !== null && !is_scalar($value)) {
142
                $this->unresolveRecursively($value, $current);
143
            }
144
            if ($key === 'x-ref-id') {
145
                $parent = (object)['$ref' => $value];
146
            }
147
        }
148
    }
149
150
    /**
151
     * @param string $path
152
     * @param object $document
153
     * @param string $uri
154
     *
155
     * @return mixed
156
     * @throws InvalidReferenceException
157
     */
158
    private function lookup($path, $document, $uri = null)
159
    {
160
        $target = $this->lookupRecursively(
161
            explode('/', trim($path, '/#')),
162
            $document
163
        );
164
        if (!$target) {
165
            throw new InvalidReferenceException(
166
                "Target '$path' does not exist'" . ($uri ? " at '$uri''" : '')
167
            );
168
        }
169
170
        return $target;
171
    }
172
173
    /**
174
     * @param array  $segments
175
     * @param object $context
176
     *
177
     * @return mixed
178
     */
179
    private function lookupRecursively(array $segments, $context)
180
    {
181
        $segment = str_replace(['~0', '~1'], ['~', '/'], array_shift($segments));
182
        if (property_exists($context, $segment)) {
183
            if (!count($segments)) {
184
                return $context->$segment;
185
            }
186
187
            return $this->lookupRecursively($segments, $context->$segment);
188
        }
189
190
        return null;
191
    }
192
193
    /**
194
     * @param string $uri
195
     *
196
     * @return object
197
     * @throws ResourceNotReadableException
198
     */
199
    private function loadExternal($uri)
200
    {
201
        $exception = new ResourceNotReadableException("Failed reading '$uri'");
202
203
        set_error_handler(function () use ($exception) {
204
            throw $exception;
205
        });
206
        $response = file_get_contents($uri);
207
        restore_error_handler();
208
209
        if (false === $response) {
210
            throw $exception;
211
        }
212
        if (preg_match('/\b(yml|yaml)\b/', $uri)) {
213
            return $this->yamlParser->parse($response);
1 ignored issue
show
Bug Best Practice introduced by
The return type of return $this->yamlParser->parse($response); (object|integer|double|string|null|boolean|array) 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...
214
        }
215
216
        return json_decode($response);
217
    }
218
219
220
    /**
221
     * @param array $uriSegs
222
     *
223
     * @return string
224
     */
225
    private function normalizeUri(array $uriSegs)
226
    {
227
        return
228
            $uriSegs['proto'] . $uriSegs['host']
229
            . rtrim($uriSegs['root'], '/') . '/'
230
            . (!$uriSegs['root'] ? ltrim("$this->directory/", '/') : '')
231
            . $uriSegs['path'];
232
    }
233
234
    /**
235
     * @param string $uri
236
     *
237
     * @return array
238
     */
239
    private function parseUri($uri)
240
    {
241
        $defaults = [
242
            'root'    => '',
243
            'proto'   => '',
244
            'host'    => '',
245
            'path'    => '',
246
            'segment' => ''
247
        ];
248
        $pattern = '@'
249
            . '(?P<proto>[a-z]+\://)?'
250
            . '(?P<host>[0-9a-z\.\@\:]+\.[a-z]+)?'
251
            . '(?P<root>/)?'
252
            . '(?P<path>[^#]*)'
253
            . '(?P<segment>#.*)?'
254
            . '@';
255
256
        preg_match($pattern, $uri, $matches);
257
258
        return array_merge($defaults, array_intersect_key($matches, $defaults));
259
    }
260
}
261