Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.78% covered (warning)
84.78%
78 / 92
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
MetadataSchemaProvider
84.78% covered (warning)
84.78%
78 / 92
80.00% covered (warning)
80.00%
8 / 10
44.36
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
 applyPropertiesToSchema
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
16
 createSchemaForMetadata
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 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 static 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    public static function applyPropertiesToSchema(Schema $schema, ComponentsBuilder $componentsBuilder, MetadataInterface $metadata, bool $display, bool $nullable): void
103    {
104        $properties = [];
105        foreach ($metadata->getHashmap() as $fieldName => $field) {
106            if (!$field->isField()) {
107                continue;
108            }
109            $type = $field->getTypehint();
110            if (!$display && ($field instanceof PublicProperty || $field instanceof SetterMethod)) {
111                $type = self::uploadedFileCheck($type);
112            }
113            $properties[$fieldName] = $type ? $componentsBuilder->getSchemaForType($type, false, $display, $nullable) : $componentsBuilder->getMixedReference();
114            if ($properties[$fieldName] instanceof Schema) {
115                $properties[$fieldName]->nullable = $field->allowsNull() || $nullable;
116            }
117            if ($properties[$fieldName] instanceof Reference && $properties[$fieldName]->getReference() !=='#/components/schemas/mixed') {
118                foreach ($field->getAttributes(Description::class) as $descriptionAttribute) {
119                    $refSchema = $componentsBuilder->getSchemaForReference($properties[$fieldName]);
120                    if ($refSchema) {
121                        $refSchema->description = $descriptionAttribute->description;
122                    }
123                    break;
124                }
125            }
126        }
127        $required = $metadata->getRequiredFields()->toArray();
128        if (!empty($required)) {
129            $schema->required = $required;
130        }
131        if (!empty($properties) || $schema->type === 'object') {
132            $schema->properties = $properties;
133        }
134    }
135
136    private function createSchemaForMetadata(ComponentsBuilder $componentsBuilder, MetadataInterface $metadata, bool $display, bool $nullable): Schema|Reference
137    {
138        $className = get_class($metadata);
139        if (isset($this->mapping[$className])) {
140            return $this->{$this->mapping[$className]}($componentsBuilder, $metadata, $display);
141        }
142
143        $schema = new Schema(['type' => 'object']);
144        self::applyPropertiesToSchema($schema, $componentsBuilder, $metadata, $display, $nullable);
145        if ($nullable) {
146            $schema->nullable = true;
147        }
148        return $schema;
149    }
150
151    public function addDisplaySchemaFor(
152        ComponentsBuilder $componentsBuilder,
153        string $componentIdentifier,
154        ReflectionClass $class,
155        bool $nullable = false
156    ): Components {
157        $componentsBuilder->setSchema(
158            $componentIdentifier,
159            $this->createSchemaForMetadata(
160                $componentsBuilder,
161                MetadataFactory::getResultMetadata($class, new ApieContext()),
162                true,
163                $nullable
164            )
165        );
166        return $componentsBuilder->getComponents();
167    }
168
169    public function addCreationSchemaFor(
170        ComponentsBuilder $componentsBuilder,
171        string $componentIdentifier,
172        ReflectionClass $class,
173        bool $nullable = false
174    ): Components {
175        $componentsBuilder->setSchema(
176            $componentIdentifier,
177            $this->createSchemaForMetadata(
178                $componentsBuilder,
179                MetadataFactory::getCreationMetadata($class, new ApieContext()),
180                false,
181                $nullable
182            )
183        );
184        return $componentsBuilder->getComponents();
185    }
186
187    public function addModificationSchemaFor(
188        ComponentsBuilder $componentsBuilder,
189        string $componentIdentifier,
190        ReflectionClass $class
191    ): Components {
192        $componentsBuilder->setSchema(
193            $componentIdentifier,
194            $this->createSchemaForMetadata(
195                $componentsBuilder,
196                MetadataFactory::getModificationMetadata($class, new ApieContext()),
197                false,
198                false
199            )
200        );
201        return $componentsBuilder->getComponents();
202    }
203}