Completed
Push — 3.x ( 463a99...c7ca51 )
by Grégoire
04:04
created

AdminHelper::appendFormFieldElement()   D

Complexity

Conditions 17
Paths 63

Size

Total Lines 109

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 109
rs 4.1733
c 0
b 0
f 0
cc 17
nc 63
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\AdminBundle\Admin;
15
16
use Doctrine\Common\Collections\Collection;
17
use Doctrine\Common\Inflector\Inflector;
18
use Doctrine\Common\Util\ClassUtils;
19
use Doctrine\ODM\MongoDB\PersistentCollection;
20
use Doctrine\ORM\PersistentCollection as DoctrinePersistentCollection;
21
use Sonata\AdminBundle\Exception\NoValueException;
22
use Sonata\AdminBundle\Util\FormBuilderIterator;
23
use Sonata\AdminBundle\Util\FormViewIterator;
24
use Symfony\Component\Form\FormBuilderInterface;
25
use Symfony\Component\Form\FormView;
26
27
/**
28
 * @author Thomas Rabaix <[email protected]>
29
 */
30
class AdminHelper
31
{
32
    /**
33
     * @var Pool
34
     */
35
    protected $pool;
36
37
    public function __construct(Pool $pool)
38
    {
39
        $this->pool = $pool;
40
    }
41
42
    /**
43
     * @param string $elementId
44
     *
45
     * @throws \RuntimeException
46
     *
47
     * @return FormBuilderInterface|null
48
     */
49
    public function getChildFormBuilder(FormBuilderInterface $formBuilder, $elementId)
50
    {
51
        foreach (new FormBuilderIterator($formBuilder) as $name => $formBuilder) {
52
            if ($name === $elementId) {
53
                return $formBuilder;
54
            }
55
        }
56
    }
57
58
    /**
59
     * @param string $elementId
60
     *
61
     * @return FormView|null
62
     */
63
    public function getChildFormView(FormView $formView, $elementId)
64
    {
65
        foreach (new \RecursiveIteratorIterator(new FormViewIterator($formView), \RecursiveIteratorIterator::SELF_FIRST) as $name => $formView) {
66
            if ($name === $elementId) {
67
                return $formView;
68
            }
69
        }
70
    }
71
72
    /**
73
     * NEXT_MAJOR: remove this method.
74
     *
75
     * @deprecated
76
     *
77
     * @param string $code
78
     *
79
     * @return AdminInterface
80
     */
81
    public function getAdmin($code)
82
    {
83
        return $this->pool->getInstance($code);
84
    }
85
86
    /**
87
     * Note:
88
     *   This code is ugly, but there is no better way of doing it.
89
     *   For now the append form element action used to add a new row works
90
     *   only for direct FieldDescription (not nested one).
91
     *
92
     *
93
     * @param object $subject
94
     * @param string $elementId
95
     *
96
     * @throws \RuntimeException
97
     * @throws \Exception
98
     *
99
     * @return array
100
     */
101
    public function appendFormFieldElement(AdminInterface $admin, $subject, $elementId)
102
    {
103
        // child rows marked as toDelete
104
        $toDelete = [];
105
        // retrieve the subject
106
        $formBuilder = $admin->getFormBuilder();
107
108
        // get the field element
109
        $childFormBuilder = $this->getChildFormBuilder($formBuilder, $elementId);
110
111
        if ($childFormBuilder) {
112
            $formData = $admin->getRequest()->get($formBuilder->getName(), []);
113
            if (array_key_exists($childFormBuilder->getName(), $formData)) {
114
                $formData = $admin->getRequest()->get($formBuilder->getName(), []);
115
                $i = 0;
116
                foreach ($formData[$childFormBuilder->getName()] as $name => &$field) {
117
                    $toDelete[$i] = false;
118
                    if (array_key_exists('_delete', $field)) {
119
                        $toDelete[$i] = true;
120
                        unset($field['_delete']);
121
                    }
122
                    ++$i;
123
                }
124
            }
125
            $admin->getRequest()->request->set($formBuilder->getName(), $formData);
126
        }
127
128
        $form = $formBuilder->getForm();
129
        $form->setData($subject);
130
        $form->handleRequest($admin->getRequest());
131
132
        //Child form not found (probably nested one)
133
        //if childFormBuilder was not found resulted in fatal error getName() method call on non object
134
        if (!$childFormBuilder) {
135
            $propertyAccessor = $this->pool->getPropertyAccessor();
136
            $entity = $admin->getSubject();
137
138
            $path = $this->getElementAccessPath($elementId, $entity);
139
140
            $collection = $propertyAccessor->getValue($entity, $path);
141
142
            if ($collection instanceof DoctrinePersistentCollection || $collection instanceof PersistentCollection) {
0 ignored issues
show
Bug introduced by
The class Doctrine\ORM\PersistentCollection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
Bug introduced by
The class Doctrine\ODM\MongoDB\PersistentCollection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
143
                //since doctrine 2.4
144
                $entityClassName = $collection->getTypeClass()->getName();
145
            } elseif ($collection instanceof Collection) {
146
                $entityClassName = $this->getEntityClassName($admin, explode('.', preg_replace('#\[\d*?\]#', '', $path)));
147
            } else {
148
                throw new \Exception('unknown collection class');
149
            }
150
151
            $collection->add(new $entityClassName());
152
            $propertyAccessor->setValue($entity, $path, $collection);
153
154
            $fieldDescription = null;
155
        } else {
156
            // retrieve the FieldDescription
157
            $fieldDescription = $admin->getFormFieldDescription($childFormBuilder->getName());
158
159
            try {
160
                $value = $fieldDescription->getValue($form->getData());
161
            } catch (NoValueException $e) {
162
                $value = null;
163
            }
164
165
            // retrieve the posted data
166
            $data = $admin->getRequest()->get($formBuilder->getName());
167
168
            if (!isset($data[$childFormBuilder->getName()])) {
169
                $data[$childFormBuilder->getName()] = [];
170
            }
171
172
            $objectCount = null === $value ? 0 : \count($value);
173
            $postCount = \count($data[$childFormBuilder->getName()]);
174
175
            $fields = array_keys($fieldDescription->getAssociationAdmin()->getFormFieldDescriptions());
176
177
            // for now, not sure how to do that
178
            $value = [];
179
            foreach ($fields as $name) {
180
                $value[$name] = '';
181
            }
182
183
            // add new elements to the subject
184
            while ($objectCount < $postCount) {
185
                // append a new instance into the object
186
                $this->addNewInstance($form->getData(), $fieldDescription);
187
                ++$objectCount;
188
            }
189
190
            $this->addNewInstance($form->getData(), $fieldDescription);
191
        }
192
193
        $finalForm = $admin->getFormBuilder()->getForm();
194
        $finalForm->setData($subject);
195
196
        // bind the data
197
        $finalForm->setData($form->getData());
198
199
        // back up delete field
200
        if (\count($toDelete) > 0) {
201
            $i = 0;
202
            foreach ($finalForm->get($childFormBuilder->getName()) as $childField) {
203
                $childField->get('_delete')->setData(isset($toDelete[$i]) && $toDelete[$i]);
204
                ++$i;
205
            }
206
        }
207
208
        return [$fieldDescription, $finalForm];
209
    }
210
211
    /**
212
     * Add a new instance to the related FieldDescriptionInterface value.
213
     *
214
     * @param object $object
215
     *
216
     * @throws \RuntimeException
217
     */
218
    public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
219
    {
220
        $instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
221
        $mapping = $fieldDescription->getAssociationMapping();
222
223
        $method = sprintf('add%s', Inflector::classify($mapping['fieldName']));
224
225
        if (!method_exists($object, $method)) {
226
            $method = rtrim($method, 's');
227
228
            if (!method_exists($object, $method)) {
229
                $method = sprintf('add%s', Inflector::classify(Inflector::singularize($mapping['fieldName'])));
230
231
                if (!method_exists($object, $method)) {
232
                    throw new \RuntimeException(
233
                        sprintf('Please add a method %s in the %s class!', $method, ClassUtils::getClass($object))
234
                    );
235
                }
236
            }
237
        }
238
239
        $object->$method($instance);
240
    }
241
242
    /**
243
     * Camelize a string.
244
     *
245
     * NEXT_MAJOR: remove this method.
246
     *
247
     * @static
248
     *
249
     * @param string $property
250
     *
251
     * @return string
252
     *
253
     * @deprecated Deprecated since version 3.1. Use \Doctrine\Common\Inflector\Inflector::classify() instead
254
     */
255
    public function camelize($property)
256
    {
257
        @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
258
            sprintf(
259
                'The %s method is deprecated since 3.1 and will be removed in 4.0. '.
260
                'Use \Doctrine\Common\Inflector\Inflector::classify() instead.',
261
                __METHOD__
262
            ),
263
            E_USER_DEPRECATED
264
        );
265
266
        return Inflector::classify($property);
267
    }
268
269
    /**
270
     * Get access path to element which works with PropertyAccessor.
271
     *
272
     * @param string $elementId expects string in format used in form id field.
273
     *                          (uniqueIdentifier_model_sub_model or uniqueIdentifier_model_1_sub_model etc.)
274
     * @param mixed  $entity
275
     *
276
     * @throws \Exception
277
     *
278
     * @return string
279
     */
280
    public function getElementAccessPath($elementId, $entity)
281
    {
282
        $propertyAccessor = $this->pool->getPropertyAccessor();
283
284
        $idWithoutIdentifier = preg_replace('/^[^_]*_/', '', $elementId);
285
        $initialPath = preg_replace('#(_(\d+)_)#', '[$2]_', $idWithoutIdentifier);
286
287
        $parts = explode('_', $initialPath);
288
        $totalPath = '';
289
        $currentPath = '';
290
291
        foreach ($parts as $part) {
292
            $currentPath .= empty($currentPath) ? $part : '_'.$part;
293
            $separator = empty($totalPath) ? '' : '.';
294
295
            if ($propertyAccessor->isReadable($entity, $totalPath.$separator.$currentPath)) {
296
                $totalPath .= $separator.$currentPath;
297
                $currentPath = '';
298
            }
299
        }
300
301
        if (!empty($currentPath)) {
302
            throw new \Exception(
303
                sprintf('Could not get element id from %s Failing part: %s', $elementId, $currentPath)
304
            );
305
        }
306
307
        return $totalPath;
308
    }
309
310
    /**
311
     * Recursively find the class name of the admin responsible for the element at the end of an association chain.
312
     *
313
     * @param array $elements
314
     *
315
     * @return string
316
     */
317
    protected function getEntityClassName(AdminInterface $admin, $elements)
318
    {
319
        $element = array_shift($elements);
320
        $associationAdmin = $admin->getFormFieldDescription($element)->getAssociationAdmin();
321
        if (0 === \count($elements)) {
322
            return $associationAdmin->getClass();
323
        }
324
325
        return $this->getEntityClassName($associationAdmin, $elements);
0 ignored issues
show
Bug introduced by
It seems like $associationAdmin defined by $admin->getFormFieldDesc...->getAssociationAdmin() on line 320 can be null; however, Sonata\AdminBundle\Admin...r::getEntityClassName() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
326
    }
327
}
328