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