Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.03% covered (warning)
86.03%
117 / 136
46.67% covered (danger)
46.67%
7 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComponentsBuilder
86.03% covered (warning)
86.03%
117 / 136
46.67% covered (danger)
46.67%
7 / 15
80.61
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createWithExistingComponents
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 runInContentType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setContentType
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getContentType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMixedReference
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getSchemaForReference
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getComponents
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 checkDuplicate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSchema
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 getSchemaForMethod
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 getSchemaForType
76.00% covered (warning)
76.00%
19 / 25
0.00% covered (danger)
0.00%
0 / 1
13.99
 addDisplaySchemaFor
90.00% covered (success)
90.00%
27 / 30
0.00% covered (danger)
0.00%
0 / 1
17.29
 addCreationSchemaFor
93.10% covered (success)
93.10%
27 / 29
0.00% covered (danger)
0.00%
0 / 1
16.08
 addModificationSchemaFor
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
1<?php
2namespace Apie\SchemaGenerator\Builders;
3
4use Apie\Core\Attributes\Context;
5use Apie\Core\Exceptions\DuplicateIdentifierException;
6use Apie\Core\ValueObjects\Utils;
7use Apie\SchemaGenerator\Exceptions\ICanNotExtractASchemaFromClassException;
8use Apie\SchemaGenerator\Interfaces\ModifySchemaProvider;
9use Apie\SchemaGenerator\Interfaces\SchemaProvider;
10use Apie\SchemaGenerator\Other\MethodSchemaInfo;
11use cebe\openapi\ReferenceContext;
12use cebe\openapi\spec\Components;
13use cebe\openapi\spec\OpenApi;
14use cebe\openapi\spec\Reference;
15use cebe\openapi\spec\Schema;
16use ReflectionClass;
17use ReflectionIntersectionType;
18use ReflectionMethod;
19use ReflectionNamedType;
20use ReflectionType;
21use ReflectionUnionType;
22
23class ComponentsBuilder
24{
25    /**
26     * @var array<int, SchemaProvider<object>>
27     */
28    private array $schemaProviders;
29
30    private Components $components;
31
32    private ?string $contentType = null;
33
34    /**
35     * @param SchemaProvider<object> $schemaProviders
36     */
37    public function __construct(SchemaProvider... $schemaProviders)
38    {
39        $this->schemaProviders = $schemaProviders;
40        $this->components = new Components([]);
41    }
42
43    /**
44     * @param SchemaProvider<object> $schemaProviders
45     */
46    public static function createWithExistingComponents(Components $components, SchemaProvider... $schemaProviders): self
47    {
48        $res = new self(...$schemaProviders);
49        $res->components = $components;
50        return $res;
51    }
52
53    public function runInContentType(?string $contentType, callable $callback): mixed
54    {
55        $previousContentType = $this->contentType;
56        try {
57            $this->contentType = $contentType;
58            return $callback();
59        } finally {
60            $this->contentType = $previousContentType;
61        }
62    }
63
64    public function setContentType(?string $contentType): self
65    {
66        $this->contentType = $contentType;
67        return $this;
68    }
69
70    public function getContentType(): ?string
71    {
72        return $this->contentType;
73    }
74
75    public function getMixedReference(): Reference
76    {
77        if (!isset($this->components->schemas['mixed'])) {
78            $this->setSchema('mixed', new Schema(['nullable' => true]));
79        }
80        return new Reference(['$ref' => '#/components/schemas/mixed']);
81    }
82
83    public function getSchemaForReference(Reference $reference): ?Schema
84    {
85        $result = $reference->resolve(
86            new ReferenceContext(
87                new OpenApi(['components' => $this->components]),
88                'file:///#/components'
89            )
90        );
91        assert($result === null || $result instanceof Schema);
92        return $result;
93    }
94
95    public function getComponents(): Components
96    {
97        $schemas = $this->components->schemas;
98        ksort($schemas);
99        $this->components->schemas = $schemas;
100        return $this->components;
101    }
102
103    private function checkDuplicate(string $identifier, Schema $original, Schema $newObject): void
104    {
105        throw new DuplicateIdentifierException($identifier, json_encode($original->getSerializableData()), json_encode($newObject->getSerializableData()));
106    }
107
108    public function setSchema(string $identifier, Schema $schema): self
109    {
110        if (isset($this->components->schemas[$identifier])) {
111            $this->checkDuplicate($identifier, $this->components->schemas[$identifier], $schema);
112        }
113        $schemas = $this->components->schemas;
114        $schemas[$identifier] = $schema;
115        
116        $this->components->schemas = $schemas;
117        return $this;
118    }
119
120    public function getSchemaForMethod(ReflectionMethod $method): MethodSchemaInfo
121    {
122        $returnValue = new MethodSchemaInfo();
123        foreach ($method->getParameters() as $parameter) {
124            if (count($parameter->getAttributes(Context::class)) > 0) {
125                continue;
126            }
127            if (!$parameter->isDefaultValueAvailable() && !$parameter->allowsNull()) {
128                $returnValue->required[] = $parameter->name;
129            }
130            $type = $parameter->getType();
131            $returnValue->schemas[$parameter->name] = $this->getSchemaForType($type, $parameter->isVariadic(), nullable: $type?->allowsNull() ?? false);
132        }
133        return $returnValue;
134    }
135
136    public function getSchemaForType(ReflectionType|null $type, bool $array = false, bool $display = false, bool $nullable = false): Schema|Reference
137    {
138        $map = $nullable ? ['nullable' => true] : [];
139        $methodName = $display ? 'addDisplaySchemaFor' : 'addCreationSchemaFor';
140        $result = $this->getMixedReference();
141        if ($type instanceof ReflectionIntersectionType) {
142            $allOfs = [];
143            foreach ($type->getTypes() as $allOfType) {
144                $allOfs[] = $this->$methodName((string) $allOfType, nullable: $nullable || $allOfType->allowsNull());
145            }
146            $result = new Schema([
147                'allOf' => $allOfs,
148            ] + $map);
149        } elseif ($type instanceof ReflectionUnionType) {
150            $oneOfs = [];
151            foreach ($type->getTypes() as $oneOfType) {
152                $oneOfs[] = $this->$methodName((string) $oneOfType, nullable: $nullable || $oneOfType->allowsNull());
153            }
154            $result = new Schema([
155                'oneOf' => $oneOfs,
156            ] + $map);
157        } elseif ($type instanceof ReflectionNamedType) {
158            $result = $this->$methodName($type->getName(), nullable: $nullable || $type->allowsNull());
159        }
160        if ($array) {
161            return new Schema([
162                'type' => 'array',
163                'items' => $result,
164            ] + $map);
165        }
166        return $result;
167    }
168
169    public function addDisplaySchemaFor(string $class, ?string $discriminatorColumn = null, bool $nullable = false): Reference|Schema
170    {
171        $map = $nullable ? ['nullable' => true] : [];
172        switch ($class) {
173            case 'mixed':
174                return $this->getMixedReference();
175            case 'string':
176                return new Schema(['type' => $class] + $map);
177            case 'array':
178                return new Schema(['type' => 'object', 'additionalProperties' => $this->getMixedReference()] + $map);
179            case 'bool':
180                return new Schema(['type' => 'boolean'] + $map);
181            case 'true':
182                return new Schema(['type' => 'boolean', 'enum' => [true]]);
183            case 'false':
184                return new Schema(['type' => 'boolean', 'enum' => [false]]);
185            case 'int':
186                return new Schema(['type' => 'integer'] + $map);
187            case 'float':
188            case 'double':
189                return new Schema(['type' => 'number'] + $map);
190            case 'void':
191            case 'null':
192                return new Schema(['nullable' => true, 'default' => null]);
193        }
194        $refl = new ReflectionClass($class);
195        $identifier = Utils::getDisplayNameForValueObject($refl) . ($nullable ? '-nullable' : '') . '-get';
196        if (isset($this->components->schemas[$identifier])) {
197            return new Reference(['$ref' => '#/components/schemas/' . $identifier]);
198        }
199
200        foreach ($this->schemaProviders as $schemaProvider) {
201            if ($schemaProvider->supports($refl)) {
202                $this->components = $schemaProvider->addDisplaySchemaFor($this, $identifier, $refl, $nullable);
203                return new Reference(['$ref' => '#/components/schemas/' . $identifier]);
204            }
205        }
206        throw new ICanNotExtractASchemaFromClassException($refl->name);
207    }
208
209    public function addCreationSchemaFor(string $class, ?string $discriminatorColumn = null, bool $nullable = false): Reference|Schema
210    {
211        $map = $nullable ? ['nullable' => true] : [];
212        switch ($class) {
213            case 'mixed':
214                return $this->getMixedReference();
215            case 'object':
216                return new Schema(['type' => 'object', 'additionalProperties' => true] + $map);
217            case 'string':
218                return new Schema(['type' => $class] + $map);
219            case 'array':
220                return new Schema(['type' => 'object', 'additionalProperties' => $this->getMixedReference()] + $map);
221            case 'bool':
222                return new Schema(['type' => 'boolean'] + $map);
223            case 'int':
224                return new Schema(['type' => 'integer'] + $map);
225            case 'float':
226            case 'double':
227                return new Schema(['type' => 'number'] + $map);
228            case 'null':
229                return new Schema(['nullable' => true, 'default' => null]);
230        }
231        $refl = new ReflectionClass($class);
232        $identifier = Utils::getDisplayNameForValueObject($refl) . ($nullable ? '-nullable' : '') . '-post';
233        if ($this->contentType) {
234            $identifier .= '-' . str_replace('/', '-', $this->contentType);
235        }
236        if (isset($this->components->schemas[$identifier])) {
237            return new Reference(['$ref' => '#/components/schemas/' . $identifier]);
238        }
239        foreach ($this->schemaProviders as $schemaProvider) {
240            if ($schemaProvider->supports($refl)) {
241                $this->components = $schemaProvider->addCreationSchemaFor($this, $identifier, $refl, $nullable);
242                return new Reference(['$ref' => '#/components/schemas/' . $identifier]);
243            }
244        }
245        throw new ICanNotExtractASchemaFromClassException($refl->name);
246    }
247
248    public function addModificationSchemaFor(string $class, ?string $discriminatorColumn = null): Reference|Schema
249    {
250        $refl = new ReflectionClass($class);
251        $identifier = Utils::getDisplayNameForValueObject($refl) . '-patch';
252        if (isset($this->components->schemas[$identifier])) {
253            return new Reference(['$ref' => '#/components/schemas/' . $identifier]);
254        }
255        foreach ($this->schemaProviders as $schemaProvider) {
256            if ($schemaProvider instanceof ModifySchemaProvider && $schemaProvider->supports($refl)) {
257                $this->components = $schemaProvider->addModificationSchemaFor($this, $identifier, $refl);
258                return new Reference(['$ref' => '#/components/schemas/' . $identifier]);
259            }
260        }
261        throw new ICanNotExtractASchemaFromClassException($refl->name);
262    }
263}