Completed
Push — master ( 15c856...4ce16e )
by Rafael
05:47
created

AbstractMutationResolver::onSubmit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

249
            $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...
250 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

250
            $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...
251 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

251
            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...
252 1
                $violation->addParameter($key, $value);
253
            }
254
255 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

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

319
                $propName = sprintf('%s%s', $propName, /** @scrutinizer ignore-type */ $index);
Loading history...
320
            }
321
        }
322 1
        unset($propName);
323
324 1
        return implode('.', $pathArray);
325
    }
326
}
327