Completed
Pull Request — master (#49)
by John
02:41
created

RefResolver::getDocument()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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