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

RonasIT / laravel-swagger / 28349344520

29 Jun 2026 04:49AM UTC coverage: 99.318% (-0.4%) from 99.671%
28349344520

Pull #193

github

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

224 of 228 new or added lines in 12 files covered. (98.25%)

1 existing line in 1 file now uncovered.

1020 of 1027 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 $requestSnapshotFactory,
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->requestSnapshotFactory->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
        $uri = $this->requestSnapshot->route->uri;
38✔
195
        $method = $this->requestSnapshot->route->httpMethod;
38✔
196

197
        if (empty(Arr::get($this->data, "paths.{$uri}.{$method}"))) {
38✔
198
            $this->data['paths'][$uri][$method] = [
36✔
199
                'tags' => [],
36✔
200
                'consumes' => [],
36✔
201
                'produces' => [],
36✔
202
                'parameters' => $this->getPathParams(),
36✔
203
                'responses' => [],
36✔
204
                'security' => [],
36✔
205
                'description' => '',
36✔
206
            ];
36✔
207
        }
208

209
        $this->item = &$this->data['paths'][$uri][$method];
38✔
210
    }
211

212
    protected function getPathParams(): array
213
    {
214
        $params = [];
36✔
215

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

218
        $params = Arr::collapse($params);
36✔
219

220
        $result = [];
36✔
221

222
        foreach ($params as $param) {
36✔
223
            $key = preg_replace('/[{}]/', '', $param);
6✔
224

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

236
        return $result;
36✔
237
    }
238

239
    protected function generatePathDescription(string $key): string
240
    {
241
        $expression = Arr::get($this->requestSnapshot->route->wheres, $key);
6✔
242

243
        if (empty($expression)) {
6✔
244
            return '';
6✔
245
        }
246

247
        $exploded = explode('|', $expression);
1✔
248

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

255
        return 'in: ' . implode(',', $exploded);
1✔
256
    }
257

258
    protected function parseRequest()
259
    {
260
        $this->saveConsume();
38✔
261
        $this->saveTags();
38✔
262
        $this->saveSecurity();
38✔
263

264
        $concreteRequest = $this->requestSnapshot->action->requestClass;
38✔
265

266
        if (empty($concreteRequest)) {
38✔
267
            $this->item['description'] = '';
4✔
268

269
            return;
4✔
270
        }
271

272
        $annotations = $this->requestSnapshot->requestData->annotations;
34✔
273

274
        $this->markAsDeprecated($annotations);
34✔
275
        $this->saveParameters($annotations);
34✔
276
        $this->saveDescription($concreteRequest, $annotations);
34✔
277
    }
278

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

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

289
        if (!empty($content) && array_is_list($content)) {
38✔
290
            $this->saveListResponseDefinitions($content, $schemaProperties);
17✔
291

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

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

303
    protected function saveListResponseDefinitions(array $content, array &$schemaProperties): void
304
    {
305
        $types = [];
17✔
306

307
        foreach ($content as $value) {
17✔
308
            $type = gettype($value);
17✔
309

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

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

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

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

330
            $schemaProperties[$name] = $property;
19✔
331
        }
332
    }
333

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

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

340
        if (is_null($produce)) {
38✔
341
            $produce = 'text/plain';
2✔
342
        }
343

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

348
        $responses = $this->item['responses'];
38✔
349

350
        $responseExampleLimitCount = config('auto-doc.response_example_limit_count');
38✔
351

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

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

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

366
            $content = Arr::except($content, $uselessKeys);
1✔
367
        }
368

369
        $code = $response->getStatusCode();
38✔
370

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

375
        $action = Str::ucfirst($this->getActionName());
38✔
376

377
        $definition = $this->requestSnapshot->action->resourceSchemaName
38✔
378
            ?? "{$this->requestSnapshot->route->httpMethod}{$action}{$code}ResponseObject";
30✔
379

380
        $this->saveResponseSchema($content, $definition);
38✔
381

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

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

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

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

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

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

430
        $actionName = $this->getActionName();
34✔
431

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

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

446
            $rules = collect(explode('|', $rules));
27✔
447

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

541
            $uselessRules = $this->ruleToTypeMap;
6✔
542
            $uselessRules['required'] = 'required';
6✔
543

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

548
            $rulesArray = array_flip(array_diff_key(array_flip($rulesArray), $uselessRules));
6✔
549

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

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

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

562
        $parameterType = 'string';
31✔
563

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

570
        return $parameterType;
29✔
571
    }
572

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

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

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

588
        return $description;
31✔
589
    }
590

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

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

598
        return $requestParametersCount > $objectParametersCount;
6✔
599
    }
600

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

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

609
        return empty($bodyParamExisted);
6✔
610
    }
611

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

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

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

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

630
        $tag = array_shift($explodedUri);
38✔
631

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

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

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

643
        $description = Arr::get($annotations, 'description');
34✔
644

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

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

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

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

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

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

676
        return $summary;
34✔
677
    }
678

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

685
        $underscoreRequestName = $this->camelCaseToUnderScore($summaryName);
33✔
686

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

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

694
        $request = $this->requestSnapshot->action->requestClass;
37✔
695

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

700
        $annotations = $this->requestSnapshot->requestData->annotations;
33✔
701

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

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

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

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

715
        return Str::camel($action);
38✔
716
    }
717

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

726
                $this->mergeOpenAPIDocs($resultDocContent, $this->data);
2✔
727

728
                return $resultDocContent;
2✔
729
            });
2✔
730
        }
731

732
        $this->driver->saveData();
4✔
733
    }
734

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

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

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

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

757
                continue;
3✔
758
            }
759

760
            $this->mergeOpenAPIDocs($documentation, $additionalDocContent);
1✔
761
        }
762

763
        return $documentation;
8✔
764
    }
765

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

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

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

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

795
        return $documentation;
7✔
796
    }
797

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

802
        Arr::forget($firstTraceEntry, 'type');
41✔
803

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

809
        return implode(PHP_EOL, $formattedTraceEntry);
41✔
810
    }
811

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

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

821
        return implode('_', $ret);
33✔
822
    }
823

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

829
        $this->replaceNullValues($parameters, $properties, $example);
6✔
830

831
        return $example;
6✔
832
    }
833

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

840
        $parameters = Arr::dot($parameters);
6✔
841
        $returnParameters = [];
6✔
842

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

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

850
            Arr::set($returnParameters, $parameter, $value);
6✔
851
        }
852

853
        return $returnParameters;
6✔
854
    }
855

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

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

885
        return $values[$type];
5✔
886
    }
887

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

892
        $license = array_filter($license);
66✔
893

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

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

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

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

911
        $fileContent = json_decode(file_get_contents($filePath), true);
3✔
912

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

917
        $this->openAPIValidator->validate($fileContent);
2✔
918

919
        return $fileContent;
1✔
920
    }
921

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

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

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

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

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