Completed
Push — master ( b85748...14ef80 )
by John
03:00
created

RefResolver::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 3
eloc 8
nc 4
nop 3
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
                $this->resolveRecursively($value, $document, $uri);
104
            }
105
        } elseif (is_object($current)) {
106
            if (property_exists($current, '$ref')) {
107
                $uri = $current->{'$ref'};
108
                if ('#' === $uri[0]) {
109
                    $current = $this->lookup($uri, $document);
110
                    $this->resolveRecursively($current, $document, $uri);
111
                } else {
112
                    $uriSegs = $this->parseUri($uri);
113
                    $normalizedUri = $this->normalizeUri($uriSegs);
114
                    $externalDocument = $this->loadExternal($normalizedUri);
115
                    $current = $this->lookup($uriSegs['segment'], $externalDocument, $normalizedUri);
116
                    $this->resolveRecursively($current, $externalDocument, $normalizedUri);
117
                }
118
                if (is_object($current)) {
119
                    $current->{'x-ref-id'} = $uri;
120
                }
121
122
                return;
123
            }
124
            foreach ($current as $propertyName => &$propertyValue) {
125
                $this->resolveRecursively($propertyValue, $document, $uri);
126
            }
127
        }
128
    }
129
130
    /**
131
     * @param object|array $current
132
     * @param object|array $parent
133
     *
134
     * @return void
135
     */
136
    private function unresolveRecursively(&$current, &$parent = null)
137
    {
138
        foreach ($current as $key => &$value) {
139
            if ($value !== null && !is_scalar($value)) {
140
                $this->unresolveRecursively($value, $current);
141
            }
142
            if ($key === 'x-ref-id') {
143
                $parent = (object)['$ref' => $value];
144
            }
145
        }
146
    }
147
148
    /**
149
     * @param string $path
150
     * @param object $document
151
     * @param string $uri
152
     *
153
     * @return mixed
154
     * @throws InvalidReferenceException
155
     */
156
    private function lookup($path, $document, $uri = null)
157
    {
158
        $target = $this->lookupRecursively(
159
            explode('/', trim($path, '/#')),
160
            $document
161
        );
162
        if (!$target) {
163
            throw new InvalidReferenceException(
164
                "Target '$path' does not exist'" . ($uri ? " at '$uri''" : '')
165
            );
166
        }
167
168
        return $target;
169
    }
170
171
    /**
172
     * @param array  $segments
173
     * @param object $context
174
     *
175
     * @return mixed
176
     */
177
    private function lookupRecursively(array $segments, $context)
178
    {
179
        $segment = str_replace(['~0', '~1'], ['~', '/'], array_shift($segments));
180
        if (property_exists($context, $segment)) {
181
            if (!count($segments)) {
182
                return $context->$segment;
183
            }
184
185
            return $this->lookupRecursively($segments, $context->$segment);
186
        }
187
188
        return null;
189
    }
190
191
    /**
192
     * @param string $uri
193
     *
194
     * @return object
195
     * @throws ResourceNotReadableException
196
     */
197
    private function loadExternal($uri)
198
    {
199
        $exception = new ResourceNotReadableException("Failed reading '$uri'");
200
201
        set_error_handler(function () use ($exception) {
202
            throw $exception;
203
        });
204
        $response = file_get_contents($uri);
205
        restore_error_handler();
206
207
        if (false === $response) {
208
            throw $exception;
209
        }
210
        if (preg_match('/\b(yml|yaml)\b/', $uri)) {
211
            return $this->yamlParser->parse($response);
212
        }
213
214
        return json_decode($response);
215
    }
216
217
218
    /**
219
     * @param array $uriSegs
220
     *
221
     * @return string
222
     */
223
    private function normalizeUri(array $uriSegs)
224
    {
225
        return
226
            $uriSegs['proto'] . $uriSegs['host']
227
            . rtrim($uriSegs['root'], '/') . '/'
228
            . (!$uriSegs['root'] ? ltrim("$this->directory/", '/') : '')
229
            . $uriSegs['path'];
230
    }
231
232
    /**
233
     * @param string $uri
234
     *
235
     * @return array
236
     */
237
    private function parseUri($uri)
238
    {
239
        $defaults = [
240
            'root'    => '',
241
            'proto'   => '',
242
            'host'    => '',
243
            'path'    => '',
244
            'segment' => ''
245
        ];
246
        $pattern = '@'
247
            . '(?P<proto>[a-z]+\://)?'
248
            . '(?P<host>[0-9a-z\.\@\:]+\.[a-z]+)?'
249
            . '(?P<root>/)?'
250
            . '(?P<path>[^#]*)'
251
            . '(?P<segment>#.*)?'
252
            . '@';
253
254
        preg_match($pattern, $uri, $matches);
255
256
        return array_merge($defaults, array_intersect_key($matches, $defaults));
257
    }
258
}
259