Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.96% covered (success)
97.96%
96 / 98
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
CreatesFromMeta
97.96% covered (success)
97.96%
96 / 98
75.00% covered (warning)
75.00%
3 / 4
37
0.00% covered (danger)
0.00%
0 / 1
 createValueOptions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 createFromMetadata
96.92% covered (success)
96.92%
63 / 65
0.00% covered (danger)
0.00%
0 / 1
19
 createFromScalar
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
10
 createFromField
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2namespace Apie\Graphql\Concerns;
3
4use Apie\Core\Context\ApieContext;
5use Apie\Core\Enums\ScalarType;
6use Apie\Core\Metadata\Fields\FieldInterface;
7use Apie\Core\Metadata\ItemHashmapMetadata;
8use Apie\Core\Metadata\ItemListMetadata;
9use Apie\Core\Metadata\MetadataFactory;
10use Apie\Core\Metadata\MetadataInterface;
11use Apie\Core\Metadata\NullableMetadataInterface;
12use Apie\Core\Metadata\UnionTypeMetadata;
13use Apie\Core\Utils\ConverterUtils;
14use Apie\Graphql\Types;
15use Apie\Graphql\Types\FromMetadataInputType;
16use Apie\Graphql\Types\MapOfType;
17use Apie\TypeConverter\ReflectionTypeFactory;
18use GraphQL\Type\Definition\EnumType;
19use GraphQL\Type\Definition\InputType;
20use GraphQL\Type\Definition\StringType;
21use GraphQL\Type\Definition\Type;
22use GraphQL\Type\Definition\UnionType;
23use GraphQL\Upload\UploadType;
24use Psr\Http\Message\UploadedFileInterface;
25use ReflectionClass;
26
27trait CreatesFromMeta
28{
29    private static function createValueOptions(MetadataInterface $metadata): ?array
30    {
31        $options = $metadata->getValueOptions(new ApieContext());
32        if ($options === null) {
33            return null;
34        }
35        $enumValues = [];
36        foreach ($options as $option) {
37            $enumValues[$option->name] = ['value' => $option->value, 'description' => $option->description];
38        }
39        return $enumValues;
40    }
41
42    public static function createFromMetadata(MetadataInterface $metadata, bool $nullable = false): Type
43    {
44        if ($metadata instanceof NullableMetadataInterface) {
45            $nullable = $nullable || $metadata->allowsNull();
46        }
47        if ($metadata instanceof UnionTypeMetadata) {
48            $metaWithoutNull = $metadata->toSkipNull();
49            
50            if ($metaWithoutNull instanceof UnionTypeMetadata && ScalarType::MIXED === $metaWithoutNull->toScalarType()) {
51                $name = 'UnionOf' . md5(static::class . $metadata->getDisplayName());
52                $result = Types::createSingleton(
53                    $name,
54                    fn () => new UnionType([
55                        'name' => $name,
56                        'types' => array_map(
57                            fn (MetadataInterface $meta) => self::createFromMetadata($meta),
58                            $metaWithoutNull->getTypes()
59                        ),
60                    ])
61                );
62                if ($nullable) {
63                    return $result;
64                }
65                return Type::nonNull($result);
66            }
67            $metadata = $metaWithoutNull;
68        }
69
70        $class = $metadata->toClass();
71        $options = self::createValueOptions($metadata);
72        if ($options !== null && $class) {
73            $result = Types::createSingleton($class->getShortName(), function () use ($class, $options) {
74                return new EnumType([
75                    'name' => $class->getShortName(),
76                    'values' => $options,
77                ]);
78            });
79            if ($nullable) {
80                return $result;
81            }
82            return Type::nonNull($result);
83        }
84
85
86        if ($metadata instanceof ItemListMetadata) {
87            $result = Type::listOf(self::createFromMetadata($metadata->getArrayItemType()));
88            if ($nullable) {
89                return $result;
90            }
91            return Type::nonNull($result);
92        }
93        if ($metadata instanceof ItemHashmapMetadata) {
94            $result = new MapOfType(self::createFromMetadata($metadata->getArrayItemType()));
95            if ($nullable) {
96                return $result;
97            }
98            return Type::nonNull($result);
99        }
100        $class = $metadata->toClass();
101        if ($class && in_array(UploadedFileInterface::class, [$class->name, ...$class->getInterfaceNames()])) {
102            if (in_array(InputType::class, (new ReflectionClass(static::class))->getInterfaceNames())) {
103                return Types::createSingleton($class->getShortName() . '_create', function () use ($class) {
104                    return new UploadType(['name' => $class->getShortName() . '_create']);
105                });
106            }
107            return Types::createSingleton($class->getShortName(), function () use ($class) {
108                return new StringType([
109                    'name' => $class->getShortName(),
110                    'description' => 'URL to download the file',
111                ]);
112            });
113        }
114        
115        $scalarType = $metadata->toScalarType($nullable);
116        if ($scalarType === ScalarType::STDCLASS) {
117            $result = new self($metadata);
118            $result = Types::createSingleton(
119                $result->name,
120                fn () => $result
121            );
122            if ($nullable) {
123                return $result;
124            }
125            return Type::nonNull($result);
126        }
127        return self::createFromScalar($scalarType, $nullable);
128    }
129
130    public static function createFromScalar(ScalarType $scalarType, bool $nullable = false): Type
131    {
132        $result = match($scalarType) {
133            ScalarType::STRING => Type::string(),
134            ScalarType::INTEGER => Type::int(),
135            ScalarType::FLOAT => Type::float(),
136            ScalarType::BOOLEAN => Type::boolean(),
137            ScalarType::NULLVALUE => Types::null(),
138            ScalarType::ARRAY => Type::listOf(Types::json()),
139            ScalarType::STDCLASS => new MapOfType(Types::json()),
140            ScalarType::MIXED => Types::json(),
141        };
142        if ($nullable) {
143            return $result;
144        }
145        return Type::nonNull($result);
146    }
147
148    public static function createFromField(FieldInterface $fieldMetadata): Type
149    {
150        $type = $fieldMetadata->getTypehint();
151        $class = ConverterUtils::toReflectionClass($type);
152        $nullable = $fieldMetadata->allowsNull() || !$fieldMetadata->isRequired();
153        $scalar = ScalarType::createFromReflectionType($type, $nullable);
154        
155        if ($class !== null && in_array($scalar, [ScalarType::STDCLASS, ScalarType::MIXED], true)) {
156            $method = static::class === FromMetadataInputType::class ? 'getCreationMetadata' : 'getResultMetadata';
157            return self::createFromMetadata(
158                MetadataFactory::getMetadataStrategyForType(
159                    $type ?? ReflectionTypeFactory::createReflectionType('mixed')
160                )->$method(new ApieContext()),
161                $nullable
162            );
163        }
164        
165        return self::createFromScalar($scalar, $nullable);
166    }
167}