StaticRouter::getResponse()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 18
ccs 8
cts 9
cp 0.8889
rs 9.9666
c 0
b 0
f 0
cc 4
nc 4
nop 1
crap 4.0218
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\MockServer;
4
5
use EdmondsCommerce\MockServer\Exception\RouterException;
6
use Symfony\Component\HttpFoundation\BinaryFileResponse;
7
use Symfony\Component\HttpFoundation\Request;
8
use Symfony\Component\HttpFoundation\Response;
9
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
10
use Symfony\Component\Routing\Exception\NoConfigurationException;
11
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
12
use Symfony\Component\Routing\Matcher\UrlMatcher;
13
use Symfony\Component\Routing\RequestContext;
14
use Symfony\Component\Routing\Route;
15
use Symfony\Component\Routing\RouteCollection;
16
17
/**
18
 * Class StaticRouter
19
 *
20
 * @package EdmondsCommerce\MockServer
21
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
22
 */
23
class StaticRouter
24
{
25
    public const NOT_FOUND = 'Not Found';
26
27
    public const STATIC_EXTENSIONS_SUPPORTED = [
28
        '3gp',
29
        'apk',
30
        'avi',
31
        'bmp',
32
        'css',
33
        'csv',
34
        'doc',
35
        'docx',
36
        'flac',
37
        'gif',
38
        'gz',
39
        'gzip',
40
        'htm',
41
        'html',
42
        'ico',
43
        'ics',
44
        'jpe',
45
        'jpeg',
46
        'jpg',
47
        'js',
48
        'json',
49
        'kml',
50
        'kmz',
51
        'm4a',
52
        'mov',
53
        'mp3',
54
        'mp4',
55
        'mpeg',
56
        'mpg',
57
        'odp',
58
        'ods',
59
        'odt',
60
        'oga',
61
        'ogg',
62
        'ogv',
63
        'pdf',
64
        'pdf',
65
        'png',
66
        'pps',
67
        'pptx',
68
        'qt',
69
        'svg',
70
        'swf',
71
        'tar',
72
        'text',
73
        'tif',
74
        'txt',
75
        'wav',
76
        'webm',
77
        'wmv',
78
        'xls',
79
        'xlsx',
80
        'xml',
81
        'xsl',
82
        'xsd',
83
        'zip',
84
    ];
85
86
    /**
87
     * @var RouteCollection
88
     */
89
    private $routes;
90
91
    /**
92
     * @var string
93
     */
94
    private $notFoundResponse;
95
96
    /**
97
     * @var string
98
     */
99
    private $htdocsPath;
100
101
    /**
102
     * @var bool
103
     */
104
    private $verbose = false;
105
106 8
    public function __construct(string $htdocsPath)
107
    {
108 8
        $this->routes = new RouteCollection();
109 8
        $this->setNotFound(static::NOT_FOUND);
110 8
        $this->htdocsPath = $htdocsPath;
111 8
        if (!is_dir($this->htdocsPath)) {
112
            throw new \RuntimeException('htdocs path does not exist: '.$this->htdocsPath);
113
        }
114 8
    }
115
116
    /**
117
     * @param string $response
118
     *
119
     * @return StaticRouter
120
     */
121 8
    public function setNotFound(string $response): StaticRouter
122
    {
123 8
        $this->notFoundResponse = $response;
124
125 8
        return $this;
126
    }
127
128
    /**
129
     * @param string $file
130
     *
131
     * @return StaticRouter
132
     * @throws \RunTimeException
133
     */
134
    public function setNotFoundStatic(string $file): StaticRouter
135
    {
136
        if (!file_exists($file)) {
137
            throw new \RuntimeException('Could not find 404 file: '.$file);
138
        }
139
140
        return $this->setNotFound(file_get_contents($file));
141
    }
142
143
    /**
144
     * @param string      $uri
145
     * @param string      $fileResponse
146
     *
147
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types
148
     *
149
     * @param null|string $contentType
150
     *
151
     * @return StaticRouter
152
     * @throws \Exception
153
     */
154 1
    public function addStaticRoute(string $uri, string $fileResponse, ?string $contentType = null): StaticRouter
155
    {
156 1
        if (!file_exists($fileResponse)) {
157
            throw new \RuntimeException('Could not find file '.$fileResponse);
158
        }
159 1
        $contentType = $contentType ?? mime_content_type($fileResponse);
160 1
        $this->addCallbackRoute(
161 1
            $uri,
162
            function (Request $request) use ($fileResponse, $contentType): Response {
163
164 1
                $response = new Response(file_get_contents($fileResponse));
165 1
                $response->prepare($request);
166 1
                $response->headers->set('Content-Type', $contentType);
167 1
                $response->headers->set('Content-Length', filesize($fileResponse));
168
169 1
                return $response;
170 1
            }
171
        );
172
173 1
        return $this;
174
    }
175
176
    /**
177
     * @param string   $uri
178
     * @param \Closure $closure - must return a Response object
179
     *
180
     * @return $this
181
     * @throws \Exception
182
     */
183 3
    public function addCallbackRoute(string $uri, \Closure $closure)
184
    {
185 3
        $returnType = (string)(new \ReflectionFunction($closure))->getReturnType();
186 3
        if ($returnType !== Response::class) {
187 1
            throw new \InvalidArgumentException(
188 1
                'invalid return type  "'.$returnType
189 1
                .'" - closure must return a "'.Response::class.'" (and type hint for that)'
190
            );
191
        }
192 2
        $this->routes->add(
193 2
            $uri,
194 2
            new Route(
195 2
                $uri,
196 2
                ['_controller' => $closure]
197
            )
198
        );
199
200 2
        return $this;
201
    }
202
203
    /**
204
     * @param string $uri
205
     * @param string $pathToFile
206
     *
207
     * @return StaticRouter
208
     * @throws \Exception
209
     */
210
    public function addFileDownloadRoute(string $uri, string $pathToFile): StaticRouter
211
    {
212
        $this->addCallbackRoute(
213
            $uri,
214
            function (Request $request) use ($pathToFile): Response {
215
                $response = new BinaryFileResponse($pathToFile);
216
                $response->setContentDisposition(
217
                    ResponseHeaderBag::DISPOSITION_ATTACHMENT,
218
                    basename($pathToFile)
219
                );
220
                $response->prepare($request);
221
222
                return $response;
223
            }
224
        );
225
226
        return $this;
227
    }
228
229
    /**
230
     * @param string $uri
231
     * @param string $response
232
     * @param array  $defaults
233
     *
234
     * @return StaticRouter
235
     */
236 3
    public function addRoute(string $uri, string $response, array $defaults = []): StaticRouter
237
    {
238 3
        $this->routes->add($uri, new Route($uri, array_merge(['response' => $response], $defaults)));
239
240 3
        return $this;
241
    }
242
243
    /**
244
     * @SuppressWarnings(PHPMD.StaticAccess)
245
     * @param Request $request
246
     *
247
     * @return array
248
     * @throws \Symfony\Component\Routing\Exception\MethodNotAllowedException
249
     * @throws RouterException
250
     */
251 5
    public function matchRoute(Request $request): array
252
    {
253 5
        $context = new RequestContext();
254 5
        $context->fromRequest($request);
255 5
        $matcher = new UrlMatcher($this->routes, $context);
256
        try {
257 5
            return $matcher->match($request->getRequestUri());
258 1
        } catch (ResourceNotFoundException $e) {
259 1
            throw new RouterException('Could not find route for '.$request->getRequestUri());
260
        }
261
    }
262
263
    /**
264
     * @throws \InvalidArgumentException
265
     */
266 1
    public function respondNotFound(): Response
267
    {
268 1
        return new Response($this->notFoundResponse, 404);
269
    }
270
271
    /**
272
     * @SuppressWarnings(PHPMD.StaticAccess)
273
     * @SuppressWarnings(PHPMD.Superglobals)
274
     * @param string $requestUri
275
     *
276
     * @return Response
277
     * @throws \Exception
278
     * @throws \InvalidArgumentException
279
     */
280 6
    public function run(string $requestUri = null): ?Response
281
    {
282 6
        if (null !== $requestUri) {
283 3
            $_SERVER['REQUEST_URI'] = $requestUri;
284
        }
285 6
        if ('/' === $_SERVER['REQUEST_URI']) {
286 1
            $_SERVER['REQUEST_URI'] = '/index.html';
287
        }
288 6
        $request = Request::createFromGlobals();
289 6
        $this->logRequest($request);
290 6
        if ($this->isStaticAsset($request)) {
291 1
            return null;
292
        }
293 5
        $response = $this->getResponse($request);
294
295 5
        return $response;
296
    }
297
298
    /**
299
     * @param Request $request
300
     *
301
     * Is the request for a static file that exists in the htdocs folder and has a supported extension?
302
     *
303
     * @return bool
304
     */
305 6
    public function isStaticAsset(Request $request): bool
306
    {
307 6
        $uri = $request->getRequestUri();
308 6
        if (file_exists($this->htdocsPath.'/'.$uri)
309 1
            && \in_array(
310 1
                \pathinfo($uri, PATHINFO_EXTENSION),
311 1
                self::STATIC_EXTENSIONS_SUPPORTED,
312 6
                true
313
            )
314
        ) {
315 1
            return true;
316
        }
317
318 5
        return false;
319
    }
320
321
    /**
322
     * @param Request $request
323
     *
324
     * @return Response
325
     * @throws \InvalidArgumentException
326
     */
327 5
    protected function getResponse(Request $request): Response
328
    {
329
        try {
330 5
            $route = $this->matchRoute($request);
331 1
        } catch (NoConfigurationException $e) {
332
            return $this->respondNotFound();
333 1
        } catch (RouterException $exception) {
334 1
            return $this->respondNotFound();
335
        }
336
337
        /**
338
         * The _controller is our callback closure
339
         */
340 4
        if (isset($route['_controller'])) {
341 2
            return $route['_controller']($request);
342
        }
343
344 2
        return new Response($route['response']);
345
    }
346
347
    /**
348
     * @SuppressWarnings(PHPMD.StaticAccess)
349
     * @param Request $request
350
     *
351
     * @throws \Exception
352
     */
353 6
    protected function logRequest(Request $request)
354
    {
355 6
        $requestPath = MockServer::getLogsPath().'/'.MockServer::REQUEST_FILE;
356
        $output      = [
357 6
            'post'   => $request->request->all(),
358 6
            'get'    => $request->query->all(),
359 6
            'server' => $request->server->all(),
360 6
            'files'  => $request->files->all(),
361
        ];
362 6
        $uri         = $request->getRequestUri();
363 6
        if (true === $this->verbose) {
364
            file_put_contents('php://stderr', "\nRequest: $uri\n".var_export($output, true));
365
        }
366
367 6
        if (file_put_contents($requestPath, serialize($request)) === false) {
368
            throw new \RuntimeException('Could not write request output to '.$requestPath);
369
        }
370 6
    }
371
372
    /**
373
     * @param bool $verbose
374
     *
375
     * @return StaticRouter
376
     */
377
    public function setVerbose(bool $verbose): StaticRouter
378
    {
379
        $this->verbose = $verbose;
380
381
        return $this;
382
    }
383
}
384