• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

contributte / api-router / 6903388818

17 Nov 2023 11:27AM UTC coverage: 88.71% (+0.5%) from 88.235%
6903388818

push

github

f3l1x
Versions: open 6.1.x

275 of 310 relevant lines covered (88.71%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.43
/src/ApiRoute.php
1
<?php declare(strict_types = 1);
2

3
namespace Contributte\ApiRouter;
4

5
use Nette\Application\Request;
6
use Nette\Http\IRequest;
7
use Nette\Http\UrlScript;
8
use Nette\Routing\Router;
9
use Nette\SmartObject;
10
use Nette\Utils\Strings;
11

12
/**
13
 * @Annotation
14
 * @Target({"CLASS", "METHOD"})
15
 */
16
class ApiRoute extends ApiRouteSpec implements Router
17
{
18

19
        use SmartObject;
20

21
        /** @var callable[] */
22
        public array $onMatch = [];
23

24
        private ?string $presenter = null;
25

26
        /** @var array<string, bool> */
27
        private array $actions = [
28
                'POST' => false,
29
                'GET' => false,
30
                'PUT' => false,
31
                'DELETE' => false,
32
                'OPTIONS' => false,
33
                'PATCH' => false,
34
                'HEAD' => false,
35
        ];
36

37
        /** @var array<string, string> */
38
        private array $defaultActions = [
39
                'POST' => 'create',
40
                'GET' => 'read',
41
                'PUT' => 'update',
42
                'DELETE' => 'delete',
43
                'OPTIONS' => 'options',
44
                'PATCH' => 'patch',
45
                'HEAD' => 'head',
46
        ];
47

48
        /** @var array<string, string> */
49
        private array $formats = [
50
                'json' => 'application/json',
51
                'xml' => 'application/xml',
52
        ];
53

54
        /** @var array<mixed> */
55
        private array $placeholderOrder = [];
56

57
        private bool $autoBasePath = true;
58

59
        /**
60
         * @param array<mixed> $data
61
         */
62
        public function __construct(mixed $path, ?string $presenter = null, array $data = [])
1✔
63
        {
64
                /**
65
                 * Interface for setting route via annotation or directly
66
                 */
67
                if (!is_array($path)) {
1✔
68
                        $data['value'] = $path;
1✔
69
                        $data['presenter'] = $presenter;
1✔
70

71
                        if (!isset($data['methods']) || !$data['methods']) {
1✔
72
                                $this->actions = $this->defaultActions;
1✔
73
                        } else {
74
                                foreach ($data['methods'] as $method => $action) {
1✔
75
                                        if (is_string($method)) {
1✔
76
                                                $this->setAction($action, $method);
1✔
77
                                        } else {
78
                                                $m = $action;
×
79

80
                                                if (isset($this->defaultActions[$m])) {
×
81
                                                        $this->setAction($this->defaultActions[$m], $m);
×
82
                                                }
83
                                        }
84
                                }
85

86
                                unset($data['methods']);
1✔
87
                        }
88
                } else {
89
                        $data = $path;
1✔
90
                }
91

92
                /**
93
                 * Set Path
94
                 */
95
                $this->setPath($data['value']);
1✔
96
                unset($data['value']);
1✔
97

98
                parent::__construct($data);
1✔
99
        }
1✔
100

101
        public function setPresenter(?string $presenter): void
1✔
102
        {
103
                $this->presenter = $presenter;
1✔
104
        }
1✔
105

106
        public function getPresenter(): ?string
107
        {
108
                return $this->presenter;
1✔
109
        }
110

111
        public function setAction(string $action, ?string $method = null): void
1✔
112
        {
113
                if ($method === null) {
1✔
114
                        $method = array_search($action, $this->defaultActions, true);
1✔
115
                }
116

117
                if (!isset($this->defaultActions[$method])) {
1✔
118
                        return;
1✔
119
                }
120

121
                $this->actions[$method] = $action;
1✔
122
        }
1✔
123

124
        /**
125
         * Get all parameters from url mask
126
         *
127
         * @return array<mixed>
128
         */
129
        public function getPlacehodlerParameters(): array
130
        {
131
                if ($this->placeholderOrder) {
1✔
132
                        return array_filter($this->placeholderOrder);
×
133
                }
134

135
                $return = [];
1✔
136

137
                // @phpcs:ignore
138
                preg_replace_callback('/<(\w+)>/', function ($item) use (&$return): void {
1✔
139
                        $return[] = end($item);
1✔
140
                }, $this->path);
1✔
141

142
                return $return;
1✔
143
        }
144

145
        /**
146
         * Get required parameters from url mask
147
         *
148
         * @return array<mixed>
149
         */
150
        public function getRequiredParams(): array
151
        {
152
                $regex = '/\[[^\[]+?\]/';
1✔
153
                $path = $this->getPath();
1✔
154

155
                while (preg_match($regex, $path)) {
1✔
156
                        $path = preg_replace($regex, '', $path);
1✔
157
                }
158

159
                $required = [];
1✔
160

161
                // @phpcs:ignore
162
                preg_replace_callback('/<(\w+)>/', function ($item) use (&$required): void {
1✔
163
                        $required[] = end($item);
1✔
164
                }, $path);
1✔
165

166
                return $required;
1✔
167
        }
168

169
        public function resolveFormat(IRequest $httpRequest): void
1✔
170
        {
171
                if ($this->getFormat()) {
1✔
172
                        return;
1✔
173
                }
174

175
                $header = $httpRequest->getHeader('Accept');
×
176

177
                foreach ($this->formats as $format => $format_full) {
×
178
                        $format_full = Strings::replace($format_full, '/\//', '\/');
×
179

180
                        if (Strings::match($header, '/' . $format_full . '/')) {
×
181
                                $this->setFormat($format);
×
182
                        }
183
                }
184

185
                $this->setFormat('json');
×
186
        }
187

188
        public function getFormatFull(): string
189
        {
190
                return $this->formats[$this->getFormat()];
×
191
        }
192

193
        /**
194
         * @param array<string, string> $methods
195
         */
196
        public function setMethods(array $methods): void
197
        {
198
                foreach ($methods as $method => $action) {
×
199
                        if (is_string($method)) {
×
200
                                $this->setAction($action, $method);
×
201
                        } else {
202
                                $m = $action;
×
203

204
                                if (isset($this->defaultActions[$m])) {
×
205
                                        $this->setAction($this->defaultActions[$m], $m);
×
206
                                }
207
                        }
208
                }
209
        }
210

211
        /**
212
         * @return array<string, string>
213
         */
214
        public function getMethods(): array
215
        {
216
                return array_keys(array_filter($this->actions));
1✔
217
        }
218

219
        public function resolveMethod(IRequest $request): string
1✔
220
        {
221
                if ($request->getHeader('X-HTTP-Method-Override')) {
1✔
222
                        return Strings::upper($request->getHeader('X-HTTP-Method-Override'));
1✔
223
                }
224

225
                if ($request->getQuery('__apiRouteMethod')) {
1✔
226
                        $method = Strings::upper($request->getQuery('__apiRouteMethod'));
1✔
227
                        if (isset($this->actions[$method])) {
1✔
228
                                return $method;
1✔
229
                        }
230
                }
231

232
                return Strings::upper($request->getMethod());
1✔
233
        }
234

235
        public function setAutoBasePath(bool $autoBasePath): void
1✔
236
        {
237
                $this->autoBasePath = $autoBasePath;
1✔
238
        }
1✔
239

240
        /********************************************************************************
241
         *                              Interface IRouter *
242
         ********************************************************************************/
243

244
        /**
245
         * Maps HTTP request to an array.
246
         *
247
         * @return array<mixed>|null
248
         */
249
        public function match(IRequest $httpRequest): ?array
1✔
250
        {
251
                /**
252
                 * ApiRoute can be easily disabled
253
                 */
254
                if ($this->disable) {
1✔
255
                        return null;
×
256
                }
257

258
                $url = $httpRequest->getUrl();
1✔
259

260
                if ($this->autoBasePath) {
1✔
261
                        // Resolve base path
262
                        $basePath = $url->getBasePath();
1✔
263
                        if (strncmp($url->getPath(), $basePath, strlen($basePath)) !== 0) {
1✔
264
                                return null;
×
265
                        }
266

267
                        $path = substr($url->getPath(), strlen($basePath));
1✔
268

269
                        // Ensure start with /
270
                        $path = '/' . ltrim($path, '/');
1✔
271
                } else {
272
                        $path = $url->getPath();
1✔
273
                }
274

275
                // Build path mask
276
                // @phpcs:ignore
277
                $order = &$this->placeholderOrder;
1✔
278
                $parameters = $this->parameters;
1✔
279

280
                // @phpcs:ignore
281
                $mask = preg_replace_callback('/(<(\w+)>)|\[|\]/', function ($item) use (&$order, $parameters) {
1✔
282
                        if ($item[0] === '[' || $item[0] === ']') {
1✔
283
                                if ($item[0] === '[') {
1✔
284
                                        $order[] = null;
1✔
285
                                }
286

287
                                return $item[0];
1✔
288
                        }
289

290
                        [, , $placeholder] = $item;
1✔
291

292
                        $parameter = $parameters[$placeholder] ?? [];
1✔
293

294
                        $regex = $parameter['requirement'] ?? '\w+';
1✔
295
                        $has_default = array_key_exists('default', $parameter);
1✔
296
                        $regex = preg_replace('~\(~', '(?:', $regex);
1✔
297

298
                        if ($has_default) {
1✔
299
                                $order[] = $placeholder;
300

301
                                return sprintf('(%s)?', $regex);
302
                        }
303

304
                        $order[] = $placeholder;
1✔
305

306
                        return sprintf('(%s)', $regex);
1✔
307
                }, $this->path);
1✔
308

309
                $mask = '^' . str_replace(['[', ']'], ['(', ')?'], $mask) . '$';
1✔
310

311
                /**
312
                 * Prepare paths for regex match (escape slashes)
313
                 */
314
                if (!preg_match_all($this->prepareForMatch($mask), $path, $matches)) {
1✔
315
                        return null;
1✔
316
                }
317

318
                /**
319
                 * Did some action to the request method exists?
320
                 */
321
                $this->resolveFormat($httpRequest);
1✔
322
                $method = $this->resolveMethod($httpRequest);
1✔
323
                $action = $this->actions[$method] ?? null;
1✔
324

325
                if (!$action) {
1✔
326
                        return null;
1✔
327
                }
328

329
                /**
330
                 * Basic params
331
                 */
332
                $params = $httpRequest->getQuery();
1✔
333
                $required_params = $this->getRequiredParams();
1✔
334

335
                /**
336
                 * Route mask parameters
337
                 */
338
                array_shift($matches);
1✔
339

340
                foreach ($this->placeholderOrder as $key => $name) {
1✔
341
                        if ($name !== null && isset($matches[$key])) {
1✔
342
                                $params[$name] = reset($matches[$key]) ?: null;
1✔
343

344
                                /**
345
                                 * Required parameters
346
                                 */
347
                                if (!$params[$name] && in_array($name, $required_params, true)) {
1✔
348
                                        return null;
×
349
                                }
350
                        }
351
                }
352

353
                $xs = array_merge([
1✔
354
                        'presenter' => $this->presenter,
1✔
355
                        'action' => $action,
1✔
356
                        'method' => $method,
1✔
357
                        'post' => $httpRequest->getPost(),
1✔
358
                        'files' => $httpRequest->getFiles(),
1✔
359
                        Request::SECURED => $httpRequest->isSecured(),
1✔
360
                ], $params);
361

362
                /**
363
                 * Trigger event - route matches
364
                 */
365
                $this->onMatch($this, $xs);
1✔
366

367
                return $xs;
1✔
368
        }
369

370
        /**
371
         * Constructs absolute URL from array.
372
         *
373
         * @param array<mixed> $params
374
         */
375
        public function constructUrl(array $params, UrlScript $url): ?string
1✔
376
        {
377
                if ($this->presenter !== $params['presenter']) {
1✔
378
                        return null;
1✔
379
                }
380

381
                $base_url = $url->getBaseUrl();
1✔
382

383
                $action = $params['action'];
1✔
384
                unset($params['presenter']);
1✔
385
                unset($params['action']);
1✔
386
                $parameters = $params;
1✔
387
                $path = ltrim($this->getPath(), '/');
1✔
388

389
                if (array_search($action, $this->actions, true) === false) {
1✔
390
                        return null;
×
391
                }
392

393
                foreach ($parameters as $name => $value) {
1✔
394
                        if (strpos($path, '<' . $name . '>') !== false && $value !== null) {
1✔
395
                                $path = str_replace('<' . $name . '>', (string) $value, $path);
1✔
396

397
                                unset($parameters[$name]);
1✔
398
                        }
399
                }
400

401
                $path = preg_replace_callback('/\[.+?\]/', function ($item) {
1✔
402
                        if (strpos(end($item), '<')) {
1✔
403
                                return '';
404
                        }
405

406
                        return end($item);
1✔
407
                }, $path);
1✔
408

409
                /**
410
                 * There are still some required parameters in url mask
411
                 */
412
                if (preg_match('/<\w+>/', $path)) {
1✔
413
                        return null;
×
414
                }
415

416
                $path = str_replace(['[', ']'], '', $path);
1✔
417

418
                $query = http_build_query($parameters);
1✔
419

420
                return $base_url . $path . ($query ? '?' . $query : '');
1✔
421
        }
422

423
        private function prepareForMatch(string $string): string
1✔
424
        {
425
                return sprintf('/%s/', str_replace('/', '\/', $string));
1✔
426
        }
427

428
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc