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

RonasIT / laravel-swagger / 21678059795

04 Feb 2026 03:45PM UTC coverage: 99.675% (+0.02%) from 99.656%
21678059795

Pull #193

github

web-flow
Merge 44dd8ad0a into e1dc9366e
Pull Request #193: feat: name response object by resource class

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

1 existing line in 1 file now uncovered.

919 of 922 relevant lines covered (99.67%)

22.7 hits per line

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

99.79
/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 ReflectionException;
14
use RonasIT\AutoDoc\Contracts\SwaggerDriverContract;
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\Extractors\ClosureExtractor;
25
use RonasIT\AutoDoc\Extractors\MethodExtractor;
26
use RonasIT\AutoDoc\Extractors\RouteExtractor;
27
use RonasIT\AutoDoc\Traits\GetDependenciesTrait;
28
use RonasIT\AutoDoc\Validators\SwaggerSpecValidator;
29
use Symfony\Component\HttpFoundation\Response;
30
use Throwable;
31

32
/**
33
 * @property SwaggerDriverContract $driver
34
 */
35
class SwaggerService
36
{
37
    use GetDependenciesTrait;
38

39
    public const string OPEN_API_VERSION = '3.1.0';
40

41
    protected $driver;
42
    protected $openAPIValidator;
43

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

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

67
    protected $booleanAnnotations = [
68
        'deprecated',
69
    ];
70

71
    public function __construct(Container $container)
72
    {
73
        $this->openAPIValidator = app(SwaggerSpecValidator::class);
106✔
74

75
        $this->initConfig();
106✔
76

77
        $this->setDriver();
101✔
78

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

84
            $this->container = $container;
98✔
85

86
            $this->security = $this->config['security'];
98✔
87

88
            $this->data = $this->driver->getProcessTmpData();
98✔
89

90
            if (empty($this->data)) {
98✔
91
                $this->data = $this->generateEmptyData();
62✔
92

93
                $this->driver->saveProcessTmpData($this->data);
62✔
94
            }
95
        }
96
    }
97

98
    protected function initConfig()
99
    {
100
        $this->config = config('auto-doc');
106✔
101

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

104
        if (empty($version)) {
106✔
105
            throw new LegacyConfigException();
1✔
106
        }
107

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

110
        if (version_compare($packageConfigs['config_version'], $version, '>')) {
105✔
111
            throw new LegacyConfigException();
1✔
112
        }
113

114
        $documentationViewer = (string) Arr::get($this->config, 'documentation_viewer');
104✔
115

116
        if (!view()->exists("auto-doc::documentation-{$documentationViewer}")) {
104✔
117
            throw new UnsupportedDocumentationViewerException($documentationViewer);
2✔
118
        }
119

120
        $securityDriver = Arr::get($this->config, 'security');
102✔
121

122
        if ($securityDriver && !array_key_exists($securityDriver, Arr::get($this->config, 'security_drivers'))) {
102✔
123
            throw new WrongSecurityConfigException();
1✔
124
        }
125
    }
126

127
    protected function setDriver()
128
    {
129
        $driver = $this->config['driver'];
101✔
130
        $className = Arr::get($this->config, "drivers.{$driver}.class");
101✔
131

132
        if (!class_exists($className)) {
101✔
133
            throw new SwaggerDriverClassNotFoundException($className);
1✔
134
        } else {
135
            $this->driver = app($className);
100✔
136
        }
137

138
        if (!$this->driver instanceof SwaggerDriverContract) {
100✔
139
            throw new InvalidDriverClassException($driver);
1✔
140
        }
141
    }
142

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

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

161
        $securityDefinitions = $this->generateSecurityDefinition();
63✔
162

163
        if (!empty($securityDefinitions)) {
63✔
164
            $data['securityDefinitions'] = $securityDefinitions;
3✔
165
        }
166

167
        return $data;
63✔
168
    }
169

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

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

183
        return [
3✔
184
            $this->security => $this->generateSecurityDefinitionObject($this->security),
3✔
185
        ];
3✔
186
    }
187

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

197
    public function addData(Request $request, $response)
198
    {
199
        $this->request = $request;
32✔
200

201
        $this->prepareItem();
32✔
202

203
        $this->parseRequest();
32✔
204
        $this->parseResponse($response);
32✔
205

206
        $this->driver->saveProcessTmpData($this->data);
32✔
207
    }
208

209
    protected function prepareItem()
210
    {
211
        $this->uri = "/{$this->getUri()}";
32✔
212
        $this->method = strtolower($this->request->getMethod());
32✔
213

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

226
        $this->item = &$this->data['paths'][$this->uri][$this->method];
32✔
227
    }
228

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

235
        return preg_replace("/^\//", '', $preparedUri);
32✔
236
    }
237

238
    protected function getPathParams(): array
239
    {
240
        $params = [];
31✔
241

242
        preg_match_all('/{.*?}/', $this->uri, $params);
31✔
243

244
        $params = Arr::collapse($params);
31✔
245

246
        $result = [];
31✔
247

248
        foreach ($params as $param) {
31✔
249
            $key = preg_replace('/[{}]/', '', $param);
6✔
250

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

262
        return $result;
31✔
263
    }
264

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

269
        if (empty($expression)) {
6✔
270
            return '';
6✔
271
        }
272

273
        $exploded = explode('|', $expression);
1✔
274

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

281
        return 'in: ' . implode(',', $exploded);
1✔
282
    }
283

284
    protected function parseRequest()
285
    {
286
        $this->saveConsume();
32✔
287
        $this->saveTags();
32✔
288
        $this->saveSecurity();
32✔
289

290
        $concreteRequest = $this->getConcreteRequest();
32✔
291

292
        if (empty($concreteRequest)) {
32✔
293
            $this->item['description'] = '';
4✔
294

295
            return;
4✔
296
        }
297

298
        $annotations = $this->getClassAnnotations($concreteRequest);
28✔
299

300
        $this->markAsDeprecated($annotations);
28✔
301
        $this->saveParameters($concreteRequest, $annotations);
28✔
302
        $this->saveDescription($concreteRequest, $annotations);
28✔
303
    }
304

305
    protected function markAsDeprecated(array $annotations)
306
    {
307
        $this->item['deprecated'] = Arr::get($annotations, 'deprecated', false);
28✔
308
    }
309

310
    protected function saveResponseSchema(?array $content, string $definition): void
311
    {
312
        $schemaProperties = [];
32✔
313
        $schemaType = 'object';
32✔
314

315
        if (!empty($content) && array_is_list($content)) {
32✔
316
            $this->saveListResponseDefinitions($content, $schemaProperties);
16✔
317

318
            $schemaType = 'array';
16✔
319
        } else {
320
            $this->saveObjectResponseDefinitions($content, $schemaProperties, $definition);
16✔
321
        }
322

323
        $this->data['components']['schemas'][$definition] = [
32✔
324
            'type' => $schemaType,
32✔
325
            'properties' => $schemaProperties,
32✔
326
        ];
32✔
327
    }
328

329
    protected function saveListResponseDefinitions(array $content, array &$schemaProperties): void
330
    {
331
        $types = [];
16✔
332

333
        foreach ($content as $value) {
16✔
334
            $type = gettype($value);
16✔
335

336
            if (!in_array($type, $types)) {
16✔
337
                $types[] = $type;
16✔
338
                $schemaProperties['items']['allOf'][]['type'] = $type;
16✔
339
            }
340
        }
341
    }
342

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

347
        foreach ($content as $name => $value) {
16✔
348
            $property = Arr::get($properties, "properties.{$name}", []);
14✔
349

350
            if (is_null($value)) {
14✔
351
                $property['nullable'] = true;
2✔
352
            } else {
353
                $property['type'] = gettype($value);
14✔
354
            }
355

356
            $schemaProperties[$name] = $property;
14✔
357
        }
358
    }
359

360
    protected function parseResponse($response)
361
    {
362
        $produceList = $this->data['paths'][$this->uri][$this->method]['produces'];
32✔
363

364
        $produce = $response->headers->get('Content-type');
32✔
365

366
        if (is_null($produce)) {
32✔
367
            $produce = 'text/plain';
2✔
368
        }
369

370
        if (!in_array($produce, $produceList)) {
32✔
371
            $this->item['produces'][] = $produce;
31✔
372
        }
373

374
        $responses = $this->item['responses'];
32✔
375

376
        $responseExampleLimitCount = config('auto-doc.response_example_limit_count');
32✔
377

378
        $content = json_decode($response->getContent(), true) ?? [];
32✔
379

380
        if (!empty($responseExampleLimitCount)) {
32✔
381
            if (!empty($content['data'])) {
32✔
382
                $limitedResponseData = array_slice($content['data'], 0, $responseExampleLimitCount, true);
3✔
383
                $content['data'] = $limitedResponseData;
3✔
384
                $content['to'] = count($limitedResponseData);
3✔
385
                $content['total'] = count($limitedResponseData);
3✔
386
            }
387
        }
388

389
        if (!empty($content['exception'])) {
32✔
390
            $uselessKeys = array_keys(Arr::except($content, ['message']));
1✔
391

392
            $content = Arr::except($content, $uselessKeys);
1✔
393
        }
394

395
        $code = $response->getStatusCode();
32✔
396

397
        if (!in_array($code, $responses)) {
32✔
398
            $this->saveExample(
32✔
399
                $code,
32✔
400
                json_encode($content, JSON_PRETTY_PRINT),
32✔
401
                $produce,
32✔
402
            );
32✔
403
        }
404

405
        $action = Str::ucfirst($this->getActionName($this->uri));
32✔
406

407
        $resourceName = $this->getResourceName();
32✔
408

409
        $definition = (!empty($resourceName))
32✔
410
            ? Str::replace('Resource', '', $resourceName)
4✔
411
            : "{$this->method}{$action}{$code}ResponseObject";
28✔
412

413
        $this->saveResponseSchema($content, $definition);
32✔
414

415
        if (is_array($this->item['responses'][$code])) {
32✔
416
            $this->item['responses'][$code]['content'][$produce]['schema']['$ref'] = "#/components/schemas/{$definition}";
31✔
417
        }
418
    }
419

420
    protected function getResourceName(): ?string
421
    {
422
        $routeExtractor = new RouteExtractor($this->request->route());
32✔
423

424
        if ($routeExtractor->usesClosure()) {
32✔
425
            return (new ClosureExtractor($routeExtractor->getClosure()))->getResource();
2✔
426
        }
427

428
        try {
429
            $methodExtractor = new MethodExtractor($routeExtractor->getControllerClass(), $routeExtractor->getMethodName());
30✔
430
        } catch (ReflectionException) {
1✔
431
            return null;
1✔
432
        }
433

434
        return $methodExtractor->getResource();
29✔
435
    }
436

437
    protected function saveExample($code, $content, $produce)
438
    {
439
        $description = $this->getResponseDescription($code);
32✔
440
        $availableContentTypes = [
32✔
441
            'application',
32✔
442
            'text',
32✔
443
            'image',
32✔
444
        ];
32✔
445
        $explodedContentType = explode('/', $produce);
32✔
446

447
        if (in_array($explodedContentType[0], $availableContentTypes)) {
32✔
448
            $this->item['responses'][$code] = $this->makeResponseExample($content, $produce, $description);
31✔
449
        } else {
450
            $this->item['responses'][$code] = '*Unavailable for preview*';
1✔
451
        }
452
    }
453

454
    protected function makeResponseExample($content, $mimeType, $description = ''): array
455
    {
456
        $example = match ($mimeType) {
31✔
457
            'application/json' => json_decode($content, true),
27✔
458
            'application/pdf' => base64_encode($content),
1✔
459
            default => $content,
3✔
460
        };
31✔
461

462
        return [
31✔
463
            'description' => $description,
31✔
464
            'content' => [
31✔
465
                $mimeType => [
31✔
466
                    'schema' => [
31✔
467
                        'type' => 'object',
31✔
468
                    ],
31✔
469
                    'example' => $example,
31✔
470
                ],
31✔
471
            ],
31✔
472
        ];
31✔
473
    }
474

475
    protected function saveParameters($request, array $annotations)
476
    {
477
        $formRequest = new $request();
28✔
478
        $formRequest->setUserResolver($this->request->getUserResolver());
28✔
479
        $formRequest->setRouteResolver($this->request->getRouteResolver());
28✔
480
        $rules = method_exists($formRequest, 'rules') ? $this->prepareRules($formRequest->rules()) : [];
28✔
481
        $attributes = method_exists($formRequest, 'attributes') ? $formRequest->attributes() : [];
28✔
482

483
        $actionName = $this->getActionName($this->uri);
28✔
484

485
        if (in_array($this->method, ['get', 'delete'])) {
28✔
486
            $this->saveGetRequestParameters($rules, $attributes, $annotations);
22✔
487
        } else {
488
            $this->savePostRequestParameters($actionName, $rules, $attributes, $annotations);
6✔
489
        }
490
    }
491

492
    protected function prepareRules(array $rules): array
493
    {
494
        $preparedRules = [];
27✔
495

496
        foreach ($rules as $field => $rulesField) {
27✔
497
            if (is_array($rulesField)) {
27✔
498
                $rulesField = array_map(function ($rule) {
25✔
499
                    return $this->getRuleAsString($rule);
25✔
500
                }, $rulesField);
25✔
501

502
                $preparedRules[$field] = implode('|', $rulesField);
25✔
503
            } else {
504
                $preparedRules[$field] = $this->getRuleAsString($rulesField);
27✔
505
            }
506
        }
507

508
        return $preparedRules;
27✔
509
    }
510

511
    protected function getRuleAsString($rule): string
512
    {
513
        if (is_object($rule)) {
27✔
514
            if (method_exists($rule, '__toString')) {
25✔
515
                return $rule->__toString();
25✔
516
            }
517

518
            $shortName = Str::afterLast(get_class($rule), '\\');
25✔
519

520
            $ruleName = preg_replace('/Rule$/', '', $shortName);
25✔
521

522
            return Str::snake($ruleName);
25✔
523
        }
524

525
        return $rule;
27✔
526
    }
527

528
    protected function saveGetRequestParameters($rules, array $attributes, array $annotations)
529
    {
530
        foreach ($rules as $parameter => $rule) {
22✔
531
            $validation = explode('|', $rule);
21✔
532

533
            $description = Arr::get($annotations, $parameter);
21✔
534

535
            if (empty($description)) {
21✔
536
                $description = Arr::get($attributes, $parameter, implode(', ', $validation));
21✔
537
            }
538

539
            $existedParameter = Arr::first($this->item['parameters'], function ($existedParameter) use ($parameter) {
21✔
540
                return $existedParameter['name'] === $parameter;
19✔
541
            });
21✔
542

543
            if (empty($existedParameter)) {
21✔
544
                $parameterDefinition = [
20✔
545
                    'in' => 'query',
20✔
546
                    'name' => $parameter,
20✔
547
                    'description' => $description,
20✔
548
                    'schema' => [
20✔
549
                        'type' => $this->getParameterType($validation),
20✔
550
                    ],
20✔
551
                ];
20✔
552
                if (in_array('required', $validation)) {
20✔
553
                    $parameterDefinition['required'] = true;
20✔
554
                }
555

556
                $this->item['parameters'][] = $parameterDefinition;
20✔
557
            }
558
        }
559
    }
560

561
    protected function savePostRequestParameters($actionName, $rules, array $attributes, array $annotations)
562
    {
563
        if ($this->requestHasMoreProperties($actionName)) {
6✔
564
            if ($this->requestHasBody()) {
6✔
565
                $type = $this->request->header('Content-Type', 'application/json');
6✔
566

567
                $this->item['requestBody'] = [
6✔
568
                    'content' => [
6✔
569
                        $type => [
6✔
570
                            'schema' => [
6✔
571
                                '$ref' => "#/components/schemas/{$actionName}Object",
6✔
572
                            ],
6✔
573
                        ],
6✔
574
                    ],
6✔
575
                    'description' => '',
6✔
576
                    'required' => true,
6✔
577
                ];
6✔
578
            }
579

580
            $this->saveDefinitions($actionName, $rules, $attributes, $annotations);
6✔
581
        }
582
    }
583

584
    protected function saveDefinitions($objectName, $rules, $attributes, array $annotations)
585
    {
586
        $data = [
6✔
587
            'type' => 'object',
6✔
588
            'properties' => [],
6✔
589
        ];
6✔
590

591
        foreach ($rules as $parameter => $rule) {
6✔
592
            $rulesArray = (is_array($rule)) ? $rule : explode('|', $rule);
6✔
593
            $parameterType = $this->getParameterType($rulesArray);
6✔
594
            $this->saveParameterType($data, $parameter, $parameterType);
6✔
595

596
            $uselessRules = $this->ruleToTypeMap;
6✔
597
            $uselessRules['required'] = 'required';
6✔
598

599
            if (in_array('required', $rulesArray)) {
6✔
600
                $data['required'][] = $parameter;
6✔
601
            }
602

603
            $rulesArray = array_flip(array_diff_key(array_flip($rulesArray), $uselessRules));
6✔
604

605
            $this->saveParameterDescription($data, $parameter, $rulesArray, $attributes, $annotations);
6✔
606
        }
607

608
        $data['example'] = $this->generateExample($data['properties']);
6✔
609
        $this->data['components']['schemas']["{$objectName}Object"] = $data;
6✔
610
    }
611

612
    protected function getParameterType(array $validation): string
613
    {
614
        $validationRules = $this->ruleToTypeMap;
26✔
615
        $validationRules['email'] = 'string';
26✔
616

617
        $parameterType = 'string';
26✔
618

619
        foreach ($validation as $item) {
26✔
620
            if (in_array($item, array_keys($validationRules))) {
26✔
621
                return $validationRules[$item];
25✔
622
            }
623
        }
624

625
        return $parameterType;
25✔
626
    }
627

628
    protected function saveParameterType(&$data, $parameter, $parameterType)
629
    {
630
        $data['properties'][$parameter] = [
6✔
631
            'type' => $parameterType,
6✔
632
        ];
6✔
633
    }
634

635
    protected function saveParameterDescription(
636
        array &$data,
637
        string $parameter,
638
        array $rulesArray,
639
        array $attributes,
640
        array $annotations,
641
    ) {
642
        $description = Arr::get($annotations, $parameter);
6✔
643

644
        if (empty($description)) {
6✔
645
            $description = Arr::get($attributes, $parameter, implode(', ', $rulesArray));
6✔
646
        }
647

648
        $data['properties'][$parameter]['description'] = $description;
6✔
649
    }
650

651
    protected function requestHasMoreProperties($actionName): bool
652
    {
653
        $requestParametersCount = count($this->request->all());
6✔
654

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

658
        return $requestParametersCount > $objectParametersCount;
6✔
659
    }
660

661
    protected function requestHasBody(): bool
662
    {
663
        $parameters = $this->data['paths'][$this->uri][$this->method]['parameters'];
6✔
664

665
        $bodyParamExisted = Arr::where($parameters, function ($value) {
6✔
666
            return $value['name'] === 'body';
1✔
667
        });
6✔
668

669
        return empty($bodyParamExisted);
6✔
670
    }
671

672
    public function getConcreteRequest()
673
    {
674
        $routeExtractor = new RouteExtractor($this->request->route());
32✔
675

676
        if ($routeExtractor->usesClosure()) {
32✔
677
            return null;
2✔
678
        }
679

680
        $class = $routeExtractor->getControllerClass();
30✔
681
        $method = $routeExtractor->getMethodName();
30✔
682

683
        if (!method_exists($class, $method)) {
30✔
684
            return null;
1✔
685
        }
686

687
        $parameters = $this->resolveClassMethodDependencies(
29✔
688
            app($class),
29✔
689
            $method,
29✔
690
        );
29✔
691

692
        return Arr::first($parameters, function ($key) {
29✔
693
            return preg_match('/Request/', $key);
29✔
694
        });
29✔
695
    }
696

697
    public function saveConsume()
698
    {
699
        $consumeList = $this->data['paths'][$this->uri][$this->method]['consumes'];
32✔
700
        $consume = $this->request->header('Content-Type');
32✔
701

702
        if (!empty($consume) && !in_array($consume, $consumeList)) {
32✔
703
            $this->item['consumes'][] = $consume;
17✔
704
        }
705
    }
706

707
    public function saveTags()
708
    {
709
        $globalPrefix = config('auto-doc.global_prefix');
32✔
710
        $globalPrefix = Str::after($globalPrefix, '/');
32✔
711

712
        $explodedUri = explode('/', $this->uri);
32✔
713
        $explodedUri = array_filter($explodedUri);
32✔
714

715
        $tag = array_shift($explodedUri);
32✔
716

717
        if ($globalPrefix === $tag) {
32✔
718
            $tag = array_shift($explodedUri);
2✔
719
        }
720

721
        $this->item['tags'] = [$tag];
32✔
722
    }
723

724
    public function saveDescription($request, array $annotations)
725
    {
726
        $this->item['summary'] = $this->getSummary($request, $annotations);
28✔
727

728
        $description = Arr::get($annotations, 'description');
28✔
729

730
        if (!empty($description)) {
28✔
731
            $this->item['description'] = $description;
1✔
732
        }
733
    }
734

735
    protected function saveSecurity()
736
    {
737
        if ($this->requestSupportAuth()) {
32✔
738
            $this->addSecurityToOperation();
5✔
739
        }
740
    }
741

742
    protected function addSecurityToOperation()
743
    {
744
        $security = &$this->data['paths'][$this->uri][$this->method]['security'];
5✔
745

746
        if (empty($security)) {
5✔
747
            $security[] = [
5✔
748
                "{$this->security}" => [],
5✔
749
            ];
5✔
750
        }
751
    }
752

753
    protected function getSummary($request, array $annotations)
754
    {
755
        $summary = Arr::get($annotations, 'summary');
28✔
756

757
        if (empty($summary)) {
28✔
758
            $summary = $this->parseRequestName($request);
27✔
759
        }
760

761
        return $summary;
28✔
762
    }
763

764
    protected function requestSupportAuth(): bool
765
    {
766
        $security = Arr::get($this->config, 'security');
32✔
767
        $securityDriver = Arr::get($this->config, "security_drivers.{$security}");
32✔
768

769
        switch (Arr::get($securityDriver, 'in')) {
32✔
770
            case 'header':
32✔
771
                // TODO Change this logic after migration on Swagger 3.0
772
                // Swagger 2.0 does not support cookie authorization.
773
                $securityToken = $this->request->hasHeader($securityDriver['name'])
8✔
774
                    ? $this->request->header($securityDriver['name'])
5✔
775
                    : $this->request->cookie($securityDriver['name']);
3✔
776

777
                break;
8✔
778
            case 'query':
24✔
779
                $securityToken = $this->request->query($securityDriver['name']);
1✔
780

781
                break;
1✔
782
            default:
783
                $securityToken = null;
23✔
784
        }
785

786
        return !empty($securityToken);
32✔
787
    }
788

789
    protected function parseRequestName($request)
790
    {
791
        $explodedRequest = explode('\\', $request);
27✔
792
        $requestName = array_pop($explodedRequest);
27✔
793
        $summaryName = str_replace('Request', '', $requestName);
27✔
794

795
        $underscoreRequestName = $this->camelCaseToUnderScore($summaryName);
27✔
796

797
        return preg_replace('/[_]/', ' ', $underscoreRequestName);
27✔
798
    }
799

800
    protected function getResponseDescription($code)
801
    {
802
        $defaultDescription = Response::$statusTexts[$code];
32✔
803

804
        $request = $this->getConcreteRequest();
32✔
805

806
        if (empty($request)) {
32✔
807
            return $defaultDescription;
4✔
808
        }
809

810
        $annotations = $this->getClassAnnotations($request);
28✔
811

812
        $localDescription = Arr::get($annotations, "_{$code}");
28✔
813

814
        if (!empty($localDescription)) {
28✔
815
            return $localDescription;
1✔
816
        }
817

818
        return Arr::get($this->config, "defaults.code-descriptions.{$code}", $defaultDescription);
27✔
819
    }
820

821
    protected function getActionName($uri): string
822
    {
823
        $action = preg_replace('[\/]', '', $uri);
32✔
824

825
        return Str::camel($action);
32✔
826
    }
827

828
    public function saveProductionData()
829
    {
830
        if (ParallelTesting::token()) {
4✔
831
            $this->driver->appendProcessDataToTmpFile(function (?array $sharedTmpData) {
2✔
832
                $resultDocContent = (empty($sharedTmpData))
2✔
833
                    ? $this->generateEmptyData($this->config['info']['description'])
1✔
834
                    : $sharedTmpData;
1✔
835

836
                $this->mergeOpenAPIDocs($resultDocContent, $this->data);
2✔
837

838
                return $resultDocContent;
2✔
839
            });
2✔
840
        }
841

842
        $this->driver->saveData();
4✔
843
    }
844

845
    public function getDocFileContent()
846
    {
847
        try {
848
            $documentation = $this->driver->getDocumentation();
46✔
849

850
            $this->openAPIValidator->validate($documentation);
44✔
851
        } catch (Throwable $exception) {
40✔
852
            return $this->generateEmptyData($this->config['defaults']['error'], [
40✔
853
                'message' => $exception->getMessage(),
40✔
854
                'type' => $exception::class,
40✔
855
                'error_place' => $this->getErrorPlace($exception),
40✔
856
            ]);
40✔
857
        }
858

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

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

867
                continue;
3✔
868
            }
869

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

873
        return $documentation;
6✔
874
    }
875

876
    protected function getErrorPlace(Throwable $exception): string
877
    {
878
        $firstTraceEntry = Arr::first($exception->getTrace());
40✔
879

880
        Arr::forget($firstTraceEntry, 'type');
40✔
881

882
        $formattedTraceEntry = Arr::map(
40✔
883
            array: $firstTraceEntry,
40✔
884
            callback: fn ($value, $key) => $key . '=' . (is_array($value) ? json_encode($value) : $value),
40✔
885
        );
40✔
886

887
        return implode(PHP_EOL, $formattedTraceEntry);
40✔
888
    }
889

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

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

899
        return implode('_', $ret);
27✔
900
    }
901

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

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

909
        return $example;
6✔
910
    }
911

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

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

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

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

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

931
        return $returnParameters;
6✔
932
    }
933

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

938
        $annotations = $reflection->getDocComment();
28✔
939

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

942
        $blocks = explode("\n", $annotations);
28✔
943

944
        $result = [];
28✔
945

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

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

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

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

963
        return $result;
28✔
964
    }
965

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

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

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

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

1002
        $license = array_filter($license);
63✔
1003

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

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

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

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

1021
        $fileContent = json_decode(file_get_contents($filePath), true);
3✔
1022

1023
        if (empty($fileContent)) {
3✔
1024
            throw new EmptyDocFileException($filePath);
1✔
1025
        }
1026

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

1029
        return $fileContent;
1✔
1030
    }
1031

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

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

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

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

1053
        foreach (Arr::get($additionalDocumentation, 'components.schemas', []) as $definitionName => $definitionData) {
3✔
1054
            if (empty($documentation['components']['schemas'][$definitionName])) {
3✔
1055
                $documentation['components']['schemas'][$definitionName] = $definitionData;
3✔
1056
            }
1057
        }
1058
    }
1059
}
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