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

RonasIT / laravel-swagger / 23838557373

01 Apr 2026 08:06AM UTC coverage: 99.665% (+0.009%) from 99.656%
23838557373

Pull #198

github

web-flow
Merge fcb06db78 into 4818bc24b
Pull Request #198: feat: expand with[] and with_count[] into separate query params per relation

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

1 existing line in 1 file now uncovered.

892 of 895 relevant lines covered (99.66%)

21.48 hits per line

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

99.8
/src/Services/SwaggerService.php
1
<?php
2

3
namespace RonasIT\AutoDoc\Services;
4

5
use Illuminate\Container\Container;
6
use Illuminate\Http\Request;
7
use Illuminate\Http\Testing\File;
8
use Illuminate\Support\Arr;
9
use Illuminate\Support\Facades\ParallelTesting;
10
use Illuminate\Support\Facades\URL;
11
use Illuminate\Support\Str;
12
use ReflectionClass;
13
use RonasIT\AutoDoc\Contracts\SwaggerDriverContract;
14
use RonasIT\AutoDoc\Enums\RelationQueryParam;
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\Traits\GetDependenciesTrait;
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
    use GetDependenciesTrait;
35

36
    public const string OPEN_API_VERSION = '3.1.0';
37

38
    protected $driver;
39
    protected $openAPIValidator;
40

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

158
        $securityDefinitions = $this->generateSecurityDefinition();
63✔
159

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

164
        return $data;
63✔
165
    }
166

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

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

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

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

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

198
        $this->prepareItem();
28✔
199

200
        $this->parseRequest();
28✔
201
        $this->parseResponse($response);
28✔
202

203
        $this->driver->saveProcessTmpData($this->data);
28✔
204
    }
205

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

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

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

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

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

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

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

241
        $params = Arr::collapse($params);
27✔
242

243
        $result = [];
27✔
244

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

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

259
        return $result;
27✔
260
    }
261

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

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

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

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

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

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

287
        $concreteRequest = $this->getConcreteRequest();
28✔
288

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

292
            return;
3✔
293
        }
294

295
        $annotations = $this->getClassAnnotations($concreteRequest);
25✔
296

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

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

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

312
        if (!empty($content) && array_is_list($content)) {
28✔
313
            $this->saveListResponseDefinitions($content, $schemaProperties);
17✔
314

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

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

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

330
        foreach ($content as $value) {
17✔
331
            $type = gettype($value);
17✔
332

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

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

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

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

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

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

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

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

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

371
        $responses = $this->item['responses'];
28✔
372

373
        $responseExampleLimitCount = config('auto-doc.response_example_limit_count');
28✔
374

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

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

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

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

392
        $code = $response->getStatusCode();
28✔
393

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

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

405
        $this->saveResponseSchema($content, $definition);
28✔
406

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

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

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

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

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

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

458
        $actionName = $this->getActionName($this->uri);
25✔
459

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

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

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

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

483
        return $preparedRules;
24✔
484
    }
485

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

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

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

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

500
        return $rule;
24✔
501
    }
502

503
    protected function saveGetRequestParameters($rules, array $attributes, array $annotations)
504
    {
505
        foreach ($rules as $parameter => $rule) {
19✔
506
            if (in_array($parameter, RelationQueryParam::values())) {
18✔
507
                continue;
1✔
508
            }
509

510
            $validation = explode('|', $rule);
18✔
511

512
            if ($this->isRelationArrayItemParameter($parameter)) {
18✔
513
                $this->saveRelationParameters(substr($parameter, 0, -2), $validation, $attributes, $annotations);
1✔
514

515
                continue;
1✔
516
            }
517

518
            $this->saveQueryParameter($parameter, $validation, $attributes, $annotations);
18✔
519
        }
520
    }
521

522
    protected function isRelationArrayItemParameter(string $parameter): bool
523
    {
524
        if (!str_ends_with($parameter, '.*')) {
18✔
525
            return false;
18✔
526
        }
527

528
        return in_array(substr($parameter, 0, -2), RelationQueryParam::values());
1✔
529
    }
530

531
    protected function saveRelationParameters(string $parameter, array $validation, array $attributes, array $annotations): void
532
    {
533
        $filteredValidation = [];
1✔
534
        $enumValues = [];
1✔
535

536
        foreach ($validation as $rule) {
1✔
537
            if ($rule === 'required') {
1✔
538
                continue;
1✔
539
            }
540

541
            if (str_starts_with($rule, 'in:')) {
1✔
542
                $enumValues = explode(',', substr($rule, 3));
1✔
543
            }
544

545
            $filteredValidation[] = $rule;
1✔
546
        }
547

548
        foreach ($enumValues as $value) {
1✔
549
            $this->saveQueryParameter("{$parameter}[]", $filteredValidation, $attributes, $annotations, example: $value);
1✔
550
        }
551
    }
552

553
    protected function saveQueryParameter(string $parameter, array $validation, array $attributes, array $annotations, ?string $example = null): void
554
    {
555
        $existedParameter = Arr::first(
18✔
556
            $this->item['parameters'],
18✔
557
            fn ($existedParameter) => $existedParameter['name'] === $parameter && ($existedParameter['example'] ?? null) === $example,
18✔
558
        );
18✔
559

560
        if (!empty($existedParameter)) {
18✔
561
            return;
1✔
562
        }
563

564
        $description = Arr::get($annotations, $parameter)
17✔
565
            ?: Arr::get($attributes, $parameter, implode(', ', $validation));
17✔
566

567
        $parameterDefinition = [
17✔
568
            'in' => 'query',
17✔
569
            'name' => $parameter,
17✔
570
            'description' => $description,
17✔
571
            'schema' => [
17✔
572
                'type' => $this->getParameterType($validation),
17✔
573
            ],
17✔
574
        ];
17✔
575

576
        if (in_array('required', $validation)) {
17✔
577
            $parameterDefinition['required'] = true;
17✔
578
        }
579

580
        if ($example !== null) {
17✔
581
            $parameterDefinition['example'] = $example;
1✔
582
        }
583

584
        $this->item['parameters'][] = $parameterDefinition;
17✔
585
    }
586

587
    protected function savePostRequestParameters($actionName, $rules, array $attributes, array $annotations)
588
    {
589
        if ($this->requestHasMoreProperties($actionName)) {
6✔
590
            if ($this->requestHasBody()) {
6✔
591
                $type = $this->request->header('Content-Type', 'application/json');
6✔
592

593
                $this->item['requestBody'] = [
6✔
594
                    'content' => [
6✔
595
                        $type => [
6✔
596
                            'schema' => [
6✔
597
                                '$ref' => "#/components/schemas/{$actionName}Object",
6✔
598
                            ],
6✔
599
                        ],
6✔
600
                    ],
6✔
601
                    'description' => '',
6✔
602
                    'required' => true,
6✔
603
                ];
6✔
604
            }
605

606
            $this->saveDefinitions($actionName, $rules, $attributes, $annotations);
6✔
607
        }
608
    }
609

610
    protected function saveDefinitions($objectName, $rules, $attributes, array $annotations)
611
    {
612
        $data = [
6✔
613
            'type' => 'object',
6✔
614
            'properties' => [],
6✔
615
        ];
6✔
616

617
        foreach ($rules as $parameter => $rule) {
6✔
618
            $rulesArray = (is_array($rule)) ? $rule : explode('|', $rule);
6✔
619
            $parameterType = $this->getParameterType($rulesArray);
6✔
620
            $this->saveParameterType($data, $parameter, $parameterType);
6✔
621

622
            $uselessRules = $this->ruleToTypeMap;
6✔
623
            $uselessRules['required'] = 'required';
6✔
624

625
            if (in_array('required', $rulesArray)) {
6✔
626
                $data['required'][] = $parameter;
6✔
627
            }
628

629
            $rulesArray = array_flip(array_diff_key(array_flip($rulesArray), $uselessRules));
6✔
630

631
            $this->saveParameterDescription($data, $parameter, $rulesArray, $attributes, $annotations);
6✔
632
        }
633

634
        $data['example'] = $this->generateExample($data['properties']);
6✔
635
        $this->data['components']['schemas']["{$objectName}Object"] = $data;
6✔
636
    }
637

638
    protected function getParameterType(array $validation): string
639
    {
640
        $validationRules = $this->ruleToTypeMap;
23✔
641
        $validationRules['email'] = 'string';
23✔
642

643
        $parameterType = 'string';
23✔
644

645
        foreach ($validation as $item) {
23✔
646
            if (in_array($item, array_keys($validationRules))) {
23✔
647
                return $validationRules[$item];
22✔
648
            }
649
        }
650

651
        return $parameterType;
21✔
652
    }
653

654
    protected function saveParameterType(&$data, $parameter, $parameterType)
655
    {
656
        $data['properties'][$parameter] = [
6✔
657
            'type' => $parameterType,
6✔
658
        ];
6✔
659
    }
660

661
    protected function saveParameterDescription(
662
        array &$data,
663
        string $parameter,
664
        array $rulesArray,
665
        array $attributes,
666
        array $annotations,
667
    ) {
668
        $description = Arr::get($annotations, $parameter);
6✔
669

670
        if (empty($description)) {
6✔
671
            $description = Arr::get($attributes, $parameter, implode(', ', $rulesArray));
6✔
672
        }
673

674
        $data['properties'][$parameter]['description'] = $description;
6✔
675
    }
676

677
    protected function requestHasMoreProperties($actionName): bool
678
    {
679
        $requestParametersCount = count($this->request->all());
6✔
680

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

684
        return $requestParametersCount > $objectParametersCount;
6✔
685
    }
686

687
    protected function requestHasBody(): bool
688
    {
689
        $parameters = $this->data['paths'][$this->uri][$this->method]['parameters'];
6✔
690

691
        $bodyParamExisted = Arr::where($parameters, function ($value) {
6✔
692
            return $value['name'] === 'body';
1✔
693
        });
6✔
694

695
        return empty($bodyParamExisted);
6✔
696
    }
697

698
    public function getConcreteRequest()
699
    {
700
        $controller = $this->request->route()->getActionName();
28✔
701

702
        if ($controller === 'Closure') {
28✔
703
            return null;
1✔
704
        }
705

706
        $explodedController = explode('@', $controller);
27✔
707

708
        $class = $explodedController[0];
27✔
709
        $method = Arr::get($explodedController, 1, '__invoke');
27✔
710

711
        if (!method_exists($class, $method)) {
27✔
712
            return null;
1✔
713
        }
714

715
        $parameters = $this->resolveClassMethodDependencies(
26✔
716
            app($class),
26✔
717
            $method,
26✔
718
        );
26✔
719

720
        return Arr::first($parameters, function ($key) {
26✔
721
            return preg_match('/Request/', $key);
26✔
722
        });
26✔
723
    }
724

725
    public function saveConsume()
726
    {
727
        $consumeList = $this->data['paths'][$this->uri][$this->method]['consumes'];
28✔
728
        $consume = $this->request->header('Content-Type');
28✔
729

730
        if (!empty($consume) && !in_array($consume, $consumeList)) {
28✔
731
            $this->item['consumes'][] = $consume;
17✔
732
        }
733
    }
734

735
    public function saveTags()
736
    {
737
        $globalPrefix = config('auto-doc.global_prefix');
28✔
738
        $globalPrefix = Str::after($globalPrefix, '/');
28✔
739

740
        $explodedUri = explode('/', $this->uri);
28✔
741
        $explodedUri = array_filter($explodedUri);
28✔
742

743
        $tag = array_shift($explodedUri);
28✔
744

745
        if ($globalPrefix === $tag) {
28✔
746
            $tag = array_shift($explodedUri);
2✔
747
        }
748

749
        $this->item['tags'] = [$tag];
28✔
750
    }
751

752
    public function saveDescription($request, array $annotations)
753
    {
754
        $this->item['summary'] = $this->getSummary($request, $annotations);
25✔
755

756
        $description = Arr::get($annotations, 'description');
25✔
757

758
        if (!empty($description)) {
25✔
759
            $this->item['description'] = $description;
1✔
760
        }
761
    }
762

763
    protected function saveSecurity()
764
    {
765
        if ($this->requestSupportAuth()) {
28✔
766
            $this->addSecurityToOperation();
5✔
767
        }
768
    }
769

770
    protected function addSecurityToOperation()
771
    {
772
        $security = &$this->data['paths'][$this->uri][$this->method]['security'];
5✔
773

774
        if (empty($security)) {
5✔
775
            $security[] = [
5✔
776
                "{$this->security}" => [],
5✔
777
            ];
5✔
778
        }
779
    }
780

781
    protected function getSummary($request, array $annotations)
782
    {
783
        $summary = Arr::get($annotations, 'summary');
25✔
784

785
        if (empty($summary)) {
25✔
786
            $summary = $this->parseRequestName($request);
24✔
787
        }
788

789
        return $summary;
25✔
790
    }
791

792
    protected function requestSupportAuth(): bool
793
    {
794
        $security = Arr::get($this->config, 'security');
28✔
795
        $securityDriver = Arr::get($this->config, "security_drivers.{$security}");
28✔
796

797
        switch (Arr::get($securityDriver, 'in')) {
28✔
798
            case 'header':
28✔
799
                // TODO Change this logic after migration on Swagger 3.0
800
                // Swagger 2.0 does not support cookie authorization.
801
                $securityToken = $this->request->hasHeader($securityDriver['name'])
7✔
802
                    ? $this->request->header($securityDriver['name'])
5✔
803
                    : $this->request->cookie($securityDriver['name']);
2✔
804

805
                break;
7✔
806
            case 'query':
21✔
807
                $securityToken = $this->request->query($securityDriver['name']);
1✔
808

809
                break;
1✔
810
            default:
811
                $securityToken = null;
20✔
812
        }
813

814
        return !empty($securityToken);
28✔
815
    }
816

817
    protected function parseRequestName($request)
818
    {
819
        $explodedRequest = explode('\\', $request);
24✔
820
        $requestName = array_pop($explodedRequest);
24✔
821
        $summaryName = str_replace('Request', '', $requestName);
24✔
822

823
        $underscoreRequestName = $this->camelCaseToUnderScore($summaryName);
24✔
824

825
        return preg_replace('/[_]/', ' ', $underscoreRequestName);
24✔
826
    }
827

828
    protected function getResponseDescription($code)
829
    {
830
        $defaultDescription = Response::$statusTexts[$code];
28✔
831

832
        $request = $this->getConcreteRequest();
28✔
833

834
        if (empty($request)) {
28✔
835
            return $defaultDescription;
3✔
836
        }
837

838
        $annotations = $this->getClassAnnotations($request);
25✔
839

840
        $localDescription = Arr::get($annotations, "_{$code}");
25✔
841

842
        if (!empty($localDescription)) {
25✔
843
            return $localDescription;
1✔
844
        }
845

846
        return Arr::get($this->config, "defaults.code-descriptions.{$code}", $defaultDescription);
24✔
847
    }
848

849
    protected function getActionName($uri): string
850
    {
851
        $action = preg_replace('[\/]', '', $uri);
28✔
852

853
        return Str::camel($action);
28✔
854
    }
855

856
    public function saveProductionData()
857
    {
858
        if (ParallelTesting::token()) {
4✔
859
            $this->driver->appendProcessDataToTmpFile(function (?array $sharedTmpData) {
2✔
860
                $resultDocContent = (empty($sharedTmpData))
2✔
861
                    ? $this->generateEmptyData($this->config['info']['description'])
1✔
862
                    : $sharedTmpData;
1✔
863

864
                $this->mergeOpenAPIDocs($resultDocContent, $this->data);
2✔
865

866
                return $resultDocContent;
2✔
867
            });
2✔
868
        }
869

870
        $this->driver->saveData();
4✔
871
    }
872

873
    public function getDocFileContent()
874
    {
875
        try {
876
            $documentation = $this->driver->getDocumentation();
46✔
877

878
            $this->openAPIValidator->validate($documentation);
44✔
879
        } catch (Throwable $exception) {
40✔
880
            return $this->generateEmptyData($this->config['defaults']['error'], [
40✔
881
                'message' => $exception->getMessage(),
40✔
882
                'type' => $exception::class,
40✔
883
                'error_place' => $this->getErrorPlace($exception),
40✔
884
            ]);
40✔
885
        }
886

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

889
        foreach ($additionalDocs as $filePath) {
6✔
890
            try {
891
                $additionalDocContent = $this->getOpenAPIFileContent(base_path($filePath));
4✔
892
            } catch (DocFileNotExistsException|EmptyDocFileException|InvalidSwaggerSpecException $exception) {
3✔
893
                report($exception);
3✔
894

895
                continue;
3✔
896
            }
897

898
            $this->mergeOpenAPIDocs($documentation, $additionalDocContent);
1✔
899
        }
900

901
        return $documentation;
6✔
902
    }
903

904
    protected function getErrorPlace(Throwable $exception): string
905
    {
906
        $firstTraceEntry = Arr::first($exception->getTrace());
40✔
907

908
        Arr::forget($firstTraceEntry, 'type');
40✔
909

910
        $formattedTraceEntry = Arr::map(
40✔
911
            array: $firstTraceEntry,
40✔
912
            callback: fn ($value, $key) => $key . '=' . (is_array($value) ? json_encode($value) : $value),
40✔
913
        );
40✔
914

915
        return implode(PHP_EOL, $formattedTraceEntry);
40✔
916
    }
917

918
    protected function camelCaseToUnderScore($input): string
919
    {
920
        preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
24✔
921
        $ret = $matches[0];
24✔
922

923
        foreach ($ret as &$match) {
24✔
924
            $match = ($match === strtoupper($match)) ? strtolower($match) : lcfirst($match);
24✔
925
        }
926

927
        return implode('_', $ret);
24✔
928
    }
929

930
    protected function generateExample($properties): array
931
    {
932
        $parameters = $this->replaceObjectValues($this->request->all());
6✔
933
        $example = [];
6✔
934

935
        $this->replaceNullValues($parameters, $properties, $example);
6✔
936

937
        return $example;
6✔
938
    }
939

940
    protected function replaceObjectValues($parameters): array
941
    {
942
        $classNamesValues = [
6✔
943
            File::class => '[uploaded_file]',
6✔
944
        ];
6✔
945

946
        $parameters = Arr::dot($parameters);
6✔
947
        $returnParameters = [];
6✔
948

949
        foreach ($parameters as $parameter => $value) {
6✔
950
            if (is_object($value)) {
6✔
951
                $class = get_class($value);
1✔
952

953
                $value = Arr::get($classNamesValues, $class, $class);
1✔
954
            }
955

956
            Arr::set($returnParameters, $parameter, $value);
6✔
957
        }
958

959
        return $returnParameters;
6✔
960
    }
961

962
    protected function getClassAnnotations($class): array
963
    {
964
        $reflection = new ReflectionClass($class);
25✔
965

966
        $annotations = $reflection->getDocComment();
25✔
967

968
        $annotations = Str::of($annotations)->remove("\r");
25✔
969

970
        $blocks = explode("\n", $annotations);
25✔
971

972
        $result = [];
25✔
973

974
        foreach ($blocks as $block) {
25✔
975
            if (Str::contains($block, '@')) {
25✔
976
                $index = strpos($block, '@');
1✔
977
                $block = substr($block, $index);
1✔
978
                $exploded = explode(' ', $block);
1✔
979

980
                $paramName = str_replace('@', '', array_shift($exploded));
1✔
981
                $paramValue = implode(' ', $exploded);
1✔
982

983
                if (in_array($paramName, $this->booleanAnnotations)) {
1✔
984
                    $paramValue = true;
1✔
985
                }
986

987
                $result[$paramName] = $paramValue;
1✔
988
            }
989
        }
990

991
        return $result;
25✔
992
    }
993

994
    /**
995
     * NOTE: All functions below are temporary solution for
996
     * this issue: https://github.com/OAI/OpenAPI-Specification/issues/229
997
     * We hope swagger developers will resolve this problem in next release of Swagger OpenAPI
998
     * */
999
    protected function replaceNullValues($parameters, $types, &$example)
1000
    {
1001
        foreach ($parameters as $parameter => $value) {
6✔
1002
            if (is_null($value) && array_key_exists($parameter, $types)) {
6✔
1003
                $example[$parameter] = $this->getDefaultValueByType($types[$parameter]['type']);
5✔
1004
            } elseif (is_array($value)) {
6✔
1005
                $this->replaceNullValues($value, $types, $example[$parameter]);
3✔
1006
            } else {
1007
                $example[$parameter] = $value;
6✔
1008
            }
1009
        }
1010
    }
1011

1012
    protected function getDefaultValueByType($type)
1013
    {
1014
        $values = [
5✔
1015
            'object' => 'null',
5✔
1016
            'boolean' => false,
5✔
1017
            'date' => '0000-00-00',
5✔
1018
            'integer' => 0,
5✔
1019
            'string' => '',
5✔
1020
            'double' => 0,
5✔
1021
        ];
5✔
1022

1023
        return $values[$type];
5✔
1024
    }
1025

1026
    protected function prepareInfo(?string $view = null, array $viewData = [], array $license = []): array
1027
    {
1028
        $info = [];
63✔
1029

1030
        $license = array_filter($license);
63✔
1031

1032
        if (!empty($license)) {
63✔
UNCOV
1033
            $info['license'] = $license;
×
1034
        }
1035

1036
        if (!empty($view)) {
63✔
1037
            $info['description'] = view($view, $viewData)->render();
62✔
1038
        }
1039

1040
        return array_merge($this->config['info'], $info);
63✔
1041
    }
1042

1043
    protected function getOpenAPIFileContent(string $filePath): array
1044
    {
1045
        if (!file_exists($filePath)) {
4✔
1046
            throw new DocFileNotExistsException($filePath);
1✔
1047
        }
1048

1049
        $fileContent = json_decode(file_get_contents($filePath), true);
3✔
1050

1051
        if (empty($fileContent)) {
3✔
1052
            throw new EmptyDocFileException($filePath);
1✔
1053
        }
1054

1055
        $this->openAPIValidator->validate($fileContent);
2✔
1056

1057
        return $fileContent;
1✔
1058
    }
1059

1060
    protected function mergeOpenAPIDocs(array &$documentation, array $additionalDocumentation): void
1061
    {
1062
        $paths = array_keys($additionalDocumentation['paths']);
3✔
1063

1064
        foreach ($paths as $path) {
3✔
1065
            $additionalDocPath = $additionalDocumentation['paths'][$path];
3✔
1066

1067
            if (empty($documentation['paths'][$path])) {
3✔
1068
                $documentation['paths'][$path] = $additionalDocPath;
3✔
1069
            } else {
1070
                $methods = array_keys($documentation['paths'][$path]);
1✔
1071
                $additionalDocMethods = array_keys($additionalDocPath);
1✔
1072

1073
                foreach ($additionalDocMethods as $method) {
1✔
1074
                    if (!in_array($method, $methods)) {
1✔
1075
                        $documentation['paths'][$path][$method] = $additionalDocPath[$method];
1✔
1076
                    }
1077
                }
1078
            }
1079
        }
1080

1081
        foreach (Arr::get($additionalDocumentation, 'components.schemas', []) as $definitionName => $definitionData) {
3✔
1082
            if (empty($documentation['components']['schemas'][$definitionName])) {
3✔
1083
                $documentation['components']['schemas'][$definitionName] = $definitionData;
3✔
1084
            }
1085
        }
1086
    }
1087
}
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