Passed
Pull Request — master (#7)
by Yonel Ceruto
07:59
created

MutationFormResolverExtension::buildConfig()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 25
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 11
cts 11
cp 1
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 20
nc 1
nop 1
crap 1
1
<?php
2
/*******************************************************************************
3
 *  This file is part of the GraphQL Bundle package.
4
 *
5
 *  (c) YnloUltratech <[email protected]>
6
 *
7
 *  For the full copyright and license information, please view the LICENSE
8
 *  file that was distributed with this source code.
9
 ******************************************************************************/
10
11
namespace Ynlo\GraphQLBundle\Definition\Extension;
12
13
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
14
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
15
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
16
use Symfony\Component\Form\Extension\Core\Type\EmailType;
17
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
18
use Symfony\Component\Form\Extension\Core\Type\NumberType;
19
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
20
use Symfony\Component\Form\Extension\Core\Type\TextType;
21
use Symfony\Component\Form\FormFactory;
22
use Symfony\Component\Form\FormInterface;
23
use Ynlo\GraphQLBundle\Definition\ArgumentDefinition;
24
use Ynlo\GraphQLBundle\Definition\DefinitionInterface;
25
use Ynlo\GraphQLBundle\Definition\FieldDefinition;
26
use Ynlo\GraphQLBundle\Definition\InputObjectDefinition;
27
use Ynlo\GraphQLBundle\Definition\MutationDefinition;
28
use Ynlo\GraphQLBundle\Definition\NodeAwareDefinitionInterface;
29
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint;
30
use Ynlo\GraphQLBundle\Form\Type\GraphQLType;
31
use Ynlo\GraphQLBundle\Form\Type\IDType;
32
use Ynlo\GraphQLBundle\Type\Types;
33
use Ynlo\GraphQLBundle\Util\ClassUtils;
34
use Ynlo\GraphQLBundle\Util\TypeUtil;
35
36
class MutationFormResolverExtension extends AbstractDefinitionExtension
37
{
38
    protected $formFactory;
39
40
    public function __construct(FormFactory $formFactory)
41
    {
42
        $this->formFactory = $formFactory;
43
    }
44
45
    /**
46
     * {@inheritDoc}
47
     */
48
    public function getName(): string
49 22
    {
50
        return 'form';
51 22
    }
52 22
53
    /**
54
     * {@inheritDoc}
55
     */
56
    public function buildConfig(ArrayNodeDefinition $root): void
57 22
    {
58
        $config = $root
59 22
            ->info('Resolve the form to use as input for mutations')
60
            ->addDefaultsIfNotSet()
61
            ->canBeDisabled()
62
            ->children();
63
64
        $config->variableNode('type')
65 22
               ->defaultNull()
66
               ->info(
67
                   'Specify the form type to use,
68 22
[string] Name of the form type to use
69 22
[true|null] The form will be automatically resolved to ...Bundle\Form\Input\{Node}\{MutationName}Input.
70 22
[true] Throw a exception if the form can`t be located
71 22
[false] The form is not required and should not be resolved'
72
               );
73 22
        $config->variableNode('options')->defaultValue([])->info('Form options');
74 22
        $config->variableNode('argument')
75 22
               ->defaultValue('input')
76 22
               ->info('Name of the argument to use as input');
77
78
        $config->booleanNode('client_mutation_id')
79
               ->defaultTrue()
80
               ->info('Automatically add a field called clientMutationId');
81
    }
82 22
83 22
    /**
84 22
     * {@inheritDoc}
85 22
     */
86
    public function configure(DefinitionInterface $definition, Endpoint $endpoint, array $config): void
87 22
    {
88 22
        if (!$definition instanceof MutationDefinition || !isset($config['enabled'])) {
89 22
            return;
90 22
        }
91
92
        $formType = $config['type'] ?? null;
93
94
        //the related class is used to match a form using naming conventions
95 22
        $relatedClass = null;
96
        if ($definition instanceof NodeAwareDefinitionInterface && $definition->getNode()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $definition->getNode() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
97 22
            $relatedClass = $definition->getNode();
98 22
            if ($class = $endpoint->getClassForType($relatedClass)) {
99
                $relatedClass = $class;
100
            }
101 22
        } elseif ($definition->getResolver()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $definition->getResolver() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
102
            $relatedClass = $definition->getResolver();
103
        }
104 22
105 22
        //try find the form using a related class
106 22
        if ($relatedClass && (!$formType || true === $formType)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $relatedClass of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
107 22
            $bundleNamespace = ClassUtils::relatedBundleNamespace($relatedClass);
108 22
            $formClass = ClassUtils::applyNamingConvention(
109
                $bundleNamespace,
110
                'Form\Input',
111
                $definition->getNode(),
0 ignored issues
show
Bug introduced by
The method getNode() does not exist on Ynlo\GraphQLBundle\Definition\DefinitionInterface. It seems like you code against a sub-type of Ynlo\GraphQLBundle\Definition\DefinitionInterface such as Ynlo\GraphQLBundle\Defin...ableDefinitionInterface or Ynlo\GraphQLBundle\Definition\ObjectDefinition or Ynlo\GraphQLBundle\Definition\ObjectDefinition or Ynlo\GraphQLBundle\Definition\ObjectDefinition or Ynlo\GraphQLBundle\Definition\ObjectDefinition. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

111
                $definition->/** @scrutinizer ignore-call */ 
112
                             getNode(),
Loading history...
112
                ucfirst($definition->getName()),
113
                'Input'
114
            );
115 22
            if (class_exists($formClass)) {
116 22
                $formType = $formClass;
117 22
            } elseif (true === $formType) {
118 22
                $error = sprintf(
119 22
                    'Can`t find a valid input form type to use in "%s".
120 22
                         Create the form "%s" or specify a custom form',
121 22
                    $definition->getName(),
122 22
                    $formClass
123
                );
124 22
                throw new \RuntimeException($error);
125 22
            }
126
        }
127
128
        if ($formType) {
129
            $config['type'] = $formType;
130
131
            $form = $this->formFactory->create($formType, null, $config['options'] ?? []);
0 ignored issues
show
Bug introduced by
It seems like $config['options'] ?? array() can also be of type string; however, parameter $options of Symfony\Component\Form\FormFactory::create() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

131
            $form = $this->formFactory->create($formType, null, /** @scrutinizer ignore-type */ $config['options'] ?? []);
Loading history...
132
            $inputObject = $this->createFormInputObject($endpoint, $form, ucfirst($definition->getName()));
133
            $endpoint->addType($inputObject);
134
135
            $input = new ArgumentDefinition();
136
            $input->setName($config['argument']);
137 22
            $input->setType($inputObject->getName());
138 22
139
            if ($config['client_mutation_id']) {
140 22
                $clientMutationId = new FieldDefinition();
141 22
                $clientMutationId->setName('clientMutationId');
142 22
                $clientMutationId->setType(Types::STRING);
143
                $clientMutationId->setDescription('A unique identifier for the client performing the mutation.');
144 22
                $inputObject->prependField($clientMutationId);
145 22
            }
146 22
147
            $definition->addArgument($input);
0 ignored issues
show
Bug introduced by
The method addArgument() does not exist on Ynlo\GraphQLBundle\Definition\DefinitionInterface. It seems like you code against a sub-type of Ynlo\GraphQLBundle\Definition\DefinitionInterface such as Ynlo\GraphQLBundle\Defin...ableDefinitionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

147
            $definition->/** @scrutinizer ignore-call */ 
148
                         addArgument($input);
Loading history...
148 22
            $definition->setMeta('form', $config);
149 22
        }
150 22
    }
151 22
152 22
    public function createFormInputObject(Endpoint $endpoint, FormInterface $form, string $name): InputObjectDefinition
153 22
    {
154
        $inputObject = new InputObjectDefinition();
155
        $inputObject->setName($name.'Input');
156 22
157 22
        foreach ($form->all() as $formField) {
158
            $field = new FieldDefinition();
159 22
            $field->setName($formField->getConfig()->getOption('label') ?? $formField->getName());
160
            $field->setNonNull($formField->isRequired());
161
            $field->setOriginName($formField->getName());
162
163
            if ($formField->all()) {
164
                $childName = $name.ucfirst($formField->getName());
165
                $child = $this->createFormInputObject($endpoint, $formField, $childName);
166
                $endpoint->addType($child);
167
                $field->setType($child->getName());
168 22
            } elseif (is_a($formField->getConfig()->getType()->getInnerType(), CollectionType::class)) {
169
                $childName = $name.ucfirst($formField->getName());
170 22
                $childFormType = $formField->getConfig()->getOptions()['entry_type'];
171 22
                $childFormOptions = $formField->getConfig()->getOptions()['entry_options'];
172
                $childForm = $this->formFactory->create($childFormType, null, $childFormOptions ?? []);
173 22
                $childForm->setParent($form);
174 22
                try {
175 22
                    //resolve type if is a valid scalar type or predefined type
176 22
                    $this->resolveFormFieldDefinition($field, $childForm);
177 22
                    $field->setList(true);
178
                } catch (\InvalidArgumentException $exception) {
179 22
                    //on exception, try build a child form for this collection
180 22
                    $child = $this->createFormInputObject($endpoint, $childForm, $childName);
181 22
                    $field->setType($child->getName());
182 22
                    $field->setList(true);
183 22
                    $endpoint->add($child);
184 22
                }
185
            } else {
186
                $this->resolveFormFieldDefinition($field, $formField);
187
            }
188
189
            $inputObject->addField($field);
190
        }
191
192
        return $inputObject;
193
    }
194
195
    public function resolveFormFieldDefinition(FieldDefinition $field, FormInterface $form)
196
    {
197
        $type = null;
198
        $resolver = $form->getConfig()->getType()->getOptionsResolver();
199
        if ($resolver->hasDefault('graphql_type')) {
200
            $type = $resolver->resolve([])['graphql_type'];
201
            $field->setList(TypeUtil::isTypeList($type));
202 22
            $type = TypeUtil::normalize($type);
203
        }
204
205 22
        if (is_a($form->getConfig()->getType()->getInnerType(), GraphQLType::class, true)) {
206
            $type = $form->getConfig()->getOptions()['graphql_type'];
207
            $field->setList(TypeUtil::isTypeList($type));
208 22
            $type = TypeUtil::normalize($type);
209
        }
210
211
        if (is_a($form->getConfig()->getType()->getInnerType(), IDType::class, true)) {
212
            if ($form->getConfig()->hasOption('multiple') && $form->getConfig()->getOption('multiple')) {
213
                $field->setList(true);
214
            }
215 22
            $type = Types::ID;
216
        }
217 22
218 22
        if (is_a($form->getConfig()->getType()->getInnerType(), TextType::class, true)) {
219 22
            $type = Types::STRING;
220
        }
221
222
        if (is_a($form->getConfig()->getType()->getInnerType(), TextareaType::class, true)) {
223
            $type = Types::STRING;
224
        }
225 22
226 22
        if (is_a($form->getConfig()->getType()->getInnerType(), EmailType::class, true)) {
227 22
            $type = Types::STRING;
228 22
        }
229
230
        if (is_a($form->getConfig()->getType()->getInnerType(), CheckboxType::class, true)) {
231 22
            $type = Types::BOOLEAN;
232 22
        }
233 22
234
        if (is_a($form->getConfig()->getType()->getInnerType(), IntegerType::class, true)) {
235 22
            $type = Types::INT;
236
        }
237
238 22
        if (is_a($form->getConfig()->getType()->getInnerType(), NumberType::class, true)) {
239 22
            $type = Types::FLOAT;
240
        }
241
242 22
        if (!$type) {
243
            $error = sprintf(
244
                'The field "%s" in the parent form "%s" does not have a valid type. 
245
                If your are using a custom type, must define a option called "graphql_type" to resolve the form to a valid GraphQL type',
246 22
                $form->getName(),
247 22
                $form->getParent()->getName()
248
            );
249
            throw new \InvalidArgumentException($error);
250 22
        }
251 22
252
        $field->setType($type);
253
    }
254
}
255