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
|
1 |
|
$options = $config['options'] ?? []; |
150
|
1 |
|
$options['endpoint'] = $endpoint->getName(); |
151
|
1 |
|
$form = $this->formFactory->create($formType, null, $options); |
152
|
1 |
|
$inputObject = $this->createFormInputObject($endpoint, $form, ucfirst($definition->getName())); |
153
|
1 |
|
$endpoint->addType($inputObject); |
154
|
|
|
|
155
|
1 |
|
$input = new ArgumentDefinition(); |
156
|
1 |
|
$input->setName($config['argument'] ?? 'input'); |
157
|
1 |
|
$input->setType($inputObject->getName()); |
158
|
|
|
|
159
|
1 |
|
if ($config['client_mutation_id'] ?? true) { |
160
|
1 |
|
$clientMutationId = new FieldDefinition(); |
161
|
1 |
|
$clientMutationId->setName('clientMutationId'); |
162
|
1 |
|
$clientMutationId->setType(Types::STRING); |
163
|
1 |
|
$clientMutationId->setDescription('A unique identifier for the client performing the mutation.'); |
164
|
1 |
|
$inputObject->prependField($clientMutationId); |
165
|
|
|
} |
166
|
|
|
|
167
|
1 |
|
$definition->addArgument($input); |
168
|
1 |
|
$definition->setMeta('form', $config); |
169
|
|
|
} |
170
|
1 |
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* @param Endpoint $endpoint |
174
|
|
|
* @param FormInterface $form |
175
|
|
|
* @param string $name |
176
|
|
|
* |
177
|
|
|
* @return InputObjectDefinition |
178
|
|
|
*/ |
179
|
1 |
|
private function createFormInputObject(Endpoint $endpoint, FormInterface $form, string $name): InputObjectDefinition |
180
|
|
|
{ |
181
|
1 |
|
$inputObject = new InputObjectDefinition(); |
182
|
1 |
|
if ($settledName = $form->getConfig()->getOption('graphql_type')) { |
183
|
|
|
$settledName = preg_replace('/Input$/', null, $settledName); |
184
|
|
|
if (\is_string($settledName) && $settledName) { |
185
|
|
|
$name = $settledName; |
186
|
|
|
} |
187
|
|
|
} |
188
|
1 |
|
$inputObject->setName("{$name}Input"); |
189
|
1 |
|
$inputObject->setDescription($form->getConfig()->getOption('graphql_description')); |
190
|
|
|
|
191
|
1 |
|
foreach ($form->all() as $formField) { |
192
|
1 |
|
$field = new FieldDefinition(); |
193
|
1 |
|
$label = $formField->getConfig()->getOption('label'); |
194
|
1 |
|
$field->setName(!empty($label) ? $label : $formField->getName()); |
195
|
1 |
|
$field->setDescription($formField->getConfig()->getOption('graphql_description') ?? null); |
196
|
1 |
|
$field->setDeprecationReason($formField->getConfig()->getOption('graphql_deprecation_reason') ?? null); |
197
|
1 |
|
$field->setNonNull($formField->isRequired()); |
198
|
1 |
|
$field->setOriginName($formField->getName()); |
199
|
|
|
|
200
|
1 |
|
if ($formField->all()) { |
201
|
1 |
|
$childName = $name.ucfirst($formField->getName()); |
202
|
1 |
|
$child = $this->createFormInputObject($endpoint, $formField, $childName); |
203
|
1 |
|
$endpoint->addType($child); |
204
|
1 |
|
$field->setType($child->getName()); |
205
|
1 |
|
} elseif (is_a($formField->getConfig()->getType()->getInnerType(), CollectionType::class)) { |
206
|
1 |
|
$childName = $name.ucfirst($formField->getName()); |
207
|
1 |
|
$childFormType = $formField->getConfig()->getOptions()['entry_type']; |
208
|
1 |
|
$childFormOptions = $formField->getConfig()->getOptions()['entry_options'] ?? []; |
209
|
1 |
|
$childFormOptions['endpoint'] = $endpoint->getName(); |
210
|
1 |
|
$childForm = $this->formFactory->create($childFormType, null, $childFormOptions); |
211
|
1 |
|
$childForm->setParent($form); |
212
|
|
|
try { |
213
|
|
|
//resolve type if is a valid scalar type or predefined type |
214
|
1 |
|
$this->resolveFormFieldDefinition($field, $childForm); |
215
|
1 |
|
$field->setList(true); |
216
|
1 |
|
} catch (\InvalidArgumentException $exception) { |
217
|
|
|
//on exception, try build a child form for this collection |
218
|
1 |
|
$child = $this->createFormInputObject($endpoint, $childForm, $childName); |
219
|
1 |
|
$field->setType($child->getName()); |
220
|
1 |
|
$field->setList(true); |
221
|
1 |
|
$endpoint->add($child); |
222
|
|
|
} |
223
|
|
|
} else { |
224
|
1 |
|
$this->resolveFormFieldDefinition($field, $formField); |
225
|
|
|
} |
226
|
|
|
|
227
|
1 |
|
$inputObject->addField($field); |
228
|
|
|
} |
229
|
|
|
|
230
|
1 |
|
return $inputObject; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* @param FieldDefinition $field |
235
|
|
|
* @param FormInterface $form |
236
|
|
|
*/ |
237
|
1 |
|
private function resolveFormFieldDefinition(FieldDefinition $field, FormInterface $form): void |
238
|
|
|
{ |
239
|
1 |
|
$type = null; |
240
|
1 |
|
$resolver = $form->getConfig()->getType()->getOptionsResolver(); |
241
|
1 |
|
if ($resolver->hasDefault('graphql_type')) { |
242
|
1 |
|
$type = $resolver->resolve([])['graphql_type']; |
243
|
1 |
|
if (!$type) { |
244
|
1 |
|
$type = $form->getConfig()->getOptions()['graphql_type']; |
245
|
|
|
} |
246
|
1 |
|
$field->setList(TypeUtil::isTypeList($type)); |
247
|
1 |
|
$type = TypeUtil::normalize($type); |
248
|
|
|
} |
249
|
|
|
|
250
|
1 |
|
if (!$type) { |
251
|
1 |
|
$guesses = []; |
252
|
1 |
|
foreach ($this->typeGuessers as $guesser) { |
253
|
1 |
|
$formType = \get_class($form->getConfig()->getType()->getInnerType()); |
254
|
1 |
|
if ($guess = $guesser->guessType($field, $formType, $form->getConfig()->getOptions())) { |
255
|
1 |
|
$guesses[] = $guess; |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
|
259
|
1 |
|
$guess = Guess::getBestGuess($guesses); |
260
|
1 |
|
if ($guess && $guess instanceof TypeGuess) { |
261
|
1 |
|
$type = $guess->getType(); |
262
|
|
|
|
263
|
1 |
|
if (isset($guess->getOptions()['required'])) { |
264
|
|
|
$field->setNonNull($guess->getOptions()['required']); |
265
|
|
|
} |
266
|
|
|
|
267
|
1 |
|
if (isset($guess->getOptions()['list'])) { |
268
|
1 |
|
$field->setList($guess->getOptions()['list']); |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
} |
272
|
|
|
|
273
|
1 |
|
if (!$type) { |
274
|
1 |
|
$error = sprintf( |
275
|
1 |
|
'The field "%s" in the parent form "%s" does not have a valid type. |
276
|
|
|
If your are using a custom type, must define a option called "graphql_type" to resolve the form to a valid GraphQL type', |
277
|
1 |
|
$form->getName(), |
278
|
1 |
|
$form->getParent()->getName() |
279
|
|
|
); |
280
|
1 |
|
throw new \InvalidArgumentException($error); |
281
|
|
|
} |
282
|
|
|
|
283
|
1 |
|
$field->setType($type); |
284
|
1 |
|
} |
285
|
|
|
} |
286
|
|
|
|