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

RonasIT / laravel-swagger / 27604341952

16 Jun 2026 08:22AM UTC coverage: 99.668% (-0.003%) from 99.671%
27604341952

Pull #203

github

web-flow
Merge 33dc9f5f5 into ead648e99
Pull Request #203: refactor: align SwaggerService/SwaggerSpecValidator with open api 3.0

15 of 15 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

901 of 904 relevant lines covered (99.67%)

22.89 hits per line

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

99.8
/src/Services/SwaggerService.php
1
<?php
2

3
namespace RonasIT\AutoDoc\Services;
4

5
use Illuminate\Container\Container;
6
use Illuminate\Http\Request;
7
use Illuminate\Http\Testing\File;
8
use Illuminate\Support\Arr;
9
use Illuminate\Support\Collection;
10
use Illuminate\Support\Facades\ParallelTesting;
11
use Illuminate\Support\Facades\URL;
12
use Illuminate\Support\Str;
13
use ReflectionClass;
14
use RonasIT\AutoDoc\Contracts\SwaggerDriverContract;
15
use RonasIT\AutoDoc\Exceptions\DocFileNotExistsException;
16
use RonasIT\AutoDoc\Exceptions\EmptyContactEmailException;
17
use RonasIT\AutoDoc\Exceptions\EmptyDocFileException;
18
use RonasIT\AutoDoc\Exceptions\InvalidDriverClassException;
19
use RonasIT\AutoDoc\Exceptions\LegacyConfigException;
20
use RonasIT\AutoDoc\Exceptions\SpecValidation\InvalidSwaggerSpecException;
21
use RonasIT\AutoDoc\Exceptions\SwaggerDriverClassNotFoundException;
22
use RonasIT\AutoDoc\Exceptions\UnsupportedDocumentationViewerException;
23
use RonasIT\AutoDoc\Exceptions\WrongSecurityConfigException;
24
use RonasIT\AutoDoc\Traits\GetDependenciesTrait;
25
use RonasIT\AutoDoc\Validators\SwaggerSpecValidator;
26
use Symfony\Component\HttpFoundation\Response;
27
use Throwable;
28

29
/**
30
 * @property SwaggerDriverContract $driver
31
 */
32
class SwaggerService
33
{
34
    use GetDependenciesTrait;
35

36
    public const string OPEN_API_VERSION = '3.1.0';
37

38
    protected $driver;
39
    protected $openAPIValidator;
40

41
    protected $data;
42
    protected $config;
43
    protected $container;
44
    private $uri;
45
    private $method;
46
    /**
47
     * @var Request
48
     */
49
    private $request;
50
    private $item;
51
    private $security;
52

53
    protected array $ruleToTypeMap = [
54
        'array' => 'object',
55
        'boolean' => 'boolean',
56
        'date' => 'date',
57
        'digits' => 'integer',
58
        'integer' => 'integer',
59
        'numeric' => 'double',
60
        'string' => 'string',
61
        'int' => 'integer',
62
    ];
63

64
    protected $booleanAnnotations = [
65
        'deprecated',
66
    ];
67

68
    public function __construct(Container $container)
69
    {
70
        $this->openAPIValidator = app(SwaggerSpecValidator::class);
107✔
71

72
        $this->initConfig();
107✔
73

74
        $this->setDriver();
102✔
75

76
        if (config('app.env') === 'testing') {
100✔
77
            // client must enter at least `contact.email` to generate a default `info` block
78
            // otherwise an exception will be called
79
            $this->checkEmail();
100✔
80

81
            $this->container = $container;
99✔
82

83
            $this->security = $this->config['security'];
99✔
84

85
            $this->data = $this->driver->getProcessTmpData();
99✔
86

87
            if (empty($this->data)) {
99✔
88
                $this->data = $this->generateEmptyData();
66✔
89

90
                $this->driver->saveProcessTmpData($this->data);
66✔
91
            }
92
        }
93
    }
94

95
    protected function initConfig()
96
    {
97
        $this->config = config('auto-doc');
107✔
98

99
        $version = Arr::get($this->config, 'config_version');
107✔
100

101
        if (empty($version)) {
107✔
102
            throw new LegacyConfigException();
1✔
103
        }
104

105
        $packageConfigs = require __DIR__ . '/../../config/auto-doc.php';
106✔
106

107
        if (version_compare($packageConfigs['config_version'], $version, '>')) {
106✔
108
            throw new LegacyConfigException();
1✔
109
        }
110

111
        $documentationViewer = (string) Arr::get($this->config, 'documentation_viewer');
105✔
112

113
        if (!view()->exists("auto-doc::documentation-{$documentationViewer}")) {
105✔
114
            throw new UnsupportedDocumentationViewerException($documentationViewer);
2✔
115
        }
116

117
        $securityDriver = Arr::get($this->config, 'security');
103✔
118

119
        if ($securityDriver && !Arr::exists(Arr::get($this->config, 'security_drivers'), $securityDriver)) {
103✔
120
            throw new WrongSecurityConfigException();
1✔
121
        }
122
    }
123

124
    protected function setDriver()
125
    {
126
        $driver = $this->config['driver'];
102✔
127
        $className = Arr::get($this->config, "drivers.{$driver}.class");
102✔
128

129
        if (!class_exists($className)) {
102✔
130
            throw new SwaggerDriverClassNotFoundException($className);
1✔
131
        } else {
132
            $this->driver = app($className);
101✔
133
        }
134

135
        if (!$this->driver instanceof SwaggerDriverContract) {
101✔
136
            throw new InvalidDriverClassException($driver);
1✔
137
        }
138
    }
139

140
    protected function generateEmptyData(?string $view = null, array $viewData = [], array $license = []): array
141
    {
142
        if (empty($view) && !empty($this->config['info'])) {
67✔
143
            $view = $this->config['info']['description'];
65✔
144
        }
145

146
        $data = [
67✔
147
            'openapi' => self::OPEN_API_VERSION,
67✔
148
            'servers' => [
67✔
149
                ['url' => URL::query($this->config['basePath'])],
67✔
150
            ],
67✔
151
            'paths' => [],
67✔
152
            'components' => [
67✔
153
                'schemas' => $this->config['definitions'],
67✔
154
            ],
67✔
155
            'info' => $this->prepareInfo($view, $viewData, $license),
67✔
156
        ];
67✔
157

158
        $securitySchemes = $this->generateSecuritySchemes();
67✔
159

160
        if (!empty($securitySchemes)) {
67✔
161
            $data['components']['securitySchemes'] = $securitySchemes;
3✔
162
        }
163

164
        return $data;
67✔
165
    }
166

167
    protected function checkEmail(): void
168
    {
169
        if (!empty($this->config['info']) && !Arr::get($this->config, 'info.contact.email')) {
100✔
170
            throw new EmptyContactEmailException();
1✔
171
        }
172
    }
173

174
    protected function generateSecuritySchemes(): ?array
175
    {
176
        if (empty($this->security)) {
67✔
177
            return null;
64✔
178
        }
179

180
        return [
3✔
181
            $this->security => $this->config['security_drivers'][$this->security],
3✔
182
        ];
3✔
183
    }
184

185
    public function addData(Request $request, $response)
186
    {
187
        $this->request = $request;
29✔
188

189
        $this->prepareItem();
29✔
190

191
        $this->parseRequest();
29✔
192
        $this->parseResponse($response);
29✔
193

194
        $this->driver->saveProcessTmpData($this->data);
29✔
195
    }
196

197
    protected function prepareItem()
198
    {
199
        $this->uri = "/{$this->getUri()}";
29✔
200
        $this->method = strtolower($this->request->getMethod());
29✔
201

202
        if (empty(Arr::get($this->data, "paths.{$this->uri}.{$this->method}"))) {
29✔
203
            $this->data['paths'][$this->uri][$this->method] = [
28✔
204
                'tags' => [],
28✔
205
                'consumes' => [],
28✔
206
                'produces' => [],
28✔
207
                'parameters' => $this->getPathParams(),
28✔
208
                'responses' => [],
28✔
209
                'security' => [],
28✔
210
                'description' => '',
28✔
211
            ];
28✔
212
        }
213

214
        $this->item = &$this->data['paths'][$this->uri][$this->method];
29✔
215
    }
216

217
    protected function getUri()
218
    {
219
        $uri = $this->request->route()->uri();
29✔
220
        $basePath = preg_replace("/^\//", '', $this->config['basePath']);
29✔
221
        $preparedUri = preg_replace("/^{$basePath}/", '', $uri);
29✔
222

223
        return preg_replace("/^\//", '', $preparedUri);
29✔
224
    }
225

226
    protected function getPathParams(): array
227
    {
228
        $params = [];
28✔
229

230
        preg_match_all('/{.*?}/', $this->uri, $params);
28✔
231

232
        $params = Arr::collapse($params);
28✔
233

234
        $result = [];
28✔
235

236
        foreach ($params as $param) {
28✔
237
            $key = preg_replace('/[{}]/', '', $param);
6✔
238

239
            $result[] = [
6✔
240
                'in' => 'path',
6✔
241
                'name' => $key,
6✔
242
                'description' => $this->generatePathDescription($key),
6✔
243
                'required' => true,
6✔
244
                'schema' => [
6✔
245
                    'type' => 'string',
6✔
246
                ],
6✔
247
            ];
6✔
248
        }
249

250
        return $result;
28✔
251
    }
252

253
    protected function generatePathDescription(string $key): string
254
    {
255
        $expression = Arr::get($this->request->route()->wheres, $key);
6✔
256

257
        if (empty($expression)) {
6✔
258
            return '';
6✔
259
        }
260

261
        $exploded = explode('|', $expression);
1✔
262

263
        foreach ($exploded as $value) {
1✔
264
            if (!preg_match('/^[a-zA-Z0-9\.]+$/', $value)) {
1✔
265
                return "regexp: {$expression}";
1✔
266
            }
267
        }
268

269
        return 'in: ' . implode(',', $exploded);
1✔
270
    }
271

272
    protected function parseRequest()
273
    {
274
        $this->saveConsume();
29✔
275
        $this->saveTags();
29✔
276
        $this->saveSecurity();
29✔
277

278
        $concreteRequest = $this->getConcreteRequest();
29✔
279

280
        if (empty($concreteRequest)) {
29✔
281
            $this->item['description'] = '';
3✔
282

283
            return;
3✔
284
        }
285

286
        $annotations = $this->getClassAnnotations($concreteRequest);
26✔
287

288
        $this->markAsDeprecated($annotations);
26✔
289
        $this->saveParameters($concreteRequest, $annotations);
26✔
290
        $this->saveDescription($concreteRequest, $annotations);
26✔
291
    }
292

293
    protected function markAsDeprecated(array $annotations)
294
    {
295
        $this->item['deprecated'] = Arr::get($annotations, 'deprecated', false);
26✔
296
    }
297

298
    protected function saveResponseSchema(?array $content, string $definition): void
299
    {
300
        $schemaProperties = [];
29✔
301
        $schemaType = 'object';
29✔
302

303
        if (!empty($content) && array_is_list($content)) {
29✔
304
            $this->saveListResponseDefinitions($content, $schemaProperties);
18✔
305

306
            $schemaType = 'array';
18✔
307
        } else {
308
            $this->saveObjectResponseDefinitions($content, $schemaProperties, $definition);
11✔
309
        }
310

311
        $this->data['components']['schemas'][$definition] = [
29✔
312
            'type' => $schemaType,
29✔
313
            'properties' => $schemaProperties,
29✔
314
        ];
29✔
315
    }
316

317
    protected function saveListResponseDefinitions(array $content, array &$schemaProperties): void
318
    {
319
        $types = [];
18✔
320

321
        foreach ($content as $value) {
18✔
322
            $type = gettype($value);
18✔
323

324
            if (!in_array($type, $types)) {
18✔
325
                $types[] = $type;
18✔
326
                $schemaProperties['items']['allOf'][]['type'] = $type;
18✔
327
            }
328
        }
329
    }
330

331
    protected function saveObjectResponseDefinitions(array $content, array &$schemaProperties, string $definition): void
332
    {
333
        $properties = Arr::get($this->data, "components.schemas.{$definition}", []);
11✔
334

335
        foreach ($content as $name => $value) {
11✔
336
            $property = Arr::get($properties, "properties.{$name}", []);
10✔
337

338
            if (is_null($value)) {
10✔
339
                $property['nullable'] = true;
2✔
340
            } else {
341
                $property['type'] = gettype($value);
10✔
342
            }
343

344
            $schemaProperties[$name] = $property;
10✔
345
        }
346
    }
347

348
    protected function parseResponse($response)
349
    {
350
        $produceList = $this->data['paths'][$this->uri][$this->method]['produces'];
29✔
351

352
        $produce = $response->headers->get('Content-type');
29✔
353

354
        if (is_null($produce)) {
29✔
355
            $produce = 'text/plain';
1✔
356
        }
357

358
        if (!in_array($produce, $produceList)) {
29✔
359
            $this->item['produces'][] = $produce;
28✔
360
        }
361

362
        $responses = $this->item['responses'];
29✔
363

364
        $responseExampleLimitCount = config('auto-doc.response_example_limit_count');
29✔
365

366
        $content = json_decode($response->getContent(), true) ?? [];
29✔
367

368
        if (!empty($responseExampleLimitCount)) {
29✔
369
            if (!empty($content['data'])) {
29✔
370
                $limitedResponseData = array_slice($content['data'], 0, $responseExampleLimitCount, true);
2✔
371
                $content['data'] = $limitedResponseData;
2✔
372
                $content['to'] = count($limitedResponseData);
2✔
373
                $content['total'] = count($limitedResponseData);
2✔
374
            }
375
        }
376

377
        if (!empty($content['exception'])) {
29✔
378
            $uselessKeys = array_keys(Arr::except($content, ['message']));
1✔
379

380
            $content = Arr::except($content, $uselessKeys);
1✔
381
        }
382

383
        $code = $response->getStatusCode();
29✔
384

385
        if (!in_array($code, $responses)) {
29✔
386
            $this->saveExample(
29✔
387
                $code,
29✔
388
                json_encode($content, JSON_PRETTY_PRINT),
29✔
389
                $produce,
29✔
390
            );
29✔
391
        }
392

393
        $action = Str::ucfirst($this->getActionName($this->uri));
29✔
394
        $definition = "{$this->method}{$action}{$code}ResponseObject";
29✔
395

396
        $this->saveResponseSchema($content, $definition);
29✔
397

398
        if (is_array($this->item['responses'][$code])) {
29✔
399
            $this->item['responses'][$code]['content'][$produce]['schema']['$ref'] = "#/components/schemas/{$definition}";
28✔
400
        }
401
    }
402

403
    protected function saveExample($code, $content, $produce)
404
    {
405
        $description = $this->getResponseDescription($code);
29✔
406
        $availableContentTypes = [
29✔
407
            'application',
29✔
408
            'text',
29✔
409
            'image',
29✔
410
        ];
29✔
411
        $explodedContentType = explode('/', $produce);
29✔
412

413
        if (in_array($explodedContentType[0], $availableContentTypes)) {
29✔
414
            $this->item['responses'][$code] = $this->makeResponseExample($content, $produce, $description);
28✔
415
        } else {
416
            $this->item['responses'][$code] = '*Unavailable for preview*';
1✔
417
        }
418
    }
419

420
    protected function makeResponseExample($content, $mimeType, $description = ''): array
421
    {
422
        $example = match ($mimeType) {
28✔
423
            'application/json' => json_decode($content, true),
25✔
424
            'application/pdf' => base64_encode($content),
1✔
425
            default => $content,
2✔
426
        };
427

428
        return [
28✔
429
            'description' => $description,
28✔
430
            'content' => [
28✔
431
                $mimeType => [
28✔
432
                    'schema' => [
28✔
433
                        'type' => 'object',
28✔
434
                    ],
28✔
435
                    'example' => $example,
28✔
436
                ],
28✔
437
            ],
28✔
438
        ];
28✔
439
    }
440

441
    protected function saveParameters($request, array $annotations)
442
    {
443
        $formRequest = new $request();
26✔
444
        $formRequest->setUserResolver($this->request->getUserResolver());
26✔
445
        $formRequest->setRouteResolver($this->request->getRouteResolver());
26✔
446
        $rules = method_exists($formRequest, 'rules') ? $this->prepareRules($formRequest->rules()) : [];
26✔
447
        $attributes = method_exists($formRequest, 'attributes') ? $formRequest->attributes() : [];
26✔
448

449
        $actionName = $this->getActionName($this->uri);
26✔
450

451
        if (in_array($this->method, ['get', 'delete'])) {
26✔
452
            $this->saveGetRequestParameters($rules, $attributes, $annotations);
20✔
453
        } else {
454
            $this->savePostRequestParameters($actionName, $rules, $attributes, $annotations);
6✔
455
        }
456
    }
457

458
    protected function prepareRules(array $rules): array
459
    {
460
        $preparedRules = [];
25✔
461

462
        foreach ($rules as $field => $rulesField) {
25✔
463
            if (is_array($rulesField)) {
25✔
464
                $rulesField = array_map(function ($rule) {
22✔
465
                    return $this->getRuleAsString($rule);
22✔
466
                }, $rulesField);
22✔
467

468
                $preparedRules[$field] = implode('|', $rulesField);
22✔
469
            } else {
470
                $preparedRules[$field] = $this->getRuleAsString($rulesField);
25✔
471
            }
472
        }
473

474
        return $preparedRules;
25✔
475
    }
476

477
    protected function getRuleAsString($rule): string
478
    {
479
        if (is_object($rule)) {
25✔
480
            if (method_exists($rule, '__toString')) {
22✔
481
                return $rule->__toString();
22✔
482
            }
483

484
            $shortName = Str::afterLast(get_class($rule), '\\');
22✔
485

486
            $ruleName = preg_replace('/Rule$/', '', $shortName);
22✔
487

488
            return Str::snake($ruleName);
22✔
489
        }
490

491
        return $rule;
25✔
492
    }
493

494
    protected function saveGetRequestParameters($validation, array $attributes, array $annotations)
495
    {
496
        foreach ($validation as $parameter => $rules) {
20✔
497
            if (Arr::exists($validation, "{$parameter}.*")) {
19✔
498
                continue;
1✔
499
            }
500

501
            $rules = collect(explode('|', $rules));
19✔
502

503
            if ($this->isArrayItemParameter($parameter, $rules)) {
19✔
504
                $this->saveListParameters(Str::remove('.*', $parameter), $rules, $attributes, $annotations);
1✔
505
            } else {
506
                $this->saveQueryParameter($parameter, $rules, $attributes, $annotations);
19✔
507
            }
508
        }
509
    }
510

511
    protected function isArrayItemParameter(string $parameter, Collection $rules): bool
512
    {
513
        return Str::endsWith($parameter, '.*')
19✔
514
            && $rules->contains(fn ($rule) => Str::startsWith($rule, 'in:'));
19✔
515
    }
516

517
    protected function saveListParameters(string $parameter, Collection $rules, array $attributes, array $annotations): void
518
    {
519
        $inRule = $rules->first(fn ($rule) => Str::startsWith($rule, 'in:'));
1✔
520
        $availableValues = Str::after($inRule, 'in:');
1✔
521
        $availableValues = explode(',', $availableValues);
1✔
522

523
        $filteredRules = $rules->reject(fn ($rule) => $rule === 'required')->values();
1✔
524

525
        foreach ($availableValues as $value) {
1✔
526
            $this->saveQueryParameter("{$parameter}[]", $filteredRules, $attributes, $annotations, $value);
1✔
527
        }
528
    }
529

530
    protected function saveQueryParameter(string $parameter, Collection $rules, array $attributes, array $annotations, ?string $example = null): void
531
    {
532
        $existedParameter = Arr::first(
19✔
533
            $this->item['parameters'],
19✔
534
            fn ($existedParameter) => $existedParameter['name'] === $parameter && Arr::get($existedParameter, 'example') === $example,
19✔
535
        );
19✔
536

537
        if (!empty($existedParameter)) {
19✔
538
            return;
1✔
539
        }
540

541
        $parameterDefinition = [
18✔
542
            'in' => 'query',
18✔
543
            'name' => $parameter,
18✔
544
            'description' => $this->generateDescription($parameter, $rules->all(), $attributes, $annotations),
18✔
545
            'schema' => [
18✔
546
                'type' => $this->getParameterType($rules->all()),
18✔
547
            ],
18✔
548
        ];
18✔
549

550
        if ($rules->contains('required')) {
18✔
551
            $parameterDefinition['required'] = true;
18✔
552
        }
553

554
        if (!is_null($example)) {
18✔
555
            $parameterDefinition['example'] = $example;
1✔
556
        }
557

558
        $this->item['parameters'][] = $parameterDefinition;
18✔
559
    }
560

561
    protected function savePostRequestParameters($actionName, $rules, array $attributes, array $annotations)
562
    {
563
        if ($this->requestHasMoreProperties($actionName)) {
6✔
564
            if ($this->requestHasBody()) {
6✔
565
                $type = $this->request->header('Content-Type', 'application/json');
6✔
566

567
                $this->item['requestBody'] = [
6✔
568
                    'content' => [
6✔
569
                        $type => [
6✔
570
                            'schema' => [
6✔
571
                                '$ref' => "#/components/schemas/{$actionName}Object",
6✔
572
                            ],
6✔
573
                        ],
6✔
574
                    ],
6✔
575
                    'description' => '',
6✔
576
                    'required' => true,
6✔
577
                ];
6✔
578
            }
579

580
            $this->saveDefinitions($actionName, $rules, $attributes, $annotations);
6✔
581
        }
582
    }
583

584
    protected function saveDefinitions($objectName, $rules, $attributes, array $annotations)
585
    {
586
        $data = [
6✔
587
            'type' => 'object',
6✔
588
            'properties' => [],
6✔
589
        ];
6✔
590

591
        foreach ($rules as $parameter => $rule) {
6✔
592
            $rulesArray = (is_array($rule)) ? $rule : explode('|', $rule);
6✔
593
            $parameterType = $this->getParameterType($rulesArray);
6✔
594
            $this->saveParameterType($data, $parameter, $parameterType);
6✔
595

596
            $uselessRules = $this->ruleToTypeMap;
6✔
597
            $uselessRules['required'] = 'required';
6✔
598

599
            if (in_array('required', $rulesArray)) {
6✔
600
                $data['required'][] = $parameter;
6✔
601
            }
602

603
            $rulesArray = array_flip(array_diff_key(array_flip($rulesArray), $uselessRules));
6✔
604

605
            $data['properties'][$parameter]['description'] = $this->generateDescription($parameter, $rulesArray, $attributes, $annotations);
6✔
606
        }
607

608
        $data['example'] = $this->generateExample($data['properties']);
6✔
609
        $this->data['components']['schemas']["{$objectName}Object"] = $data;
6✔
610
    }
611

612
    protected function getParameterType(array $validation): string
613
    {
614
        $validationRules = $this->ruleToTypeMap;
24✔
615
        $validationRules['email'] = 'string';
24✔
616

617
        $parameterType = 'string';
24✔
618

619
        foreach ($validation as $item) {
24✔
620
            if (in_array($item, array_keys($validationRules))) {
24✔
621
                return $validationRules[$item];
23✔
622
            }
623
        }
624

625
        return $parameterType;
22✔
626
    }
627

628
    protected function saveParameterType(&$data, $parameter, $parameterType)
629
    {
630
        $data['properties'][$parameter] = [
6✔
631
            'type' => $parameterType,
6✔
632
        ];
6✔
633
    }
634

635
    protected function generateDescription(string $parameter, array $rules, array $attributes, array $annotations): string
636
    {
637
        $description = Arr::get($annotations, $parameter);
24✔
638

639
        if (empty($description)) {
24✔
640
            $description = Arr::get($attributes, $parameter, implode(', ', $rules));
24✔
641
        }
642

643
        return $description;
24✔
644
    }
645

646
    protected function requestHasMoreProperties($actionName): bool
647
    {
648
        $requestParametersCount = count($this->request->all());
6✔
649

650
        $properties = Arr::get($this->data, "components.schemas.{$actionName}Object.properties", []);
6✔
651
        $objectParametersCount = count($properties);
6✔
652

653
        return $requestParametersCount > $objectParametersCount;
6✔
654
    }
655

656
    protected function requestHasBody(): bool
657
    {
658
        $parameters = $this->data['paths'][$this->uri][$this->method]['parameters'];
6✔
659

660
        $bodyParamExisted = Arr::where($parameters, function ($value) {
6✔
661
            return $value['name'] === 'body';
1✔
662
        });
6✔
663

664
        return empty($bodyParamExisted);
6✔
665
    }
666

667
    public function getConcreteRequest()
668
    {
669
        $controller = $this->request->route()->getActionName();
29✔
670

671
        if ($controller === 'Closure') {
29✔
672
            return null;
1✔
673
        }
674

675
        $explodedController = explode('@', $controller);
28✔
676

677
        $class = $explodedController[0];
28✔
678
        $method = Arr::get($explodedController, 1, '__invoke');
28✔
679

680
        if (!method_exists($class, $method)) {
28✔
681
            return null;
1✔
682
        }
683

684
        $parameters = $this->resolveClassMethodDependencies(
27✔
685
            app($class),
27✔
686
            $method,
27✔
687
        );
27✔
688

689
        return Arr::first($parameters, function ($key) {
27✔
690
            return preg_match('/Request/', $key);
27✔
691
        });
27✔
692
    }
693

694
    public function saveConsume()
695
    {
696
        $consumeList = $this->data['paths'][$this->uri][$this->method]['consumes'];
29✔
697
        $consume = $this->request->header('Content-Type');
29✔
698

699
        if (!empty($consume) && !in_array($consume, $consumeList)) {
29✔
700
            $this->item['consumes'][] = $consume;
18✔
701
        }
702
    }
703

704
    public function saveTags()
705
    {
706
        $globalPrefix = config('auto-doc.global_prefix');
29✔
707
        $globalPrefix = Str::after($globalPrefix, '/');
29✔
708

709
        $explodedUri = explode('/', $this->uri);
29✔
710
        $explodedUri = array_filter($explodedUri);
29✔
711

712
        $tag = array_shift($explodedUri);
29✔
713

714
        if ($globalPrefix === $tag) {
29✔
715
            $tag = array_shift($explodedUri);
2✔
716
        }
717

718
        $this->item['tags'] = [$tag];
29✔
719
    }
720

721
    public function saveDescription($request, array $annotations)
722
    {
723
        $this->item['summary'] = $this->getSummary($request, $annotations);
26✔
724

725
        $description = Arr::get($annotations, 'description');
26✔
726

727
        if (!empty($description)) {
26✔
728
            $this->item['description'] = $description;
1✔
729
        }
730
    }
731

732
    protected function saveSecurity()
733
    {
734
        if ($this->requestSupportAuth()) {
29✔
735
            $this->addSecurityToOperation();
5✔
736
        }
737
    }
738

739
    protected function addSecurityToOperation()
740
    {
741
        $security = &$this->data['paths'][$this->uri][$this->method]['security'];
5✔
742

743
        if (empty($security)) {
5✔
744
            $security[] = [
5✔
745
                "{$this->security}" => [],
5✔
746
            ];
5✔
747
        }
748
    }
749

750
    protected function getSummary($request, array $annotations)
751
    {
752
        $summary = Arr::get($annotations, 'summary');
26✔
753

754
        if (empty($summary)) {
26✔
755
            $summary = $this->parseRequestName($request);
25✔
756
        }
757

758
        return $summary;
26✔
759
    }
760

761
    protected function requestSupportAuth(): bool
762
    {
763
        $security = Arr::get($this->config, 'security');
29✔
764
        $securityDriver = Arr::get($this->config, "security_drivers.{$security}");
29✔
765

766
        if (Arr::get($securityDriver, 'type') === 'apiKey') {
29✔
767
            $securityToken = match ($securityDriver['in']) {
3✔
768
                'header' => $this->request->header($securityDriver['name']),
1✔
769
                'query' => $this->request->query($securityDriver['name']),
1✔
770
                'cookie' => $this->request->cookie($securityDriver['name']),
1✔
771
            };
772

773
            return !empty($securityToken);
3✔
774
        }
775

776
        return $this->request->hasHeader('authorization');
26✔
777
    }
778

779
    protected function parseRequestName($request)
780
    {
781
        $explodedRequest = explode('\\', $request);
25✔
782
        $requestName = array_pop($explodedRequest);
25✔
783
        $summaryName = str_replace('Request', '', $requestName);
25✔
784

785
        $underscoreRequestName = $this->camelCaseToUnderScore($summaryName);
25✔
786

787
        return preg_replace('/[_]/', ' ', $underscoreRequestName);
25✔
788
    }
789

790
    protected function getResponseDescription($code)
791
    {
792
        $defaultDescription = Response::$statusTexts[$code];
29✔
793

794
        $request = $this->getConcreteRequest();
29✔
795

796
        if (empty($request)) {
29✔
797
            return $defaultDescription;
3✔
798
        }
799

800
        $annotations = $this->getClassAnnotations($request);
26✔
801

802
        $localDescription = Arr::get($annotations, "_{$code}");
26✔
803

804
        if (!empty($localDescription)) {
26✔
805
            return $localDescription;
1✔
806
        }
807

808
        return Arr::get($this->config, "defaults.code-descriptions.{$code}", $defaultDescription);
25✔
809
    }
810

811
    protected function getActionName($uri): string
812
    {
813
        $action = preg_replace('[\/]', '', $uri);
29✔
814

815
        return Str::camel($action);
29✔
816
    }
817

818
    public function saveProductionData()
819
    {
820
        if (ParallelTesting::token()) {
4✔
821
            $this->driver->appendProcessDataToTmpFile(function (?array $sharedTmpData) {
2✔
822
                $resultDocContent = (empty($sharedTmpData))
2✔
823
                    ? $this->generateEmptyData($this->config['info']['description'])
1✔
824
                    : $sharedTmpData;
1✔
825

826
                $this->mergeOpenAPIDocs($resultDocContent, $this->data);
2✔
827

828
                return $resultDocContent;
2✔
829
            });
2✔
830
        }
831

832
        $this->driver->saveData();
4✔
833
    }
834

835
    public function getDocFileContent()
836
    {
837
        try {
838
            $documentation = $this->driver->getDocumentation();
50✔
839

840
            $this->openAPIValidator->validate($documentation);
48✔
841
        } catch (Throwable $exception) {
42✔
842
            return $this->generateEmptyData($this->config['defaults']['error'], [
42✔
843
                'message' => $exception->getMessage(),
42✔
844
                'type' => $exception::class,
42✔
845
                'error_place' => $this->getErrorPlace($exception),
42✔
846
            ]);
42✔
847
        }
848

849
        $additionalDocs = config('auto-doc.additional_paths', []);
8✔
850

851
        foreach ($additionalDocs as $filePath) {
8✔
852
            try {
853
                $additionalDocContent = $this->getOpenAPIFileContent(base_path($filePath));
4✔
854
            } catch (DocFileNotExistsException|EmptyDocFileException|InvalidSwaggerSpecException $exception) {
3✔
855
                report($exception);
3✔
856

857
                continue;
3✔
858
            }
859

860
            $this->mergeOpenAPIDocs($documentation, $additionalDocContent);
1✔
861
        }
862

863
        return $documentation;
8✔
864
    }
865

866
    public function getPrettyDocFileContent(): array
867
    {
868
        $documentation = $this->getDocFileContent();
7✔
869

870
        foreach ($documentation['paths'] as $path => $pathItem) {
7✔
871
            foreach ($pathItem as $method => $operation) {
7✔
872
                if (Arr::has($operation, 'parameters')) {
7✔
873
                    $documentation['paths'][$path][$method]['parameters'] = collect($operation['parameters'])
7✔
874
                        ->groupBy('name')
7✔
875
                        ->map(function ($params, $name) {
7✔
876
                            if ($params->count() === 1 || !Str::endsWith($name, '[]')) {
7✔
877
                                return $params->first();
1✔
878
                            }
879

880
                            $base = $params->first();
7✔
881
                            $base['schema']['enum'] = $params
7✔
882
                                ->pluck('example')
7✔
883
                                ->filter(fn ($value) => !is_null($value))
7✔
884
                                ->values()
7✔
885
                                ->all();
7✔
886

887
                            return $base;
7✔
888
                        })
7✔
889
                        ->values()
7✔
890
                        ->all();
7✔
891
                }
892
            }
893
        }
894

895
        return $documentation;
7✔
896
    }
897

898
    protected function getErrorPlace(Throwable $exception): string
899
    {
900
        $firstTraceEntry = Arr::first($exception->getTrace());
42✔
901

902
        Arr::forget($firstTraceEntry, 'type');
42✔
903

904
        $formattedTraceEntry = Arr::map(
42✔
905
            array: $firstTraceEntry,
42✔
906
            callback: fn ($value, $key) => $key . '=' . (is_array($value) ? json_encode($value) : $value),
42✔
907
        );
42✔
908

909
        return implode(PHP_EOL, $formattedTraceEntry);
42✔
910
    }
911

912
    protected function camelCaseToUnderScore($input): string
913
    {
914
        preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
25✔
915
        $ret = $matches[0];
25✔
916

917
        foreach ($ret as &$match) {
25✔
918
            $match = ($match === strtoupper($match)) ? strtolower($match) : lcfirst($match);
25✔
919
        }
920

921
        return implode('_', $ret);
25✔
922
    }
923

924
    protected function generateExample($properties): array
925
    {
926
        $parameters = $this->replaceObjectValues($this->request->all());
6✔
927
        $example = [];
6✔
928

929
        $this->replaceNullValues($parameters, $properties, $example);
6✔
930

931
        return $example;
6✔
932
    }
933

934
    protected function replaceObjectValues($parameters): array
935
    {
936
        $classNamesValues = [
6✔
937
            File::class => '[uploaded_file]',
6✔
938
        ];
6✔
939

940
        $parameters = Arr::dot($parameters);
6✔
941
        $returnParameters = [];
6✔
942

943
        foreach ($parameters as $parameter => $value) {
6✔
944
            if (is_object($value)) {
6✔
945
                $class = get_class($value);
1✔
946

947
                $value = Arr::get($classNamesValues, $class, $class);
1✔
948
            }
949

950
            Arr::set($returnParameters, $parameter, $value);
6✔
951
        }
952

953
        return $returnParameters;
6✔
954
    }
955

956
    protected function getClassAnnotations($class): array
957
    {
958
        $reflection = new ReflectionClass($class);
26✔
959

960
        $annotations = $reflection->getDocComment();
26✔
961

962
        $annotations = Str::of($annotations)->remove("\r");
26✔
963

964
        $blocks = explode("\n", $annotations);
26✔
965

966
        $result = [];
26✔
967

968
        foreach ($blocks as $block) {
26✔
969
            if (Str::contains($block, '@')) {
26✔
970
                $index = strpos($block, '@');
1✔
971
                $block = substr($block, $index);
1✔
972
                $exploded = explode(' ', $block);
1✔
973

974
                $paramName = str_replace('@', '', array_shift($exploded));
1✔
975
                $paramValue = implode(' ', $exploded);
1✔
976

977
                if (in_array($paramName, $this->booleanAnnotations)) {
1✔
978
                    $paramValue = true;
1✔
979
                }
980

981
                $result[$paramName] = $paramValue;
1✔
982
            }
983
        }
984

985
        return $result;
26✔
986
    }
987

988
    /**
989
     * NOTE: All functions below are temporary solution for
990
     * this issue: https://github.com/OAI/OpenAPI-Specification/issues/229
991
     * We hope swagger developers will resolve this problem in next release of Swagger OpenAPI
992
     * */
993
    protected function replaceNullValues($parameters, $types, &$example)
994
    {
995
        foreach ($parameters as $parameter => $value) {
6✔
996
            if (is_null($value) && Arr::exists($types, $parameter)) {
6✔
997
                $example[$parameter] = $this->getDefaultValueByType($types[$parameter]['type']);
5✔
998
            } elseif (is_array($value)) {
6✔
999
                $this->replaceNullValues($value, $types, $example[$parameter]);
3✔
1000
            } else {
1001
                $example[$parameter] = $value;
6✔
1002
            }
1003
        }
1004
    }
1005

1006
    protected function getDefaultValueByType($type)
1007
    {
1008
        $values = [
5✔
1009
            'object' => 'null',
5✔
1010
            'boolean' => false,
5✔
1011
            'date' => '0000-00-00',
5✔
1012
            'integer' => 0,
5✔
1013
            'string' => '',
5✔
1014
            'double' => 0,
5✔
1015
        ];
5✔
1016

1017
        return $values[$type];
5✔
1018
    }
1019

1020
    protected function prepareInfo(?string $view = null, array $viewData = [], array $license = []): array
1021
    {
1022
        $info = [];
67✔
1023

1024
        $license = array_filter($license);
67✔
1025

1026
        if (!empty($license)) {
67✔
UNCOV
1027
            $info['license'] = $license;
×
1028
        }
1029

1030
        if (!empty($view)) {
67✔
1031
            $info['description'] = view($view, $viewData)->render();
66✔
1032
        }
1033

1034
        return array_merge($this->config['info'], $info);
67✔
1035
    }
1036

1037
    protected function getOpenAPIFileContent(string $filePath): array
1038
    {
1039
        if (!file_exists($filePath)) {
4✔
1040
            throw new DocFileNotExistsException($filePath);
1✔
1041
        }
1042

1043
        $fileContent = json_decode(file_get_contents($filePath), true);
3✔
1044

1045
        if (empty($fileContent)) {
3✔
1046
            throw new EmptyDocFileException($filePath);
1✔
1047
        }
1048

1049
        $this->openAPIValidator->validate($fileContent);
2✔
1050

1051
        return $fileContent;
1✔
1052
    }
1053

1054
    protected function mergeOpenAPIDocs(array &$documentation, array $additionalDocumentation): void
1055
    {
1056
        $paths = array_keys($additionalDocumentation['paths']);
3✔
1057

1058
        foreach ($paths as $path) {
3✔
1059
            $additionalDocPath = $additionalDocumentation['paths'][$path];
3✔
1060

1061
            if (empty($documentation['paths'][$path])) {
3✔
1062
                $documentation['paths'][$path] = $additionalDocPath;
3✔
1063
            } else {
1064
                $methods = array_keys($documentation['paths'][$path]);
1✔
1065
                $additionalDocMethods = array_keys($additionalDocPath);
1✔
1066

1067
                foreach ($additionalDocMethods as $method) {
1✔
1068
                    if (!in_array($method, $methods)) {
1✔
1069
                        $documentation['paths'][$path][$method] = $additionalDocPath[$method];
1✔
1070
                    }
1071
                }
1072
            }
1073
        }
1074

1075
        foreach (Arr::get($additionalDocumentation, 'components.schemas', []) as $definitionName => $definitionData) {
3✔
1076
            if (empty($documentation['components']['schemas'][$definitionName])) {
3✔
1077
                $documentation['components']['schemas'][$definitionName] = $definitionData;
3✔
1078
            }
1079
        }
1080
    }
1081
}
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

© 2026 Coveralls, Inc