AbstractMutationResolver::getPayloadClass()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 19
c 0
b 0
f 0
ccs 0
cts 12
cp 0
rs 9.9
cc 4
nc 4
nop 0
crap 20
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
Unused Code introduced by
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 ignore-call  annotation

52
                        $this->eventDispatcher->/** @scrutinizer ignore-call */ 
53
                                                dispatch(GraphQLEvents::MUTATION_SUBMITTED, $mutationEvent);

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.

Loading history...
Bug introduced by
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 ignore-type  annotation

52
                        $this->eventDispatcher->dispatch(/** @scrutinizer ignore-type */ GraphQLEvents::MUTATION_SUBMITTED, $mutationEvent);
Loading history...
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
Unused Code introduced by
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 ignore-call  annotation

109
            $this->eventDispatcher->/** @scrutinizer ignore-call */ 
110
                                    dispatch(GraphQLEvents::MUTATION_COMPLETED, $mutationEvent);

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.

Loading history...
Bug introduced by
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 ignore-type  annotation

109
            $this->eventDispatcher->dispatch(/** @scrutinizer ignore-type */ GraphQLEvents::MUTATION_COMPLETED, $mutationEvent);
Loading history...
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
Bug Best Practice introduced by
The expression return $class could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
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
Bug introduced by
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 ignore-call  annotation

275
            $violation->setMessage($error->/** @scrutinizer ignore-call */ getMessage());

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.

Loading history...
276
            $violation->setMessageTemplate($error->getMessageTemplate() ?? $error->getMessage());
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

276
            $violation->setMessageTemplate($error->/** @scrutinizer ignore-call */ getMessageTemplate() ?? $error->getMessage());

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.

Loading history...
277
            foreach ($error->getMessageParameters() as $key => $value) {
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

277
            foreach ($error->/** @scrutinizer ignore-call */ getMessageParameters() as $key => $value) {

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.

Loading history...
278
                $violation->addParameter($key, $value);
279
            }
280
281
            $cause = $error->getCause();
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

281
            /** @scrutinizer ignore-call */ 
282
            $cause = $error->getCause();

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.

Loading history...
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
Unused Code introduced by
The assignment to $pathArray is dead and can be removed.
Loading history...
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
Bug introduced by
$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 ignore-type  annotation

347
                $propName = sprintf('%s%s', $propName, /** @scrutinizer ignore-type */ $index);
Loading history...
348
            }
349
        }
350
        unset($propName);
351
352
        return implode('.', $pathArray);
353
    }
354
}
355