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

RonasIT / laravel-swagger / 19128119802

06 Nov 2025 07:27AM UTC coverage: 99.311% (-0.3%) from 99.647%
19128119802

Pull #174

github

web-flow
Merge 4f0254cdc into 4ec8d4d34
Pull Request #174: 163 modify 500 code error response page

26 of 26 new or added lines in 5 files covered. (100.0%)

3 existing lines in 3 files now uncovered.

865 of 871 relevant lines covered (99.31%)

21.5 hits per line

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

99.58
/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\Facades\ParallelTesting;
10
use Illuminate\Support\Facades\URL;
11
use Illuminate\Support\Str;
12
use ReflectionClass;
13
use RonasIT\AutoDoc\Contracts\SwaggerDriverContract;
14
use RonasIT\AutoDoc\Exceptions\DocFileNotExistsException;
15
use RonasIT\AutoDoc\Exceptions\EmptyContactEmailException;
16
use RonasIT\AutoDoc\Exceptions\EmptyDocFileException;
17
use RonasIT\AutoDoc\Exceptions\InvalidDriverClassException;
18
use RonasIT\AutoDoc\Exceptions\LegacyConfigException;
19
use RonasIT\AutoDoc\Exceptions\SpecValidation\InvalidSwaggerSpecException;
20
use RonasIT\AutoDoc\Exceptions\SwaggerDriverClassNotFoundException;
21
use RonasIT\AutoDoc\Exceptions\UnsupportedDocumentationViewerException;
22
use RonasIT\AutoDoc\Exceptions\WrongSecurityConfigException;
23
use RonasIT\AutoDoc\Traits\GetDependenciesTrait;
24
use RonasIT\AutoDoc\Validators\SwaggerSpecValidator;
25
use Symfony\Component\HttpFoundation\Response;
26
use Throwable;
27

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

35
    public const string OPEN_API_VERSION = '3.1.0';
36

37
    protected $driver;
38
    protected $openAPIValidator;
39

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

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

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

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

71
        $this->initConfig();
100✔
72

73
        $this->setDriver();
95✔
74

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

80
            $this->container = $container;
92✔
81

82
            $this->security = $this->config['security'];
92✔
83

84
            $this->data = $this->driver->getProcessTmpData();
92✔
85

86
            if (empty($this->data)) {
92✔
87
                $this->data = $this->generateEmptyData();
61✔
88

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

157
        $securityDefinitions = $this->generateSecurityDefinition();
62✔
158

159
        if (!empty($securityDefinitions)) {
62✔
160
            $data['securityDefinitions'] = $securityDefinitions;
3✔
161
        }
162

163
        return $data;
62✔
164
    }
165

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

173
    protected function generateSecurityDefinition(): ?array
174
    {
175
        if (empty($this->security)) {
62✔
176
            return null;
59✔
177
        }
178

179
        return [
3✔
180
            $this->security => $this->generateSecurityDefinitionObject($this->security)
3✔
181
        ];
3✔
182
    }
183

184
    protected function generateSecurityDefinitionObject($type): array
185
    {
186
        return [
3✔
187
            'type' => $this->config['security_drivers'][$type]['type'],
3✔
188
            'name' => $this->config['security_drivers'][$type]['name'],
3✔
189
            'in' => $this->config['security_drivers'][$type]['in']
3✔
190
        ];
3✔
191
    }
192

193
    public function addData(Request $request, $response)
194
    {
195
        $this->request = $request;
27✔
196

197
        $this->prepareItem();
27✔
198

199
        $this->parseRequest();
27✔
200
        $this->parseResponse($response);
27✔
201

202
        $this->driver->saveProcessTmpData($this->data);
27✔
203
    }
204

205
    protected function prepareItem()
206
    {
207
        $this->uri = "/{$this->getUri()}";
27✔
208
        $this->method = strtolower($this->request->getMethod());
27✔
209

210
        if (empty(Arr::get($this->data, "paths.{$this->uri}.{$this->method}"))) {
27✔
211
            $this->data['paths'][$this->uri][$this->method] = [
26✔
212
                'tags' => [],
26✔
213
                'consumes' => [],
26✔
214
                'produces' => [],
26✔
215
                'parameters' => $this->getPathParams(),
26✔
216
                'responses' => [],
26✔
217
                'security' => [],
26✔
218
                'description' => ''
26✔
219
            ];
26✔
220
        }
221

222
        $this->item = &$this->data['paths'][$this->uri][$this->method];
27✔
223
    }
224

225
    protected function getUri()
226
    {
227
        $uri = $this->request->route()->uri();
27✔
228
        $basePath = preg_replace("/^\//", '', $this->config['basePath']);
27✔
229
        $preparedUri = preg_replace("/^{$basePath}/", '', $uri);
27✔
230

231
        return preg_replace("/^\//", '', $preparedUri);
27✔
232
    }
233

234
    protected function getPathParams(): array
235
    {
236
        $params = [];
26✔
237

238
        preg_match_all('/{.*?}/', $this->uri, $params);
26✔
239

240
        $params = Arr::collapse($params);
26✔
241

242
        $result = [];
26✔
243

244
        foreach ($params as $param) {
26✔
245
            $key = preg_replace('/[{}]/', '', $param);
6✔
246

247
            $result[] = [
6✔
248
                'in' => 'path',
6✔
249
                'name' => $key,
6✔
250
                'description' => $this->generatePathDescription($key),
6✔
251
                'required' => true,
6✔
252
                'schema' => [
6✔
253
                    'type' => 'string'
6✔
254
                ]
6✔
255
            ];
6✔
256
        }
257

258
        return $result;
26✔
259
    }
260

261
    protected function generatePathDescription(string $key): string
262
    {
263
        $expression = Arr::get($this->request->route()->wheres, $key);
6✔
264

265
        if (empty($expression)) {
6✔
266
            return '';
6✔
267
        }
268

269
        $exploded = explode('|', $expression);
1✔
270

271
        foreach ($exploded as $value) {
1✔
272
            if (!preg_match('/^[a-zA-Z0-9\.]+$/', $value)) {
1✔
273
                return "regexp: {$expression}";
1✔
274
            }
275
        }
276

277
        return 'in: ' . implode(',', $exploded);
1✔
278
    }
279

280
    protected function parseRequest()
281
    {
282
        $this->saveConsume();
27✔
283
        $this->saveTags();
27✔
284
        $this->saveSecurity();
27✔
285

286
        $concreteRequest = $this->getConcreteRequest();
27✔
287

288
        if (empty($concreteRequest)) {
27✔
289
            $this->item['description'] = '';
3✔
290

291
            return;
3✔
292
        }
293

294
        $annotations = $this->getClassAnnotations($concreteRequest);
24✔
295

296
        $this->markAsDeprecated($annotations);
24✔
297
        $this->saveParameters($concreteRequest, $annotations);
24✔
298
        $this->saveDescription($concreteRequest, $annotations);
24✔
299
    }
300

301
    protected function markAsDeprecated(array $annotations)
302
    {
303
        $this->item['deprecated'] = Arr::get($annotations, 'deprecated', false);
24✔
304
    }
305

306
    protected function saveResponseSchema(?array $content, string $definition): void
307
    {
308
        $schemaProperties = [];
27✔
309
        $schemaType = 'object';
27✔
310

311
        if (!empty($content) && array_is_list($content)) {
27✔
312
            $this->saveListResponseDefinitions($content, $schemaProperties);
16✔
313

314
            $schemaType = 'array';
16✔
315
        } else {
316
            $this->saveObjectResponseDefinitions($content, $schemaProperties, $definition);
11✔
317
        }
318

319
        $this->data['components']['schemas'][$definition] = [
27✔
320
            'type' => $schemaType,
27✔
321
            'properties' => $schemaProperties
27✔
322
        ];
27✔
323
    }
324

325
    protected function saveListResponseDefinitions(array $content, array &$schemaProperties): void
326
    {
327
        $types = [];
16✔
328

329
        foreach ($content as $value) {
16✔
330
            $type = gettype($value);
16✔
331

332
            if (!in_array($type, $types)) {
16✔
333
                $types[] = $type;
16✔
334
                $schemaProperties['items']['allOf'][]['type'] = $type;
16✔
335
            }
336
        }
337
    }
338

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

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

346
            if (is_null($value)) {
10✔
347
                $property['nullable'] = true;
2✔
348
            } else {
349
                $property['type'] = gettype($value);
10✔
350
            }
351

352
            $schemaProperties[$name] = $property;
10✔
353
        }
354
    }
355

356
    protected function parseResponse($response)
357
    {
358
        $produceList = $this->data['paths'][$this->uri][$this->method]['produces'];
27✔
359

360
        $produce = $response->headers->get('Content-type');
27✔
361

362
        if (is_null($produce)) {
27✔
363
            $produce = 'text/plain';
1✔
364
        }
365

366
        if (!in_array($produce, $produceList)) {
27✔
367
            $this->item['produces'][] = $produce;
26✔
368
        }
369

370
        $responses = $this->item['responses'];
27✔
371

372
        $responseExampleLimitCount = config('auto-doc.response_example_limit_count');
27✔
373

374
        $content = json_decode($response->getContent(), true) ?? [];
27✔
375

376
        if (!empty($responseExampleLimitCount)) {
27✔
377
            if (!empty($content['data'])) {
27✔
378
                $limitedResponseData = array_slice($content['data'], 0, $responseExampleLimitCount, true);
2✔
379
                $content['data'] = $limitedResponseData;
2✔
380
                $content['to'] = count($limitedResponseData);
2✔
381
                $content['total'] = count($limitedResponseData);
2✔
382
            }
383
        }
384

385
        if (!empty($content['exception'])) {
27✔
386
            $uselessKeys = array_keys(Arr::except($content, ['message']));
1✔
387

388
            $content = Arr::except($content, $uselessKeys);
1✔
389
        }
390

391
        $code = $response->getStatusCode();
27✔
392

393
        if (!in_array($code, $responses)) {
27✔
394
            $this->saveExample(
27✔
395
                $code,
27✔
396
                json_encode($content, JSON_PRETTY_PRINT),
27✔
397
                $produce
27✔
398
            );
27✔
399
        }
400

401
        $action = Str::ucfirst($this->getActionName($this->uri));
27✔
402
        $definition = "{$this->method}{$action}{$code}ResponseObject";
27✔
403

404
        $this->saveResponseSchema($content, $definition);
27✔
405

406
        if (is_array($this->item['responses'][$code])) {
27✔
407
            $this->item['responses'][$code]['content'][$produce]['schema']['$ref'] = "#/components/schemas/{$definition}";
26✔
408
        }
409
    }
410

411
    protected function saveExample($code, $content, $produce)
412
    {
413
        $description = $this->getResponseDescription($code);
27✔
414
        $availableContentTypes = [
27✔
415
            'application',
27✔
416
            'text',
27✔
417
            'image',
27✔
418
        ];
27✔
419
        $explodedContentType = explode('/', $produce);
27✔
420

421
        if (in_array($explodedContentType[0], $availableContentTypes)) {
27✔
422
            $this->item['responses'][$code] = $this->makeResponseExample($content, $produce, $description);
26✔
423
        } else {
424
            $this->item['responses'][$code] = '*Unavailable for preview*';
1✔
425
        }
426
    }
427

428
    protected function makeResponseExample($content, $mimeType, $description = ''): array
429
    {
430
        $example = match ($mimeType) {
26✔
431
            'application/json' => json_decode($content, true),
23✔
432
            'application/pdf' => base64_encode($content),
1✔
433
            default => $content,
2✔
434
        };
26✔
435

436
        return [
26✔
437
            'description' => $description,
26✔
438
            'content' => [
26✔
439
                $mimeType => [
26✔
440
                    'schema' => [
26✔
441
                        'type' => 'object',
26✔
442
                    ],
26✔
443
                    'example' => $example,
26✔
444
                ],
26✔
445
            ],
26✔
446
        ];
26✔
447
    }
448

449
    protected function saveParameters($request, array $annotations)
450
    {
451
        $formRequest = new $request();
24✔
452
        $formRequest->setUserResolver($this->request->getUserResolver());
24✔
453
        $formRequest->setRouteResolver($this->request->getRouteResolver());
24✔
454
        $rules = method_exists($formRequest, 'rules') ? $this->prepareRules($formRequest->rules()) : [];
24✔
455
        $attributes = method_exists($formRequest, 'attributes') ? $formRequest->attributes() : [];
24✔
456

457
        $actionName = $this->getActionName($this->uri);
24✔
458

459
        if (in_array($this->method, ['get', 'delete'])) {
24✔
460
            $this->saveGetRequestParameters($rules, $attributes, $annotations);
18✔
461
        } else {
462
            $this->savePostRequestParameters($actionName, $rules, $attributes, $annotations);
6✔
463
        }
464
    }
465

466
    protected function prepareRules(array $rules): array
467
    {
468
        $preparedRules = [];
23✔
469

470
        foreach ($rules as $field => $rulesField) {
23✔
471
            if (is_array($rulesField)) {
23✔
472
                $rulesField = array_map(function ($rule) {
21✔
473
                    return $this->getRuleAsString($rule);
21✔
474
                }, $rulesField);
21✔
475

476
                $preparedRules[$field] = implode('|', $rulesField);
21✔
477
            } else {
478
                $preparedRules[$field] = $this->getRuleAsString($rulesField);
23✔
479
            }
480
        }
481

482
        return $preparedRules;
23✔
483
    }
484

485
    protected function getRuleAsString($rule): string
486
    {
487
        if (is_object($rule)) {
23✔
488
            if (method_exists($rule, '__toString')) {
21✔
489
                return $rule->__toString();
21✔
490
            }
491

492
            $shortName = Str::afterLast(get_class($rule), '\\');
21✔
493

494
            $ruleName = preg_replace('/Rule$/', '', $shortName);
21✔
495

496
            return Str::snake($ruleName);
21✔
497
        }
498

499
        return $rule;
23✔
500
    }
501

502
    protected function saveGetRequestParameters($rules, array $attributes, array $annotations)
503
    {
504
        foreach ($rules as $parameter => $rule) {
18✔
505
            $validation = explode('|', $rule);
17✔
506

507
            $description = Arr::get($annotations, $parameter);
17✔
508

509
            if (empty($description)) {
17✔
510
                $description = Arr::get($attributes, $parameter, implode(', ', $validation));
17✔
511
            }
512

513
            $existedParameter = Arr::first($this->item['parameters'], function ($existedParameter) use ($parameter) {
17✔
514
                return $existedParameter['name'] === $parameter;
15✔
515
            });
17✔
516

517
            if (empty($existedParameter)) {
17✔
518
                $parameterDefinition = [
16✔
519
                    'in' => 'query',
16✔
520
                    'name' => $parameter,
16✔
521
                    'description' => $description,
16✔
522
                    'schema' => [
16✔
523
                        'type' => $this->getParameterType($validation),
16✔
524
                    ],
16✔
525
                ];
16✔
526
                if (in_array('required', $validation)) {
16✔
527
                    $parameterDefinition['required'] = true;
16✔
528
                }
529

530
                $this->item['parameters'][] = $parameterDefinition;
16✔
531
            }
532
        }
533
    }
534

535
    protected function savePostRequestParameters($actionName, $rules, array $attributes, array $annotations)
536
    {
537
        if ($this->requestHasMoreProperties($actionName)) {
6✔
538
            if ($this->requestHasBody()) {
6✔
539
                $type = $this->request->header('Content-Type', 'application/json');
6✔
540

541
                $this->item['requestBody'] = [
6✔
542
                    'content' => [
6✔
543
                        $type => [
6✔
544
                            'schema' => [
6✔
545
                                '$ref' => "#/components/schemas/{$actionName}Object",
6✔
546
                            ],
6✔
547
                        ],
6✔
548
                    ],
6✔
549
                    'description' => '',
6✔
550
                    'required' => true,
6✔
551
                ];
6✔
552
            }
553

554
            $this->saveDefinitions($actionName, $rules, $attributes, $annotations);
6✔
555
        }
556
    }
557

558
    protected function saveDefinitions($objectName, $rules, $attributes, array $annotations)
559
    {
560
        $data = [
6✔
561
            'type' => 'object',
6✔
562
            'properties' => []
6✔
563
        ];
6✔
564

565
        foreach ($rules as $parameter => $rule) {
6✔
566
            $rulesArray = (is_array($rule)) ? $rule : explode('|', $rule);
6✔
567
            $parameterType = $this->getParameterType($rulesArray);
6✔
568
            $this->saveParameterType($data, $parameter, $parameterType);
6✔
569

570
            $uselessRules = $this->ruleToTypeMap;
6✔
571
            $uselessRules['required'] = 'required';
6✔
572

573
            if (in_array('required', $rulesArray)) {
6✔
574
                $data['required'][] = $parameter;
6✔
575
            }
576

577
            $rulesArray = array_flip(array_diff_key(array_flip($rulesArray), $uselessRules));
6✔
578

579
            $this->saveParameterDescription($data, $parameter, $rulesArray, $attributes, $annotations);
6✔
580
        }
581

582
        $data['example'] = $this->generateExample($data['properties']);
6✔
583
        $this->data['components']['schemas']["{$objectName}Object"] = $data;
6✔
584
    }
585

586
    protected function getParameterType(array $validation): string
587
    {
588
        $validationRules = $this->ruleToTypeMap;
22✔
589
        $validationRules['email'] = 'string';
22✔
590

591
        $parameterType = 'string';
22✔
592

593
        foreach ($validation as $item) {
22✔
594
            if (in_array($item, array_keys($validationRules))) {
22✔
595
                return $validationRules[$item];
21✔
596
            }
597
        }
598

599
        return $parameterType;
21✔
600
    }
601

602
    protected function saveParameterType(&$data, $parameter, $parameterType)
603
    {
604
        $data['properties'][$parameter] = [
6✔
605
            'type' => $parameterType
6✔
606
        ];
6✔
607
    }
608

609
    protected function saveParameterDescription(
610
        array &$data,
611
        string $parameter,
612
        array $rulesArray,
613
        array $attributes,
614
        array $annotations
615
    ) {
616
        $description = Arr::get($annotations, $parameter);
6✔
617

618
        if (empty($description)) {
6✔
619
            $description = Arr::get($attributes, $parameter, implode(', ', $rulesArray));
6✔
620
        }
621

622
        $data['properties'][$parameter]['description'] = $description;
6✔
623
    }
624

625
    protected function requestHasMoreProperties($actionName): bool
626
    {
627
        $requestParametersCount = count($this->request->all());
6✔
628

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

632
        return $requestParametersCount > $objectParametersCount;
6✔
633
    }
634

635
    protected function requestHasBody(): bool
636
    {
637
        $parameters = $this->data['paths'][$this->uri][$this->method]['parameters'];
6✔
638

639
        $bodyParamExisted = Arr::where($parameters, function ($value) {
6✔
640
            return $value['name'] === 'body';
1✔
641
        });
6✔
642

643
        return empty($bodyParamExisted);
6✔
644
    }
645

646
    public function getConcreteRequest()
647
    {
648
        $controller = $this->request->route()->getActionName();
27✔
649

650
        if ($controller === 'Closure') {
27✔
651
            return null;
1✔
652
        }
653

654
        $explodedController = explode('@', $controller);
26✔
655

656
        $class = $explodedController[0];
26✔
657
        $method = Arr::get($explodedController, 1, '__invoke');
26✔
658

659
        if (!method_exists($class, $method)) {
26✔
660
            return null;
1✔
661
        }
662

663
        $parameters = $this->resolveClassMethodDependencies(
25✔
664
            app($class),
25✔
665
            $method
25✔
666
        );
25✔
667

668
        return Arr::first($parameters, function ($key) {
25✔
669
            return preg_match('/Request/', $key);
25✔
670
        });
25✔
671
    }
672

673
    public function saveConsume()
674
    {
675
        $consumeList = $this->data['paths'][$this->uri][$this->method]['consumes'];
27✔
676
        $consume = $this->request->header('Content-Type');
27✔
677

678
        if (!empty($consume) && !in_array($consume, $consumeList)) {
27✔
679
            $this->item['consumes'][] = $consume;
16✔
680
        }
681
    }
682

683
    public function saveTags()
684
    {
685
        $globalPrefix = config('auto-doc.global_prefix');
27✔
686
        $globalPrefix = Str::after($globalPrefix, '/');
27✔
687

688
        $explodedUri = explode('/', $this->uri);
27✔
689
        $explodedUri = array_filter($explodedUri);
27✔
690

691
        $tag = array_shift($explodedUri);
27✔
692

693
        if ($globalPrefix === $tag) {
27✔
694
            $tag = array_shift($explodedUri);
2✔
695
        }
696

697
        $this->item['tags'] = [$tag];
27✔
698
    }
699

700
    public function saveDescription($request, array $annotations)
701
    {
702
        $this->item['summary'] = $this->getSummary($request, $annotations);
24✔
703

704
        $description = Arr::get($annotations, 'description');
24✔
705

706
        if (!empty($description)) {
24✔
707
            $this->item['description'] = $description;
1✔
708
        }
709
    }
710

711
    protected function saveSecurity()
712
    {
713
        if ($this->requestSupportAuth()) {
27✔
714
            $this->addSecurityToOperation();
5✔
715
        }
716
    }
717

718
    protected function addSecurityToOperation()
719
    {
720
        $security = &$this->data['paths'][$this->uri][$this->method]['security'];
5✔
721

722
        if (empty($security)) {
5✔
723
            $security[] = [
5✔
724
                "{$this->security}" => []
5✔
725
            ];
5✔
726
        }
727
    }
728

729
    protected function getSummary($request, array $annotations)
730
    {
731
        $summary = Arr::get($annotations, 'summary');
24✔
732

733
        if (empty($summary)) {
24✔
734
            $summary = $this->parseRequestName($request);
23✔
735
        }
736

737
        return $summary;
24✔
738
    }
739

740
    protected function requestSupportAuth(): bool
741
    {
742
        $security = Arr::get($this->config, 'security');
27✔
743
        $securityDriver = Arr::get($this->config, "security_drivers.{$security}");
27✔
744

745
        switch (Arr::get($securityDriver, 'in')) {
27✔
746
            case 'header':
27✔
747
                // TODO Change this logic after migration on Swagger 3.0
748
                // Swagger 2.0 does not support cookie authorization.
749
                $securityToken = $this->request->hasHeader($securityDriver['name'])
7✔
750
                    ? $this->request->header($securityDriver['name'])
5✔
751
                    : $this->request->cookie($securityDriver['name']);
2✔
752

753
                break;
7✔
754
            case 'query':
20✔
755
                $securityToken = $this->request->query($securityDriver['name']);
1✔
756

757
                break;
1✔
758
            default:
759
                $securityToken = null;
19✔
760
        }
761

762
        return !empty($securityToken);
27✔
763
    }
764

765
    protected function parseRequestName($request)
766
    {
767
        $explodedRequest = explode('\\', $request);
23✔
768
        $requestName = array_pop($explodedRequest);
23✔
769
        $summaryName = str_replace('Request', '', $requestName);
23✔
770

771
        $underscoreRequestName = $this->camelCaseToUnderScore($summaryName);
23✔
772

773
        return preg_replace('/[_]/', ' ', $underscoreRequestName);
23✔
774
    }
775

776
    protected function getResponseDescription($code)
777
    {
778
        $defaultDescription = Response::$statusTexts[$code];
27✔
779

780
        $request = $this->getConcreteRequest();
27✔
781

782
        if (empty($request)) {
27✔
783
            return $defaultDescription;
3✔
784
        }
785

786
        $annotations = $this->getClassAnnotations($request);
24✔
787

788
        $localDescription = Arr::get($annotations, "_{$code}");
24✔
789

790
        if (!empty($localDescription)) {
24✔
791
            return $localDescription;
1✔
792
        }
793

794
        return Arr::get($this->config, "defaults.code-descriptions.{$code}", $defaultDescription);
23✔
795
    }
796

797
    protected function getActionName($uri): string
798
    {
799
        $action = preg_replace('[\/]', '', $uri);
27✔
800

801
        return Str::camel($action);
27✔
802
    }
803

804
    public function saveProductionData()
805
    {
806
        if (ParallelTesting::token()) {
4✔
807
            $this->driver->appendProcessDataToTmpFile(function (?array $sharedTmpData) {
2✔
808
                $resultDocContent = (empty($sharedTmpData))
2✔
809
                    ? $this->generateEmptyData($this->config['info']['description'])
1✔
810
                    : $sharedTmpData;
1✔
811

812
                $this->mergeOpenAPIDocs($resultDocContent, $this->data);
2✔
813

814
                return $resultDocContent;
2✔
815
            });
2✔
816
        }
817

818
        $this->driver->saveData();
4✔
819
    }
820

821
    public function getDocFileContent()
822
    {
823
        try {
824
            $documentation = $this->driver->getDocumentation();
45✔
825

826
            $this->openAPIValidator->validate($documentation);
43✔
827
        } catch (Throwable $exception) {
39✔
828
            return $this->generateEmptyData($this->config['defaults']['error'], [
39✔
829
                'message' => $exception->getMessage(),
39✔
830
                'type' => $exception::class,
39✔
831
                'error_place' => $this->getErrorPlace($exception),
39✔
832
            ]);
39✔
833
        }
834

835
        $additionalDocs = config('auto-doc.additional_paths', []);
6✔
836

837
        foreach ($additionalDocs as $filePath) {
6✔
838
            try {
839
                $additionalDocContent = $this->getOpenAPIFileContent(base_path($filePath));
4✔
840
            } catch (DocFileNotExistsException|EmptyDocFileException|InvalidSwaggerSpecException $exception) {
3✔
841
                report($exception);
3✔
842

843
                continue;
3✔
844
            }
845

846
            $this->mergeOpenAPIDocs($documentation, $additionalDocContent);
1✔
847
        }
848

849
        return $documentation;
6✔
850
    }
851

852
    protected function getErrorPlace(Throwable $exception): string
853
    {
854
        $firstTraceEntry = Arr::first($exception->getTrace());
39✔
855

856
        $formattedTraceEntry = Arr::map(
39✔
857
            array: $firstTraceEntry,
39✔
858
            callback: fn ($value, $key) => $key . '=' . (is_array($value) ? json_encode($value) : $value),
39✔
859
        );
39✔
860

861
        return implode(', ', $formattedTraceEntry);
39✔
862
    }
863

864
    protected function camelCaseToUnderScore($input): string
865
    {
866
        preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
23✔
867
        $ret = $matches[0];
23✔
868

869
        foreach ($ret as &$match) {
23✔
870
            $match = ($match === strtoupper($match)) ? strtolower($match) : lcfirst($match);
23✔
871
        }
872

873
        return implode('_', $ret);
23✔
874
    }
875

876
    protected function generateExample($properties): array
877
    {
878
        $parameters = $this->replaceObjectValues($this->request->all());
6✔
879
        $example = [];
6✔
880

881
        $this->replaceNullValues($parameters, $properties, $example);
6✔
882

883
        return $example;
6✔
884
    }
885

886
    protected function replaceObjectValues($parameters): array
887
    {
888
        $classNamesValues = [
6✔
889
            File::class => '[uploaded_file]',
6✔
890
        ];
6✔
891

892
        $parameters = Arr::dot($parameters);
6✔
893
        $returnParameters = [];
6✔
894

895
        foreach ($parameters as $parameter => $value) {
6✔
896
            if (is_object($value)) {
6✔
897
                $class = get_class($value);
1✔
898

899
                $value = Arr::get($classNamesValues, $class, $class);
1✔
900
            }
901

902
            Arr::set($returnParameters, $parameter, $value);
6✔
903
        }
904

905
        return $returnParameters;
6✔
906
    }
907

908
    protected function getClassAnnotations($class): array
909
    {
910
        $reflection = new ReflectionClass($class);
24✔
911

912
        $annotations = $reflection->getDocComment();
24✔
913

914
        $annotations = Str::of($annotations)->remove("\r");
24✔
915

916
        $blocks = explode("\n", $annotations);
24✔
917

918
        $result = [];
24✔
919

920
        foreach ($blocks as $block) {
24✔
921
            if (Str::contains($block, '@')) {
24✔
922
                $index = strpos($block, '@');
1✔
923
                $block = substr($block, $index);
1✔
924
                $exploded = explode(' ', $block);
1✔
925

926
                $paramName = str_replace('@', '', array_shift($exploded));
1✔
927
                $paramValue = implode(' ', $exploded);
1✔
928

929
                if (in_array($paramName, $this->booleanAnnotations)) {
1✔
930
                    $paramValue = true;
1✔
931
                }
932

933
                $result[$paramName] = $paramValue;
1✔
934
            }
935
        }
936

937
        return $result;
24✔
938
    }
939

940
    /**
941
     * NOTE: All functions below are temporary solution for
942
     * this issue: https://github.com/OAI/OpenAPI-Specification/issues/229
943
     * We hope swagger developers will resolve this problem in next release of Swagger OpenAPI
944
     * */
945
    protected function replaceNullValues($parameters, $types, &$example)
946
    {
947
        foreach ($parameters as $parameter => $value) {
6✔
948
            if (is_null($value) && array_key_exists($parameter, $types)) {
6✔
949
                $example[$parameter] = $this->getDefaultValueByType($types[$parameter]['type']);
5✔
950
            } elseif (is_array($value)) {
6✔
951
                $this->replaceNullValues($value, $types, $example[$parameter]);
3✔
952
            } else {
953
                $example[$parameter] = $value;
6✔
954
            }
955
        }
956
    }
957

958
    protected function getDefaultValueByType($type)
959
    {
960
        $values = [
5✔
961
            'object' => 'null',
5✔
962
            'boolean' => false,
5✔
963
            'date' => "0000-00-00",
5✔
964
            'integer' => 0,
5✔
965
            'string' => '',
5✔
966
            'double' => 0
5✔
967
        ];
5✔
968

969
        return $values[$type];
5✔
970
    }
971

972
    protected function prepareInfo(?string $view = null, array $viewData = [], array $license = []): array
973
    {
974
        $info = [];
62✔
975

976
        $license = array_filter($license);
62✔
977

978
        if (!empty($license)) {
62✔
979
            $info['license'] = $license;
×
980
        }
981

982
        if (!empty($view)) {
62✔
983
            $info['description'] = view($view, $viewData)->render();
61✔
984
        }
985

986
        return array_merge($this->config['info'], $info);
62✔
987
    }
988

989
    protected function getOpenAPIFileContent(string $filePath): array
990
    {
991
        if (!file_exists($filePath)) {
4✔
992
            throw new DocFileNotExistsException($filePath);
2✔
993
        }
994

995
        $fileContent = json_decode(file_get_contents($filePath), true);
2✔
996

997
        if (empty($fileContent)) {
2✔
UNCOV
998
            throw new EmptyDocFileException($filePath);
×
999
        }
1000

1001
        $this->openAPIValidator->validate($fileContent);
2✔
1002

1003
        return $fileContent;
1✔
1004
    }
1005

1006
    protected function mergeOpenAPIDocs(array &$documentation, array $additionalDocumentation): void
1007
    {
1008
        $paths = array_keys($additionalDocumentation['paths']);
3✔
1009

1010
        foreach ($paths as $path) {
3✔
1011
            $additionalDocPath = $additionalDocumentation['paths'][$path];
3✔
1012

1013
            if (empty($documentation['paths'][$path])) {
3✔
1014
                $documentation['paths'][$path] = $additionalDocPath;
3✔
1015
            } else {
1016
                $methods = array_keys($documentation['paths'][$path]);
1✔
1017
                $additionalDocMethods = array_keys($additionalDocPath);
1✔
1018

1019
                foreach ($additionalDocMethods as $method) {
1✔
1020
                    if (!in_array($method, $methods)) {
1✔
1021
                        $documentation['paths'][$path][$method] = $additionalDocPath[$method];
1✔
1022
                    }
1023
                }
1024
            }
1025
        }
1026

1027
        $definitions = array_keys($additionalDocumentation['components']['schemas']);
3✔
1028

1029
        foreach ($definitions as $definition) {
3✔
1030
            $documentation = Arr::add(
3✔
1031
                array: $documentation,
3✔
1032
                key: "components.schemas.{$definition}",
3✔
1033
                value: $additionalDocumentation['components']['schemas'][$definition],
3✔
1034
            );
3✔
1035
        }
1036
    }
1037
}
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