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

RonasIT / laravel-swagger / 27260021599

10 Jun 2026 07:19AM UTC coverage: 99.316% (-0.4%) from 99.671%
27260021599

Pull #193

github

web-flow
Merge 07e604ca2 into ead648e99
Pull Request #193: feat: name response object by resource class

221 of 225 new or added lines in 12 files covered. (98.22%)

1 existing line in 1 file now uncovered.

1017 of 1024 relevant lines covered (99.32%)

24.49 hits per line

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

99.77
/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 RonasIT\AutoDoc\Contracts\SwaggerDriverContract;
14
use RonasIT\AutoDoc\DTO\RequestSnapshot;
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\Support\Factories\RequestSnapshotFactory;
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
    public const string OPEN_API_VERSION = '3.1.0';
35

36
    protected $driver;
37

38
    protected $data;
39
    protected $config;
40
    protected RequestSnapshot $requestSnapshot;
41
    private $item;
42
    private $security;
43

44
    protected array $ruleToTypeMap = [
45
        'array' => 'object',
46
        'boolean' => 'boolean',
47
        'date' => 'date',
48
        'digits' => 'integer',
49
        'integer' => 'integer',
50
        'numeric' => 'double',
51
        'string' => 'string',
52
        'int' => 'integer',
53
    ];
54

55
    public function __construct(
56
        protected Container $container,
57
        protected RequestSnapshotFactory $snapshotFactory,
58
        protected SwaggerSpecValidator $openAPIValidator,
59
    ) {
60
        $this->initConfig();
115✔
61

62
        $this->setDriver();
110✔
63

64
        if (config('app.env') === 'testing') {
108✔
65
            // client must enter at least `contact.email` to generate a default `info` block
66
            // otherwise an exception will be called
67
            $this->checkEmail();
108✔
68

69
            $this->security = $this->config['security'];
107✔
70

71
            $this->data = $this->driver->getProcessTmpData();
107✔
72

73
            if (empty($this->data)) {
107✔
74
                $this->data = $this->generateEmptyData();
65✔
75

76
                $this->driver->saveProcessTmpData($this->data);
65✔
77
            }
78
        }
79
    }
80

81
    protected function initConfig()
82
    {
83
        $this->config = config('auto-doc');
115✔
84

85
        $version = Arr::get($this->config, 'config_version');
115✔
86

87
        if (empty($version)) {
115✔
88
            throw new LegacyConfigException();
1✔
89
        }
90

91
        $packageConfigs = require __DIR__ . '/../../config/auto-doc.php';
114✔
92

93
        if (version_compare($packageConfigs['config_version'], $version, '>')) {
114✔
94
            throw new LegacyConfigException();
1✔
95
        }
96

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

99
        if (!view()->exists("auto-doc::documentation-{$documentationViewer}")) {
113✔
100
            throw new UnsupportedDocumentationViewerException($documentationViewer);
2✔
101
        }
102

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

105
        if ($securityDriver && !Arr::exists(Arr::get($this->config, 'security_drivers'), $securityDriver)) {
111✔
106
            throw new WrongSecurityConfigException();
1✔
107
        }
108
    }
109

110
    protected function setDriver()
111
    {
112
        $driver = $this->config['driver'];
110✔
113
        $className = Arr::get($this->config, "drivers.{$driver}.class");
110✔
114

115
        if (!class_exists($className)) {
110✔
116
            throw new SwaggerDriverClassNotFoundException($className);
1✔
117
        } else {
118
            $this->driver = app($className);
109✔
119
        }
120

121
        if (!$this->driver instanceof SwaggerDriverContract) {
109✔
122
            throw new InvalidDriverClassException($driver);
1✔
123
        }
124
    }
125

126
    protected function generateEmptyData(?string $view = null, array $viewData = [], array $license = []): array
127
    {
128
        if (empty($view) && !empty($this->config['info'])) {
66✔
129
            $view = $this->config['info']['description'];
64✔
130
        }
131

132
        $data = [
66✔
133
            'openapi' => self::OPEN_API_VERSION,
66✔
134
            'servers' => [
66✔
135
                ['url' => URL::query($this->config['basePath'])],
66✔
136
            ],
66✔
137
            'paths' => [],
66✔
138
            'components' => [
66✔
139
                'schemas' => $this->config['definitions'],
66✔
140
            ],
66✔
141
            'info' => $this->prepareInfo($view, $viewData, $license),
66✔
142
        ];
66✔
143

144
        $securityDefinitions = $this->generateSecurityDefinition();
66✔
145

146
        if (!empty($securityDefinitions)) {
66✔
147
            $data['securityDefinitions'] = $securityDefinitions;
3✔
148
        }
149

150
        return $data;
66✔
151
    }
152

153
    protected function checkEmail(): void
154
    {
155
        if (!empty($this->config['info']) && !Arr::get($this->config, 'info.contact.email')) {
108✔
156
            throw new EmptyContactEmailException();
1✔
157
        }
158
    }
159

160
    protected function generateSecurityDefinition(): ?array
161
    {
162
        if (empty($this->security)) {
66✔
163
            return null;
63✔
164
        }
165

166
        return [
3✔
167
            $this->security => $this->generateSecurityDefinitionObject($this->security),
3✔
168
        ];
3✔
169
    }
170

171
    protected function generateSecurityDefinitionObject($type): array
172
    {
173
        return [
3✔
174
            'type' => $this->config['security_drivers'][$type]['type'],
3✔
175
            'name' => $this->config['security_drivers'][$type]['name'],
3✔
176
            'in' => $this->config['security_drivers'][$type]['in'],
3✔
177
        ];
3✔
178
    }
179

180
    public function addData(Request $request, $response)
181
    {
182
        $this->requestSnapshot = $this->snapshotFactory->make($request);
38✔
183

184
        $this->prepareItem();
38✔
185

186
        $this->parseRequest();
38✔
187
        $this->parseResponse($response);
38✔
188

189
        $this->driver->saveProcessTmpData($this->data);
38✔
190
    }
191

192
    protected function prepareItem()
193
    {
194
        if (empty(Arr::get($this->data, "paths.{$this->requestSnapshot->route->uri}.{$this->requestSnapshot->route->httpMethod}"))) {
38✔
195
            $this->data['paths'][$this->requestSnapshot->route->uri][$this->requestSnapshot->route->httpMethod] = [
36✔
196
                'tags' => [],
36✔
197
                'consumes' => [],
36✔
198
                'produces' => [],
36✔
199
                'parameters' => $this->getPathParams(),
36✔
200
                'responses' => [],
36✔
201
                'security' => [],
36✔
202
                'description' => '',
36✔
203
            ];
36✔
204
        }
205

206
        $this->item = &$this->data['paths'][$this->requestSnapshot->route->uri][$this->requestSnapshot->route->httpMethod];
38✔
207
    }
208

209
    protected function getPathParams(): array
210
    {
211
        $params = [];
36✔
212

213
        preg_match_all('/{[^}]*}/', $this->requestSnapshot->route->uri, $params);
36✔
214

215
        $params = Arr::collapse($params);
36✔
216

217
        $result = [];
36✔
218

219
        foreach ($params as $param) {
36✔
220
            $key = preg_replace('/[{}]/', '', $param);
6✔
221

222
            $result[] = [
6✔
223
                'in' => 'path',
6✔
224
                'name' => $key,
6✔
225
                'description' => $this->generatePathDescription($key),
6✔
226
                'required' => true,
6✔
227
                'schema' => [
6✔
228
                    'type' => 'string',
6✔
229
                ],
6✔
230
            ];
6✔
231
        }
232

233
        return $result;
36✔
234
    }
235

236
    protected function generatePathDescription(string $key): string
237
    {
238
        $expression = Arr::get($this->requestSnapshot->route->routeWheres, $key);
6✔
239

240
        if (empty($expression)) {
6✔
241
            return '';
6✔
242
        }
243

244
        $exploded = explode('|', $expression);
1✔
245

246
        foreach ($exploded as $value) {
1✔
247
            if (!preg_match('/^[a-zA-Z0-9\.]+$/', $value)) {
1✔
248
                return "regexp: {$expression}";
1✔
249
            }
250
        }
251

252
        return 'in: ' . implode(',', $exploded);
1✔
253
    }
254

255
    protected function parseRequest()
256
    {
257
        $this->saveConsume();
38✔
258
        $this->saveTags();
38✔
259
        $this->saveSecurity();
38✔
260

261
        $concreteRequest = $this->requestSnapshot->action->requestClass;
38✔
262

263
        if (empty($concreteRequest)) {
38✔
264
            $this->item['description'] = '';
4✔
265

266
            return;
4✔
267
        }
268

269
        $annotations = $this->requestSnapshot->requestData->annotations;
34✔
270

271
        $this->markAsDeprecated($annotations);
34✔
272
        $this->saveParameters($annotations);
34✔
273
        $this->saveDescription($concreteRequest, $annotations);
34✔
274
    }
275

276
    protected function markAsDeprecated(array $annotations)
277
    {
278
        $this->item['deprecated'] = Arr::get($annotations, 'deprecated', false);
34✔
279
    }
280

281
    protected function saveResponseSchema(?array $content, string $definition): void
282
    {
283
        $schemaProperties = [];
38✔
284
        $schemaType = 'object';
38✔
285

286
        if (!empty($content) && array_is_list($content)) {
38✔
287
            $this->saveListResponseDefinitions($content, $schemaProperties);
17✔
288

289
            $schemaType = 'array';
17✔
290
        } else {
291
            $this->saveObjectResponseDefinitions($content, $schemaProperties, $definition);
21✔
292
        }
293

294
        $this->data['components']['schemas'][$definition] = [
38✔
295
            'type' => $schemaType,
38✔
296
            'properties' => $schemaProperties,
38✔
297
        ];
38✔
298
    }
299

300
    protected function saveListResponseDefinitions(array $content, array &$schemaProperties): void
301
    {
302
        $types = [];
17✔
303

304
        foreach ($content as $value) {
17✔
305
            $type = gettype($value);
17✔
306

307
            if (!in_array($type, $types)) {
17✔
308
                $types[] = $type;
17✔
309
                $schemaProperties['items']['allOf'][]['type'] = $type;
17✔
310
            }
311
        }
312
    }
313

314
    protected function saveObjectResponseDefinitions(array $content, array &$schemaProperties, string $definition): void
315
    {
316
        $properties = Arr::get($this->data, "components.schemas.{$definition}", []);
21✔
317

318
        foreach ($content as $name => $value) {
21✔
319
            $property = Arr::get($properties, "properties.{$name}", []);
19✔
320

321
            if (is_null($value)) {
19✔
322
                $property['nullable'] = true;
2✔
323
            } else {
324
                $property['type'] = gettype($value);
19✔
325
            }
326

327
            $schemaProperties[$name] = $property;
19✔
328
        }
329
    }
330

331
    protected function parseResponse($response)
332
    {
333
        $produceList = $this->data['paths'][$this->requestSnapshot->route->uri][$this->requestSnapshot->route->httpMethod]['produces'];
38✔
334

335
        $produce = $response->headers->get('Content-type');
38✔
336

337
        if (is_null($produce)) {
38✔
338
            $produce = 'text/plain';
2✔
339
        }
340

341
        if (!in_array($produce, $produceList)) {
38✔
342
            $this->item['produces'][] = $produce;
36✔
343
        }
344

345
        $responses = $this->item['responses'];
38✔
346

347
        $responseExampleLimitCount = config('auto-doc.response_example_limit_count');
38✔
348

349
        $content = json_decode($response->getContent(), true) ?? [];
38✔
350

351
        if (!empty($responseExampleLimitCount)) {
38✔
352
            if (!empty($content['data'])) {
38✔
353
                $limitedResponseData = array_slice($content['data'], 0, $responseExampleLimitCount, true);
4✔
354
                $content['data'] = $limitedResponseData;
4✔
355
                $content['to'] = count($limitedResponseData);
4✔
356
                $content['total'] = count($limitedResponseData);
4✔
357
            }
358
        }
359

360
        if (!empty($content['exception'])) {
38✔
361
            $uselessKeys = array_keys(Arr::except($content, ['message']));
1✔
362

363
            $content = Arr::except($content, $uselessKeys);
1✔
364
        }
365

366
        $code = $response->getStatusCode();
38✔
367

368
        if (!array_key_exists($code, $responses)) {
38✔
369
            $this->saveExample($code, json_encode($content, JSON_PRETTY_PRINT), $produce);
37✔
370
        }
371

372
        $action = Str::ucfirst($this->getActionName());
38✔
373

374
        $definition = $this->requestSnapshot->action->resourceSchemaName
38✔
375
            ?? "{$this->requestSnapshot->route->httpMethod}{$action}{$code}ResponseObject";
30✔
376

377
        $this->saveResponseSchema($content, $definition);
38✔
378

379
        if (is_array($this->item['responses'][$code])) {
38✔
380
            $this->item['responses'][$code]['content'][$produce]['schema']['$ref'] = "#/components/schemas/{$definition}";
37✔
381
        }
382
    }
383

384
    protected function saveExample($code, $content, $produce)
385
    {
386
        $description = $this->getResponseDescription($code);
37✔
387
        $availableContentTypes = [
37✔
388
            'application',
37✔
389
            'text',
37✔
390
            'image',
37✔
391
        ];
37✔
392
        $explodedContentType = explode('/', $produce);
37✔
393

394
        if (in_array($explodedContentType[0], $availableContentTypes)) {
37✔
395
            $this->item['responses'][$code] = $this->makeResponseExample($content, $produce, $description);
36✔
396
        } else {
397
            $this->item['responses'][$code] = '*Unavailable for preview*';
1✔
398
        }
399
    }
400

401
    protected function makeResponseExample($content, $mimeType, $description = ''): array
402
    {
403
        $example = match ($mimeType) {
36✔
404
            'application/json' => json_decode($content, true),
32✔
405
            'application/pdf' => base64_encode($content),
1✔
406
            default => $content,
3✔
407
        };
408

409
        return [
36✔
410
            'description' => $description,
36✔
411
            'content' => [
36✔
412
                $mimeType => [
36✔
413
                    'schema' => [
36✔
414
                        'type' => 'object',
36✔
415
                    ],
36✔
416
                    'example' => $example,
36✔
417
                ],
36✔
418
            ],
36✔
419
        ];
36✔
420
    }
421

422
    protected function saveParameters(array $annotations)
423
    {
424
        $rules = $this->requestSnapshot->requestData->rules;
34✔
425
        $attributes = $this->requestSnapshot->requestData->attributes;
34✔
426

427
        $actionName = $this->getActionName();
34✔
428

429
        if (in_array($this->requestSnapshot->route->httpMethod, ['get', 'delete'])) {
34✔
430
            $this->saveGetRequestParameters($rules, $attributes, $annotations);
28✔
431
        } else {
432
            $this->savePostRequestParameters($actionName, $rules, $attributes, $annotations);
6✔
433
        }
434
    }
435

436
    protected function saveGetRequestParameters($validation, array $attributes, array $annotations)
437
    {
438
        foreach ($validation as $parameter => $rules) {
28✔
439
            if (Arr::exists($validation, "{$parameter}.*")) {
27✔
440
                continue;
1✔
441
            }
442

443
            $rules = collect(explode('|', $rules));
27✔
444

445
            if ($this->isArrayItemParameter($parameter, $rules)) {
27✔
446
                $this->saveListParameters(Str::remove('.*', $parameter), $rules, $attributes, $annotations);
1✔
447
            } else {
448
                $this->saveQueryParameter($parameter, $rules, $attributes, $annotations);
27✔
449
            }
450
        }
451
    }
452

453
    protected function isArrayItemParameter(string $parameter, Collection $rules): bool
454
    {
455
        return Str::endsWith($parameter, '.*')
27✔
456
            && $rules->contains(fn ($rule) => Str::startsWith($rule, 'in:'));
27✔
457
    }
458

459
    protected function saveListParameters(string $parameter, Collection $rules, array $attributes, array $annotations): void
460
    {
461
        $inRule = $rules->first(fn ($rule) => Str::startsWith($rule, 'in:'));
1✔
462
        $availableValues = Str::after($inRule, 'in:');
1✔
463
        $availableValues = explode(',', $availableValues);
1✔
464

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

467
        foreach ($availableValues as $value) {
1✔
468
            $this->saveQueryParameter("{$parameter}[]", $filteredRules, $attributes, $annotations, $value);
1✔
469
        }
470
    }
471

472
    protected function saveQueryParameter(string $parameter, Collection $rules, array $attributes, array $annotations, ?string $example = null): void
473
    {
474
        $existedParameter = Arr::first(
27✔
475
            $this->item['parameters'],
27✔
476
            fn ($existedParameter) => $existedParameter['name'] === $parameter && Arr::get($existedParameter, 'example') === $example,
27✔
477
        );
27✔
478

479
        if (!empty($existedParameter)) {
27✔
480
            return;
2✔
481
        }
482

483
        $parameterDefinition = [
25✔
484
            'in' => 'query',
25✔
485
            'name' => $parameter,
25✔
486
            'description' => $this->generateDescription($parameter, $rules->all(), $attributes, $annotations),
25✔
487
            'schema' => [
25✔
488
                'type' => $this->getParameterType($rules->all()),
25✔
489
            ],
25✔
490
        ];
25✔
491

492
        if ($rules->contains('required')) {
25✔
493
            $parameterDefinition['required'] = true;
25✔
494
        }
495

496
        if (!is_null($example)) {
25✔
497
            $parameterDefinition['example'] = $example;
1✔
498
        }
499

500
        $this->item['parameters'][] = $parameterDefinition;
25✔
501
    }
502

503
    protected function savePostRequestParameters($actionName, $rules, array $attributes, array $annotations)
504
    {
505
        if ($this->requestHasMoreProperties($actionName)) {
6✔
506
            if ($this->requestHasBody()) {
6✔
507
                $type = $this->requestSnapshot->requestData->contentType ?? 'application/json';
6✔
508

509
                $this->item['requestBody'] = [
6✔
510
                    'content' => [
6✔
511
                        $type => [
6✔
512
                            'schema' => [
6✔
513
                                '$ref' => "#/components/schemas/{$actionName}Object",
6✔
514
                            ],
6✔
515
                        ],
6✔
516
                    ],
6✔
517
                    'description' => '',
6✔
518
                    'required' => true,
6✔
519
                ];
6✔
520
            }
521

522
            $this->saveDefinitions($actionName, $rules, $attributes, $annotations);
6✔
523
        }
524
    }
525

526
    protected function saveDefinitions($objectName, $rules, $attributes, array $annotations)
527
    {
528
        $data = [
6✔
529
            'type' => 'object',
6✔
530
            'properties' => [],
6✔
531
        ];
6✔
532

533
        foreach ($rules as $parameter => $rule) {
6✔
534
            $rulesArray = (is_array($rule)) ? $rule : explode('|', $rule);
6✔
535
            $parameterType = $this->getParameterType($rulesArray);
6✔
536
            $this->saveParameterType($data, $parameter, $parameterType);
6✔
537

538
            $uselessRules = $this->ruleToTypeMap;
6✔
539
            $uselessRules['required'] = 'required';
6✔
540

541
            if (in_array('required', $rulesArray)) {
6✔
542
                $data['required'][] = $parameter;
6✔
543
            }
544

545
            $rulesArray = array_flip(array_diff_key(array_flip($rulesArray), $uselessRules));
6✔
546

547
            $data['properties'][$parameter]['description'] = $this->generateDescription($parameter, $rulesArray, $attributes, $annotations);
6✔
548
        }
549

550
        $data['example'] = $this->generateExample($data['properties']);
6✔
551
        $this->data['components']['schemas']["{$objectName}Object"] = $data;
6✔
552
    }
553

554
    protected function getParameterType(array $validation): string
555
    {
556
        $validationRules = $this->ruleToTypeMap;
31✔
557
        $validationRules['email'] = 'string';
31✔
558

559
        $parameterType = 'string';
31✔
560

561
        foreach ($validation as $item) {
31✔
562
            if (in_array($item, array_keys($validationRules))) {
31✔
563
                return $validationRules[$item];
30✔
564
            }
565
        }
566

567
        return $parameterType;
29✔
568
    }
569

570
    protected function saveParameterType(&$data, $parameter, $parameterType)
571
    {
572
        $data['properties'][$parameter] = [
6✔
573
            'type' => $parameterType,
6✔
574
        ];
6✔
575
    }
576

577
    protected function generateDescription(string $parameter, array $rules, array $attributes, array $annotations): string
578
    {
579
        $description = Arr::get($annotations, $parameter);
31✔
580

581
        if (empty($description)) {
31✔
582
            $description = Arr::get($attributes, $parameter, implode(', ', $rules));
31✔
583
        }
584

585
        return $description;
31✔
586
    }
587

588
    protected function requestHasMoreProperties($actionName): bool
589
    {
590
        $requestParametersCount = count($this->requestSnapshot->requestData->payload);
6✔
591

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

595
        return $requestParametersCount > $objectParametersCount;
6✔
596
    }
597

598
    protected function requestHasBody(): bool
599
    {
600
        $parameters = $this->data['paths'][$this->requestSnapshot->route->uri][$this->requestSnapshot->route->httpMethod]['parameters'];
6✔
601

602
        $bodyParamExisted = Arr::where($parameters, function ($value) {
6✔
603
            return $value['name'] === 'body';
1✔
604
        });
6✔
605

606
        return empty($bodyParamExisted);
6✔
607
    }
608

609
    public function saveConsume()
610
    {
611
        $consumeList = $this->data['paths'][$this->requestSnapshot->route->uri][$this->requestSnapshot->route->httpMethod]['consumes'];
38✔
612
        $consume = $this->requestSnapshot->requestData->contentType;
38✔
613

614
        if (!empty($consume) && !in_array($consume, $consumeList)) {
38✔
615
            $this->item['consumes'][] = $consume;
18✔
616
        }
617
    }
618

619
    public function saveTags()
620
    {
621
        $globalPrefix = config('auto-doc.global_prefix');
38✔
622
        $globalPrefix = Str::after($globalPrefix, '/');
38✔
623

624
        $explodedUri = explode('/', $this->requestSnapshot->route->uri);
38✔
625
        $explodedUri = array_filter($explodedUri);
38✔
626

627
        $tag = array_shift($explodedUri);
38✔
628

629
        if ($globalPrefix === $tag) {
38✔
630
            $tag = array_shift($explodedUri);
2✔
631
        }
632

633
        $this->item['tags'] = [$tag];
38✔
634
    }
635

636
    public function saveDescription($request, array $annotations)
637
    {
638
        $this->item['summary'] = $this->getSummary($request, $annotations);
34✔
639

640
        $description = Arr::get($annotations, 'description');
34✔
641

642
        if (!empty($description)) {
34✔
643
            $this->item['description'] = $description;
1✔
644
        }
645
    }
646

647
    protected function saveSecurity()
648
    {
649
        if ($this->requestSnapshot->hasSecurityToken) {
38✔
650
            $this->addSecurityToOperation();
5✔
651
        }
652
    }
653

654
    protected function addSecurityToOperation()
655
    {
656
        $security = &$this->data['paths'][$this->requestSnapshot->route->uri][$this->requestSnapshot->route->httpMethod]['security'];
5✔
657

658
        if (empty($security)) {
5✔
659
            $security[] = [
5✔
660
                "{$this->security}" => [],
5✔
661
            ];
5✔
662
        }
663
    }
664

665
    protected function getSummary($request, array $annotations)
666
    {
667
        $summary = Arr::get($annotations, 'summary');
34✔
668

669
        if (empty($summary)) {
34✔
670
            $summary = $this->parseRequestName($request);
33✔
671
        }
672

673
        return $summary;
34✔
674
    }
675

676
    protected function parseRequestName($request)
677
    {
678
        $explodedRequest = explode('\\', $request);
33✔
679
        $requestName = array_pop($explodedRequest);
33✔
680
        $summaryName = str_replace('Request', '', $requestName);
33✔
681

682
        $underscoreRequestName = $this->camelCaseToUnderScore($summaryName);
33✔
683

684
        return preg_replace('/[_]/', ' ', $underscoreRequestName);
33✔
685
    }
686

687
    protected function getResponseDescription($code)
688
    {
689
        $defaultDescription = Response::$statusTexts[$code];
37✔
690

691
        $request = $this->requestSnapshot->action->requestClass;
37✔
692

693
        if (empty($request)) {
37✔
694
            return $defaultDescription;
4✔
695
        }
696

697
        $annotations = $this->requestSnapshot->requestData->annotations;
33✔
698

699
        $localDescription = Arr::get($annotations, "_{$code}");
33✔
700

701
        if (!empty($localDescription)) {
33✔
702
            return $localDescription;
1✔
703
        }
704

705
        return Arr::get($this->config, "defaults.code-descriptions.{$code}", $defaultDescription);
32✔
706
    }
707

708
    protected function getActionName(): string
709
    {
710
        $action = str_replace('/', '', $this->requestSnapshot->route->uri);
38✔
711

712
        return Str::camel($action);
38✔
713
    }
714

715
    public function saveProductionData()
716
    {
717
        if (ParallelTesting::token()) {
4✔
718
            $this->driver->appendProcessDataToTmpFile(function (?array $sharedTmpData) {
2✔
719
                $resultDocContent = (empty($sharedTmpData))
2✔
720
                    ? $this->generateEmptyData($this->config['info']['description'])
1✔
721
                    : $sharedTmpData;
1✔
722

723
                $this->mergeOpenAPIDocs($resultDocContent, $this->data);
2✔
724

725
                return $resultDocContent;
2✔
726
            });
2✔
727
        }
728

729
        $this->driver->saveData();
4✔
730
    }
731

732
    public function getDocFileContent()
733
    {
734
        try {
735
            $documentation = $this->driver->getDocumentation();
49✔
736

737
            $this->openAPIValidator->validate($documentation);
47✔
738
        } catch (Throwable $exception) {
41✔
739
            return $this->generateEmptyData($this->config['defaults']['error'], [
41✔
740
                'message' => $exception->getMessage(),
41✔
741
                'type' => $exception::class,
41✔
742
                'error_place' => $this->getErrorPlace($exception),
41✔
743
            ]);
41✔
744
        }
745

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

748
        foreach ($additionalDocs as $filePath) {
8✔
749
            try {
750
                $additionalDocContent = $this->getOpenAPIFileContent(base_path($filePath));
4✔
751
            } catch (DocFileNotExistsException|EmptyDocFileException|InvalidSwaggerSpecException $exception) {
3✔
752
                report($exception);
3✔
753

754
                continue;
3✔
755
            }
756

757
            $this->mergeOpenAPIDocs($documentation, $additionalDocContent);
1✔
758
        }
759

760
        return $documentation;
8✔
761
    }
762

763
    public function getPrettyDocFileContent(): array
764
    {
765
        $documentation = $this->getDocFileContent();
7✔
766

767
        foreach ($documentation['paths'] as $path => $pathItem) {
7✔
768
            foreach ($pathItem as $method => $operation) {
7✔
769
                if (Arr::has($operation, 'parameters')) {
7✔
770
                    $documentation['paths'][$path][$method]['parameters'] = collect($operation['parameters'])
7✔
771
                        ->groupBy('name')
7✔
772
                        ->map(function ($params, $name) {
7✔
773
                            if ($params->count() === 1 || !Str::endsWith($name, '[]')) {
7✔
774
                                return $params->first();
1✔
775
                            }
776

777
                            $base = $params->first();
7✔
778
                            $base['schema']['enum'] = $params
7✔
779
                                ->pluck('example')
7✔
780
                                ->filter(fn ($value) => !is_null($value))
7✔
781
                                ->values()
7✔
782
                                ->all();
7✔
783

784
                            return $base;
7✔
785
                        })
7✔
786
                        ->values()
7✔
787
                        ->all();
7✔
788
                }
789
            }
790
        }
791

792
        return $documentation;
7✔
793
    }
794

795
    protected function getErrorPlace(Throwable $exception): string
796
    {
797
        $firstTraceEntry = Arr::first($exception->getTrace());
41✔
798

799
        Arr::forget($firstTraceEntry, 'type');
41✔
800

801
        $formattedTraceEntry = Arr::map(
41✔
802
            array: $firstTraceEntry,
41✔
803
            callback: fn ($value, $key) => $key . '=' . (is_array($value) ? json_encode($value) : $value),
41✔
804
        );
41✔
805

806
        return implode(PHP_EOL, $formattedTraceEntry);
41✔
807
    }
808

809
    protected function camelCaseToUnderScore($input): string
810
    {
811
        preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
33✔
812
        $ret = $matches[0];
33✔
813

814
        foreach ($ret as &$match) {
33✔
815
            $match = ($match === strtoupper($match)) ? strtolower($match) : lcfirst($match);
33✔
816
        }
817

818
        return implode('_', $ret);
33✔
819
    }
820

821
    protected function generateExample($properties): array
822
    {
823
        $parameters = $this->replaceObjectValues($this->requestSnapshot->requestData->payload);
6✔
824
        $example = [];
6✔
825

826
        $this->replaceNullValues($parameters, $properties, $example);
6✔
827

828
        return $example;
6✔
829
    }
830

831
    protected function replaceObjectValues($parameters): array
832
    {
833
        $classNamesValues = [
6✔
834
            File::class => '[uploaded_file]',
6✔
835
        ];
6✔
836

837
        $parameters = Arr::dot($parameters);
6✔
838
        $returnParameters = [];
6✔
839

840
        foreach ($parameters as $parameter => $value) {
6✔
841
            if (is_object($value)) {
6✔
842
                $class = get_class($value);
1✔
843

844
                $value = Arr::get($classNamesValues, $class, $class);
1✔
845
            }
846

847
            Arr::set($returnParameters, $parameter, $value);
6✔
848
        }
849

850
        return $returnParameters;
6✔
851
    }
852

853
    /**
854
     * NOTE: All functions below are temporary solution for
855
     * this issue: https://github.com/OAI/OpenAPI-Specification/issues/229
856
     * We hope swagger developers will resolve this problem in next release of Swagger OpenAPI
857
     * */
858
    protected function replaceNullValues($parameters, $types, &$example)
859
    {
860
        foreach ($parameters as $parameter => $value) {
6✔
861
            if (is_null($value) && Arr::exists($types, $parameter)) {
6✔
862
                $example[$parameter] = $this->getDefaultValueByType($types[$parameter]['type']);
5✔
863
            } elseif (is_array($value)) {
6✔
864
                $this->replaceNullValues($value, $types, $example[$parameter]);
3✔
865
            } else {
866
                $example[$parameter] = $value;
6✔
867
            }
868
        }
869
    }
870

871
    protected function getDefaultValueByType($type)
872
    {
873
        $values = [
5✔
874
            'object' => 'null',
5✔
875
            'boolean' => false,
5✔
876
            'date' => '0000-00-00',
5✔
877
            'integer' => 0,
5✔
878
            'string' => '',
5✔
879
            'double' => 0,
5✔
880
        ];
5✔
881

882
        return $values[$type];
5✔
883
    }
884

885
    protected function prepareInfo(?string $view = null, array $viewData = [], array $license = []): array
886
    {
887
        $info = [];
66✔
888

889
        $license = array_filter($license);
66✔
890

891
        if (!empty($license)) {
66✔
UNCOV
892
            $info['license'] = $license;
×
893
        }
894

895
        if (!empty($view)) {
66✔
896
            $info['description'] = view($view, $viewData)->render();
65✔
897
        }
898

899
        return array_merge($this->config['info'], $info);
66✔
900
    }
901

902
    protected function getOpenAPIFileContent(string $filePath): array
903
    {
904
        if (!file_exists($filePath)) {
4✔
905
            throw new DocFileNotExistsException($filePath);
1✔
906
        }
907

908
        $fileContent = json_decode(file_get_contents($filePath), true);
3✔
909

910
        if (empty($fileContent)) {
3✔
911
            throw new EmptyDocFileException($filePath);
1✔
912
        }
913

914
        $this->openAPIValidator->validate($fileContent);
2✔
915

916
        return $fileContent;
1✔
917
    }
918

919
    protected function mergeOpenAPIDocs(array &$documentation, array $additionalDocumentation): void
920
    {
921
        $paths = array_keys($additionalDocumentation['paths']);
3✔
922

923
        foreach ($paths as $path) {
3✔
924
            $additionalDocPath = $additionalDocumentation['paths'][$path];
3✔
925

926
            if (empty($documentation['paths'][$path])) {
3✔
927
                $documentation['paths'][$path] = $additionalDocPath;
3✔
928
            } else {
929
                $methods = array_keys($documentation['paths'][$path]);
1✔
930
                $additionalDocMethods = array_keys($additionalDocPath);
1✔
931

932
                foreach ($additionalDocMethods as $method) {
1✔
933
                    if (!in_array($method, $methods)) {
1✔
934
                        $documentation['paths'][$path][$method] = $additionalDocPath[$method];
1✔
935
                    }
936
                }
937
            }
938
        }
939

940
        foreach (Arr::get($additionalDocumentation, 'components.schemas', []) as $definitionName => $definitionData) {
3✔
941
            if (empty($documentation['components']['schemas'][$definitionName])) {
3✔
942
                $documentation['components']['schemas'][$definitionName] = $definitionData;
3✔
943
            }
944
        }
945
    }
946
}
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