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

RonasIT / laravel-swagger / 16471156199

23 Jul 2025 12:51PM UTC coverage: 99.527% (-0.002%) from 99.529%
16471156199

Pull #176

github

web-flow
Merge bc3bf13a9 into c82cea35e
Pull Request #176: fix: use invocable controllers with DI with bind by contracts

2 of 2 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

841 of 845 relevant lines covered (99.53%)

41.78 hits per line

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

99.57
/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\Exceptions\DocFileNotExistsException;
15
use RonasIT\AutoDoc\Exceptions\EmptyContactEmailException;
16
use RonasIT\AutoDoc\Exceptions\EmptyDocFileException;
17
use RonasIT\AutoDoc\Exceptions\InvalidDriverClassException;
18
use RonasIT\AutoDoc\Exceptions\LegacyConfigException;
19
use RonasIT\AutoDoc\Exceptions\SpecValidation\InvalidSwaggerSpecException;
20
use RonasIT\AutoDoc\Exceptions\SwaggerDriverClassNotFoundException;
21
use RonasIT\AutoDoc\Exceptions\UnsupportedDocumentationViewerException;
22
use RonasIT\AutoDoc\Exceptions\WrongSecurityConfigException;
23
use RonasIT\AutoDoc\Traits\GetDependenciesTrait;
24
use RonasIT\AutoDoc\Validators\SwaggerSpecValidator;
25
use Symfony\Component\HttpFoundation\Response;
26
use Exception;
27

28
/**
29
 * @property SwaggerDriverContract $driver
30
 */
31
class SwaggerService
32
{
33
    use GetDependenciesTrait;
34

35
    public const string OPEN_API_VERSION = '3.1.0';
36

37
    protected $driver;
38
    protected $openAPIValidator;
39

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

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

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

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

71
        $this->initConfig();
194✔
72

73
        $this->setDriver();
184✔
74

75
        if (config('app.env') === 'testing') {
180✔
76
            $this->container = $container;
180✔
77

78
            $this->security = $this->config['security'];
180✔
79

80
            $this->data = $this->driver->getProcessTmpData();
180✔
81

82
            if (empty($this->data)) {
180✔
83
                $this->data = $this->generateEmptyData();
120✔
84

85
                $this->driver->saveProcessTmpData($this->data);
118✔
86
            }
87
        }
88
    }
89

90
    protected function initConfig()
91
    {
92
        $this->config = config('auto-doc');
194✔
93

94
        $version = Arr::get($this->config, 'config_version');
194✔
95

96
        if (empty($version)) {
194✔
97
            throw new LegacyConfigException();
2✔
98
        }
99

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

102
        if (version_compare($packageConfigs['config_version'], $version, '>')) {
192✔
103
            throw new LegacyConfigException();
2✔
104
        }
105

106
        $documentationViewer = (string) Arr::get($this->config, 'documentation_viewer');
190✔
107

108
        if (!view()->exists("auto-doc::documentation-{$documentationViewer}")) {
190✔
109
            throw new UnsupportedDocumentationViewerException($documentationViewer);
4✔
110
        }
111

112
        $securityDriver = Arr::get($this->config, 'security');
186✔
113

114
        if ($securityDriver && !array_key_exists($securityDriver, Arr::get($this->config, 'security_drivers'))) {
186✔
115
            throw new WrongSecurityConfigException();
2✔
116
        }
117
    }
118

119
    protected function setDriver()
120
    {
121
        $driver = $this->config['driver'];
184✔
122
        $className = Arr::get($this->config, "drivers.{$driver}.class");
184✔
123

124
        if (!class_exists($className)) {
184✔
125
            throw new SwaggerDriverClassNotFoundException($className);
2✔
126
        } else {
127
            $this->driver = app($className);
182✔
128
        }
129

130
        if (!$this->driver instanceof SwaggerDriverContract) {
182✔
131
            throw new InvalidDriverClassException($driver);
2✔
132
        }
133
    }
134

135
    protected function generateEmptyData(?string $view = null, array $viewData = [], array $license = []): array
136
    {
137
        // client must enter at least `contact.email` to generate a default `info` block
138
        // otherwise an exception will be called
139
        if (!empty($this->config['info']) && !Arr::get($this->config, 'info.contact.email')) {
120✔
140
            throw new EmptyContactEmailException();
2✔
141
        }
142

143
        if (empty($view) && !empty($this->config['info'])) {
118✔
144
            $view = $this->config['info']['description'];
116✔
145
        }
146

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

159
        $securityDefinitions = $this->generateSecurityDefinition();
118✔
160

161
        if (!empty($securityDefinitions)) {
118✔
162
            $data['securityDefinitions'] = $securityDefinitions;
6✔
163
        }
164

165
        return $data;
118✔
166
    }
167

168
    protected function generateSecurityDefinition(): ?array
169
    {
170
        if (empty($this->security)) {
118✔
171
            return null;
112✔
172
        }
173

174
        return [
6✔
175
            $this->security => $this->generateSecurityDefinitionObject($this->security)
6✔
176
        ];
6✔
177
    }
178

179
    protected function generateSecurityDefinitionObject($type): array
180
    {
181
        return [
6✔
182
            'type' => $this->config['security_drivers'][$type]['type'],
6✔
183
            'name' => $this->config['security_drivers'][$type]['name'],
6✔
184
            'in' => $this->config['security_drivers'][$type]['in']
6✔
185
        ];
6✔
186
    }
187

188
    public function addData(Request $request, $response)
189
    {
190
        $this->request = $request;
54✔
191

192
        $this->prepareItem();
54✔
193

194
        $this->parseRequest();
54✔
195
        $this->parseResponse($response);
54✔
196

197
        $this->driver->saveProcessTmpData($this->data);
54✔
198
    }
199

200
    protected function prepareItem()
201
    {
202
        $this->uri = "/{$this->getUri()}";
54✔
203
        $this->method = strtolower($this->request->getMethod());
54✔
204

205
        if (empty(Arr::get($this->data, "paths.{$this->uri}.{$this->method}"))) {
54✔
206
            $this->data['paths'][$this->uri][$this->method] = [
52✔
207
                'tags' => [],
52✔
208
                'consumes' => [],
52✔
209
                'produces' => [],
52✔
210
                'parameters' => $this->getPathParams(),
52✔
211
                'responses' => [],
52✔
212
                'security' => [],
52✔
213
                'description' => ''
52✔
214
            ];
52✔
215
        }
216

217
        $this->item = &$this->data['paths'][$this->uri][$this->method];
54✔
218
    }
219

220
    protected function getUri()
221
    {
222
        $uri = $this->request->route()->uri();
54✔
223
        $basePath = preg_replace("/^\//", '', $this->config['basePath']);
54✔
224
        $preparedUri = preg_replace("/^{$basePath}/", '', $uri);
54✔
225

226
        return preg_replace("/^\//", '', $preparedUri);
54✔
227
    }
228

229
    protected function getPathParams(): array
230
    {
231
        $params = [];
52✔
232

233
        preg_match_all('/{.*?}/', $this->uri, $params);
52✔
234

235
        $params = Arr::collapse($params);
52✔
236

237
        $result = [];
52✔
238

239
        foreach ($params as $param) {
52✔
240
            $key = preg_replace('/[{}]/', '', $param);
10✔
241

242
            $result[] = [
10✔
243
                'in' => 'path',
10✔
244
                'name' => $key,
10✔
245
                'description' => $this->generatePathDescription($key),
10✔
246
                'required' => true,
10✔
247
                'schema' => [
10✔
248
                    'type' => 'string'
10✔
249
                ]
10✔
250
            ];
10✔
251
        }
252

253
        return $result;
52✔
254
    }
255

256
    protected function generatePathDescription(string $key): string
257
    {
258
        $expression = Arr::get($this->request->route()->wheres, $key);
10✔
259

260
        if (empty($expression)) {
10✔
261
            return '';
10✔
262
        }
263

264
        $exploded = explode('|', $expression);
2✔
265

266
        foreach ($exploded as $value) {
2✔
267
            if (!preg_match('/^[a-zA-Z0-9\.]+$/', $value)) {
2✔
268
                return "regexp: {$expression}";
2✔
269
            }
270
        }
271

272
        return 'in: ' . implode(',', $exploded);
2✔
273
    }
274

275
    protected function parseRequest()
276
    {
277
        $this->saveConsume();
54✔
278
        $this->saveTags();
54✔
279
        $this->saveSecurity();
54✔
280

281
        $concreteRequest = $this->getConcreteRequest();
54✔
282

283
        if (empty($concreteRequest)) {
54✔
284
            $this->item['description'] = '';
6✔
285

286
            return;
6✔
287
        }
288

289
        $annotations = $this->getClassAnnotations($concreteRequest);
48✔
290

291
        $this->markAsDeprecated($annotations);
48✔
292
        $this->saveParameters($concreteRequest, $annotations);
48✔
293
        $this->saveDescription($concreteRequest, $annotations);
48✔
294
    }
295

296
    protected function markAsDeprecated(array $annotations)
297
    {
298
        $this->item['deprecated'] = Arr::get($annotations, 'deprecated', false);
48✔
299
    }
300

301
    protected function saveResponseSchema(?array $content, string $definition): void
302
    {
303
        $schemaProperties = [];
54✔
304
        $schemaType = 'object';
54✔
305

306
        if (!empty($content) && array_is_list($content)) {
54✔
307
            $this->saveListResponseDefinitions($content, $schemaProperties);
32✔
308

309
            $schemaType = 'array';
32✔
310
        } else {
311
            $this->saveObjectResponseDefinitions($content, $schemaProperties, $definition);
22✔
312
        }
313

314
        $this->data['components']['schemas'][$definition] = [
54✔
315
            'type' => $schemaType,
54✔
316
            'properties' => $schemaProperties
54✔
317
        ];
54✔
318
    }
319

320
    protected function saveListResponseDefinitions(array $content, array &$schemaProperties): void
321
    {
322
        $types = [];
32✔
323

324
        foreach ($content as $value) {
32✔
325
            $type = gettype($value);
32✔
326

327
            if (!in_array($type, $types)) {
32✔
328
                $types[] = $type;
32✔
329
                $schemaProperties['items']['allOf'][]['type'] = $type;
32✔
330
            }
331
        }
332
    }
333

334
    protected function saveObjectResponseDefinitions(array $content, array &$schemaProperties, string $definition): void
335
    {
336
        $properties = Arr::get($this->data, "components.schemas.{$definition}", []);
22✔
337

338
        foreach ($content as $name => $value) {
22✔
339
            $property = Arr::get($properties, "properties.{$name}", []);
20✔
340

341
            if (is_null($value)) {
20✔
342
                $property['nullable'] = true;
4✔
343
            } else {
344
                $property['type'] = gettype($value);
20✔
345
            }
346

347
            $schemaProperties[$name] = $property;
20✔
348
        }
349
    }
350

351
    protected function parseResponse($response)
352
    {
353
        $produceList = $this->data['paths'][$this->uri][$this->method]['produces'];
54✔
354

355
        $produce = $response->headers->get('Content-type');
54✔
356

357
        if (is_null($produce)) {
54✔
358
            $produce = 'text/plain';
2✔
359
        }
360

361
        if (!in_array($produce, $produceList)) {
54✔
362
            $this->item['produces'][] = $produce;
52✔
363
        }
364

365
        $responses = $this->item['responses'];
54✔
366

367
        $responseExampleLimitCount = config('auto-doc.response_example_limit_count');
54✔
368

369
        $content = json_decode($response->getContent(), true) ?? [];
54✔
370

371
        if (!empty($responseExampleLimitCount)) {
54✔
372
            if (!empty($content['data'])) {
54✔
373
                $limitedResponseData = array_slice($content['data'], 0, $responseExampleLimitCount, true);
4✔
374
                $content['data'] = $limitedResponseData;
4✔
375
                $content['to'] = count($limitedResponseData);
4✔
376
                $content['total'] = count($limitedResponseData);
4✔
377
            }
378
        }
379

380
        if (!empty($content['exception'])) {
54✔
381
            $uselessKeys = array_keys(Arr::except($content, ['message']));
2✔
382

383
            $content = Arr::except($content, $uselessKeys);
2✔
384
        }
385

386
        $code = $response->getStatusCode();
54✔
387

388
        if (!in_array($code, $responses)) {
54✔
389
            $this->saveExample(
54✔
390
                $code,
54✔
391
                json_encode($content, JSON_PRETTY_PRINT),
54✔
392
                $produce
54✔
393
            );
54✔
394
        }
395

396
        $action = Str::ucfirst($this->getActionName($this->uri));
54✔
397
        $definition = "{$this->method}{$action}{$code}ResponseObject";
54✔
398

399
        $this->saveResponseSchema($content, $definition);
54✔
400

401
        if (is_array($this->item['responses'][$code])) {
54✔
402
            $this->item['responses'][$code]['content'][$produce]['schema']['$ref'] = "#/components/schemas/{$definition}";
52✔
403
        }
404
    }
405

406
    protected function saveExample($code, $content, $produce)
407
    {
408
        $description = $this->getResponseDescription($code);
54✔
409
        $availableContentTypes = [
54✔
410
            'application',
54✔
411
            'text',
54✔
412
            'image',
54✔
413
        ];
54✔
414
        $explodedContentType = explode('/', $produce);
54✔
415

416
        if (in_array($explodedContentType[0], $availableContentTypes)) {
54✔
417
            $this->item['responses'][$code] = $this->makeResponseExample($content, $produce, $description);
52✔
418
        } else {
419
            $this->item['responses'][$code] = '*Unavailable for preview*';
2✔
420
        }
421
    }
422

423
    protected function makeResponseExample($content, $mimeType, $description = ''): array
424
    {
425
        $example = match ($mimeType) {
52✔
426
            'application/json' => json_decode($content, true),
46✔
427
            'application/pdf' => base64_encode($content),
2✔
428
            default => $content,
4✔
429
        };
52✔
430

431
        return [
52✔
432
            'description' => $description,
52✔
433
            'content' => [
52✔
434
                $mimeType => [
52✔
435
                    'schema' => [
52✔
436
                        'type' => 'object',
52✔
437
                    ],
52✔
438
                    'example' => $example,
52✔
439
                ],
52✔
440
            ],
52✔
441
        ];
52✔
442
    }
443

444
    protected function saveParameters($request, array $annotations)
445
    {
446
        $formRequest = new $request();
48✔
447
        $formRequest->setUserResolver($this->request->getUserResolver());
48✔
448
        $formRequest->setRouteResolver($this->request->getRouteResolver());
48✔
449
        $rules = method_exists($formRequest, 'rules') ? $this->prepareRules($formRequest->rules()) : [];
48✔
450
        $attributes = method_exists($formRequest, 'attributes') ? $formRequest->attributes() : [];
48✔
451

452
        $actionName = $this->getActionName($this->uri);
48✔
453

454
        if (in_array($this->method, ['get', 'delete'])) {
48✔
455
            $this->saveGetRequestParameters($rules, $attributes, $annotations);
36✔
456
        } else {
457
            $this->savePostRequestParameters($actionName, $rules, $attributes, $annotations);
12✔
458
        }
459
    }
460

461
    protected function prepareRules(array $rules): array
462
    {
463
        $preparedRules = [];
46✔
464

465
        foreach ($rules as $field => $rulesField) {
46✔
466
            if (is_array($rulesField)) {
46✔
467
                $rulesField = array_map(function ($rule) {
42✔
468
                    return $this->getRuleAsString($rule);
42✔
469
                }, $rulesField);
42✔
470

471
                $preparedRules[$field] = implode('|', $rulesField);
42✔
472
            } else {
473
                $preparedRules[$field] = $this->getRuleAsString($rulesField);
46✔
474
            }
475
        }
476

477
        return $preparedRules;
46✔
478
    }
479

480
    protected function getRuleAsString($rule): string
481
    {
482
        if (is_object($rule)) {
46✔
483
            if (method_exists($rule, '__toString')) {
42✔
484
                return $rule->__toString();
42✔
485
            }
486

487
            $shortName = Str::afterLast(get_class($rule), '\\');
42✔
488

489
            $ruleName = preg_replace('/Rule$/', '', $shortName);
42✔
490

491
            return Str::snake($ruleName);
42✔
492
        }
493

494
        return $rule;
46✔
495
    }
496

497
    protected function saveGetRequestParameters($rules, array $attributes, array $annotations)
498
    {
499
        foreach ($rules as $parameter => $rule) {
36✔
500
            $validation = explode('|', $rule);
34✔
501

502
            $description = Arr::get($annotations, $parameter);
34✔
503

504
            if (empty($description)) {
34✔
505
                $description = Arr::get($attributes, $parameter, implode(', ', $validation));
34✔
506
            }
507

508
            $existedParameter = Arr::first($this->item['parameters'], function ($existedParameter) use ($parameter) {
34✔
509
                return $existedParameter['name'] === $parameter;
30✔
510
            });
34✔
511

512
            if (empty($existedParameter)) {
34✔
513
                $parameterDefinition = [
32✔
514
                    'in' => 'query',
32✔
515
                    'name' => $parameter,
32✔
516
                    'description' => $description,
32✔
517
                    'schema' => [
32✔
518
                        'type' => $this->getParameterType($validation),
32✔
519
                    ],
32✔
520
                ];
32✔
521
                if (in_array('required', $validation)) {
32✔
522
                    $parameterDefinition['required'] = true;
32✔
523
                }
524

525
                $this->item['parameters'][] = $parameterDefinition;
32✔
526
            }
527
        }
528
    }
529

530
    protected function savePostRequestParameters($actionName, $rules, array $attributes, array $annotations)
531
    {
532
        if ($this->requestHasMoreProperties($actionName)) {
12✔
533
            if ($this->requestHasBody()) {
12✔
534
                $type = $this->request->header('Content-Type', 'application/json');
12✔
535

536
                $this->item['requestBody'] = [
12✔
537
                    'content' => [
12✔
538
                        $type => [
12✔
539
                            'schema' => [
12✔
540
                                '$ref' => "#/components/schemas/{$actionName}Object",
12✔
541
                            ],
12✔
542
                        ],
12✔
543
                    ],
12✔
544
                    'description' => '',
12✔
545
                    'required' => true,
12✔
546
                ];
12✔
547
            }
548

549
            $this->saveDefinitions($actionName, $rules, $attributes, $annotations);
12✔
550
        }
551
    }
552

553
    protected function saveDefinitions($objectName, $rules, $attributes, array $annotations)
554
    {
555
        $data = [
12✔
556
            'type' => 'object',
12✔
557
            'properties' => []
12✔
558
        ];
12✔
559

560
        foreach ($rules as $parameter => $rule) {
12✔
561
            $rulesArray = (is_array($rule)) ? $rule : explode('|', $rule);
12✔
562
            $parameterType = $this->getParameterType($rulesArray);
12✔
563
            $this->saveParameterType($data, $parameter, $parameterType);
12✔
564

565
            $uselessRules = $this->ruleToTypeMap;
12✔
566
            $uselessRules['required'] = 'required';
12✔
567

568
            if (in_array('required', $rulesArray)) {
12✔
569
                $data['required'][] = $parameter;
12✔
570
            }
571

572
            $rulesArray = array_flip(array_diff_key(array_flip($rulesArray), $uselessRules));
12✔
573

574
            $this->saveParameterDescription($data, $parameter, $rulesArray, $attributes, $annotations);
12✔
575
        }
576

577
        $data['example'] = $this->generateExample($data['properties']);
12✔
578
        $this->data['components']['schemas']["{$objectName}Object"] = $data;
12✔
579
    }
580

581
    protected function getParameterType(array $validation): string
582
    {
583
        $validationRules = $this->ruleToTypeMap;
44✔
584
        $validationRules['email'] = 'string';
44✔
585

586
        $parameterType = 'string';
44✔
587

588
        foreach ($validation as $item) {
44✔
589
            if (in_array($item, array_keys($validationRules))) {
44✔
590
                return $validationRules[$item];
42✔
591
            }
592
        }
593

594
        return $parameterType;
42✔
595
    }
596

597
    protected function saveParameterType(&$data, $parameter, $parameterType)
598
    {
599
        $data['properties'][$parameter] = [
12✔
600
            'type' => $parameterType
12✔
601
        ];
12✔
602
    }
603

604
    protected function saveParameterDescription(
605
        array &$data,
606
        string $parameter,
607
        array $rulesArray,
608
        array $attributes,
609
        array $annotations
610
    ) {
611
        $description = Arr::get($annotations, $parameter);
12✔
612

613
        if (empty($description)) {
12✔
614
            $description = Arr::get($attributes, $parameter, implode(', ', $rulesArray));
12✔
615
        }
616

617
        $data['properties'][$parameter]['description'] = $description;
12✔
618
    }
619

620
    protected function requestHasMoreProperties($actionName): bool
621
    {
622
        $requestParametersCount = count($this->request->all());
12✔
623

624
        $properties = Arr::get($this->data, "components.schemas.{$actionName}Object.properties", []);
12✔
625
        $objectParametersCount = count($properties);
12✔
626

627
        return $requestParametersCount > $objectParametersCount;
12✔
628
    }
629

630
    protected function requestHasBody(): bool
631
    {
632
        $parameters = $this->data['paths'][$this->uri][$this->method]['parameters'];
12✔
633

634
        $bodyParamExisted = Arr::where($parameters, function ($value) {
12✔
635
            return $value['name'] === 'body';
2✔
636
        });
12✔
637

638
        return empty($bodyParamExisted);
12✔
639
    }
640

641
    public function getConcreteRequest()
642
    {
643
        $controller = $this->request->route()->getActionName();
54✔
644

645
        if ($controller === 'Closure') {
54✔
646
            return null;
2✔
647
        }
648

649
        $explodedController = explode('@', $controller);
52✔
650

651
        $class = $explodedController[0];
52✔
652
        $method = Arr::get($explodedController, 1, '__invoke');
52✔
653

654
        if (!method_exists($class, $method)) {
52✔
655
            return null;
2✔
656
        }
657

658
        $parameters = $this->resolveClassMethodDependencies(app($class), $method);
50✔
659

660
        return Arr::first($parameters, function ($key) {
50✔
661
            return preg_match('/Request/', $key);
50✔
662
        });
50✔
663
    }
664

665
    public function saveConsume()
666
    {
667
        $consumeList = $this->data['paths'][$this->uri][$this->method]['consumes'];
54✔
668
        $consume = $this->request->header('Content-Type');
54✔
669

670
        if (!empty($consume) && !in_array($consume, $consumeList)) {
54✔
671
            $this->item['consumes'][] = $consume;
32✔
672
        }
673
    }
674

675
    public function saveTags()
676
    {
677
        $globalPrefix = config('auto-doc.global_prefix');
54✔
678
        $globalPrefix = Str::after($globalPrefix, '/');
54✔
679

680
        $explodedUri = explode('/', $this->uri);
54✔
681
        $explodedUri = array_filter($explodedUri);
54✔
682

683
        $tag = array_shift($explodedUri);
54✔
684

685
        if ($globalPrefix === $tag) {
54✔
686
            $tag = array_shift($explodedUri);
4✔
687
        }
688

689
        $this->item['tags'] = [$tag];
54✔
690
    }
691

692
    public function saveDescription($request, array $annotations)
693
    {
694
        $this->item['summary'] = $this->getSummary($request, $annotations);
48✔
695

696
        $description = Arr::get($annotations, 'description');
48✔
697

698
        if (!empty($description)) {
48✔
699
            $this->item['description'] = $description;
2✔
700
        }
701
    }
702

703
    protected function saveSecurity()
704
    {
705
        if ($this->requestSupportAuth()) {
54✔
706
            $this->addSecurityToOperation();
10✔
707
        }
708
    }
709

710
    protected function addSecurityToOperation()
711
    {
712
        $security = &$this->data['paths'][$this->uri][$this->method]['security'];
10✔
713

714
        if (empty($security)) {
10✔
715
            $security[] = [
10✔
716
                "{$this->security}" => []
10✔
717
            ];
10✔
718
        }
719
    }
720

721
    protected function getSummary($request, array $annotations)
722
    {
723
        $summary = Arr::get($annotations, 'summary');
48✔
724

725
        if (empty($summary)) {
48✔
726
            $summary = $this->parseRequestName($request);
46✔
727
        }
728

729
        return $summary;
48✔
730
    }
731

732
    protected function requestSupportAuth(): bool
733
    {
734
        $security = Arr::get($this->config, 'security');
54✔
735
        $securityDriver = Arr::get($this->config, "security_drivers.{$security}");
54✔
736

737
        switch (Arr::get($securityDriver, 'in')) {
54✔
738
            case 'header':
54✔
739
                // TODO Change this logic after migration on Swagger 3.0
740
                // Swagger 2.0 does not support cookie authorization.
741
                $securityToken = $this->request->hasHeader($securityDriver['name'])
14✔
742
                    ? $this->request->header($securityDriver['name'])
10✔
743
                    : $this->request->cookie($securityDriver['name']);
4✔
744

745
                break;
14✔
746
            case 'query':
40✔
747
                $securityToken = $this->request->query($securityDriver['name']);
2✔
748

749
                break;
2✔
750
            default:
751
                $securityToken = null;
38✔
752
        }
753

754
        return !empty($securityToken);
54✔
755
    }
756

757
    protected function parseRequestName($request)
758
    {
759
        $explodedRequest = explode('\\', $request);
46✔
760
        $requestName = array_pop($explodedRequest);
46✔
761
        $summaryName = str_replace('Request', '', $requestName);
46✔
762

763
        $underscoreRequestName = $this->camelCaseToUnderScore($summaryName);
46✔
764

765
        return preg_replace('/[_]/', ' ', $underscoreRequestName);
46✔
766
    }
767

768
    protected function getResponseDescription($code)
769
    {
770
        $defaultDescription = Response::$statusTexts[$code];
54✔
771

772
        $request = $this->getConcreteRequest();
54✔
773

774
        if (empty($request)) {
54✔
775
            return $defaultDescription;
6✔
776
        }
777

778
        $annotations = $this->getClassAnnotations($request);
48✔
779

780
        $localDescription = Arr::get($annotations, "_{$code}");
48✔
781

782
        if (!empty($localDescription)) {
48✔
783
            return $localDescription;
2✔
784
        }
785

786
        return Arr::get($this->config, "defaults.code-descriptions.{$code}", $defaultDescription);
46✔
787
    }
788

789
    protected function getActionName($uri): string
790
    {
791
        $action = preg_replace('[\/]', '', $uri);
54✔
792

793
        return Str::camel($action);
54✔
794
    }
795

796
    /**
797
     * @deprecated method is not in use
798
     * @codeCoverageIgnore
799
     */
800
    protected function saveTempData()
801
    {
802
        $exportFile = Arr::get($this->config, 'files.temporary');
803
        $data = json_encode($this->data);
804

805
        file_put_contents($exportFile, $data);
806
    }
807

808
    public function saveProductionData()
809
    {
810
        if (ParallelTesting::token()) {
6✔
811
            $this->driver->appendProcessDataToTmpFile(function (array $sharedTmpData) {
2✔
812
                $resultDocContent = (empty($sharedTmpData))
2✔
UNCOV
813
                    ? $this->generateEmptyData($this->config['info']['description'])
×
814
                    : $sharedTmpData;
2✔
815

816
                $this->mergeOpenAPIDocs($resultDocContent, $this->data);
2✔
817

818
                return $resultDocContent;
2✔
819
            });
2✔
820
        }
821

822
        $this->driver->saveData();
6✔
823
    }
824

825
    public function getDocFileContent()
826
    {
827
        try {
828
            $documentation = $this->driver->getDocumentation();
86✔
829

830
            $this->openAPIValidator->validate($documentation);
86✔
831
        } catch (Exception $exception) {
74✔
832
            return $this->generateEmptyData($this->config['defaults']['error'], ['message' => $exception->getMessage()]);
74✔
833
        }
834

835
        $additionalDocs = config('auto-doc.additional_paths', []);
12✔
836

837
        foreach ($additionalDocs as $filePath) {
12✔
838
            try {
839
                $additionalDocContent = $this->getOpenAPIFileContent(base_path($filePath));
8✔
840
            } catch (DocFileNotExistsException|EmptyDocFileException|InvalidSwaggerSpecException $exception) {
6✔
841
                report($exception);
6✔
842

843
                continue;
6✔
844
            }
845

846
            $this->mergeOpenAPIDocs($documentation, $additionalDocContent);
2✔
847
        }
848

849
        return $documentation;
12✔
850
    }
851

852
    protected function camelCaseToUnderScore($input): string
853
    {
854
        preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
46✔
855
        $ret = $matches[0];
46✔
856

857
        foreach ($ret as &$match) {
46✔
858
            $match = ($match === strtoupper($match)) ? strtolower($match) : lcfirst($match);
46✔
859
        }
860

861
        return implode('_', $ret);
46✔
862
    }
863

864
    protected function generateExample($properties): array
865
    {
866
        $parameters = $this->replaceObjectValues($this->request->all());
12✔
867
        $example = [];
12✔
868

869
        $this->replaceNullValues($parameters, $properties, $example);
12✔
870

871
        return $example;
12✔
872
    }
873

874
    protected function replaceObjectValues($parameters): array
875
    {
876
        $classNamesValues = [
12✔
877
            File::class => '[uploaded_file]',
12✔
878
        ];
12✔
879

880
        $parameters = Arr::dot($parameters);
12✔
881
        $returnParameters = [];
12✔
882

883
        foreach ($parameters as $parameter => $value) {
12✔
884
            if (is_object($value)) {
12✔
885
                $class = get_class($value);
2✔
886

887
                $value = Arr::get($classNamesValues, $class, $class);
2✔
888
            }
889

890
            Arr::set($returnParameters, $parameter, $value);
12✔
891
        }
892

893
        return $returnParameters;
12✔
894
    }
895

896
    protected function getClassAnnotations($class): array
897
    {
898
        $reflection = new ReflectionClass($class);
48✔
899

900
        $annotations = $reflection->getDocComment();
48✔
901

902
        $annotations = Str::of($annotations)->remove("\r");
48✔
903

904
        $blocks = explode("\n", $annotations);
48✔
905

906
        $result = [];
48✔
907

908
        foreach ($blocks as $block) {
48✔
909
            if (Str::contains($block, '@')) {
48✔
910
                $index = strpos($block, '@');
2✔
911
                $block = substr($block, $index);
2✔
912
                $exploded = explode(' ', $block);
2✔
913

914
                $paramName = str_replace('@', '', array_shift($exploded));
2✔
915
                $paramValue = implode(' ', $exploded);
2✔
916

917
                if (in_array($paramName, $this->booleanAnnotations)) {
2✔
918
                    $paramValue = true;
2✔
919
                }
920

921
                $result[$paramName] = $paramValue;
2✔
922
            }
923
        }
924

925
        return $result;
48✔
926
    }
927

928
    /**
929
     * NOTE: All functions below are temporary solution for
930
     * this issue: https://github.com/OAI/OpenAPI-Specification/issues/229
931
     * We hope swagger developers will resolve this problem in next release of Swagger OpenAPI
932
     * */
933
    protected function replaceNullValues($parameters, $types, &$example)
934
    {
935
        foreach ($parameters as $parameter => $value) {
12✔
936
            if (is_null($value) && array_key_exists($parameter, $types)) {
12✔
937
                $example[$parameter] = $this->getDefaultValueByType($types[$parameter]['type']);
10✔
938
            } elseif (is_array($value)) {
12✔
939
                $this->replaceNullValues($value, $types, $example[$parameter]);
6✔
940
            } else {
941
                $example[$parameter] = $value;
12✔
942
            }
943
        }
944
    }
945

946
    protected function getDefaultValueByType($type)
947
    {
948
        $values = [
10✔
949
            'object' => 'null',
10✔
950
            'boolean' => false,
10✔
951
            'date' => "0000-00-00",
10✔
952
            'integer' => 0,
10✔
953
            'string' => '',
10✔
954
            'double' => 0
10✔
955
        ];
10✔
956

957
        return $values[$type];
10✔
958
    }
959

960
    protected function prepareInfo(?string $view = null, array $viewData = [], array $license = []): array
961
    {
962
        $info = [];
118✔
963

964
        $license = array_filter($license);
118✔
965

966
        if (!empty($license)) {
118✔
UNCOV
967
            $info['license'] = $license;
×
968
        }
969

970
        if (!empty($view)) {
118✔
971
            $info['description'] = view($view, $viewData)->render();
116✔
972
        }
973
        
974
        return array_merge($this->config['info'], $info);
118✔
975
    }
976

977
    protected function getOpenAPIFileContent(string $filePath): array
978
    {
979
        if (!file_exists($filePath)) {
8✔
980
            throw new DocFileNotExistsException($filePath);
2✔
981
        }
982

983
        $fileContent = json_decode(file_get_contents($filePath), true);
6✔
984

985
        if (empty($fileContent)) {
6✔
986
            throw new EmptyDocFileException($filePath);
2✔
987
        }
988

989
        $this->openAPIValidator->validate($fileContent);
4✔
990

991
        return $fileContent;
2✔
992
    }
993

994
    protected function mergeOpenAPIDocs(array &$documentation, array $additionalDocumentation): void
995
    {
996
        $paths = array_keys($additionalDocumentation['paths']);
4✔
997

998
        foreach ($paths as $path) {
4✔
999
            $additionalDocPath = $additionalDocumentation['paths'][$path];
4✔
1000

1001
            if (empty($documentation['paths'][$path])) {
4✔
1002
                $documentation['paths'][$path] = $additionalDocPath;
4✔
1003
            } else {
1004
                $methods = array_keys($documentation['paths'][$path]);
2✔
1005
                $additionalDocMethods = array_keys($additionalDocPath);
2✔
1006

1007
                foreach ($additionalDocMethods as $method) {
2✔
1008
                    if (!in_array($method, $methods)) {
2✔
1009
                        $documentation['paths'][$path][$method] = $additionalDocPath[$method];
2✔
1010
                    }
1011
                }
1012
            }
1013
        }
1014

1015
        $definitions = array_keys($additionalDocumentation['components']['schemas']);
4✔
1016

1017
        foreach ($definitions as $definition) {
4✔
1018
            $documentation = Arr::add(
4✔
1019
                array: $documentation,
4✔
1020
                key: "components.schemas.{$definition}",
4✔
1021
                value: $additionalDocumentation['components']['schemas'][$definition],
4✔
1022
            );
4✔
1023
        }
1024
    }
1025
}
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