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

RonasIT / laravel-swagger / 18343004013

08 Oct 2025 11:22AM UTC coverage: 99.431% (-0.2%) from 99.647%
18343004013

Pull #183

github

web-flow
Merge 15f9f2791 into 4ec8d4d34
Pull Request #183: Test2 163 modify 500 code error response page

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

2 existing lines in 2 files now uncovered.

874 of 879 relevant lines covered (99.43%)

21.54 hits per line

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

99.6
/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
use Exception;
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);
100✔
71

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

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

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

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

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

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

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

90
                $this->driver->saveProcessTmpData($this->data);
60✔
91
            }
92
        } else {
93
            try {
94
                $this->checkEmail();
1✔
95
            } catch (EmptyContactEmailException $exception) {
1✔
96
                $this->data = $this->generateEmptyData(
1✔
97
                    $this->config['defaults']['error'],
1✔
98
                    [
1✔
99
                        'message' => $exception->getMessage(),
1✔
100
                        'type' => $exception::class,
1✔
101
                        'error_place' => $this->getErrorPlace($exception),
1✔
102
                    ],
1✔
103
                );
1✔
104

105
                $this->driver->saveProcessTmpData($this->data);
1✔
106

107
                $this->driver->saveData();
1✔
108
            }
109
        }
110
    }
111

112
    protected function initConfig()
113
    {
114
        $this->config = config('auto-doc');
100✔
115

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

118
        if (empty($version)) {
100✔
119
            throw new LegacyConfigException();
1✔
120
        }
121

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

124
        if (version_compare($packageConfigs['config_version'], $version, '>')) {
99✔
125
            throw new LegacyConfigException();
1✔
126
        }
127

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

130
        if (!view()->exists("auto-doc::documentation-{$documentationViewer}")) {
98✔
131
            throw new UnsupportedDocumentationViewerException($documentationViewer);
2✔
132
        }
133

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

136
        if ($securityDriver && !array_key_exists($securityDriver, Arr::get($this->config, 'security_drivers'))) {
96✔
137
            throw new WrongSecurityConfigException();
1✔
138
        }
139
    }
140

141
    protected function setDriver()
142
    {
143
        $driver = $this->config['driver'];
95✔
144
        $className = Arr::get($this->config, "drivers.{$driver}.class");
95✔
145

146
        if (!class_exists($className)) {
95✔
147
            throw new SwaggerDriverClassNotFoundException($className);
1✔
148
        } else {
149
            $this->driver = app($className);
94✔
150
        }
151

152
        if (!$this->driver instanceof SwaggerDriverContract) {
94✔
153
            throw new InvalidDriverClassException($driver);
1✔
154
        }
155
    }
156

157
    protected function generateEmptyData(?string $view = null, array $viewData = [], array $license = []): array
158
    {
159
        if (empty($view) && !empty($this->config['info'])) {
62✔
160
            $view = $this->config['info']['description'];
59✔
161
        }
162

163
        $data = [
62✔
164
            'openapi' => self::OPEN_API_VERSION,
62✔
165
            'servers' => [
62✔
166
                ['url' => URL::query($this->config['basePath'])],
62✔
167
            ],
62✔
168
            'paths' => [],
62✔
169
            'components' => [
62✔
170
                'schemas' => $this->config['definitions'],
62✔
171
            ],
62✔
172
            'info' => $this->prepareInfo($view, $viewData, $license),
62✔
173
        ];
62✔
174

175
        $securityDefinitions = $this->generateSecurityDefinition();
62✔
176

177
        if (!empty($securityDefinitions)) {
62✔
178
            $data['securityDefinitions'] = $securityDefinitions;
3✔
179
        }
180

181
        return $data;
62✔
182
    }
183

184
    protected function checkEmail(): void
185
    {
186
        if (!empty($this->config['info']) && !Arr::get($this->config, 'info.contact.email')) {
93✔
187
            throw new EmptyContactEmailException();
2✔
188
        }
189
    }
190

191
    protected function generateSecurityDefinition(): ?array
192
    {
193
        if (empty($this->security)) {
62✔
194
            return null;
59✔
195
        }
196

197
        return [
3✔
198
            $this->security => $this->generateSecurityDefinitionObject($this->security)
3✔
199
        ];
3✔
200
    }
201

202
    protected function generateSecurityDefinitionObject($type): array
203
    {
204
        return [
3✔
205
            'type' => $this->config['security_drivers'][$type]['type'],
3✔
206
            'name' => $this->config['security_drivers'][$type]['name'],
3✔
207
            'in' => $this->config['security_drivers'][$type]['in']
3✔
208
        ];
3✔
209
    }
210

211
    public function addData(Request $request, $response)
212
    {
213
        $this->request = $request;
27✔
214

215
        $this->prepareItem();
27✔
216

217
        $this->parseRequest();
27✔
218
        $this->parseResponse($response);
27✔
219

220
        $this->driver->saveProcessTmpData($this->data);
27✔
221
    }
222

223
    protected function prepareItem()
224
    {
225
        $this->uri = "/{$this->getUri()}";
27✔
226
        $this->method = strtolower($this->request->getMethod());
27✔
227

228
        if (empty(Arr::get($this->data, "paths.{$this->uri}.{$this->method}"))) {
27✔
229
            $this->data['paths'][$this->uri][$this->method] = [
26✔
230
                'tags' => [],
26✔
231
                'consumes' => [],
26✔
232
                'produces' => [],
26✔
233
                'parameters' => $this->getPathParams(),
26✔
234
                'responses' => [],
26✔
235
                'security' => [],
26✔
236
                'description' => ''
26✔
237
            ];
26✔
238
        }
239

240
        $this->item = &$this->data['paths'][$this->uri][$this->method];
27✔
241
    }
242

243
    protected function getUri()
244
    {
245
        $uri = $this->request->route()->uri();
27✔
246
        $basePath = preg_replace("/^\//", '', $this->config['basePath']);
27✔
247
        $preparedUri = preg_replace("/^{$basePath}/", '', $uri);
27✔
248

249
        return preg_replace("/^\//", '', $preparedUri);
27✔
250
    }
251

252
    protected function getPathParams(): array
253
    {
254
        $params = [];
26✔
255

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

258
        $params = Arr::collapse($params);
26✔
259

260
        $result = [];
26✔
261

262
        foreach ($params as $param) {
26✔
263
            $key = preg_replace('/[{}]/', '', $param);
6✔
264

265
            $result[] = [
6✔
266
                'in' => 'path',
6✔
267
                'name' => $key,
6✔
268
                'description' => $this->generatePathDescription($key),
6✔
269
                'required' => true,
6✔
270
                'schema' => [
6✔
271
                    'type' => 'string'
6✔
272
                ]
6✔
273
            ];
6✔
274
        }
275

276
        return $result;
26✔
277
    }
278

279
    protected function generatePathDescription(string $key): string
280
    {
281
        $expression = Arr::get($this->request->route()->wheres, $key);
6✔
282

283
        if (empty($expression)) {
6✔
284
            return '';
6✔
285
        }
286

287
        $exploded = explode('|', $expression);
1✔
288

289
        foreach ($exploded as $value) {
1✔
290
            if (!preg_match('/^[a-zA-Z0-9\.]+$/', $value)) {
1✔
291
                return "regexp: {$expression}";
1✔
292
            }
293
        }
294

295
        return 'in: ' . implode(',', $exploded);
1✔
296
    }
297

298
    protected function parseRequest()
299
    {
300
        $this->saveConsume();
27✔
301
        $this->saveTags();
27✔
302
        $this->saveSecurity();
27✔
303

304
        $concreteRequest = $this->getConcreteRequest();
27✔
305

306
        if (empty($concreteRequest)) {
27✔
307
            $this->item['description'] = '';
3✔
308

309
            return;
3✔
310
        }
311

312
        $annotations = $this->getClassAnnotations($concreteRequest);
24✔
313

314
        $this->markAsDeprecated($annotations);
24✔
315
        $this->saveParameters($concreteRequest, $annotations);
24✔
316
        $this->saveDescription($concreteRequest, $annotations);
24✔
317
    }
318

319
    protected function markAsDeprecated(array $annotations)
320
    {
321
        $this->item['deprecated'] = Arr::get($annotations, 'deprecated', false);
24✔
322
    }
323

324
    protected function saveResponseSchema(?array $content, string $definition): void
325
    {
326
        $schemaProperties = [];
27✔
327
        $schemaType = 'object';
27✔
328

329
        if (!empty($content) && array_is_list($content)) {
27✔
330
            $this->saveListResponseDefinitions($content, $schemaProperties);
16✔
331

332
            $schemaType = 'array';
16✔
333
        } else {
334
            $this->saveObjectResponseDefinitions($content, $schemaProperties, $definition);
11✔
335
        }
336

337
        $this->data['components']['schemas'][$definition] = [
27✔
338
            'type' => $schemaType,
27✔
339
            'properties' => $schemaProperties
27✔
340
        ];
27✔
341
    }
342

343
    protected function saveListResponseDefinitions(array $content, array &$schemaProperties): void
344
    {
345
        $types = [];
16✔
346

347
        foreach ($content as $value) {
16✔
348
            $type = gettype($value);
16✔
349

350
            if (!in_array($type, $types)) {
16✔
351
                $types[] = $type;
16✔
352
                $schemaProperties['items']['allOf'][]['type'] = $type;
16✔
353
            }
354
        }
355
    }
356

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

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

364
            if (is_null($value)) {
10✔
365
                $property['nullable'] = true;
2✔
366
            } else {
367
                $property['type'] = gettype($value);
10✔
368
            }
369

370
            $schemaProperties[$name] = $property;
10✔
371
        }
372
    }
373

374
    protected function parseResponse($response)
375
    {
376
        $produceList = $this->data['paths'][$this->uri][$this->method]['produces'];
27✔
377

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

380
        if (is_null($produce)) {
27✔
381
            $produce = 'text/plain';
1✔
382
        }
383

384
        if (!in_array($produce, $produceList)) {
27✔
385
            $this->item['produces'][] = $produce;
26✔
386
        }
387

388
        $responses = $this->item['responses'];
27✔
389

390
        $responseExampleLimitCount = config('auto-doc.response_example_limit_count');
27✔
391

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

394
        if (!empty($responseExampleLimitCount)) {
27✔
395
            if (!empty($content['data'])) {
27✔
396
                $limitedResponseData = array_slice($content['data'], 0, $responseExampleLimitCount, true);
2✔
397
                $content['data'] = $limitedResponseData;
2✔
398
                $content['to'] = count($limitedResponseData);
2✔
399
                $content['total'] = count($limitedResponseData);
2✔
400
            }
401
        }
402

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

406
            $content = Arr::except($content, $uselessKeys);
1✔
407
        }
408

409
        $code = $response->getStatusCode();
27✔
410

411
        if (!in_array($code, $responses)) {
27✔
412
            $this->saveExample(
27✔
413
                $code,
27✔
414
                json_encode($content, JSON_PRETTY_PRINT),
27✔
415
                $produce
27✔
416
            );
27✔
417
        }
418

419
        $action = Str::ucfirst($this->getActionName($this->uri));
27✔
420
        $definition = "{$this->method}{$action}{$code}ResponseObject";
27✔
421

422
        $this->saveResponseSchema($content, $definition);
27✔
423

424
        if (is_array($this->item['responses'][$code])) {
27✔
425
            $this->item['responses'][$code]['content'][$produce]['schema']['$ref'] = "#/components/schemas/{$definition}";
26✔
426
        }
427
    }
428

429
    protected function saveExample($code, $content, $produce)
430
    {
431
        $description = $this->getResponseDescription($code);
27✔
432
        $availableContentTypes = [
27✔
433
            'application',
27✔
434
            'text',
27✔
435
            'image',
27✔
436
        ];
27✔
437
        $explodedContentType = explode('/', $produce);
27✔
438

439
        if (in_array($explodedContentType[0], $availableContentTypes)) {
27✔
440
            $this->item['responses'][$code] = $this->makeResponseExample($content, $produce, $description);
26✔
441
        } else {
442
            $this->item['responses'][$code] = '*Unavailable for preview*';
1✔
443
        }
444
    }
445

446
    protected function makeResponseExample($content, $mimeType, $description = ''): array
447
    {
448
        $example = match ($mimeType) {
26✔
449
            'application/json' => json_decode($content, true),
23✔
450
            'application/pdf' => base64_encode($content),
1✔
451
            default => $content,
2✔
452
        };
26✔
453

454
        return [
26✔
455
            'description' => $description,
26✔
456
            'content' => [
26✔
457
                $mimeType => [
26✔
458
                    'schema' => [
26✔
459
                        'type' => 'object',
26✔
460
                    ],
26✔
461
                    'example' => $example,
26✔
462
                ],
26✔
463
            ],
26✔
464
        ];
26✔
465
    }
466

467
    protected function saveParameters($request, array $annotations)
468
    {
469
        $formRequest = new $request();
24✔
470
        $formRequest->setUserResolver($this->request->getUserResolver());
24✔
471
        $formRequest->setRouteResolver($this->request->getRouteResolver());
24✔
472
        $rules = method_exists($formRequest, 'rules') ? $this->prepareRules($formRequest->rules()) : [];
24✔
473
        $attributes = method_exists($formRequest, 'attributes') ? $formRequest->attributes() : [];
24✔
474

475
        $actionName = $this->getActionName($this->uri);
24✔
476

477
        if (in_array($this->method, ['get', 'delete'])) {
24✔
478
            $this->saveGetRequestParameters($rules, $attributes, $annotations);
18✔
479
        } else {
480
            $this->savePostRequestParameters($actionName, $rules, $attributes, $annotations);
6✔
481
        }
482
    }
483

484
    protected function prepareRules(array $rules): array
485
    {
486
        $preparedRules = [];
23✔
487

488
        foreach ($rules as $field => $rulesField) {
23✔
489
            if (is_array($rulesField)) {
23✔
490
                $rulesField = array_map(function ($rule) {
21✔
491
                    return $this->getRuleAsString($rule);
21✔
492
                }, $rulesField);
21✔
493

494
                $preparedRules[$field] = implode('|', $rulesField);
21✔
495
            } else {
496
                $preparedRules[$field] = $this->getRuleAsString($rulesField);
23✔
497
            }
498
        }
499

500
        return $preparedRules;
23✔
501
    }
502

503
    protected function getRuleAsString($rule): string
504
    {
505
        if (is_object($rule)) {
23✔
506
            if (method_exists($rule, '__toString')) {
21✔
507
                return $rule->__toString();
21✔
508
            }
509

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

512
            $ruleName = preg_replace('/Rule$/', '', $shortName);
21✔
513

514
            return Str::snake($ruleName);
21✔
515
        }
516

517
        return $rule;
23✔
518
    }
519

520
    protected function saveGetRequestParameters($rules, array $attributes, array $annotations)
521
    {
522
        foreach ($rules as $parameter => $rule) {
18✔
523
            $validation = explode('|', $rule);
17✔
524

525
            $description = Arr::get($annotations, $parameter);
17✔
526

527
            if (empty($description)) {
17✔
528
                $description = Arr::get($attributes, $parameter, implode(', ', $validation));
17✔
529
            }
530

531
            $existedParameter = Arr::first($this->item['parameters'], function ($existedParameter) use ($parameter) {
17✔
532
                return $existedParameter['name'] === $parameter;
15✔
533
            });
17✔
534

535
            if (empty($existedParameter)) {
17✔
536
                $parameterDefinition = [
16✔
537
                    'in' => 'query',
16✔
538
                    'name' => $parameter,
16✔
539
                    'description' => $description,
16✔
540
                    'schema' => [
16✔
541
                        'type' => $this->getParameterType($validation),
16✔
542
                    ],
16✔
543
                ];
16✔
544
                if (in_array('required', $validation)) {
16✔
545
                    $parameterDefinition['required'] = true;
16✔
546
                }
547

548
                $this->item['parameters'][] = $parameterDefinition;
16✔
549
            }
550
        }
551
    }
552

553
    protected function savePostRequestParameters($actionName, $rules, array $attributes, array $annotations)
554
    {
555
        if ($this->requestHasMoreProperties($actionName)) {
6✔
556
            if ($this->requestHasBody()) {
6✔
557
                $type = $this->request->header('Content-Type', 'application/json');
6✔
558

559
                $this->item['requestBody'] = [
6✔
560
                    'content' => [
6✔
561
                        $type => [
6✔
562
                            'schema' => [
6✔
563
                                '$ref' => "#/components/schemas/{$actionName}Object",
6✔
564
                            ],
6✔
565
                        ],
6✔
566
                    ],
6✔
567
                    'description' => '',
6✔
568
                    'required' => true,
6✔
569
                ];
6✔
570
            }
571

572
            $this->saveDefinitions($actionName, $rules, $attributes, $annotations);
6✔
573
        }
574
    }
575

576
    protected function saveDefinitions($objectName, $rules, $attributes, array $annotations)
577
    {
578
        $data = [
6✔
579
            'type' => 'object',
6✔
580
            'properties' => []
6✔
581
        ];
6✔
582

583
        foreach ($rules as $parameter => $rule) {
6✔
584
            $rulesArray = (is_array($rule)) ? $rule : explode('|', $rule);
6✔
585
            $parameterType = $this->getParameterType($rulesArray);
6✔
586
            $this->saveParameterType($data, $parameter, $parameterType);
6✔
587

588
            $uselessRules = $this->ruleToTypeMap;
6✔
589
            $uselessRules['required'] = 'required';
6✔
590

591
            if (in_array('required', $rulesArray)) {
6✔
592
                $data['required'][] = $parameter;
6✔
593
            }
594

595
            $rulesArray = array_flip(array_diff_key(array_flip($rulesArray), $uselessRules));
6✔
596

597
            $this->saveParameterDescription($data, $parameter, $rulesArray, $attributes, $annotations);
6✔
598
        }
599

600
        $data['example'] = $this->generateExample($data['properties']);
6✔
601
        $this->data['components']['schemas']["{$objectName}Object"] = $data;
6✔
602
    }
603

604
    protected function getParameterType(array $validation): string
605
    {
606
        $validationRules = $this->ruleToTypeMap;
22✔
607
        $validationRules['email'] = 'string';
22✔
608

609
        $parameterType = 'string';
22✔
610

611
        foreach ($validation as $item) {
22✔
612
            if (in_array($item, array_keys($validationRules))) {
22✔
613
                return $validationRules[$item];
21✔
614
            }
615
        }
616

617
        return $parameterType;
21✔
618
    }
619

620
    protected function saveParameterType(&$data, $parameter, $parameterType)
621
    {
622
        $data['properties'][$parameter] = [
6✔
623
            'type' => $parameterType
6✔
624
        ];
6✔
625
    }
626

627
    protected function saveParameterDescription(
628
        array &$data,
629
        string $parameter,
630
        array $rulesArray,
631
        array $attributes,
632
        array $annotations
633
    ) {
634
        $description = Arr::get($annotations, $parameter);
6✔
635

636
        if (empty($description)) {
6✔
637
            $description = Arr::get($attributes, $parameter, implode(', ', $rulesArray));
6✔
638
        }
639

640
        $data['properties'][$parameter]['description'] = $description;
6✔
641
    }
642

643
    protected function requestHasMoreProperties($actionName): bool
644
    {
645
        $requestParametersCount = count($this->request->all());
6✔
646

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

650
        return $requestParametersCount > $objectParametersCount;
6✔
651
    }
652

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

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

661
        return empty($bodyParamExisted);
6✔
662
    }
663

664
    public function getConcreteRequest()
665
    {
666
        $controller = $this->request->route()->getActionName();
27✔
667

668
        if ($controller === 'Closure') {
27✔
669
            return null;
1✔
670
        }
671

672
        $explodedController = explode('@', $controller);
26✔
673

674
        $class = $explodedController[0];
26✔
675
        $method = Arr::get($explodedController, 1, '__invoke');
26✔
676

677
        if (!method_exists($class, $method)) {
26✔
678
            return null;
1✔
679
        }
680

681
        $parameters = $this->resolveClassMethodDependencies(
25✔
682
            app($class),
25✔
683
            $method
25✔
684
        );
25✔
685

686
        return Arr::first($parameters, function ($key) {
25✔
687
            return preg_match('/Request/', $key);
25✔
688
        });
25✔
689
    }
690

691
    public function saveConsume()
692
    {
693
        $consumeList = $this->data['paths'][$this->uri][$this->method]['consumes'];
27✔
694
        $consume = $this->request->header('Content-Type');
27✔
695

696
        if (!empty($consume) && !in_array($consume, $consumeList)) {
27✔
697
            $this->item['consumes'][] = $consume;
16✔
698
        }
699
    }
700

701
    public function saveTags()
702
    {
703
        $globalPrefix = config('auto-doc.global_prefix');
27✔
704
        $globalPrefix = Str::after($globalPrefix, '/');
27✔
705

706
        $explodedUri = explode('/', $this->uri);
27✔
707
        $explodedUri = array_filter($explodedUri);
27✔
708

709
        $tag = array_shift($explodedUri);
27✔
710

711
        if ($globalPrefix === $tag) {
27✔
712
            $tag = array_shift($explodedUri);
2✔
713
        }
714

715
        $this->item['tags'] = [$tag];
27✔
716
    }
717

718
    public function saveDescription($request, array $annotations)
719
    {
720
        $this->item['summary'] = $this->getSummary($request, $annotations);
24✔
721

722
        $description = Arr::get($annotations, 'description');
24✔
723

724
        if (!empty($description)) {
24✔
725
            $this->item['description'] = $description;
1✔
726
        }
727
    }
728

729
    protected function saveSecurity()
730
    {
731
        if ($this->requestSupportAuth()) {
27✔
732
            $this->addSecurityToOperation();
5✔
733
        }
734
    }
735

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

740
        if (empty($security)) {
5✔
741
            $security[] = [
5✔
742
                "{$this->security}" => []
5✔
743
            ];
5✔
744
        }
745
    }
746

747
    protected function getSummary($request, array $annotations)
748
    {
749
        $summary = Arr::get($annotations, 'summary');
24✔
750

751
        if (empty($summary)) {
24✔
752
            $summary = $this->parseRequestName($request);
23✔
753
        }
754

755
        return $summary;
24✔
756
    }
757

758
    protected function requestSupportAuth(): bool
759
    {
760
        $security = Arr::get($this->config, 'security');
27✔
761
        $securityDriver = Arr::get($this->config, "security_drivers.{$security}");
27✔
762

763
        switch (Arr::get($securityDriver, 'in')) {
27✔
764
            case 'header':
27✔
765
                // TODO Change this logic after migration on Swagger 3.0
766
                // Swagger 2.0 does not support cookie authorization.
767
                $securityToken = $this->request->hasHeader($securityDriver['name'])
7✔
768
                    ? $this->request->header($securityDriver['name'])
5✔
769
                    : $this->request->cookie($securityDriver['name']);
2✔
770

771
                break;
7✔
772
            case 'query':
20✔
773
                $securityToken = $this->request->query($securityDriver['name']);
1✔
774

775
                break;
1✔
776
            default:
777
                $securityToken = null;
19✔
778
        }
779

780
        return !empty($securityToken);
27✔
781
    }
782

783
    protected function parseRequestName($request)
784
    {
785
        $explodedRequest = explode('\\', $request);
23✔
786
        $requestName = array_pop($explodedRequest);
23✔
787
        $summaryName = str_replace('Request', '', $requestName);
23✔
788

789
        $underscoreRequestName = $this->camelCaseToUnderScore($summaryName);
23✔
790

791
        return preg_replace('/[_]/', ' ', $underscoreRequestName);
23✔
792
    }
793

794
    protected function getResponseDescription($code)
795
    {
796
        $defaultDescription = Response::$statusTexts[$code];
27✔
797

798
        $request = $this->getConcreteRequest();
27✔
799

800
        if (empty($request)) {
27✔
801
            return $defaultDescription;
3✔
802
        }
803

804
        $annotations = $this->getClassAnnotations($request);
24✔
805

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

808
        if (!empty($localDescription)) {
24✔
809
            return $localDescription;
1✔
810
        }
811

812
        return Arr::get($this->config, "defaults.code-descriptions.{$code}", $defaultDescription);
23✔
813
    }
814

815
    protected function getActionName($uri): string
816
    {
817
        $action = preg_replace('[\/]', '', $uri);
27✔
818

819
        return Str::camel($action);
27✔
820
    }
821

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

830
                $this->mergeOpenAPIDocs($resultDocContent, $this->data);
2✔
831

832
                return $resultDocContent;
2✔
833
            });
2✔
834
        }
835

836
        $this->driver->saveData();
4✔
837
    }
838

839
    public function getDocFileContent()
840
    {
841
        try {
842
            $documentation = $this->driver->getDocumentation();
45✔
843

844
            $this->openAPIValidator->validate($documentation);
44✔
845
        } catch (Throwable $exception) {
38✔
846
            $message = ($exception instanceof Exception)
38✔
847
                ? $exception->getMessage()
37✔
848
                : __('validation.unhandled_error_message');
1✔
849

850
            return $this->generateEmptyData(
38✔
851
                $this->config['defaults']['error'],
38✔
852
                [
38✔
853
                    'message' => $message,
38✔
854
                    'type' => $exception::class,
38✔
855
                    'error_place' => $this->getErrorPlace($exception),
38✔
856
                ]
38✔
857
            );
38✔
858
        }
859

860
        $additionalDocs = config('auto-doc.additional_paths', []);
7✔
861

862
        foreach ($additionalDocs as $filePath) {
7✔
863
            try {
864
                $additionalDocContent = $this->getOpenAPIFileContent(base_path($filePath));
4✔
865
            } catch (DocFileNotExistsException|EmptyDocFileException|InvalidSwaggerSpecException $exception) {
3✔
866
                report($exception);
3✔
867

868
                continue;
3✔
869
            }
870

871
            $this->mergeOpenAPIDocs($documentation, $additionalDocContent);
1✔
872
        }
873

874
        return $documentation;
7✔
875
    }
876

877
    protected function getErrorPlace(Throwable $exception): string
878
    {
879
        $errorPlaceInTrace = Arr::first($exception->getTrace());
39✔
880

881
        $errorPlaceInTrace = implode(', ', Arr::map(
39✔
882
            $errorPlaceInTrace,
39✔
883
            fn ($value, $key) => $key . '=' . (is_array($value) ? json_encode($value) : $value),
39✔
884
        ));
39✔
885

886
        return $errorPlaceInTrace;
39✔
887
    }
888

889
    protected function camelCaseToUnderScore($input): string
890
    {
891
        preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
23✔
892
        $ret = $matches[0];
23✔
893

894
        foreach ($ret as &$match) {
23✔
895
            $match = ($match === strtoupper($match)) ? strtolower($match) : lcfirst($match);
23✔
896
        }
897

898
        return implode('_', $ret);
23✔
899
    }
900

901
    protected function generateExample($properties): array
902
    {
903
        $parameters = $this->replaceObjectValues($this->request->all());
6✔
904
        $example = [];
6✔
905

906
        $this->replaceNullValues($parameters, $properties, $example);
6✔
907

908
        return $example;
6✔
909
    }
910

911
    protected function replaceObjectValues($parameters): array
912
    {
913
        $classNamesValues = [
6✔
914
            File::class => '[uploaded_file]',
6✔
915
        ];
6✔
916

917
        $parameters = Arr::dot($parameters);
6✔
918
        $returnParameters = [];
6✔
919

920
        foreach ($parameters as $parameter => $value) {
6✔
921
            if (is_object($value)) {
6✔
922
                $class = get_class($value);
1✔
923

924
                $value = Arr::get($classNamesValues, $class, $class);
1✔
925
            }
926

927
            Arr::set($returnParameters, $parameter, $value);
6✔
928
        }
929

930
        return $returnParameters;
6✔
931
    }
932

933
    protected function getClassAnnotations($class): array
934
    {
935
        $reflection = new ReflectionClass($class);
24✔
936

937
        $annotations = $reflection->getDocComment();
24✔
938

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

941
        $blocks = explode("\n", $annotations);
24✔
942

943
        $result = [];
24✔
944

945
        foreach ($blocks as $block) {
24✔
946
            if (Str::contains($block, '@')) {
24✔
947
                $index = strpos($block, '@');
1✔
948
                $block = substr($block, $index);
1✔
949
                $exploded = explode(' ', $block);
1✔
950

951
                $paramName = str_replace('@', '', array_shift($exploded));
1✔
952
                $paramValue = implode(' ', $exploded);
1✔
953

954
                if (in_array($paramName, $this->booleanAnnotations)) {
1✔
955
                    $paramValue = true;
1✔
956
                }
957

958
                $result[$paramName] = $paramValue;
1✔
959
            }
960
        }
961

962
        return $result;
24✔
963
    }
964

965
    /**
966
     * NOTE: All functions below are temporary solution for
967
     * this issue: https://github.com/OAI/OpenAPI-Specification/issues/229
968
     * We hope swagger developers will resolve this problem in next release of Swagger OpenAPI
969
     * */
970
    protected function replaceNullValues($parameters, $types, &$example)
971
    {
972
        foreach ($parameters as $parameter => $value) {
6✔
973
            if (is_null($value) && array_key_exists($parameter, $types)) {
6✔
974
                $example[$parameter] = $this->getDefaultValueByType($types[$parameter]['type']);
5✔
975
            } elseif (is_array($value)) {
6✔
976
                $this->replaceNullValues($value, $types, $example[$parameter]);
3✔
977
            } else {
978
                $example[$parameter] = $value;
6✔
979
            }
980
        }
981
    }
982

983
    protected function getDefaultValueByType($type)
984
    {
985
        $values = [
5✔
986
            'object' => 'null',
5✔
987
            'boolean' => false,
5✔
988
            'date' => "0000-00-00",
5✔
989
            'integer' => 0,
5✔
990
            'string' => '',
5✔
991
            'double' => 0
5✔
992
        ];
5✔
993

994
        return $values[$type];
5✔
995
    }
996

997
    protected function prepareInfo(?string $view = null, array $viewData = [], array $license = []): array
998
    {
999
        $info = [];
62✔
1000

1001
        $license = array_filter($license);
62✔
1002

1003
        if (!empty($license)) {
62✔
1004
            $info['license'] = $license;
×
1005
        }
1006

1007
        if (!empty($view)) {
62✔
1008
            $info['description'] = view($view, $viewData)->render();
61✔
1009
        }
1010

1011
        return array_merge($this->config['info'], $info);
62✔
1012
    }
1013

1014
    protected function getOpenAPIFileContent(string $filePath): array
1015
    {
1016
        if (!file_exists($filePath)) {
4✔
1017
            throw new DocFileNotExistsException($filePath);
2✔
1018
        }
1019

1020
        $fileContent = json_decode(file_get_contents($filePath), true);
2✔
1021

1022
        if (empty($fileContent)) {
2✔
UNCOV
1023
            throw new EmptyDocFileException($filePath);
×
1024
        }
1025

1026
        $this->openAPIValidator->validate($fileContent);
2✔
1027

1028
        return $fileContent;
1✔
1029
    }
1030

1031
    protected function mergeOpenAPIDocs(array &$documentation, array $additionalDocumentation): void
1032
    {
1033
        $paths = array_keys($additionalDocumentation['paths']);
3✔
1034

1035
        foreach ($paths as $path) {
3✔
1036
            $additionalDocPath = $additionalDocumentation['paths'][$path];
3✔
1037

1038
            if (empty($documentation['paths'][$path])) {
3✔
1039
                $documentation['paths'][$path] = $additionalDocPath;
3✔
1040
            } else {
1041
                $methods = array_keys($documentation['paths'][$path]);
1✔
1042
                $additionalDocMethods = array_keys($additionalDocPath);
1✔
1043

1044
                foreach ($additionalDocMethods as $method) {
1✔
1045
                    if (!in_array($method, $methods)) {
1✔
1046
                        $documentation['paths'][$path][$method] = $additionalDocPath[$method];
1✔
1047
                    }
1048
                }
1049
            }
1050
        }
1051

1052
        $definitions = array_keys($additionalDocumentation['components']['schemas']);
3✔
1053

1054
        foreach ($definitions as $definition) {
3✔
1055
            $documentation = Arr::add(
3✔
1056
                array: $documentation,
3✔
1057
                key: "components.schemas.{$definition}",
3✔
1058
                value: $additionalDocumentation['components']['schemas'][$definition],
3✔
1059
            );
3✔
1060
        }
1061
    }
1062
}
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