Passed
Push — master ( 5cd32c...1a1aa9 )
by Rafael
05:46
created

AbstractMutationResolver::createDefinitionForm()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.1308

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 11
cts 13
cp 0.8462
rs 8.5125
c 0
b 0
f 0
cc 6
eloc 13
nc 4
nop 1
crap 6.1308
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\Events\GraphQLEvents;
20
use Ynlo\GraphQLBundle\Events\GraphQLMutationEvent;
21
use Ynlo\GraphQLBundle\Model\ConstraintViolation;
22
use Ynlo\GraphQLBundle\Resolver\AbstractResolver;
23
use Ynlo\GraphQLBundle\Util\IDEncoder;
24
use Ynlo\GraphQLBundle\Validator\ConstraintViolationList;
25
26
/**
27
 * Base class for mutations
28
 * Implement the method "process()" and "returnPayload()" is enough in many scenarios
29
 */
30
abstract class AbstractMutationResolver extends AbstractResolver implements EventSubscriberInterface
31
{
32
    /**
33
     * @param array $input
34
     *
35
     * @return mixed
36
     */
37 8
    public function __invoke($input)
38
    {
39 8
        $formBuilder = $this->createDefinitionForm($this->initialFormData($input));
40 8
        $mutationEvent = null;
41
42 8
        $form = null;
43 8
        if ($formBuilder) {
44 8
            $formBuilder->addEventListener(
45 8
                FormEvents::SUBMIT,
46 8
                function (FormEvent $event) use (&$mutationEvent) {
47 8
                    if ($this->eventDispatcher) {
48 8
                        $mutationEvent = new GraphQLMutationEvent($this->context, $event);
49 8
                        $this->eventDispatcher->dispatch(GraphQLEvents::MUTATION_SUBMITTED, $mutationEvent);
50
                    }
51 8
                }
52
            );
53
54 8
            $formBuilder->addEventSubscriber($this);
55
56
            $extensionExecutor = function ($method) {
57 8
                return function (FormEvent $event) use ($method) {
58 8
                    foreach ($this->extensions as $extension) {
59 4
                        return call_user_func_array([$extension, $method], [$event]);
60
                    }
61 8
                };
62 8
            };
63
64 8
            foreach (self::getSubscribedEvents() as $event => $method) {
65 8
                $formBuilder->addEventListener($event, $extensionExecutor($method));
66
            }
67
68 8
            $form = $formBuilder->getForm();
69
        }
70
71 8
        if ($form) {
72 8
            $form->submit($input, false);
73 8
            $data = $form->getData();
74
        } else {
75
            $data = $input;
76
        }
77
78 8
        $violations = new ConstraintViolationList();
79 8
        if ($form) {
80 8
            $this->extractFormErrors($form, $violations);
81
        }
82
83 8
        $dryRun = $input['dryRun'] ?? false;
84
85 8
        if ($dryRun) {
86
            $data = null;
87
        } else {
88 8
            if ((!$form && !$violations->count())
89 8
                || ($form->isSubmitted() && $form->isValid() && !$violations->count())
90
            ) {
91 7
                $this->process($data);
92
            }
93
        }
94
95 8
        $payload = $this->returnPayload($data, $violations, $input);
96
97 8
        if ($mutationEvent instanceof GraphQLMutationEvent) {
98 8
            $mutationEvent->setPayload($payload);
99 8
            $this->eventDispatcher->dispatch(GraphQLEvents::MUTATION_COMPLETED, $mutationEvent);
100 8
            $payload = $mutationEvent->getPayload();
101
        }
102
103 8
        return $payload;
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109 8
    public static function getSubscribedEvents()
110
    {
111
        return [
112 8
            FormEvents::PRE_SET_DATA => 'preSetData',
113 8
            FormEvents::POST_SET_DATA => 'postSetData',
114 8
            FormEvents::PRE_SUBMIT => 'preSubmit',
115 8
            FormEvents::SUBMIT => 'onSubmit',
116 8
            FormEvents::POST_SUBMIT => 'postSubmit',
117
        ];
118
    }
119
120
    /**
121
     * @see http://api.symfony.com/4.0/Symfony/Component/Form/FormEvents.html
122
     *
123
     * @param FormEvent $event
124
     */
125 8
    public function preSetData(FormEvent $event)
126
    {
127 8
    }
128
129
    /**
130
     * @see http://api.symfony.com/4.0/Symfony/Component/Form/FormEvents.html
131
     *
132
     * @param FormEvent $event
133
     */
134 8
    public function postSetData(FormEvent $event)
135
    {
136 8
    }
137
138
    /**
139
     * @see http://api.symfony.com/4.0/Symfony/Component/Form/FormEvents.html
140
     *
141
     * @param FormEvent $event
142
     */
143 8
    public function preSubmit(FormEvent $event)
144
    {
145 8
    }
146
147
    /**
148
     * @see http://api.symfony.com/4.0/Symfony/Component/Form/FormEvents.html
149
     *
150
     * @param FormEvent $event
151
     */
152 4
    public function onSubmit(FormEvent $event)
153
    {
154 4
    }
155
156
    /**
157
     * @see http://api.symfony.com/4.0/Symfony/Component/Form/FormEvents.html
158
     *
159
     * @param FormEvent $event
160
     */
161 8
    public function postSubmit(FormEvent $event)
162
    {
163 8
    }
164
165
    /**
166
     * Actions to process
167
     * the result processed data is given to payload
168
     *
169
     * @param mixed $data
170
     */
171
    abstract public function process(&$data);
172
173
    /**
174
     * The payload object or array matching the GraphQL definition
175
     *
176
     * @param mixed                   $data        normalized data, its the input data processed by the form
177
     * @param ConstraintViolationList $violations  violations returned by the form validation process
178
     * @param array                   $inputSource the original submitted data in array
179
     *
180
     * @return mixed
181
     */
182
    abstract public function returnPayload($data, ConstraintViolationList $violations, $inputSource);
183
184
    /**
185
     * @return null|string
186
     */
187 8
    protected function getPayloadClass(): string
188
    {
189 8
        $type = $this->getContext()->getDefinition()->getType();
190
191 8
        if (class_exists($type)) {
192 3
            return $type;
193
        }
194
195 5
        if ($this->context->getEndpoint()->hasType($type)) {
196 5
            $class = $this->context->getEndpoint()->getClassForType($type);
197 5
            if (class_exists($class)) {
198 5
                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...
199
            }
200
        }
201
202
        throw new \RuntimeException(
203
            sprintf(
204
                'Can\'t find a valid payload class for "%s".',
205
                $this->getContext()->getDefinition()->getName()
206
            )
207
        );
208
    }
209
210
    /**
211
     * @param array $input
212
     *
213
     * @return mixed
214
     */
215 8
    public function initialFormData($input)
216
    {
217 8
        if (is_array($input) && isset($input['id'])) {
218 3
            return IDEncoder::decode($input['id']);
219
        }
220
221 6
        return null;
222
    }
223
    /**
224
     * @param mixed|null $data
225
     *
226
     * @return FormBuilderInterface|null
227
     */
228 8
    public function createDefinitionForm($data): ?FormBuilderInterface
229
    {
230 8
        if (!$this->context->getDefinition()->hasMeta('form')) {
231
            return null;
232
        }
233
234 8
        $formConfig = $this->context->getDefinition()->getMeta('form') ?? [];
235 8
        $formType = $formConfig['type'] ?? null;
236 8
        if (!$formConfig || !$formType) {
237
            throw new \RuntimeException(sprintf('Can`t find a valid form for %s', $this->context->getDefinition()->getName()));
238
        }
239
240
        $options = [
241 8
            'allow_extra_fields' => true,
242
        ];
243
244 8
        if ($this->container->hasParameter('form.type_extension.csrf.enabled')
245 8
            && $this->container->getParameter('form.type_extension.csrf.enabled')) {
246 8
            $options['csrf_protection'] = false;
247
        }
248
249 8
        $options = array_merge($options, $formConfig['options'] ?? []);
250
251 8
        return $this->createFormBuilder($formType, $data, $options);
252
    }
253
254
    /**
255
     * @param FormInterface           $form
256
     * @param ConstraintViolationList $violations
257
     * @param null|string             $parentName
258
     */
259 8
    public function extractFormErrors(FormInterface $form, ConstraintViolationList $violations, ?string $parentName = null)
260
    {
261 8
        $errors = $form->getErrors(true);
262 8
        foreach ($errors as $error) {
263 1
            $violation = new ConstraintViolation();
264 1
            $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

264
            $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...
265 1
            $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

265
            $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...
266 1
            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

266
            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...
267 1
                $violation->addParameter($key, $value);
268
            }
269
270 1
            $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

270
            /** @scrutinizer ignore-call */ 
271
            $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...
271 1
            if ($cause instanceof SymfonyConstraintViolation) {
272 1
                $violation->setCode($cause->getCode() ?? md5($violation->getMessageTemplate()));
273 1
                $violation->setInvalidValue($cause->getInvalidValue());
274 1
                $violation->setPlural($cause->getPlural());
275
276 1
                $path = $this->publicPropertyPath($form, $cause->getPropertyPath());
277 1
                if ($path) {
278 1
                    $violation->setPropertyPath($path);
279
                }
280
            }
281
282 1
            $violations->addViolation($violation);
283
        }
284 8
    }
285
286
    /**
287
     * Convert internal validation property path to the public one,
288
     * required when use `property_path` in the form
289
     *
290
     * @param FormInterface $form
291
     * @param string        $path
292
     *
293
     * @return string
294
     */
295 1
    private function publicPropertyPath(FormInterface $form, $path)
296
    {
297 1
        $pathArray = [$path];
298
299 1
        if (strpos($path, '.') !== false) { // object.child.property
300 1
            $pathArray = explode('.', $path);
301
        }
302
303 1
        if (strpos($path, '[') !== false) { //[array][child][property]
304 1
            $path = str_replace(']', null, $path);
305 1
            $pathArray = explode('[', $path);
306
        }
307
308 1
        if (in_array($pathArray[0], ['data', 'children'])) {
309 1
            array_shift($pathArray);
310
        }
311
312 1
        $contextForm = $form;
313 1
        foreach ($pathArray as &$propName) {
314
            //for some reason some inputs are resolved as "inputName.data"
315
            //because the original form property is children[inputName].data
316
            //this is the case of DEMO AddUserInput form the login field is validated as path children[login].data
317
            //the following statements remove the trailing ".data"
318 1
            if (preg_match('/\.data$/', $propName)) {
319 1
                $propName = preg_replace('/\.data$/', null, $propName);
320
            }
321
322 1
            $index = null;
323 1
            if (preg_match('/(\w+)(\[\d+\])$/', $propName, $matches)) {
324
                list(, $propName, $index) = $matches;
325
            }
326 1
            if (!$contextForm->has($propName)) {
327
                foreach ($contextForm->all() as $child) {
328
                    if ($child->getConfig()->getOption('property_path') === $propName) {
329
                        $propName = $child->getName();
330
                    }
331
                }
332
            }
333 1
            if ($index) {
334 1
                $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

334
                $propName = sprintf('%s%s', $propName, /** @scrutinizer ignore-type */ $index);
Loading history...
335
            }
336
        }
337 1
        unset($propName);
338
339 1
        return implode('.', $pathArray);
340
    }
341
}
342