• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

overblog / GraphQLBundle / 25039715876

28 Apr 2026 07:25AM UTC coverage: 98.531% (-0.02%) from 98.547%
25039715876

Pull #1246

github

web-flow
Merge 794936211 into 6c49418a4
Pull Request #1246: Preserve omitted input fields in DTO hydration

18 of 19 new or added lines in 2 files covered. (94.74%)

12 existing lines in 2 files now uncovered.

4561 of 4629 relevant lines covered (98.53%)

76.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.43
/src/Transformer/ArgumentsTransformer.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Overblog\GraphQLBundle\Transformer;
6

7
use GraphQL\Type\Definition\EnumType;
8
use GraphQL\Type\Definition\InputObjectType;
9
use GraphQL\Type\Definition\ListOfType;
10
use GraphQL\Type\Definition\NonNull;
11
use GraphQL\Type\Definition\ResolveInfo;
12
use GraphQL\Type\Definition\Type;
13
use Overblog\GraphQLBundle\Definition\Omittable;
14
use Overblog\GraphQLBundle\Definition\Type\PhpEnumType;
15
use Overblog\GraphQLBundle\Error\InvalidArgumentError;
16
use Overblog\GraphQLBundle\Error\InvalidArgumentsError;
17
use ReflectionClass;
18
use ReflectionNamedType;
19
use Symfony\Component\PropertyAccess\PropertyAccess;
20
use Symfony\Component\PropertyAccess\PropertyAccessor;
21
use Symfony\Component\Validator\ConstraintViolationList;
22
use Symfony\Component\Validator\Validator\ValidatorInterface;
23

24
use function array_key_exists;
25
use function array_map;
26
use function count;
27
use function is_a;
28
use function is_array;
29
use function is_object;
30
use function sprintf;
31
use function strlen;
32
use function substr;
33

34
final class ArgumentsTransformer
35
{
36
    public const RESOLVE_INFO_TOKEN = '#ResolveInfo';
37

38
    private PropertyAccessor $accessor;
39
    private ?ValidatorInterface $validator;
40
    private array $classesMap;
41

42
    public function __construct(?ValidatorInterface $validator = null, array $classesMap = [])
43
    {
44
        $this->validator = $validator;
28✔
45
        $this->accessor = PropertyAccess::createPropertyAccessor();
28✔
46
        $this->classesMap = $classesMap;
28✔
47
    }
48

49
    /**
50
     * Get the PHP class for a given type.
51
     *
52
     * @return object|false
53
     */
54
    private function getTypeClassInstance(string $type)
55
    {
56
        $classname = isset($this->classesMap[$type]) ? $this->classesMap[$type]['class'] : false;
22✔
57

58
        return $classname ? new $classname() : false;
22✔
59
    }
60

61
    private function isOmittableProperty(object $instance, string $property): bool
62
    {
63
        $reflectionClass = new ReflectionClass($instance);
22✔
64

65
        if (!$reflectionClass->hasProperty($property)) {
22✔
NEW
66
            return false;
×
67
        }
68

69
        $reflectionType = $reflectionClass->getProperty($property)->getType();
22✔
70

71
        return $reflectionType instanceof ReflectionNamedType
22✔
72
            && is_a($reflectionType->getName(), Omittable::class, true);
22✔
73
    }
74

75
    /**
76
     * Extract given type from Resolve Info.
77
     */
78
    private function getType(string $type, ResolveInfo $info): ?Type
79
    {
80
        return $info->schema->getType($type);
24✔
81
    }
82

83
    /**
84
     * Populate an object based on type with given data.
85
     *
86
     * @param mixed $data
87
     *
88
     * @return mixed
89
     */
90
    private function populateObject(Type $type, $data, bool $multiple, ResolveInfo $info)
91
    {
92
        if (null === $data) {
24✔
93
            return null;
22✔
94
        }
95

96
        if ($type instanceof NonNull) {
24✔
97
            $type = $type->getWrappedType();
×
98
        }
99

100
        if ($multiple) {
24✔
101
            return array_map(fn ($data) => $this->populateObject($type, $data, false, $info), $data);
18✔
102
        }
103

104
        if ($type instanceof EnumType) {
24✔
105
            /** Enum based on PHP Enum are already processed by PhpEnumType */
106
            if ($type instanceof PhpEnumType && $type->isEnumPhp()) { /** @phpstan-ignore-line */
8✔
107
                return $data;
2✔
108
            }
109
            $instance = $this->getTypeClassInstance($type->name);
8✔
110
            if ($instance) {
8✔
111
                $this->accessor->setValue($instance, 'value', $data);
4✔
112

113
                return $instance;
4✔
114
            }
115

116
            return $data;
6✔
117
        } elseif ($type instanceof InputObjectType) {
24✔
118
            $instance = $this->getTypeClassInstance($type->name);
22✔
119
            if (!$instance) {
22✔
120
                return $data;
×
121
            }
122

123
            $fields = $type->getFields();
22✔
124

125
            foreach ($fields as $name => $field) {
22✔
126
                $isFieldProvided = array_key_exists($name, $data);
22✔
127
                $isOmittableProperty = $this->isOmittableProperty($instance, $name);
22✔
128

129
                if ($field->defaultValueExists() && !$isFieldProvided && !$isOmittableProperty) {
22✔
130
                    continue;
22✔
131
                }
132
                $fieldData = $isFieldProvided ? $this->accessor->getValue($data, sprintf('[%s]', $name)) : null;
22✔
133
                $fieldType = $field->getType();
22✔
134

135
                if ($fieldType instanceof NonNull) {
22✔
136
                    $fieldType = $fieldType->getWrappedType();
8✔
137
                }
138

139
                if ($fieldType instanceof ListOfType) {
22✔
140
                    $fieldValue = $this->populateObject($fieldType->getWrappedType(), $fieldData, true, $info);
10✔
141
                } else {
142
                    $fieldValue = $this->populateObject($fieldType, $fieldData, false, $info);
22✔
143
                }
144

145
                if ($isOmittableProperty) {
22✔
146
                    $fieldValue = $isFieldProvided ? Omittable::set($fieldValue) : Omittable::omitted();
2✔
147
                }
148

149
                $this->accessor->setValue($instance, $name, $fieldValue);
22✔
150
            }
151

152
            return $instance;
22✔
153
        }
154

155
        return $data;
24✔
156
    }
157

158
    /**
159
     * Given a GraphQL type and an array of data, populate corresponding object recursively
160
     * using annotated classes.
161
     *
162
     * @param mixed $data
163
     *
164
     * @return mixed
165
     */
166
    public function getInstanceAndValidate(string $argType, $data, ResolveInfo $info, string $argName)
167
    {
168
        $isRequired = '!' === $argType[strlen($argType) - 1];
24✔
169
        $isMultiple = '[' === $argType[0];
24✔
170
        $isStrictMultiple = false;
24✔
171
        if ($isMultiple) {
24✔
172
            $isStrictMultiple = '!' === $argType[strpos($argType, ']') - 1];
10✔
173
        }
174

175
        $endIndex = ($isRequired ? 1 : 0) + ($isMultiple ? 1 : 0) + ($isStrictMultiple ? 1 : 0);
24✔
176
        $type = substr($argType, $isMultiple ? 1 : 0, $endIndex > 0 ? -$endIndex : strlen($argType));
24✔
177

178
        $gqlType = $this->getType($type, $info);
24✔
179
        $result = $this->populateObject($gqlType, $data, $isMultiple, $info);
24✔
180

181
        // We only want to validate input object types and not the scalar or object types that are already validated by the GraphQL library.
182
        $shouldValidate = $gqlType instanceof InputObjectType;
24✔
183

184
        if (null !== $this->validator && $shouldValidate) {
24✔
185
            $errors = new ConstraintViolationList();
22✔
186
            if (is_object($result)) {
22✔
187
                $errors = $this->validator->validate($result);
12✔
188
            }
189
            if (is_array($result) && $isMultiple) {
22✔
190
                foreach ($result as $element) {
10✔
191
                    if (is_object($element)) {
10✔
192
                        $errors->addAll(
10✔
193
                            $this->validator->validate($element)
10✔
194
                        );
10✔
195
                    }
196
                }
197
            }
198

199
            if (count($errors) > 0) {
22✔
200
                throw new InvalidArgumentError($argName, $errors);
4✔
201
            }
202
        }
203

204
        return $result;
20✔
205
    }
206

207
    /**
208
     * Transform a list of arguments into their corresponding php class and validate them.
209
     *
210
     * @param mixed $data
211
     *
212
     * @return array
213
     */
214
    public function getArguments(array $mapping, $data, ResolveInfo $info)
215
    {
216
        $args = [];
12✔
217
        $exceptions = [];
12✔
218

219
        foreach ($mapping as $name => $type) {
12✔
220
            try {
221
                if (self::RESOLVE_INFO_TOKEN === $type) {
10✔
222
                    $args[] = $info;
2✔
223
                    continue;
2✔
224
                }
225
                $value = $this->getInstanceAndValidate($type, $data[$name], $info, $name);
8✔
226
                $args[] = $value;
4✔
227
            } catch (InvalidArgumentError $exception) {
4✔
228
                $exceptions[] = $exception;
4✔
229
            }
230
        }
231

232
        if (!empty($exceptions)) {
12✔
233
            throw new InvalidArgumentsError($exceptions);
4✔
234
        }
235

236
        return $args;
8✔
237
    }
238
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc