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