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