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\Mutation; |
||||||||
12 | |||||||||
13 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
||||||||
14 | use Symfony\Component\Form\FormBuilderInterface; |
||||||||
15 | use Symfony\Component\Form\FormEvent; |
||||||||
16 | use Symfony\Component\Form\FormEvents; |
||||||||
17 | use Symfony\Component\Form\FormInterface; |
||||||||
18 | use Symfony\Component\Validator\ConstraintViolation as SymfonyConstraintViolation; |
||||||||
19 | use Ynlo\GraphQLBundle\Error\ErrorQueue; |
||||||||
20 | use Ynlo\GraphQLBundle\Events\GraphQLEvents; |
||||||||
21 | use Ynlo\GraphQLBundle\Events\GraphQLMutationEvent; |
||||||||
22 | use Ynlo\GraphQLBundle\Exception\Controlled\ValidationError; |
||||||||
23 | use Ynlo\GraphQLBundle\Model\ConstraintViolation; |
||||||||
24 | use Ynlo\GraphQLBundle\Resolver\AbstractResolver; |
||||||||
25 | use Ynlo\GraphQLBundle\Util\IDEncoder; |
||||||||
26 | use Ynlo\GraphQLBundle\Util\Uuid; |
||||||||
27 | use Ynlo\GraphQLBundle\Validator\ConstraintViolationList; |
||||||||
28 | |||||||||
29 | /** |
||||||||
30 | * Base class for mutations |
||||||||
31 | * Implement the method "process()" and "returnPayload()" is enough in many scenarios |
||||||||
32 | */ |
||||||||
33 | abstract class AbstractMutationResolver extends AbstractResolver implements EventSubscriberInterface |
||||||||
34 | { |
||||||||
35 | /** |
||||||||
36 | * @param array $input |
||||||||
37 | * |
||||||||
38 | * @return mixed |
||||||||
39 | */ |
||||||||
40 | public function __invoke($input) |
||||||||
41 | { |
||||||||
42 | $formBuilder = $this->createDefinitionForm($this->initialFormData($input)); |
||||||||
43 | $mutationEvent = null; |
||||||||
44 | |||||||||
45 | $form = null; |
||||||||
46 | if ($formBuilder) { |
||||||||
47 | $formBuilder->addEventListener( |
||||||||
48 | FormEvents::SUBMIT, |
||||||||
49 | function (FormEvent $event) use (&$mutationEvent) { |
||||||||
50 | if ($this->eventDispatcher) { |
||||||||
51 | $mutationEvent = new GraphQLMutationEvent($this->context, $event); |
||||||||
52 | $this->eventDispatcher->dispatch(GraphQLEvents::MUTATION_SUBMITTED, $mutationEvent); |
||||||||
0 ignored issues
–
show
Ynlo\GraphQLBundle\Event...nts::MUTATION_SUBMITTED of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||||
53 | } |
||||||||
54 | } |
||||||||
55 | ); |
||||||||
56 | |||||||||
57 | $formBuilder->addEventSubscriber($this); |
||||||||
58 | |||||||||
59 | $extensionExecutor = function ($method) { |
||||||||
60 | return function (FormEvent $event) use ($method) { |
||||||||
61 | foreach ($this->extensions as $extension) { |
||||||||
62 | call_user_func_array([$extension, $method], [$event]); |
||||||||
63 | } |
||||||||
64 | }; |
||||||||
65 | }; |
||||||||
66 | |||||||||
67 | foreach (self::getSubscribedEvents() as $event => $method) { |
||||||||
68 | $formBuilder->addEventListener($event, $extensionExecutor($method)); |
||||||||
69 | } |
||||||||
70 | |||||||||
71 | $form = $formBuilder->getForm(); |
||||||||
72 | } |
||||||||
73 | |||||||||
74 | if ($form) { |
||||||||
75 | $form->submit($input, false); |
||||||||
76 | $data = $form->getData(); |
||||||||
77 | } else { |
||||||||
78 | $data = $input; |
||||||||
79 | } |
||||||||
80 | |||||||||
81 | $violations = new ConstraintViolationList(); |
||||||||
82 | if ($form) { |
||||||||
83 | $this->extractFormErrors($form, $violations); |
||||||||
84 | } |
||||||||
85 | |||||||||
86 | $dryRun = $input['dryRun'] ?? false; |
||||||||
87 | |||||||||
88 | if ($violations->count()) { |
||||||||
89 | $errorHandling = $this->container->getParameter('graphql.error_handling'); |
||||||||
90 | if (\in_array($errorHandling['validation_messages'] ?? null, ['both', 'error'])) { |
||||||||
91 | ErrorQueue::throw(new ValidationError($violations)); |
||||||||
92 | } |
||||||||
93 | } |
||||||||
94 | |||||||||
95 | if ($dryRun) { |
||||||||
96 | $data = null; |
||||||||
97 | } else { |
||||||||
98 | if ((!$form && !$violations->count()) |
||||||||
99 | || ($form->isSubmitted() && $form->isValid() && !$violations->count()) |
||||||||
100 | ) { |
||||||||
101 | $this->process($data); |
||||||||
102 | } |
||||||||
103 | } |
||||||||
104 | |||||||||
105 | $payload = $this->returnPayload($data, $violations, $input); |
||||||||
106 | |||||||||
107 | if ($mutationEvent instanceof GraphQLMutationEvent) { |
||||||||
108 | $mutationEvent->setPayload($payload); |
||||||||
109 | $this->eventDispatcher->dispatch(GraphQLEvents::MUTATION_COMPLETED, $mutationEvent); |
||||||||
0 ignored issues
–
show
The call to
Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $mutationEvent .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() Ynlo\GraphQLBundle\Event...nts::MUTATION_COMPLETED of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||||
110 | $payload = $mutationEvent->getPayload(); |
||||||||
111 | } |
||||||||
112 | |||||||||
113 | return $payload; |
||||||||
114 | } |
||||||||
115 | |||||||||
116 | /** |
||||||||
117 | * {@inheritdoc} |
||||||||
118 | */ |
||||||||
119 | public static function getSubscribedEvents() |
||||||||
120 | { |
||||||||
121 | return [ |
||||||||
122 | FormEvents::PRE_SET_DATA => 'preSetData', |
||||||||
123 | FormEvents::POST_SET_DATA => 'postSetData', |
||||||||
124 | FormEvents::PRE_SUBMIT => 'preSubmit', |
||||||||
125 | FormEvents::SUBMIT => 'onSubmit', |
||||||||
126 | FormEvents::POST_SUBMIT => 'postSubmit', |
||||||||
127 | ]; |
||||||||
128 | } |
||||||||
129 | |||||||||
130 | /** |
||||||||
131 | * @see http://api.symfony.com/4.0/Symfony/Component/Form/FormEvents.html |
||||||||
132 | * |
||||||||
133 | * @param FormEvent $event |
||||||||
134 | */ |
||||||||
135 | public function preSetData(FormEvent $event) |
||||||||
136 | { |
||||||||
137 | } |
||||||||
138 | |||||||||
139 | /** |
||||||||
140 | * @see http://api.symfony.com/4.0/Symfony/Component/Form/FormEvents.html |
||||||||
141 | * |
||||||||
142 | * @param FormEvent $event |
||||||||
143 | */ |
||||||||
144 | public function postSetData(FormEvent $event) |
||||||||
145 | { |
||||||||
146 | } |
||||||||
147 | |||||||||
148 | /** |
||||||||
149 | * @see http://api.symfony.com/4.0/Symfony/Component/Form/FormEvents.html |
||||||||
150 | * |
||||||||
151 | * @param FormEvent $event |
||||||||
152 | */ |
||||||||
153 | public function preSubmit(FormEvent $event) |
||||||||
154 | { |
||||||||
155 | } |
||||||||
156 | |||||||||
157 | /** |
||||||||
158 | * @see http://api.symfony.com/4.0/Symfony/Component/Form/FormEvents.html |
||||||||
159 | * |
||||||||
160 | * @param FormEvent $event |
||||||||
161 | */ |
||||||||
162 | public function onSubmit(FormEvent $event) |
||||||||
163 | { |
||||||||
164 | } |
||||||||
165 | |||||||||
166 | /** |
||||||||
167 | * @see http://api.symfony.com/4.0/Symfony/Component/Form/FormEvents.html |
||||||||
168 | * |
||||||||
169 | * @param FormEvent $event |
||||||||
170 | */ |
||||||||
171 | public function postSubmit(FormEvent $event) |
||||||||
172 | { |
||||||||
173 | } |
||||||||
174 | |||||||||
175 | /** |
||||||||
176 | * Actions to process |
||||||||
177 | * the result processed data is given to payload |
||||||||
178 | * |
||||||||
179 | * @param mixed $data |
||||||||
180 | */ |
||||||||
181 | abstract public function process(&$data); |
||||||||
182 | |||||||||
183 | /** |
||||||||
184 | * The payload object or array matching the GraphQL definition |
||||||||
185 | * |
||||||||
186 | * @param mixed $data normalized data, its the input data processed by the form |
||||||||
187 | * @param ConstraintViolationList $violations violations returned by the form validation process |
||||||||
188 | * @param array $inputSource the original submitted data in array |
||||||||
189 | * |
||||||||
190 | * @return mixed |
||||||||
191 | */ |
||||||||
192 | abstract public function returnPayload($data, ConstraintViolationList $violations, $inputSource); |
||||||||
193 | |||||||||
194 | /** |
||||||||
195 | * @return null|string |
||||||||
196 | */ |
||||||||
197 | protected function getPayloadClass(): string |
||||||||
198 | { |
||||||||
199 | $type = $this->getContext()->getDefinition()->getType(); |
||||||||
200 | |||||||||
201 | if (class_exists($type)) { |
||||||||
202 | return $type; |
||||||||
203 | } |
||||||||
204 | |||||||||
205 | if ($this->context->getEndpoint()->hasType($type)) { |
||||||||
206 | $class = $this->context->getEndpoint()->getClassForType($type); |
||||||||
207 | if (class_exists($class)) { |
||||||||
208 | return $class; |
||||||||
0 ignored issues
–
show
|
|||||||||
209 | } |
||||||||
210 | } |
||||||||
211 | |||||||||
212 | throw new \RuntimeException( |
||||||||
213 | sprintf( |
||||||||
214 | 'Can\'t find a valid payload class for "%s".', |
||||||||
215 | $this->getContext()->getDefinition()->getName() |
||||||||
216 | ) |
||||||||
217 | ); |
||||||||
218 | } |
||||||||
219 | |||||||||
220 | /** |
||||||||
221 | * @param array $input |
||||||||
222 | * |
||||||||
223 | * @return mixed |
||||||||
224 | */ |
||||||||
225 | public function initialFormData($input) |
||||||||
226 | { |
||||||||
227 | if (is_array($input) && isset($input['id'])) { |
||||||||
228 | return IDEncoder::decode($input['id']); |
||||||||
229 | } |
||||||||
230 | |||||||||
231 | return null; |
||||||||
232 | } |
||||||||
233 | /** |
||||||||
234 | * @param mixed|null $data |
||||||||
235 | * |
||||||||
236 | * @return FormBuilderInterface|null |
||||||||
237 | */ |
||||||||
238 | public function createDefinitionForm($data): ?FormBuilderInterface |
||||||||
239 | { |
||||||||
240 | if (!$this->context->getDefinition()->hasMeta('form')) { |
||||||||
241 | return null; |
||||||||
242 | } |
||||||||
243 | |||||||||
244 | $formConfig = $this->context->getDefinition()->getMeta('form') ?? []; |
||||||||
245 | $formType = $formConfig['type'] ?? null; |
||||||||
246 | if (!$formConfig || !$formType) { |
||||||||
247 | throw new \RuntimeException(sprintf('Can`t find a valid form for %s', $this->context->getDefinition()->getName())); |
||||||||
248 | } |
||||||||
249 | |||||||||
250 | $options = [ |
||||||||
251 | 'allow_extra_fields' => true, |
||||||||
252 | 'endpoint' => $this->context->getEndpoint()->getName(), |
||||||||
253 | ]; |
||||||||
254 | |||||||||
255 | if ($this->container->hasParameter('form.type_extension.csrf.enabled') |
||||||||
256 | && $this->container->getParameter('form.type_extension.csrf.enabled')) { |
||||||||
257 | $options['csrf_protection'] = false; |
||||||||
258 | } |
||||||||
259 | |||||||||
260 | $options = array_merge($options, $formConfig['options'] ?? []); |
||||||||
261 | |||||||||
262 | return $this->createFormBuilder($formType, $data, $options); |
||||||||
263 | } |
||||||||
264 | |||||||||
265 | /** |
||||||||
266 | * @param FormInterface $form |
||||||||
267 | * @param ConstraintViolationList $violations |
||||||||
268 | * @param null|string $parentName |
||||||||
269 | */ |
||||||||
270 | public function extractFormErrors(FormInterface $form, ConstraintViolationList $violations, ?string $parentName = null) |
||||||||
271 | { |
||||||||
272 | $errors = $form->getErrors(true); |
||||||||
273 | foreach ($errors as $error) { |
||||||||
274 | $violation = new ConstraintViolation(); |
||||||||
275 | $violation->setMessage($error->getMessage()); |
||||||||
0 ignored issues
–
show
The method
getMessage() does not exist on Symfony\Component\Form\FormErrorIterator .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||||
276 | $violation->setMessageTemplate($error->getMessageTemplate() ?? $error->getMessage()); |
||||||||
0 ignored issues
–
show
The method
getMessageTemplate() does not exist on Symfony\Component\Form\FormErrorIterator .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||||
277 | foreach ($error->getMessageParameters() as $key => $value) { |
||||||||
0 ignored issues
–
show
The method
getMessageParameters() does not exist on Symfony\Component\Form\FormErrorIterator .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||||
278 | $violation->addParameter($key, $value); |
||||||||
279 | } |
||||||||
280 | |||||||||
281 | $cause = $error->getCause(); |
||||||||
0 ignored issues
–
show
The method
getCause() does not exist on Symfony\Component\Form\FormErrorIterator .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||||
282 | if ($cause instanceof SymfonyConstraintViolation) { |
||||||||
283 | $violation->setCode($cause->getCode() ?? Uuid::createFromData($violation->getMessageTemplate())); |
||||||||
284 | $violation->setInvalidValue($cause->getInvalidValue()); |
||||||||
285 | $violation->setPlural($cause->getPlural()); |
||||||||
286 | |||||||||
287 | $path = $this->publicPropertyPath($form, $cause->getPropertyPath()); |
||||||||
288 | if ($path) { |
||||||||
289 | $violation->setPropertyPath($path); |
||||||||
290 | } |
||||||||
291 | } |
||||||||
292 | |||||||||
293 | $violations->addViolation($violation); |
||||||||
294 | } |
||||||||
295 | } |
||||||||
296 | |||||||||
297 | /** |
||||||||
298 | * Convert internal validation property path to the public one, |
||||||||
299 | * required when use `property_path` in the form |
||||||||
300 | * |
||||||||
301 | * @param FormInterface $form |
||||||||
302 | * @param string $path |
||||||||
303 | * |
||||||||
304 | * @return string |
||||||||
305 | */ |
||||||||
306 | private function publicPropertyPath(FormInterface $form, $path) |
||||||||
307 | { |
||||||||
308 | $pathArray = [$path]; |
||||||||
0 ignored issues
–
show
|
|||||||||
309 | |||||||||
310 | //for some reason some inputs are resolved as "inputName.data" or "inputName.children" |
||||||||
311 | //because the original form property is children[inputName].data |
||||||||
312 | //this is the case of DEMO AddUserInput form the login field is validated as path children[login].data |
||||||||
313 | //the following statements remove the trailing ".data" |
||||||||
314 | if (preg_match('/\.(data)$/', $path)) { |
||||||||
315 | $path = preg_replace('/\.(data)/', null, $path); |
||||||||
316 | } |
||||||||
317 | if (preg_match('/children\[/', $path)) { |
||||||||
318 | $path = preg_replace('/children\[/', null, $path); |
||||||||
319 | } |
||||||||
320 | |||||||||
321 | $path = str_replace([']', '['], [null, '.'], $path); |
||||||||
322 | |||||||||
323 | if (strpos($path, '.') !== false) { // object.child.property |
||||||||
324 | $pathArray = explode('.', $path); |
||||||||
325 | } else { |
||||||||
326 | $pathArray = [$path]; |
||||||||
327 | } |
||||||||
328 | |||||||||
329 | if (in_array($pathArray[0], ['data', 'children'])) { |
||||||||
330 | array_shift($pathArray); |
||||||||
331 | } |
||||||||
332 | |||||||||
333 | $contextForm = $form; |
||||||||
334 | foreach ($pathArray as &$propName) { |
||||||||
335 | $index = null; |
||||||||
336 | if (preg_match('/(\w+)(\[\d+\])$/', $propName, $matches)) { |
||||||||
337 | list(, $propName, $index) = $matches; |
||||||||
338 | } |
||||||||
339 | if (!$contextForm->has($propName)) { |
||||||||
340 | foreach ($contextForm->all() as $child) { |
||||||||
341 | if ($child->getConfig()->getOption('property_path') === $propName) { |
||||||||
342 | $propName = $child->getName(); |
||||||||
343 | } |
||||||||
344 | } |
||||||||
345 | } |
||||||||
346 | if ($index) { |
||||||||
347 | $propName = sprintf('%s%s', $propName, $index); |
||||||||
0 ignored issues
–
show
$index of type void is incompatible with the type string expected by parameter $args of sprintf() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||||
348 | } |
||||||||
349 | } |
||||||||
350 | unset($propName); |
||||||||
351 | |||||||||
352 | return implode('.', $pathArray); |
||||||||
353 | } |
||||||||
354 | } |
||||||||
355 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.