Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (warning)
83.33%
70 / 84
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
MetadataSchemaProvider
83.33% covered (warning)
83.33%
70 / 84
77.78% covered (warning)
77.78%
7 / 9
35.45
0.00% covered (danger)
0.00%
0 / 1
 supports
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createFromUnionType
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 createFromScalar
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 createFromEnum
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 uploadedFileCheck
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
 createSchemaForMetadata
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
11
 addDisplaySchemaFor
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 addCreationSchemaFor
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 addModificationSchemaFor
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\SchemaGenerator\SchemaProviders;
3
4use Apie\Core\Context\ApieContext;
5use Apie\Core\Enums\DoNotChangeUploadedFile;
6use Apie\Core\Enums\ScalarType;
7use Apie\Core\Metadata\EnumMetadata;
8use Apie\Core\Metadata\Fields\PublicProperty;
9use Apie\Core\Metadata\Fields\SetterMethod;
10use Apie\Core\Metadata\MetadataFactory;
11use Apie\Core\Metadata\MetadataInterface;
12use Apie\Core\Metadata\ScalarMetadata;
13use Apie\Core\Metadata\UnionTypeMetadata;
14use Apie\Core\Utils\ConverterUtils;
15use Apie\SchemaGenerator\Builders\ComponentsBuilder;
16use Apie\SchemaGenerator\Interfaces\ModifySchemaProvider;
17use Apie\TypeConverter\ReflectionTypeFactory;
18use cebe\openapi\spec\Components;
19use cebe\openapi\spec\Reference;
20use cebe\openapi\spec\Schema;
21use Psr\Http\Message\UploadedFileInterface;
22use ReflectionClass;
23use ReflectionIntersectionType;
24use ReflectionNamedType;
25use ReflectionType;
26use ReflectionUnionType;
27
28/**
29 * Get OpenAPI schema from the apie/core MetadataFactory class.
30 *
31 * @implements ModifySchemaProvider<object>
32 */
33class MetadataSchemaProvider implements ModifySchemaProvider
34{
35    /**
36     * @var array<class-string<MetadataInterface>, string> $mapping
37     */
38    private array $mapping = [
39        EnumMetadata::class => 'createFromEnum',
40        ScalarMetadata::class => 'createFromScalar',
41        UnionTypeMetadata::class => 'createFromUnionType',
42    ];
43
44    public function supports(ReflectionClass $class): bool
45    {
46        return true;
47    }
48
49    private function createFromUnionType(ComponentsBuilder $componentsBuilder, UnionTypeMetadata $metadata, bool $display): Schema
50    {
51        $oneOfs = [];
52        foreach ($metadata->getTypes() as $type) {
53            $oneOfs[] = $this->createSchemaForMetadata($componentsBuilder, $metadata, $display, false);
54        }
55        return new Schema([
56            'oneOf' => $oneOfs,
57        ]);
58    }
59
60    private function createFromScalar(ComponentsBuilder $componentsBuilder, ScalarMetadata $metadata, bool $display): Schema
61    {
62        return match ($metadata->toScalarType()) {
63            ScalarType::NULLVALUE => new Schema(['nullable' => true]),
64            ScalarType::ARRAY => new Schema(['type' => 'array', 'items' => $componentsBuilder->getMixedReference()]),
65            ScalarType::STDCLASS => new Schema(['type' => 'object', 'additionalProperties' => $componentsBuilder->getMixedReference()]),
66            default => new Schema([
67                'type' => $metadata->toScalarType()->toJsonSchemaType(),
68            ]),
69        };
70    }
71
72    private function createFromEnum(ComponentsBuilder $componentsBuilder, EnumMetadata $metadata, bool $display): Schema
73    {
74        return new Schema([
75            'type' => $metadata->toScalarType()->toJsonSchemaType(),
76            'enum' => array_values($metadata->getOptions(new ApieContext())),
77        ]);
78    }
79
80    private function uploadedFileCheck(?ReflectionType $type): ?ReflectionType
81    {
82        if ($type === null) {
83            return null;
84        }
85        if ($type instanceof ReflectionUnionType || $type instanceof ReflectionIntersectionType) {
86            return $type;
87        }
88        assert($type instanceof ReflectionNamedType);
89        $class = ConverterUtils::toReflectionClass($type);
90        if ($class !== null && ($class->name === UploadedFileInterface::class || in_array(UploadedFileInterface::class, $class->getInterfaceNames()))) {
91            return ReflectionTypeFactory::createReflectionType(implode(
92                '|',
93                $type->allowsNull()
94                    ? [$class->name, DoNotChangeUploadedFile::class, 'null']
95                    : [$class->name, DoNotChangeUploadedFile::class]
96            ));
97        }
98        return $type;
99    }
100
101    private function createSchemaForMetadata(ComponentsBuilder $componentsBuilder, MetadataInterface $metadata, bool $display, bool $nullable): Schema|Reference
102    {
103        $className = get_class($metadata);
104        if (isset($this->mapping[$className])) {
105            return $this->{$this->mapping[$className]}($componentsBuilder, $metadata, $display);
106        }
107
108        $schema = new Schema(['type' => 'object']);
109        $properties = [];
110        foreach ($metadata->getHashmap() as $fieldName => $field) {
111            if (!$field->isField()) {
112                continue;
113            }
114            $type = $field->getTypehint();
115            if (!$display && ($field instanceof PublicProperty || $field instanceof SetterMethod)) {
116                $type = $this->uploadedFileCheck($type);
117            }
118            $properties[$fieldName] = $type ? $componentsBuilder->getSchemaForType($type, false, $display) : $componentsBuilder->getMixedReference();
119            if ($properties[$fieldName] instanceof Schema) {
120                $properties[$fieldName]->nullable = $field->allowsNull();
121            }
122        }
123        $required = $metadata->getRequiredFields()->toArray();
124        if (!empty($required)) {
125            $schema->required = $required;
126        }
127        if ($nullable) {
128            $schema->nullable = true;
129        }
130        $schema->properties = $properties;
131        return $schema;
132    }
133
134    public function addDisplaySchemaFor(
135        ComponentsBuilder $componentsBuilder,
136        string $componentIdentifier,
137        ReflectionClass $class,
138        bool $nullable = false
139    ): Components {
140        $componentsBuilder->setSchema(
141            $componentIdentifier,
142            $this->createSchemaForMetadata(
143                $componentsBuilder,
144                MetadataFactory::getResultMetadata($class, new ApieContext()),
145                true,
146                $nullable
147            )
148        );
149        return $componentsBuilder->getComponents();
150    }
151
152    public function addCreationSchemaFor(
153        ComponentsBuilder $componentsBuilder,
154        string $componentIdentifier,
155        ReflectionClass $class,
156        bool $nullable = false
157    ): Components {
158        $componentsBuilder->setSchema(
159            $componentIdentifier,
160            $this->createSchemaForMetadata(
161                $componentsBuilder,
162                MetadataFactory::getCreationMetadata($class, new ApieContext()),
163                false,
164                $nullable
165            )
166        );
167        return $componentsBuilder->getComponents();
168    }
169
170    public function addModificationSchemaFor(
171        ComponentsBuilder $componentsBuilder,
172        string $componentIdentifier,
173        ReflectionClass $class
174    ): Components {
175        $componentsBuilder->setSchema(
176            $componentIdentifier,
177            $this->createSchemaForMetadata(
178                $componentsBuilder,
179                MetadataFactory::getModificationMetadata($class, new ApieContext()),
180                false,
181                false
182            )
183        );
184        return $componentsBuilder->getComponents();
185    }
186}