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

cnizzardini / cakephp-swagger-bake / 14286059275

05 Apr 2025 09:43PM UTC coverage: 95.456% (-0.02%) from 95.475%
14286059275

push

github

web-flow
Add support for collections in OpenApiResponse (#566)

* Add support for collections in OpenApiResponse

140 of 145 new or added lines in 37 files covered. (96.55%)

1 existing line in 1 file now uncovered.

2584 of 2707 relevant lines covered (95.46%)

37.09 hits per line

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

91.56
/src/Lib/Configuration.php
1
<?php
2
declare(strict_types=1);
3

4
namespace SwaggerBake\Lib;
5

6
use Cake\Core\Configure;
7
use Cake\Datasource\ConnectionManager;
8
use InvalidArgumentException;
9
use LogicException;
10
use RuntimeException;
11
use SwaggerBake\Lib\Exception\SwaggerBakeRunTimeException;
12
use Symfony\Component\Yaml\Yaml;
13

14
/**
15
 * Stores values of swagger_bake.php configuration file.
16
 *
17
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
18
 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
19
 */
20
class Configuration
21
{
22
    /**
23
     * @var string APP root, this is just for testing
24
     */
25
    protected string $root;
26

27
    /**
28
     * @var string The base prefix for your API, e.g. `/` or `/api/`
29
     */
30
    protected string $prefix;
31

32
    /**
33
     * @var string A base Swagger YML file, see example in assets (e.g. `/config/swagger.yml`).
34
     */
35
    protected string $yml;
36

37
    /**
38
     * @var string Web accessible file path the JSON file is written to (e.g. `/webroot/swagger.json`).
39
     */
40
    protected string $json;
41

42
    /**
43
     * @var string The URL browsers will use to access the JSON file (e.g. `/swagger.json`).
44
     */
45
    protected string $webPath;
46

47
    /**
48
     * @var string The default document type, either swagger or redoc.
49
     */
50
    protected string $docType = 'swagger';
51

52
    /**
53
     * @var bool Should OpenAPI be reloaded when the SwaggerBake::index route is called.
54
     */
55
    protected bool $hotReload = false;
56

57
    /**
58
     * @var string Default exception schema in your OpenAPI YAML file.
59
     */
60
    protected string $exceptionSchema = 'Exception';
61

62
    /**
63
     * @var array<string>  The requested mimetypes accepted by your API.
64
     */
65
    protected array $requestAccepts = ['application/json'];
66

67
    /**
68
     * @var array<string>  The mimetypes your API responds with.
69
     */
70
    protected array $responseContentTypes = ['application/json'];
71

72
    /**
73
     * @var int json_encode flags to be used when generation OpenAPI JSON file.
74
     * @link https://www.php.net/manual/en/function.json-encode.php
75
     */
76
    protected int $jsonOptions = JSON_PRETTY_PRINT;
77

78
    /**
79
     * @var array<string>  The HTTP methods implemented for edit() actions.
80
     */
81
    protected array $editActionMethods = ['PATCH'];
82

83
    /**
84
     * @var string The connection name to use when loading tables for building schemas from models.
85
     */
86
    protected string $connectionName = 'default';
87

88
    /**
89
     * @var array Array of namespaces. Useful if your controllers or entities exist in non-standard namespace such
90
     *      as a plugin. This was mostly added to aid in unit testing, but there are cases where controllers may
91
     *      exist in a plugin namespace etc...
92
     */
93
    protected array $namespaces = [
94
        'controllers' => ['\App\\'],
95
        'entities' => ['\App\\'],
96
        'tables' => ['\App\\'],
97
    ];
98

99
    /**
100
     * @param array $config SwaggerBake configurations (useful for unit tests mainly). Default: []
101
     * @param string $root The application ROOT (useful for unit tests mainly). Default: ROOT
102
     */
103
    public function __construct(array $config = [], string $root = ROOT)
104
    {
105
        $this->root = $root;
148✔
106
        try {
107
            $config = !empty($config) ? $config : Configure::readOrFail('SwaggerBake');
148✔
108
        } catch (RuntimeException $e) {
×
109
            throw new SwaggerBakeRunTimeException(
×
110
                'SwaggerBake config missing. Have you added it to your `config/bootstrap.php`? ' . $e->getMessage(),
×
111
                500,
×
NEW
112
                $e,
×
113
            );
×
114
        }
115

116
        foreach (['yml', 'json', 'webPath', 'prefix'] as $property) {
148✔
117
            if (!array_key_exists(key: $property, array: $config)) {
148✔
118
                throw new InvalidArgumentException(
1✔
119
                    "Property `$property` must be defined in your config/swagger_bake.php configuration file.",
1✔
120
                );
1✔
121
            }
122
        }
123

124
        foreach ($config as $property => $value) {
148✔
125
            if (!property_exists($this, $property)) {
148✔
126
                throw new LogicException("Property $property does not exist in class " . static::class);
1✔
127
            }
128
            $setter = 'set' . ucfirst($property);
148✔
129
            if (!method_exists($this, $setter)) {
148✔
130
                throw new LogicException(
×
131
                    sprintf(
×
132
                        'Method %s does not exist in class %s but is trying to be called.',
×
133
                        $setter,
×
NEW
134
                        self::class,
×
NEW
135
                    ),
×
UNCOV
136
                );
×
137
            }
138
            $this->{$setter}($value);
148✔
139
        }
140
    }
141

142
    /**
143
     * @return string
144
     */
145
    public function getPrefix(): string
146
    {
147
        return $this->prefix;
112✔
148
    }
149

150
    /**
151
     * @param string $prefix The base prefix for your API, e.g. `/` or `/api/`
152
     * @return $this
153
     */
154
    public function setPrefix(string $prefix)
155
    {
156
        $this->throwInvalidArgExceptionIfPrefixInvalid(
148✔
157
            $prefix,
148✔
158
            "Invalid prefix: $prefix. Prefix must be a valid URI path such as `/` or `/api`.",
148✔
159
        );
148✔
160

161
        $this->prefix = $prefix;
148✔
162

163
        return $this;
148✔
164
    }
165

166
    /**
167
     * @return string
168
     */
169
    public function getYml(): string
170
    {
171
        return $this->root . $this->yml;
78✔
172
    }
173

174
    /**
175
     * @param string $yml A base Swagger YML file, see example in assets (e.g. `/config/swagger.yml`).
176
     * @return $this
177
     */
178
    public function setYml(string $yml)
179
    {
180
        $message = 'Generally this value should be placed in your projects webroot directory.';
148✔
181

182
        if (!str_starts_with(haystack: $yml, needle: '/')) {
148✔
183
            throw new InvalidArgumentException(
1✔
184
                sprintf(
1✔
185
                    "Invalid yml: `%s`. Value should start with a / an be relative to your 
1✔
186
                    applications ROOT. $message",
1✔
187
                    $yml,
1✔
188
                ),
1✔
189
            );
1✔
190
        }
191

192
        $path = $this->root . $yml;
148✔
193
        if (!file_exists($path)) {
148✔
194
            throw new InvalidArgumentException(
1✔
195
                sprintf(
1✔
196
                    'A YML file is required but none was found at "%s". See "%s" for a sample file.',
1✔
197
                    $path,
1✔
198
                    '$vendor/cnizzardini/cakephp-swagger-bake/assets/swagger.yml',
1✔
199
                ),
1✔
200
            );
1✔
201
        }
202

203
        $this->yml = $yml;
148✔
204

205
        return $this;
148✔
206
    }
207

208
    /**
209
     * @return string
210
     */
211
    public function getJson(): string
212
    {
213
        return $this->root . $this->json;
4✔
214
    }
215

216
    /**
217
     * @param string $json Web accessible file path the JSON file is written to (e.g. `/webroot/swagger.json`).
218
     * @return $this
219
     */
220
    public function setJson(string $json)
221
    {
222
        $message = 'Generally this value should be placed in your projects webroot directory.';
148✔
223

224
        if (!str_starts_with(haystack: $json, needle: '/')) {
148✔
225
            throw new InvalidArgumentException(
1✔
226
                sprintf(
1✔
227
                    "Invalid json: `%s`. Value should start with a `/` and be relative to your 
1✔
228
                    applications ROOT. $message",
1✔
229
                    $json,
1✔
230
                ),
1✔
231
            );
1✔
232
        }
233

234
        $path = $this->root . $json;
148✔
235
        if ((!file_exists($path) && !is_writable($path)) || !touch($path)) {
148✔
236
            throw new InvalidArgumentException(
1✔
237
                sprintf(
1✔
238
                    "Invalid json: `%s`. Config value for `json` must exist on the file system. An attempt was 
1✔
239
                    made to create %s, but permission was denied or the file path is bad. Either fix the file system 
240
                    permissions, create the file and/or both. $message",
1✔
241
                    $json,
1✔
242
                    $path,
1✔
243
                ),
1✔
244
            );
1✔
245
        }
246

247
        $this->json = $json;
148✔
248

249
        return $this;
148✔
250
    }
251

252
    /**
253
     * @return string
254
     */
255
    public function getWebPath(): string
256
    {
257
        return $this->webPath;
3✔
258
    }
259

260
    /**
261
     * @param string|null $webPath The URL browsers will use to access the JSON file (e.g. `/swagger.json`).
262
     * @return $this
263
     */
264
    public function setWebPath(?string $webPath)
265
    {
266
        $this->throwInvalidArgExceptionIfPrefixInvalid(
148✔
267
            $webPath,
148✔
268
            "Invalid webPath: `$webPath`. webPath must be a valid web accessible path based e.g. /swagger.json. 
148✔
269
            Generally if your application serves the json file from something like https://example.com/swagger.json 
270
            this value should be /swagger.json ",
148✔
271
        );
148✔
272

273
        $this->webPath = $webPath;
148✔
274

275
        return $this;
148✔
276
    }
277

278
    /**
279
     * @return string
280
     */
281
    public function getDocType(): string
282
    {
283
        return $this->docType;
3✔
284
    }
285

286
    /**
287
     * @param string $docType Valid types are swagger and redoc
288
     * @return $this
289
     */
290
    public function setDocType(string $docType)
291
    {
292
        $docType = strtolower($docType);
2✔
293
        $allowed = ['swagger','redoc'];
2✔
294
        if (!in_array($docType, $allowed)) {
2✔
295
            throw new InvalidArgumentException(
1✔
296
                "Invalid docType: $docType. Doctype must be one of " . implode(', ', $allowed),
1✔
297
            );
1✔
298
        }
299
        $this->docType = $docType;
1✔
300

301
        return $this;
1✔
302
    }
303

304
    /**
305
     * @return bool
306
     */
307
    public function isHotReload(): bool
308
    {
309
        return $this->hotReload;
3✔
310
    }
311

312
    /**
313
     * @param bool $hotReload Should OpenAPI be reloaded when the SwaggerBake::index route is called.
314
     * @return $this
315
     */
316
    public function setHotReload(bool $hotReload)
317
    {
318
        $this->hotReload = $hotReload;
132✔
319

320
        return $this;
132✔
321
    }
322

323
    /**
324
     * @return string
325
     */
326
    public function getExceptionSchema(): string
327
    {
328
        return $this->exceptionSchema;
70✔
329
    }
330

331
    /**
332
     * @param string $exceptionSchema The exception schema, for example if your default exception schema is
333
     *  `#/components/Schema/Exception` then the argument should be: Exception
334
     * @return $this
335
     */
336
    public function setExceptionSchema(string $exceptionSchema)
337
    {
338
        $this->exceptionSchema = $exceptionSchema;
123✔
339

340
        return $this;
123✔
341
    }
342

343
    /**
344
     * @return array
345
     */
346
    public function getNamespaces(): array
347
    {
348
        foreach ($this->namespaces as $k => $ns) {
83✔
349
            $this->namespaces[$k] = array_unique($ns);
83✔
350
        }
351

352
        return $this->namespaces;
83✔
353
    }
354

355
    /**
356
     * @param array $namespaces Array of namespaces. Useful if your controllers or entities exist in non-standard
357
     *  namespace such as a plugin. This was mostly added to aid in unit testing, but there are cases where controllers
358
     *  may exist in a plugin namespace etc
359
     * @return $this
360
     */
361
    public function setNamespaces(array $namespaces)
362
    {
363
        $this->namespaces = $namespaces;
136✔
364

365
        return $this;
136✔
366
    }
367

368
    /**
369
     * @return array
370
     */
371
    public function getParsedYml(): array
372
    {
373
        return Yaml::parseFile($this->getYml());
3✔
374
    }
375

376
    /**
377
     * @return mixed|string
378
     */
379
    public function getTitleFromYml(): mixed
380
    {
381
        $yml = $this->getParsedYml();
3✔
382

383
        return $yml['info']['title'] ?? '';
3✔
384
    }
385

386
    /**
387
     * @param string|null $doctype The layout type ("redoc" or null for Swagger). Default: null
388
     * @return string
389
     */
390
    public function getLayout(?string $doctype = null): string
391
    {
392
        $doctype = empty($doctype) ? $this->getDocType() : $doctype;
3✔
393
        if ($doctype == 'redoc') {
3✔
394
            return 'SwaggerBake.redoc';
1✔
395
        }
396

397
        return 'SwaggerBake.default';
2✔
398
    }
399

400
    /**
401
     * @param string|null $doctype The documentation type ("redoc" or null for Swagger). Default: null
402
     * @return string
403
     */
404
    public function getView(?string $doctype = null): string
405
    {
406
        $doctype = empty($doctype) ? $this->getDocType() : $doctype;
3✔
407
        if ($doctype == 'redoc') {
3✔
408
            return 'SwaggerBake.Swagger/redoc';
1✔
409
        }
410

411
        return 'SwaggerBake.Swagger/index';
2✔
412
    }
413

414
    /**
415
     * @return array
416
     */
417
    public function getRequestAccepts(): array
418
    {
419
        return $this->requestAccepts;
68✔
420
    }
421

422
    /**
423
     * @param array<string> $requestAccepts The requested mimetypes accepted by your API.
424
     * @return $this
425
     */
426
    public function setRequestAccepts(array $requestAccepts)
427
    {
428
        $this->requestAccepts = $requestAccepts;
123✔
429

430
        return $this;
123✔
431
    }
432

433
    /**
434
     * @return array
435
     */
436
    public function getResponseContentTypes(): array
437
    {
438
        return $this->responseContentTypes;
85✔
439
    }
440

441
    /**
442
     * @param array<string> $responseContentTypes The mimetypes your API responds with.
443
     * @return $this
444
     */
445
    public function setResponseContentTypes(array $responseContentTypes)
446
    {
447
        $this->responseContentTypes = $responseContentTypes;
123✔
448

449
        return $this;
123✔
450
    }
451

452
    /**
453
     * @return int
454
     */
455
    public function getJsonOptions(): int
456
    {
457
        return $this->jsonOptions;
50✔
458
    }
459

460
    /**
461
     * @param int $jsonOptions json_encode flags to be used when generation OpenAPI JSON file.
462
     * @link https://www.php.net/manual/en/function.json-encode.php
463
     * @return $this
464
     */
465
    public function setJsonOptions(int $jsonOptions)
466
    {
467
        $this->jsonOptions = $jsonOptions;
1✔
468

469
        return $this;
1✔
470
    }
471

472
    /**
473
     * @return string
474
     */
475
    public function getConnectionName(): string
476
    {
477
        return $this->connectionName;
79✔
478
    }
479

480
    /**
481
     * @param string $connectionName Connection name to use when loading tables for building schemas from models.
482
     * @return $this
483
     */
484
    public function setConnectionName(string $connectionName)
485
    {
486
        $configuredConnections = ConnectionManager::configured();
3✔
487

488
        if (!in_array($connectionName, $configuredConnections)) {
3✔
489
            throw new InvalidArgumentException(
1✔
490
                sprintf(
1✔
491
                    'Invalid connectionName supplied: %s. Must be one of %s',
1✔
492
                    $connectionName,
1✔
493
                    implode(', ', $configuredConnections),
1✔
494
                ),
1✔
495
            );
1✔
496
        }
497

498
        $this->connectionName = $connectionName;
2✔
499

500
        return $this;
2✔
501
    }
502

503
    /**
504
     * @return array<string>
505
     */
506
    public function getEditActionMethods(): array
507
    {
508
        return $this->editActionMethods;
56✔
509
    }
510

511
    /**
512
     * @param array<string> $editActionMethods Valid types are POST, PUT, and PATCH.
513
     * @return $this
514
     */
515
    public function setEditActionMethods(array $editActionMethods)
516
    {
517
        $methods = ['POST', 'PUT', 'PATCH'];
2✔
518
        $results = array_filter($editActionMethods, function ($method) use ($methods) {
2✔
519
            return !in_array(strtoupper($method), $methods);
2✔
520
        });
2✔
521

522
        if (count($results)) {
2✔
523
            throw new InvalidArgumentException(
1✔
524
                sprintf(
1✔
525
                    'Invalid editActionMethod supplied: %s. Must be one of %s',
1✔
526
                    implode(', ', $results),
1✔
527
                    implode(', ', $methods),
1✔
528
                ),
1✔
529
            );
1✔
530
        }
531

532
        $this->editActionMethods = $editActionMethods;
1✔
533

534
        return $this;
1✔
535
    }
536

537
    /**
538
     * @param string $prefix The prefix to validate
539
     * @param string $message The exception message
540
     * @return void
541
     */
542
    private function throwInvalidArgExceptionIfPrefixInvalid(string $prefix, string $message): void
543
    {
544
        if (
545
            !str_starts_with(haystack: $prefix, needle: '/')
148✔
546
            || !filter_var('https://example.com' . $prefix, FILTER_VALIDATE_URL)
148✔
547
        ) {
548
            throw new InvalidArgumentException($message);
2✔
549
        }
550
    }
551
}
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