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