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

RonasIT / laravel-swagger / 27257748646

10 Jun 2026 06:29AM UTC coverage: 99.317% (-0.4%) from 99.671%
27257748646

Pull #193

github

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

222 of 226 new or added lines in 13 files covered. (98.23%)

1 existing line in 1 file now uncovered.

1018 of 1025 relevant lines covered (99.32%)

24.5 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 = (empty($this->requestSnapshot->action->resourceClass))
38✔
375
            ? "{$this->requestSnapshot->route->httpMethod}{$action}{$code}ResponseObject"
30✔
376
            : Str::replaceLast('Resource', '', class_basename($this->requestSnapshot->action->resourceClass));
8✔
377

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

560
        $parameterType = 'string';
31✔
561

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

568
        return $parameterType;
29✔
569
    }
570

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

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

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

586
        return $description;
31✔
587
    }
588

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

674
        return $summary;
34✔
675
    }
676

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

755
                continue;
3✔
756
            }
757

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

761
        return $documentation;
8✔
762
    }
763

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

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

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

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

793
        return $documentation;
7✔
794
    }
795

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

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

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

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

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

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

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

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

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

829
        return $example;
6✔
830
    }
831

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

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

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

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

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

851
        return $returnParameters;
6✔
852
    }
853

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

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

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

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

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

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

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

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

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

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

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

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

917
        return $fileContent;
1✔
918
    }
919

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

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

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

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

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