Completed
Push — master ( 3e44d5...49b4bd )
by Rafael
06:52
created

MutationFormResolverPlugin   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 248
Duplicated Lines 0 %

Test Coverage

Coverage 75.74%

Importance

Changes 0
Metric Value
wmc 39
eloc 139
dl 0
loc 248
ccs 103
cts 136
cp 0.7574
rs 9.28
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 3 1
A __construct() 0 4 1
A buildConfig() 0 24 1
B createFormInputObject() 0 51 9
B resolveFormFieldDefinition() 0 47 11
C configure() 0 70 16
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\Plugin;
12
13
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
14
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
15
use Symfony\Component\Form\FormFactory;
16
use Symfony\Component\Form\FormInterface;
17
use Symfony\Component\Form\Guess\Guess;
18
use Symfony\Component\Form\Guess\TypeGuess;
19
use Ynlo\GraphQLBundle\Definition\ArgumentDefinition;
20
use Ynlo\GraphQLBundle\Definition\DefinitionInterface;
21
use Ynlo\GraphQLBundle\Definition\FieldDefinition;
22
use Ynlo\GraphQLBundle\Definition\InputObjectDefinition;
23
use Ynlo\GraphQLBundle\Definition\MutationDefinition;
24
use Ynlo\GraphQLBundle\Definition\NodeAwareDefinitionInterface;
25
use Ynlo\GraphQLBundle\Definition\Registry\Endpoint;
26
use Ynlo\GraphQLBundle\Form\Input\InputFieldTypeGuesser;
27
use Ynlo\GraphQLBundle\Type\Types;
28
use Ynlo\GraphQLBundle\Util\ClassUtils;
29
use Ynlo\GraphQLBundle\Util\TypeUtil;
30
31
/**
32
 * MutationFormResolverPlugin
33
 */
34
class MutationFormResolverPlugin extends AbstractDefinitionPlugin
35
{
36
    /**
37
     * @var FormFactory
38
     */
39
    protected $formFactory;
40
41
    /**
42
     * @var InputFieldTypeGuesser[]|iterable
43
     */
44
    protected $typeGuessers;
45
46
    /**
47
     * MutationFormResolverPlugin constructor.
48
     *
49
     * @param FormFactory                      $formFactory
50
     * @param iterable|InputFieldTypeGuesser[] $typeGuessers
51
     */
52 1
    public function __construct(FormFactory $formFactory, iterable $typeGuessers = [])
53
    {
54 1
        $this->formFactory = $formFactory;
55 1
        $this->typeGuessers = $typeGuessers;
56 1
    }
57
58
    /**
59
     * {@inheritDoc}
60
     */
61
    public function getName(): string
62
    {
63
        return 'form';
64
    }
65
66
    /**
67
     * {@inheritDoc}
68
     */
69
    public function buildConfig(ArrayNodeDefinition $root): void
70
    {
71
        $config = $root
72
            ->info('Resolve the form to use as input for mutations')
73
            ->addDefaultsIfNotSet()
74
            ->canBeDisabled()
75
            ->children();
76
77
        $config
78
            ->variableNode('type')
79
            ->defaultNull()
80
            ->info(
81
                'Specify the form type to use,
82
[string] Name of the form type to use
83
[true|null] The form will be automatically resolved to ...Bundle\Form\Input\{Node}\{MutationName}Input.'
84
            );
85
        $config->variableNode('options')->defaultValue([])->info('Form options');
86
        $config->variableNode('argument')
87
               ->defaultValue('input')
88
               ->info('Name of the argument to use as input');
89
90
        $config->booleanNode('client_mutation_id')
91
               ->defaultTrue()
92
               ->info('Automatically add a field called clientMutationId');
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     */
98 1
    public function configure(DefinitionInterface $definition, Endpoint $endpoint, array $config): void
99
    {
100 1
        if (!$definition instanceof MutationDefinition || !isset($config['enabled'])) {
101
            return;
102
        }
103
104 1
        $formType = $config['type'] ?? null;
105
106
        //the related class is used to match a form using naming conventions
107 1
        $relatedClass = null;
108 1
        if ($definition instanceof NodeAwareDefinitionInterface && $definition->getNode()) {
109 1
            $relatedClass = $definition->getNode();
110 1
            if ($class = $endpoint->getClassForType($relatedClass)) {
111 1
                $relatedClass = $class;
112
            }
113
        }
114
115 1
        if (!class_exists($relatedClass) && $definition->getResolver()) {
116
            $relatedClass = $definition->getResolver();
117
        }
118
119
        //try find the form using a related class
120 1
        if ($relatedClass && (!$formType || true === $formType)) {
121 1
            $bundleNamespace = ClassUtils::relatedBundleNamespace($relatedClass);
122 1
            if ($endpoint->hasType($definition->getNode())) {
123 1
                $nodeName = $endpoint->getType($definition->getNode())->getName();
124
            } else {
125
                $nodeName = ClassUtils::getNodeFromClass($relatedClass);
126
            }
127 1
            $formClass = ClassUtils::applyNamingConvention(
128 1
                $bundleNamespace,
129 1
                'Form\Input',
130 1
                $nodeName,
131 1
                ucfirst($definition->getName()),
132 1
                'Input'
133
            );
134 1
            if (class_exists($formClass)) {
135 1
                $formType = $formClass;
136
            } elseif (true === $formType) {
137
                $error = sprintf(
138
                    'Can`t find a valid input form type to use in "%s".
139
                         Create the form "%s" or specify a custom form',
140
                    $definition->getName(),
141
                    $formClass
142
                );
143
                throw new \RuntimeException($error);
144
            }
145
        }
146
147 1
        if ($formType) {
148 1
            $config['type'] = $formType;
149
150 1
            $form = $this->formFactory->create($formType, null, $config['options'] ?? []);
151 1
            $inputObject = $this->createFormInputObject($endpoint, $form, ucfirst($definition->getName()));
152 1
            $endpoint->addType($inputObject);
153
154 1
            $input = new ArgumentDefinition();
155 1
            $input->setName($config['argument'] ?? 'input');
156 1
            $input->setType($inputObject->getName());
157
158 1
            if ($config['client_mutation_id'] ?? true) {
159 1
                $clientMutationId = new FieldDefinition();
160 1
                $clientMutationId->setName('clientMutationId');
161 1
                $clientMutationId->setType(Types::STRING);
162 1
                $clientMutationId->setDescription('A unique identifier for the client performing the mutation.');
163 1
                $inputObject->prependField($clientMutationId);
164
            }
165
166 1
            $definition->addArgument($input);
167 1
            $definition->setMeta('form', $config);
168
        }
169 1
    }
170
171
    /**
172
     * @param Endpoint      $endpoint
173
     * @param FormInterface $form
174
     * @param string        $name
175
     *
176
     * @return InputObjectDefinition
177
     */
178 1
    private function createFormInputObject(Endpoint $endpoint, FormInterface $form, string $name): InputObjectDefinition
179
    {
180 1
        $inputObject = new InputObjectDefinition();
181 1
        if ($settledName = $form->getConfig()->getOption('graphql_type')) {
182
            $settledName = preg_replace('/Input$/', null, $settledName);
183
            if (\is_string($settledName) && $settledName) {
184
                $name = $settledName;
185
            }
186
        }
187 1
        $inputObject->setName("{$name}Input");
188 1
        $inputObject->setDescription($form->getConfig()->getOption('graphql_description'));
189
190 1
        foreach ($form->all() as $formField) {
191 1
            $field = new FieldDefinition();
192 1
            $label = $formField->getConfig()->getOption('label');
193 1
            $field->setName(!empty($label) ? $label : $formField->getName());
194 1
            $field->setDescription($formField->getConfig()->getOption('graphql_description') ?? null);
195 1
            $field->setDeprecationReason($formField->getConfig()->getOption('graphql_deprecation_reason') ?? null);
196 1
            $field->setNonNull($formField->isRequired());
197 1
            $field->setOriginName($formField->getName());
198
199 1
            if ($formField->all()) {
200 1
                $childName = $name.ucfirst($formField->getName());
201 1
                $child = $this->createFormInputObject($endpoint, $formField, $childName);
202 1
                $endpoint->addType($child);
203 1
                $field->setType($child->getName());
204 1
            } elseif (is_a($formField->getConfig()->getType()->getInnerType(), CollectionType::class)) {
205 1
                $childName = $name.ucfirst($formField->getName());
206 1
                $childFormType = $formField->getConfig()->getOptions()['entry_type'];
207 1
                $childFormOptions = $formField->getConfig()->getOptions()['entry_options'];
208 1
                $childForm = $this->formFactory->create($childFormType, null, $childFormOptions ?? []);
209 1
                $childForm->setParent($form);
210
                try {
211
                    //resolve type if is a valid scalar type or predefined type
212 1
                    $this->resolveFormFieldDefinition($field, $childForm);
213 1
                    $field->setList(true);
214 1
                } catch (\InvalidArgumentException $exception) {
215
                    //on exception, try build a child form for this collection
216 1
                    $child = $this->createFormInputObject($endpoint, $childForm, $childName);
217 1
                    $field->setType($child->getName());
218 1
                    $field->setList(true);
219 1
                    $endpoint->add($child);
220
                }
221
            } else {
222 1
                $this->resolveFormFieldDefinition($field, $formField);
223
            }
224
225 1
            $inputObject->addField($field);
226
        }
227
228 1
        return $inputObject;
229
    }
230
231
    /**
232
     * @param FieldDefinition $field
233
     * @param FormInterface   $form
234
     */
235 1
    private function resolveFormFieldDefinition(FieldDefinition $field, FormInterface $form): void
236
    {
237 1
        $type = null;
238 1
        $resolver = $form->getConfig()->getType()->getOptionsResolver();
239 1
        if ($resolver->hasDefault('graphql_type')) {
240 1
            $type = $resolver->resolve([])['graphql_type'];
241 1
            if (!$type) {
242
                $type = $form->getConfig()->getOptions()['graphql_type'];
243
            }
244 1
            $field->setList(TypeUtil::isTypeList($type));
245 1
            $type = TypeUtil::normalize($type);
246
        }
247
248 1
        if (!$type) {
249 1
            $guesses = [];
250 1
            foreach ($this->typeGuessers as $guesser) {
251 1
                $formType = \get_class($form->getConfig()->getType()->getInnerType());
252 1
                if ($guess = $guesser->guessType($field, $formType, $form->getConfig()->getOptions())) {
253 1
                    $guesses[] = $guess;
254
                }
255
            }
256
257 1
            $guess = Guess::getBestGuess($guesses);
258 1
            if ($guess && $guess instanceof TypeGuess) {
259 1
                $type = $guess->getType();
260
261 1
                if (isset($guess->getOptions()['required'])) {
262
                    $field->setNonNull($guess->getOptions()['required']);
263
                }
264
265 1
                if (isset($guess->getOptions()['list'])) {
266 1
                    $field->setList($guess->getOptions()['list']);
267
                }
268
            }
269
        }
270
271 1
        if (!$type) {
272 1
            $error = sprintf(
273 1
                'The field "%s" in the parent form "%s" does not have a valid type. 
274
                If your are using a custom type, must define a option called "graphql_type" to resolve the form to a valid GraphQL type',
275 1
                $form->getName(),
276 1
                $form->getParent()->getName()
277
            );
278 1
            throw new \InvalidArgumentException($error);
279
        }
280
281 1
        $field->setType($type);
282 1
    }
283
}
284