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

cnizzardini / cakephp-swagger-bake / 18595271939

17 Oct 2025 02:09PM UTC coverage: 95.351% (+0.002%) from 95.349%
18595271939

Pull #577

github

web-flow
Merge b56e3c05e into 03a7d1c08
Pull Request #577: Fix OperationResponseAssociation not using configured connection

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

4 existing lines in 1 file now uncovered.

2584 of 2710 relevant lines covered (95.35%)

37.2 hits per line

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

95.54
/src/Lib/Operation/OperationResponse.php
1
<?php
2
declare(strict_types=1);
3

4
namespace SwaggerBake\Lib\Operation;
5

6
use Cake\Utility\Inflector;
7
use ReflectionClass;
8
use ReflectionMethod;
9
use SwaggerBake\Lib\Attribute\AttributeFactory;
10
use SwaggerBake\Lib\Attribute\OpenApiPaginator;
11
use SwaggerBake\Lib\Attribute\OpenApiResponse;
12
use SwaggerBake\Lib\Attribute\OpenApiSchema;
13
use SwaggerBake\Lib\Attribute\OpenApiSchemaProperty;
14
use SwaggerBake\Lib\Configuration;
15
use SwaggerBake\Lib\MediaType\Generic;
16
use SwaggerBake\Lib\MediaType\HalJson;
17
use SwaggerBake\Lib\MediaType\JsonLd;
18
use SwaggerBake\Lib\OpenApi\Content;
19
use SwaggerBake\Lib\OpenApi\CustomSchemaInterface;
20
use SwaggerBake\Lib\OpenApi\Operation;
21
use SwaggerBake\Lib\OpenApi\Response;
22
use SwaggerBake\Lib\OpenApi\Schema;
23
use SwaggerBake\Lib\OpenApi\Xml as OpenApiXml;
24
use SwaggerBake\Lib\Route\RouteDecorator;
25
use SwaggerBake\Lib\Swagger;
26
use UnexpectedValueException;
27

28
/**
29
 * Builds OpenAPI Operation Responses for CRUD actions and controller actions annotated with SwagResponseSchema
30
 *
31
 * @internal
32
 */
33
class OperationResponse
34
{
35
    /**
36
     * @param \SwaggerBake\Lib\Swagger $swagger Swagger
37
     * @param \SwaggerBake\Lib\Configuration $config Configuration
38
     * @param \SwaggerBake\Lib\OpenApi\Operation $operation Operation
39
     * @param \SwaggerBake\Lib\Route\RouteDecorator $route RouteDecorator
40
     * @param \SwaggerBake\Lib\OpenApi\Schema|null $schema Schema or null
41
     * @param \ReflectionMethod|null $refMethod ReflectionMethod of the controller action or null
42
     */
43
    public function __construct(
44
        private Swagger $swagger,
45
        private Configuration $config,
46
        private Operation $operation,
47
        private RouteDecorator $route,
48
        private ?Schema $schema = null,
49
        private ?ReflectionMethod $refMethod = null,
50
    ) {
51
    }
88✔
52

53
    /**
54
     * Gets an Operation with Responses
55
     *
56
     * @return \SwaggerBake\Lib\OpenApi\Operation
57
     */
58
    public function getOperationWithResponses(): Operation
59
    {
60
        $this->assignFromAttributes();
88✔
61
        $this->assignFromCrudActions();
88✔
62
        $this->assignDefaultResponses();
88✔
63

64
        return $this->operation;
88✔
65
    }
66

67
    /**
68
     * @throws \ReflectionException
69
     * @return void
70
     */
71
    private function assignFromAttributes(): void
72
    {
73
        if (!$this->refMethod instanceof ReflectionMethod) {
88✔
74
            return;
41✔
75
        }
76

77
        /** @var array<\SwaggerBake\Lib\Attribute\OpenApiResponse> $openApiResponses */
78
        $openApiResponses = (new AttributeFactory($this->refMethod, OpenApiResponse::class))->createMany();
87✔
79

80
        foreach ($openApiResponses as $openApiResponse) {
87✔
81
            $mimeTypes = $openApiResponse->mimeTypes ?? $this->config->getResponseContentTypes();
24✔
82

83
            foreach ($mimeTypes as $mimeType) {
24✔
84
                $response = new Response($openApiResponse->statusCode, $openApiResponse->description);
24✔
85

86
                if ($this->addResponseRef($response, $mimeType, $openApiResponse)) {
24✔
87
                    continue;
6✔
88
                }
89

90
                if ($this->addResponseSchema($response, $mimeType, $openApiResponse)) {
22✔
91
                    continue;
10✔
92
                }
93

94
                if ($this->addAssociatedSchema($response, $mimeType, $openApiResponse)) {
16✔
95
                    continue;
1✔
96
                }
97

98
                if ($this->addPlainText($response, $mimeType, $openApiResponse)) {
15✔
99
                    continue;
2✔
100
                }
101

102
                if ($this->addControllerSchema($response, $mimeType)) {
13✔
103
                    continue;
1✔
104
                }
105

106
                $this->operation->pushResponse($response);
12✔
107
            }
108
        }
109
    }
110

111
    /**
112
     * @param \SwaggerBake\Lib\OpenApi\Response $response Response
113
     * @param string $mimeType The mime type
114
     * @param \SwaggerBake\Lib\Attribute\OpenApiResponse $openApiResponse OpenApiResponse attribute
115
     * @return bool
116
     */
117
    private function addResponseRef(Response $response, string $mimeType, OpenApiResponse $openApiResponse): bool
118
    {
119
        if ($openApiResponse->ref) {
24✔
120
            if ($openApiResponse->schemaType == 'array') {
6✔
121
                $schema = (new Schema())
2✔
122
                    ->setItems(['$ref' => $openApiResponse->ref])
2✔
123
                    ->setType($openApiResponse->schemaType);
2✔
124
            } else {
125
                $schema = (new Schema())
4✔
126
                    ->setAllOf([['$ref' => $openApiResponse->ref]])
4✔
127
                    ->setType($openApiResponse->schemaType);
4✔
128
            }
129

130
            $response->pushContent(new Content($mimeType, $schema));
6✔
131
            $this->operation->pushResponse($response);
6✔
132

133
            return true;
6✔
134
        }
135

136
        return false;
22✔
137
    }
138

139
    /**
140
     * Parses the value of OpenApiResponse::schema into an OpenAPI response schema.
141
     *
142
     * @param \SwaggerBake\Lib\OpenApi\Response $response Response
143
     * @param string $mimeType The mime type
144
     * @param \SwaggerBake\Lib\Attribute\OpenApiResponse $openApiResponse OpenApiResponse attribute
145
     * @return bool
146
     * @throws \ReflectionException
147
     */
148
    private function addResponseSchema(Response $response, string $mimeType, OpenApiResponse $openApiResponse): bool
149
    {
150
        if ($openApiResponse->schema === null) {
22✔
151
            return false;
16✔
152
        }
153

154
        $reflection = new ReflectionClass($openApiResponse->schema);
10✔
155
        /** @var \SwaggerBake\Lib\Attribute\OpenApiSchema|null $openApiSchema */
156
        $openApiSchema = (new AttributeFactory(
10✔
157
            $reflection,
10✔
158
            OpenApiSchema::class,
10✔
159
        ))->createOneOrNull();
10✔
160

161
        $schema = $this->createResponseSchema($reflection, $openApiResponse, $openApiSchema);
10✔
162

163
        // class level attributes
164
        $schemaProperties = (new AttributeFactory(
10✔
165
            $reflection,
10✔
166
            OpenApiSchemaProperty::class,
10✔
167
        ))->createMany();
10✔
168

169
        foreach ($schemaProperties as $schemaProperty) {
10✔
170
            $schema->pushProperty($schemaProperty->create());
8✔
171
        }
172

173
        // property level attributes
174
        foreach ($reflection->getProperties() as $reflectionProperty) {
10✔
175
            $schemaProperty = (new AttributeFactory(
10✔
176
                $reflectionProperty,
10✔
177
                OpenApiSchemaProperty::class,
10✔
178
            ))->createOneOrNull();
10✔
179

180
            if ($schemaProperty instanceof OpenApiSchemaProperty) {
10✔
181
                $schema->pushProperty($schemaProperty->create());
10✔
182
            }
183
        }
184

185
        if ($openApiResponse->schemaType == 'array') {
10✔
186
            $schema->setType('array');
7✔
187
            if (
188
                !$openApiSchema instanceof OpenApiSchema
7✔
189
                || $openApiSchema->visibility === OpenApiSchema::VISIBLE_NEVER
7✔
190
            ) {
191
                $clonedSchema = clone $schema;
2✔
192
                $schema = $clonedSchema
2✔
193
                    ->setName($schema->getName())
2✔
194
                    ->setProperties([])
2✔
195
                    ->setItems(['properties' => $schema->getProperties()]);
2✔
196
                unset($clonedSchema);
7✔
197
            }
198
        } elseif ($openApiResponse->schemaType == 'collection') {
7✔
199
            $schema = $this->getMimeTypeSchema($mimeType, $openApiResponse->schemaType, $schema);
×
200
        }
201

202
        $response->pushContent(new Content($mimeType, $schema));
10✔
203
        $this->operation->pushResponse($response);
10✔
204

205
        return true;
10✔
206
    }
207

208
    /**
209
     * @param \ReflectionClass $reflectionClass A reflected instance of OpenApiResponse::schema class
210
     * @param \SwaggerBake\Lib\Attribute\OpenApiResponse $openApiResponse The attribute
211
     * @param \SwaggerBake\Lib\Attribute\OpenApiSchema|null $openApiSchema An instance of OpenApiResponse::schema or null if none was set
212
     * @return \SwaggerBake\Lib\OpenApi\Schema
213
     */
214
    private function createResponseSchema(
215
        ReflectionClass $reflectionClass,
216
        OpenApiResponse $openApiResponse,
217
        ?OpenApiSchema $openApiSchema,
218
    ): Schema {
219
        // create base schema from implementation
220
        if ($reflectionClass->implementsInterface(CustomSchemaInterface::class)) {
10✔
221
            /** @var \SwaggerBake\Lib\OpenApi\Schema $schema */
222
            $schema = $openApiResponse->schema::getOpenApiSchema();
6✔
223
            // if OpenApiSchema attribute exists set the visibility from that
224
            if ($openApiSchema instanceof OpenApiSchema) {
6✔
225
                $schema->setVisibility($openApiSchema->visibility);
×
226
            } else {
227
                $schema->setVisibility(OpenApiSchema::VISIBLE_NEVER);
6✔
228
            }
229
            // create base schema from attributes only
230
        } else {
231
            // if OpenApiSchema attribute exists set the visibility from that
232
            if ($openApiSchema instanceof OpenApiSchema) {
8✔
233
                $schema = $openApiSchema->createSchema();
6✔
234
            } else {
235
                $schema = (new Schema())->setVisibility(OpenApiSchema::VISIBLE_NEVER);
2✔
236
            }
237
        }
238

239
        $schema
10✔
240
            ->setName($schema->getName() ?? $reflectionClass->getShortName())
10✔
241
            ->setType($schema->getType() ?? 'object');
10✔
242

243
        // denote this is a user created schema
244
        $schema->setIsCustomSchema(true);
10✔
245

246
        return $schema;
10✔
247
    }
248

249
    /**
250
     * Adds plain text to the response if mime type is `text/plain` and returns true, otherwise returns false
251
     *
252
     * @param \SwaggerBake\Lib\OpenApi\Response $response Response
253
     * @param string $mimeType The mime type
254
     * @param \SwaggerBake\Lib\Attribute\OpenApiResponse $openApiResponse OpenApiResponse attribute
255
     * @return bool
256
     */
257
    private function addPlainText(Response $response, string $mimeType, OpenApiResponse $openApiResponse): bool
258
    {
259
        if ($mimeType == 'text/plain') {
15✔
260
            $schema = (new Schema())
2✔
261
                ->setType('string')
2✔
262
                ->setFormat($openApiResponse->schemaFormat ?? '');
2✔
263
            $response->pushContent(new Content($mimeType, $schema));
2✔
264
            $this->operation->pushResponse($response);
2✔
265

266
            return true;
2✔
267
        }
268

269
        return false;
13✔
270
    }
271

272
    /**
273
     * Adds associated schema.
274
     *
275
     * @param \SwaggerBake\Lib\OpenApi\Response $response Response
276
     * @param string $mimeType The mime type
277
     * @param \SwaggerBake\Lib\Attribute\OpenApiResponse $openApiResponse OpenApiResponse attribute
278
     * @return bool
279
     * @throws \ReflectionException
280
     */
281
    private function addAssociatedSchema(Response $response, string $mimeType, OpenApiResponse $openApiResponse): bool
282
    {
283
        if (is_array($openApiResponse->associations)) {
16✔
284
            $assocSchema = (new OperationResponseAssociation($this->swagger, $this->config, $this->route, $this->schema))
1✔
285
                ->build($openApiResponse);
1✔
286
            $schema = $this->getMimeTypeSchema(
1✔
287
                $mimeType,
1✔
288
                $openApiResponse->schemaType,
1✔
289
                $assocSchema,
1✔
290
            );
1✔
291
            $response->pushContent(new Content(
1✔
292
                $mimeType,
1✔
293
                $openApiResponse->schemaFormat ? $schema->setFormat($openApiResponse->schemaFormat) : $schema,
1✔
294
            ));
1✔
295
            $this->operation->pushResponse($response);
1✔
296

297
            return true;
1✔
298
        }
299

300
        return false;
15✔
301
    }
302

303
    /**
304
     * Adds $this->schema which is derived from the Controller per Cake conventions.
305
     *
306
     * @param \SwaggerBake\Lib\OpenApi\Response $response Response
307
     * @param string $mimeType The mime type
308
     * @return bool
309
     */
310
    private function addControllerSchema(Response $response, string $mimeType): bool
311
    {
312
        if ($this->schema != null) {
13✔
313
            $response->pushContent(new Content($mimeType, $this->schema));
1✔
314
            $this->operation->pushResponse($response);
1✔
315

316
            return true;
1✔
317
        }
318

319
        return false;
12✔
320
    }
321

322
    /**
323
     * Set response from Crud actions
324
     *
325
     * @return void
326
     */
327
    private function assignFromCrudActions(): void
328
    {
329
        if ($this->operation->hasSuccessResponseCode() || !$this->schema) {
88✔
330
            return;
65✔
331
        }
332

333
        $action = strtolower($this->route->getAction());
67✔
334
        $actionTypes = [
67✔
335
            'index' => 'array',
67✔
336
            'add' => 'object',
67✔
337
            'view' => 'object',
67✔
338
            'edit' => 'object',
67✔
339
        ];
67✔
340

341
        if (!array_key_exists($action, $actionTypes)) {
67✔
342
            return;
61✔
343
        }
344

345
        $schemaType = $actionTypes[$action];
67✔
346

347
        $schemaMode = $this->swagger->getSchemaByName($this->schema->getName()) ?? $this->schema;
67✔
348

349
        $response = new Response('200');
67✔
350

351
        foreach ($this->config->getResponseContentTypes() as $mimeType) {
67✔
352
            $schema = $this->getMimeTypeSchema($mimeType, $schemaType, $schemaMode->getRefPath());
67✔
353
            $response->pushContent(new Content($mimeType, $schema));
67✔
354
        }
355

356
        $this->operation->pushResponse($response);
67✔
357
    }
358

359
    /**
360
     * Gets a schema based on mimetype
361
     *
362
     * @param string $mimeType a mime type (e.g. application/xml, application/json)
363
     * @param string $schemaType object or array
364
     * @param \SwaggerBake\Lib\OpenApi\Schema|string $schema Schema or an OpenApi $ref string
365
     * @return \SwaggerBake\Lib\OpenApi\Schema
366
     */
367
    private function getMimeTypeSchema(string $mimeType, string $schemaType, Schema|string $schema): Schema
368
    {
369
        return match ($mimeType) {
67✔
UNCOV
370
            'application/xml' => (new Generic($this->swagger))
×
UNCOV
371
                ->buildSchema($schema, $schemaType)
×
UNCOV
372
                ->setXml((new OpenApiXml())->setName('response')),
×
373
            'application/hal+json','application/vnd.hal+json' => (new HalJson())->buildSchema($schema, $schemaType),
4✔
374
            'application/ld+json' => (new JsonLd())->buildSchema($schema, $schemaType),
4✔
375
            'text/plain' => (new Schema())->setType('string'),
×
376
            default => (new Generic($this->swagger))->buildSchema($schema, $schemaType)
67✔
377
        };
67✔
378
    }
379

380
    /**
381
     * Assigns a default response:
382
     *
383
     * HTTP DELETE: 204 with empty response body
384
     * DEFAULT: 200 with empty response body and first element from responseContentTypes config as mimeType
385
     *
386
     * @return void
387
     */
388
    private function assignDefaultResponses(): void
389
    {
390
        if ($this->operation->hasSuccessResponseCode()) {
88✔
391
            return;
78✔
392
        }
393

394
        if (strtolower($this->route->getAction()) == 'delete') {
71✔
395
            $this->operation->pushResponse(new Response('204', 'Resource deleted'));
61✔
396

397
            return;
61✔
398
        }
399

400
        $response = new Response('200');
54✔
401

402
        if (in_array($this->operation->getHttpMethod(), ['OPTIONS','HEAD'])) {
54✔
403
            $this->operation->pushResponse($response);
6✔
404

405
            return;
6✔
406
        }
407

408
        foreach ($this->config->getResponseContentTypes() as $mimeType) {
53✔
409
            $schema = (new Schema())->setDescription('');
53✔
410

411
            if ($mimeType == 'application/xml') {
53✔
UNCOV
412
                $schema->setXml((new OpenApiXml())->setName('response'));
×
413
            }
414

415
            if (isset($this->refMethod) && !empty($this->refMethod->getAttributes(OpenApiPaginator::class))) {
53✔
416
                $schema = Inflector::singularize($this->route->getController() ?? throw new UnexpectedValueException());
33✔
417
                $schema = $this->getMimeTypeSchema($mimeType, 'array', '#/components/schemas/' . $schema);
33✔
418
            }
419

420
            $response->pushContent(new Content($mimeType, $schema));
53✔
421
        }
422

423
        $this->operation->pushResponse($response);
53✔
424
    }
425
}
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