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

RonasIT / laravel-swagger / 14859415713

06 May 2025 12:17PM UTC coverage: 99.645% (-0.1%) from 99.752%
14859415713

Pull #136

github

web-flow
Merge 026e18b26 into 7ed8231c5
Pull Request #136: feat: set up project to use Paratest

48 of 49 new or added lines in 6 files covered. (97.96%)

843 of 846 relevant lines covered (99.65%)

20.18 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 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

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

34
    public const string OPEN_API_VERSION = '3.1.0';
35

36
    protected $driver;
37
    protected $openAPIValidator;
38

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

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

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

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

70
        $this->initConfig();
95✔
71

72
        $this->setDriver();
90✔
73

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

77
            $this->security = $this->config['security'];
88✔
78

79
            $this->data = $this->driver->getProcessTmpData();
88✔
80

81
            if (empty($this->data)) {
88✔
82
                $this->data = $this->generateEmptyData();
60✔
83

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

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

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

95
        if (empty($version)) {
95✔
96
            throw new LegacyConfigException();
1✔
97
        }
98

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

101
        if (version_compare($packageConfigs['config_version'], $version, '>')) {
94✔
102
            throw new LegacyConfigException();
1✔
103
        }
104

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

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

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

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

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

123
        if (!class_exists($className)) {
90✔
124
            throw new SwaggerDriverClassNotFoundException($className);
1✔
125
        } else {
126
            $this->driver = app($className);
89✔
127
        }
128

129
        if (!$this->driver instanceof SwaggerDriverContract) {
89✔
130
            throw new InvalidDriverClassException($driver);
1✔
131
        }
132
    }
133

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

142
        $data = [
59✔
143
            'openapi' => self::OPEN_API_VERSION,
59✔
144
            'servers' => [
59✔
145
                ['url' => URL::query($this->config['basePath'])],
59✔
146
            ],
59✔
147
            'paths' => [],
59✔
148
            'components' => [
59✔
149
                'schemas' => $this->config['definitions'],
59✔
150
            ],
59✔
151
            'info' => $this->prepareInfo($this->config['info'])
59✔
152
        ];
59✔
153

154
        $securityDefinitions = $this->generateSecurityDefinition();
59✔
155

156
        if (!empty($securityDefinitions)) {
59✔
157
            $data['securityDefinitions'] = $securityDefinitions;
3✔
158
        }
159

160
        return $data;
59✔
161
    }
162

163
    protected function generateSecurityDefinition(): ?array
164
    {
165
        if (empty($this->security)) {
59✔
166
            return null;
56✔
167
        }
168

169
        return [
3✔
170
            $this->security => $this->generateSecurityDefinitionObject($this->security)
3✔
171
        ];
3✔
172
    }
173

174
    protected function generateSecurityDefinitionObject($type): array
175
    {
176
        return [
3✔
177
            'type' => $this->config['security_drivers'][$type]['type'],
3✔
178
            'name' => $this->config['security_drivers'][$type]['name'],
3✔
179
            'in' => $this->config['security_drivers'][$type]['in']
3✔
180
        ];
3✔
181
    }
182

183
    public function addData(Request $request, $response)
184
    {
185
        $this->request = $request;
25✔
186

187
        $this->prepareItem();
25✔
188

189
        $this->parseRequest();
25✔
190
        $this->parseResponse($response);
25✔
191

192
        $this->driver->saveProcessTmpData($this->data);
25✔
193
    }
194

195
    protected function prepareItem()
196
    {
197
        $this->uri = "/{$this->getUri()}";
25✔
198
        $this->method = strtolower($this->request->getMethod());
25✔
199

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

212
        $this->item = &$this->data['paths'][$this->uri][$this->method];
25✔
213
    }
214

215
    protected function getUri()
216
    {
217
        $uri = $this->request->route()->uri();
25✔
218
        $basePath = preg_replace("/^\//", '', $this->config['basePath']);
25✔
219
        $preparedUri = preg_replace("/^{$basePath}/", '', $uri);
25✔
220

221
        return preg_replace("/^\//", '', $preparedUri);
25✔
222
    }
223

224
    protected function getPathParams(): array
225
    {
226
        $params = [];
24✔
227

228
        preg_match_all('/{.*?}/', $this->uri, $params);
24✔
229

230
        $params = Arr::collapse($params);
24✔
231

232
        $result = [];
24✔
233

234
        foreach ($params as $param) {
24✔
235
            $key = preg_replace('/[{}]/', '', $param);
5✔
236

237
            $result[] = [
5✔
238
                'in' => 'path',
5✔
239
                'name' => $key,
5✔
240
                'description' => $this->generatePathDescription($key),
5✔
241
                'required' => true,
5✔
242
                'schema' => [
5✔
243
                    'type' => 'string'
5✔
244
                ]
5✔
245
            ];
5✔
246
        }
247

248
        return $result;
24✔
249
    }
250

251
    protected function generatePathDescription(string $key): string
252
    {
253
        $expression = Arr::get($this->request->route()->wheres, $key);
5✔
254

255
        if (empty($expression)) {
5✔
256
            return '';
5✔
257
        }
258

259
        $exploded = explode('|', $expression);
1✔
260

261
        foreach ($exploded as $value) {
1✔
262
            if (!preg_match('/^[a-zA-Z0-9\.]+$/', $value)) {
1✔
263
                return  "regexp: {$expression}";
1✔
264
            }
265
        }
266

267
        return 'in: ' . implode(',', $exploded);
1✔
268
    }
269

270
    protected function parseRequest()
271
    {
272
        $this->saveConsume();
25✔
273
        $this->saveTags();
25✔
274
        $this->saveSecurity();
25✔
275

276
        $concreteRequest = $this->getConcreteRequest();
25✔
277

278
        if (empty($concreteRequest)) {
25✔
279
            $this->item['description'] = '';
3✔
280

281
            return;
3✔
282
        }
283

284
        $annotations = $this->getClassAnnotations($concreteRequest);
22✔
285

286
        $this->markAsDeprecated($annotations);
22✔
287
        $this->saveParameters($concreteRequest, $annotations);
22✔
288
        $this->saveDescription($concreteRequest, $annotations);
22✔
289
    }
290

291
    protected function markAsDeprecated(array $annotations)
292
    {
293
        $this->item['deprecated'] = Arr::get($annotations, 'deprecated', false);
22✔
294
    }
295

296
    protected function saveResponseSchema(?array $content, string $definition): void
297
    {
298
        $schemaProperties = [];
25✔
299
        $schemaType = 'object';
25✔
300

301
        if (!empty($content) && array_is_list($content)) {
25✔
302
            $this->saveListResponseDefinitions($content, $schemaProperties);
16✔
303

304
            $schemaType = 'array';
16✔
305
        } else {
306
            $this->saveObjectResponseDefinitions($content, $schemaProperties, $definition);
9✔
307
        }
308

309
        $this->data['components']['schemas'][$definition] = [
25✔
310
            'type' => $schemaType,
25✔
311
            'properties' => $schemaProperties
25✔
312
        ];
25✔
313
    }
314

315
    protected function saveListResponseDefinitions(array $content, array &$schemaProperties): void
316
    {
317
        $types = [];
16✔
318

319
        foreach ($content as $value) {
16✔
320
            $type = gettype($value);
16✔
321

322
            if (!in_array($type, $types)) {
16✔
323
                $types[] = $type;
16✔
324
                $schemaProperties['items']['allOf'][]['type'] = $type;
16✔
325
            }
326
        }
327
    }
328

329
    protected function saveObjectResponseDefinitions(array $content, array &$schemaProperties, string $definition): void
330
    {
331
        $properties = Arr::get($this->data, "components.schemas.{$definition}", []);
9✔
332

333
        foreach ($content as $name => $value) {
9✔
334
            $property = Arr::get($properties, "properties.{$name}", []);
8✔
335

336
            if (is_null($value)) {
8✔
337
                $property['nullable'] = true;
2✔
338
            } else {
339
                $property['type'] = gettype($value);
8✔
340
            }
341

342
            $schemaProperties[$name] = $property;
8✔
343
        }
344
    }
345

346
    protected function parseResponse($response)
347
    {
348
        $produceList = $this->data['paths'][$this->uri][$this->method]['produces'];
25✔
349

350
        $produce = $response->headers->get('Content-type');
25✔
351

352
        if (is_null($produce)) {
25✔
353
            $produce = 'text/plain';
1✔
354
        }
355

356
        if (!in_array($produce, $produceList)) {
25✔
357
            $this->item['produces'][] = $produce;
24✔
358
        }
359

360
        $responses = $this->item['responses'];
25✔
361

362
        $responseExampleLimitCount = config('auto-doc.response_example_limit_count');
25✔
363

364
        $content = json_decode($response->getContent(), true) ?? [];
25✔
365

366
        if (!empty($responseExampleLimitCount)) {
25✔
367
            if (!empty($content['data'])) {
25✔
368
                $limitedResponseData = array_slice($content['data'], 0, $responseExampleLimitCount, true);
2✔
369
                $content['data'] = $limitedResponseData;
2✔
370
                $content['to'] = count($limitedResponseData);
2✔
371
                $content['total'] = count($limitedResponseData);
2✔
372
            }
373
        }
374

375
        if (!empty($content['exception'])) {
25✔
376
            $uselessKeys = array_keys(Arr::except($content, ['message']));
1✔
377

378
            $content = Arr::except($content, $uselessKeys);
1✔
379
        }
380

381
        $code = $response->getStatusCode();
25✔
382

383
        if (!in_array($code, $responses)) {
25✔
384
            $this->saveExample(
25✔
385
                $code,
25✔
386
                json_encode($content, JSON_PRETTY_PRINT),
25✔
387
                $produce
25✔
388
            );
25✔
389
        }
390

391
        $action = Str::ucfirst($this->getActionName($this->uri));
25✔
392
        $definition = "{$this->method}{$action}{$code}ResponseObject";
25✔
393

394
        $this->saveResponseSchema($content, $definition);
25✔
395

396
        if (is_array($this->item['responses'][$code])) {
25✔
397
            $this->item['responses'][$code]['content'][$produce]['schema']['$ref'] = "#/components/schemas/{$definition}";
24✔
398
        }
399
    }
400

401
    protected function saveExample($code, $content, $produce)
402
    {
403
        $description = $this->getResponseDescription($code);
25✔
404
        $availableContentTypes = [
25✔
405
            'application',
25✔
406
            'text',
25✔
407
            'image',
25✔
408
        ];
25✔
409
        $explodedContentType = explode('/', $produce);
25✔
410

411
        if (in_array($explodedContentType[0], $availableContentTypes)) {
25✔
412
            $this->item['responses'][$code] = $this->makeResponseExample($content, $produce, $description);
24✔
413
        } else {
414
            $this->item['responses'][$code] = '*Unavailable for preview*';
1✔
415
        }
416
    }
417

418
    protected function makeResponseExample($content, $mimeType, $description = ''): array
419
    {
420
        $example = match ($mimeType) {
24✔
421
            'application/json' => json_decode($content, true),
21✔
422
            'application/pdf' => base64_encode($content),
1✔
423
            default => $content,
2✔
424
        };
24✔
425

426
        return [
24✔
427
            'description' => $description,
24✔
428
            'content' => [
24✔
429
                $mimeType => [
24✔
430
                    'schema' => [
24✔
431
                        'type' => 'object',
24✔
432
                    ],
24✔
433
                    'example' => $example,
24✔
434
                ],
24✔
435
            ],
24✔
436
        ];
24✔
437
    }
438

439
    protected function saveParameters($request, array $annotations)
440
    {
441
        $formRequest = new $request();
22✔
442
        $formRequest->setUserResolver($this->request->getUserResolver());
22✔
443
        $formRequest->setRouteResolver($this->request->getRouteResolver());
22✔
444
        $rules = method_exists($formRequest, 'rules') ? $this->prepareRules($formRequest->rules()) : [];
22✔
445
        $attributes = method_exists($formRequest, 'attributes') ? $formRequest->attributes() : [];
22✔
446

447
        $actionName = $this->getActionName($this->uri);
22✔
448

449
        if (in_array($this->method, ['get', 'delete'])) {
22✔
450
            $this->saveGetRequestParameters($rules, $attributes, $annotations);
16✔
451
        } else {
452
            $this->savePostRequestParameters($actionName, $rules, $attributes, $annotations);
6✔
453
        }
454
    }
455

456
    protected function prepareRules(array $rules): array
457
    {
458
        $preparedRules = [];
22✔
459

460
        foreach ($rules as $field => $rulesField) {
22✔
461
            if (is_array($rulesField)) {
22✔
462
                $rulesField = array_map(function ($rule) {
20✔
463
                    return $this->getRuleAsString($rule);
20✔
464
                }, $rulesField);
20✔
465

466
                $preparedRules[$field] = implode('|', $rulesField);
20✔
467
            } else {
468
                $preparedRules[$field] = $this->getRuleAsString($rulesField);
22✔
469
            }
470
        }
471

472
        return $preparedRules;
22✔
473
    }
474

475
    protected function getRuleAsString($rule): string
476
    {
477
        if (is_object($rule)) {
22✔
478
            if (method_exists($rule, '__toString')) {
20✔
479
                return $rule->__toString();
20✔
480
            }
481

482
            $shortName = Str::afterLast(get_class($rule), '\\');
20✔
483

484
            $ruleName = preg_replace('/Rule$/', '', $shortName);
20✔
485

486
            return Str::snake($ruleName);
20✔
487
        }
488

489
        return $rule;
22✔
490
    }
491

492
    protected function saveGetRequestParameters($rules, array $attributes, array $annotations)
493
    {
494
        foreach ($rules as $parameter => $rule) {
16✔
495
            $validation = explode('|', $rule);
16✔
496

497
            $description = Arr::get($annotations, $parameter);
16✔
498

499
            if (empty($description)) {
16✔
500
                $description = Arr::get($attributes, $parameter, implode(', ', $validation));
16✔
501
            }
502

503
            $existedParameter = Arr::first($this->item['parameters'], function ($existedParameter) use ($parameter) {
16✔
504
                return $existedParameter['name'] === $parameter;
14✔
505
            });
16✔
506

507
            if (empty($existedParameter)) {
16✔
508
                $parameterDefinition = [
15✔
509
                    'in' => 'query',
15✔
510
                    'name' => $parameter,
15✔
511
                    'description' => $description,
15✔
512
                    'schema' => [
15✔
513
                        'type' => $this->getParameterType($validation),
15✔
514
                    ],
15✔
515
                ];
15✔
516
                if (in_array('required', $validation)) {
15✔
517
                    $parameterDefinition['required'] = true;
15✔
518
                }
519

520
                $this->item['parameters'][] = $parameterDefinition;
15✔
521
            }
522
        }
523
    }
524

525
    protected function savePostRequestParameters($actionName, $rules, array $attributes, array $annotations)
526
    {
527
        if ($this->requestHasMoreProperties($actionName)) {
6✔
528
            if ($this->requestHasBody()) {
6✔
529
                $type = $this->request->header('Content-Type', 'application/json');
6✔
530

531
                $this->item['requestBody'] = [
6✔
532
                    'content' => [
6✔
533
                        $type => [
6✔
534
                            'schema' => [
6✔
535
                                '$ref' => "#/components/schemas/{$actionName}Object",
6✔
536
                            ],
6✔
537
                        ],
6✔
538
                    ],
6✔
539
                    'description' => '',
6✔
540
                    'required' => true,
6✔
541
                ];
6✔
542
            }
543

544
            $this->saveDefinitions($actionName, $rules, $attributes, $annotations);
6✔
545
        }
546
    }
547

548
    protected function saveDefinitions($objectName, $rules, $attributes, array $annotations)
549
    {
550
        $data = [
6✔
551
            'type' => 'object',
6✔
552
            'properties' => []
6✔
553
        ];
6✔
554

555
        foreach ($rules as $parameter => $rule) {
6✔
556
            $rulesArray = (is_array($rule)) ? $rule : explode('|', $rule);
6✔
557
            $parameterType = $this->getParameterType($rulesArray);
6✔
558
            $this->saveParameterType($data, $parameter, $parameterType);
6✔
559

560
            $uselessRules = $this->ruleToTypeMap;
6✔
561
            $uselessRules['required'] = 'required';
6✔
562

563
            if (in_array('required', $rulesArray)) {
6✔
564
                $data['required'][] = $parameter;
6✔
565
            }
566

567
            $rulesArray = array_flip(array_diff_key(array_flip($rulesArray), $uselessRules));
6✔
568

569
            $this->saveParameterDescription($data, $parameter, $rulesArray, $attributes, $annotations);
6✔
570
        }
571

572
        $data['example'] = $this->generateExample($data['properties']);
6✔
573
        $this->data['components']['schemas']["{$objectName}Object"] = $data;
6✔
574
    }
575

576
    protected function getParameterType(array $validation): string
577
    {
578
        $validationRules = $this->ruleToTypeMap;
21✔
579
        $validationRules['email'] = 'string';
21✔
580

581
        $parameterType = 'string';
21✔
582

583
        foreach ($validation as $item) {
21✔
584
            if (in_array($item, array_keys($validationRules))) {
21✔
585
                return $validationRules[$item];
20✔
586
            }
587
        }
588

589
        return $parameterType;
20✔
590
    }
591

592
    protected function saveParameterType(&$data, $parameter, $parameterType)
593
    {
594
        $data['properties'][$parameter] = [
6✔
595
            'type' => $parameterType
6✔
596
        ];
6✔
597
    }
598

599
    protected function saveParameterDescription(&$data, $parameter, array $rulesArray, array $attributes, array $annotations)
600
    {
601
        $description = Arr::get($annotations, $parameter);
6✔
602

603
        if (empty($description)) {
6✔
604
            $description = Arr::get($attributes, $parameter, implode(', ', $rulesArray));
6✔
605
        }
606

607
        $data['properties'][$parameter]['description'] = $description;
6✔
608
    }
609

610
    protected function requestHasMoreProperties($actionName): bool
611
    {
612
        $requestParametersCount = count($this->request->all());
6✔
613

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

617
        return $requestParametersCount > $objectParametersCount;
6✔
618
    }
619

620
    protected function requestHasBody(): bool
621
    {
622
        $parameters = $this->data['paths'][$this->uri][$this->method]['parameters'];
6✔
623

624
        $bodyParamExisted = Arr::where($parameters, function ($value) {
6✔
625
            return $value['name'] === 'body';
1✔
626
        });
6✔
627

628
        return empty($bodyParamExisted);
6✔
629
    }
630

631
    public function getConcreteRequest()
632
    {
633
        $controller = $this->request->route()->getActionName();
25✔
634

635
        if ($controller === 'Closure') {
25✔
636
            return null;
1✔
637
        }
638

639
        $explodedController = explode('@', $controller);
24✔
640

641
        $class = $explodedController[0];
24✔
642
        $method = $explodedController[1];
24✔
643

644
        if (!method_exists($class, $method)) {
24✔
645
            return null;
1✔
646
        }
647

648
        $parameters = $this->resolveClassMethodDependencies(
23✔
649
            app($class),
23✔
650
            $method
23✔
651
        );
23✔
652

653
        return Arr::first($parameters, function ($key) {
23✔
654
            return preg_match('/Request/', $key);
23✔
655
        });
23✔
656
    }
657

658
    public function saveConsume()
659
    {
660
        $consumeList = $this->data['paths'][$this->uri][$this->method]['consumes'];
25✔
661
        $consume = $this->request->header('Content-Type');
25✔
662

663
        if (!empty($consume) && !in_array($consume, $consumeList)) {
25✔
664
            $this->item['consumes'][] = $consume;
16✔
665
        }
666
    }
667

668
    public function saveTags()
669
    {
670
        $globalPrefix = config('auto-doc.global_prefix');
25✔
671
        $globalPrefix = Str::after($globalPrefix, '/');
25✔
672

673
        $explodedUri = explode('/', $this->uri);
25✔
674
        $explodedUri = array_filter($explodedUri);
25✔
675

676
        $tag = array_shift($explodedUri);
25✔
677

678
        if ($globalPrefix === $tag) {
25✔
679
            $tag = array_shift($explodedUri);
2✔
680
        }
681

682
        $this->item['tags'] = [$tag];
25✔
683
    }
684

685
    public function saveDescription($request, array $annotations)
686
    {
687
        $this->item['summary'] = $this->getSummary($request, $annotations);
22✔
688

689
        $description = Arr::get($annotations, 'description');
22✔
690

691
        if (!empty($description)) {
22✔
692
            $this->item['description'] = $description;
1✔
693
        }
694
    }
695

696
    protected function saveSecurity()
697
    {
698
        if ($this->requestSupportAuth()) {
25✔
699
            $this->addSecurityToOperation();
5✔
700
        }
701
    }
702

703
    protected function addSecurityToOperation()
704
    {
705
        $security = &$this->data['paths'][$this->uri][$this->method]['security'];
5✔
706

707
        if (empty($security)) {
5✔
708
            $security[] = [
5✔
709
                "{$this->security}" => []
5✔
710
            ];
5✔
711
        }
712
    }
713

714
    protected function getSummary($request, array $annotations)
715
    {
716
        $summary = Arr::get($annotations, 'summary');
22✔
717

718
        if (empty($summary)) {
22✔
719
            $summary = $this->parseRequestName($request);
21✔
720
        }
721

722
        return $summary;
22✔
723
    }
724

725
    protected function requestSupportAuth(): bool
726
    {
727
        $security = Arr::get($this->config, 'security');
25✔
728
        $securityDriver = Arr::get($this->config, "security_drivers.{$security}");
25✔
729

730
        switch (Arr::get($securityDriver, 'in')) {
25✔
731
            case 'header':
25✔
732
                // TODO Change this logic after migration on Swagger 3.0
733
                // Swagger 2.0 does not support cookie authorization.
734
                $securityToken = $this->request->hasHeader($securityDriver['name'])
7✔
735
                    ? $this->request->header($securityDriver['name'])
5✔
736
                    : $this->request->cookie($securityDriver['name']);
2✔
737

738
                break;
7✔
739
            case 'query':
18✔
740
                $securityToken = $this->request->query($securityDriver['name']);
1✔
741

742
                break;
1✔
743
            default:
744
                $securityToken = null;
17✔
745
        }
746

747
        return !empty($securityToken);
25✔
748
    }
749

750
    protected function parseRequestName($request)
751
    {
752
        $explodedRequest = explode('\\', $request);
21✔
753
        $requestName = array_pop($explodedRequest);
21✔
754
        $summaryName = str_replace('Request', '', $requestName);
21✔
755

756
        $underscoreRequestName = $this->camelCaseToUnderScore($summaryName);
21✔
757

758
        return preg_replace('/[_]/', ' ', $underscoreRequestName);
21✔
759
    }
760

761
    protected function getResponseDescription($code)
762
    {
763
        $defaultDescription = Response::$statusTexts[$code];
25✔
764

765
        $request = $this->getConcreteRequest();
25✔
766

767
        if (empty($request)) {
25✔
768
            return $defaultDescription;
3✔
769
        }
770

771
        $annotations = $this->getClassAnnotations($request);
22✔
772

773
        $localDescription = Arr::get($annotations, "_{$code}");
22✔
774

775
        if (!empty($localDescription)) {
22✔
776
            return $localDescription;
1✔
777
        }
778

779
        return Arr::get($this->config, "defaults.code-descriptions.{$code}", $defaultDescription);
21✔
780
    }
781

782
    protected function getActionName($uri): string
783
    {
784
        $action = preg_replace('[\/]', '', $uri);
25✔
785

786
        return Str::camel($action);
25✔
787
    }
788

789
    /**
790
     * @deprecated method is not in use
791
     * @codeCoverageIgnore
792
     */
793
    protected function saveTempData()
794
    {
795
        $exportFile = Arr::get($this->config, 'files.temporary');
796
        $data = json_encode($this->data);
797

798
        file_put_contents($exportFile, $data);
799
    }
800

801
    public function saveProductionData()
802
    {
803
        if (ParallelTesting::token()) {
3✔
804
            $this->driver->appendProcessDataToTmpFile(function (array $sharedTmpData) {
1✔
805
                $resultDocContent = (empty($sharedTmpData))
1✔
NEW
806
                    ? $this->generateEmptyData()
×
807
                    : $sharedTmpData;
1✔
808

809
                $this->mergeOpenAPIDocs($resultDocContent, $this->data);
1✔
810

811
                return $resultDocContent;
1✔
812
            });
1✔
813
        }
814

815
        $this->driver->saveData();
3✔
816
    }
817

818
    public function getDocFileContent()
819
    {
820
        $documentation = $this->driver->getDocumentation();
43✔
821

822
        $this->openAPIValidator->validate($documentation);
43✔
823

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

826
        foreach ($additionalDocs as $filePath) {
6✔
827
            try {
828
                $additionalDocContent = $this->getOpenAPIFileContent(base_path($filePath));
4✔
829
            } catch (DocFileNotExistsException|EmptyDocFileException|InvalidSwaggerSpecException $exception) {
3✔
830
                report($exception);
3✔
831

832
                continue;
3✔
833
            }
834

835
            $this->mergeOpenAPIDocs($documentation, $additionalDocContent);
1✔
836
        }
837

838
        return $documentation;
6✔
839
    }
840

841
    protected function camelCaseToUnderScore($input): string
842
    {
843
        preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
21✔
844
        $ret = $matches[0];
21✔
845

846
        foreach ($ret as &$match) {
21✔
847
            $match = ($match === strtoupper($match)) ? strtolower($match) : lcfirst($match);
21✔
848
        }
849

850
        return implode('_', $ret);
21✔
851
    }
852

853
    protected function generateExample($properties): array
854
    {
855
        $parameters = $this->replaceObjectValues($this->request->all());
6✔
856
        $example = [];
6✔
857

858
        $this->replaceNullValues($parameters, $properties, $example);
6✔
859

860
        return $example;
6✔
861
    }
862

863
    protected function replaceObjectValues($parameters): array
864
    {
865
        $classNamesValues = [
6✔
866
            File::class => '[uploaded_file]',
6✔
867
        ];
6✔
868

869
        $parameters = Arr::dot($parameters);
6✔
870
        $returnParameters = [];
6✔
871

872
        foreach ($parameters as $parameter => $value) {
6✔
873
            if (is_object($value)) {
6✔
874
                $class = get_class($value);
1✔
875

876
                $value = Arr::get($classNamesValues, $class, $class);
1✔
877
            }
878

879
            Arr::set($returnParameters, $parameter, $value);
6✔
880
        }
881

882
        return $returnParameters;
6✔
883
    }
884

885
    protected function getClassAnnotations($class): array
886
    {
887
        $reflection = new ReflectionClass($class);
22✔
888

889
        $annotations = $reflection->getDocComment();
22✔
890

891
        $annotations = Str::of($annotations)->remove("\r");
22✔
892

893
        $blocks = explode("\n", $annotations);
22✔
894

895
        $result = [];
22✔
896

897
        foreach ($blocks as $block) {
22✔
898
            if (Str::contains($block, '@')) {
22✔
899
                $index = strpos($block, '@');
1✔
900
                $block = substr($block, $index);
1✔
901
                $exploded = explode(' ', $block);
1✔
902

903
                $paramName = str_replace('@', '', array_shift($exploded));
1✔
904
                $paramValue = implode(' ', $exploded);
1✔
905

906
                if (in_array($paramName, $this->booleanAnnotations)) {
1✔
907
                    $paramValue = true;
1✔
908
                }
909

910
                $result[$paramName] = $paramValue;
1✔
911
            }
912
        }
913

914
        return $result;
22✔
915
    }
916

917
    /**
918
     * NOTE: All functions below are temporary solution for
919
     * this issue: https://github.com/OAI/OpenAPI-Specification/issues/229
920
     * We hope swagger developers will resolve this problem in next release of Swagger OpenAPI
921
     * */
922
    protected function replaceNullValues($parameters, $types, &$example)
923
    {
924
        foreach ($parameters as $parameter => $value) {
6✔
925
            if (is_null($value) && array_key_exists($parameter, $types)) {
6✔
926
                $example[$parameter] = $this->getDefaultValueByType($types[$parameter]['type']);
5✔
927
            } elseif (is_array($value)) {
6✔
928
                $this->replaceNullValues($value, $types, $example[$parameter]);
3✔
929
            } else {
930
                $example[$parameter] = $value;
6✔
931
            }
932
        }
933
    }
934

935
    protected function getDefaultValueByType($type)
936
    {
937
        $values = [
5✔
938
            'object' => 'null',
5✔
939
            'boolean' => false,
5✔
940
            'date' => "0000-00-00",
5✔
941
            'integer' => 0,
5✔
942
            'string' => '',
5✔
943
            'double' => 0
5✔
944
        ];
5✔
945

946
        return $values[$type];
5✔
947
    }
948

949
    protected function prepareInfo(array $info): array
950
    {
951
        if (empty($info)) {
59✔
952
            return $info;
1✔
953
        }
954

955
        foreach ($info['license'] as $key => $value) {
58✔
956
            if (empty($value)) {
58✔
957
                unset($info['license'][$key]);
58✔
958
            }
959
        }
960

961
        if (empty($info['license'])) {
58✔
962
            unset($info['license']);
58✔
963
        }
964

965
        if (!empty($info['description'])) {
58✔
966
            $info['description'] = view($info['description'])->render();
58✔
967
        }
968

969
        return $info;
58✔
970
    }
971

972
    protected function getOpenAPIFileContent(string $filePath): array
973
    {
974
        if (!file_exists($filePath)) {
4✔
975
            throw new DocFileNotExistsException($filePath);
1✔
976
        }
977

978
        $fileContent = json_decode(file_get_contents($filePath), true);
3✔
979

980
        if (empty($fileContent)) {
3✔
981
            throw new EmptyDocFileException($filePath);
1✔
982
        }
983

984
        $this->openAPIValidator->validate($fileContent);
2✔
985

986
        return $fileContent;
1✔
987
    }
988

989
    protected function mergeOpenAPIDocs(array &$documentation, array $additionalDocumentation): void
990
    {
991
        $paths = array_keys($additionalDocumentation['paths']);
2✔
992

993
        foreach ($paths as $path) {
2✔
994
            $additionalDocPath = $additionalDocumentation['paths'][$path];
2✔
995

996
            if (empty($documentation['paths'][$path])) {
2✔
997
                $documentation['paths'][$path] = $additionalDocPath;
2✔
998
            } else {
999
                $methods = array_keys($documentation['paths'][$path]);
1✔
1000
                $additionalDocMethods = array_keys($additionalDocPath);
1✔
1001

1002
                foreach ($additionalDocMethods as $method) {
1✔
1003
                    if (!in_array($method, $methods)) {
1✔
1004
                        $documentation['paths'][$path][$method] = $additionalDocPath[$method];
1✔
1005
                    }
1006
                }
1007
            }
1008
        }
1009

1010
        $definitions = array_keys($additionalDocumentation['components']['schemas']);
2✔
1011

1012
        foreach ($definitions as $definition) {
2✔
1013
            $documentation = Arr::add(
2✔
1014
                array: $documentation,
2✔
1015
                key: "components.schemas.{$definition}",
2✔
1016
                value: $additionalDocumentation['components']['schemas'][$definition],
2✔
1017
            );
2✔
1018
        }
1019
    }
1020
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc