Completed
Pull Request — master (#6060)
by Grégoire
02:49
created

AdminHelper   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 294
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 11
dl 0
loc 294
rs 9.0399
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
B getElementAccessPath() 0 29 6
A getEntityClassName() 0 10 2
A __construct() 0 4 1
A getChildFormBuilder() 0 10 3
A getChildFormView() 0 10 3
A getAdmin() 0 4 1
D appendFormFieldElement() 0 105 16
B addNewInstance() 0 44 10

How to fix   Complexity   

Complex Class

Complex classes like AdminHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AdminHelper, and based on these observations, apply Extract Interface, too.

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
 * @final since sonata-project/admin-bundle 3.52
29
 *
30
 * @author Thomas Rabaix <[email protected]>
31
 */
32
class AdminHelper
33
{
34
    /**
35
     * @var string
36
     */
37
    private const FORM_FIELD_DELETE = '_delete';
38
39
    /**
40
     * @var Pool
41
     */
42
    protected $pool;
43
44
    public function __construct(Pool $pool)
45
    {
46
        $this->pool = $pool;
47
    }
48
49
    /**
50
     * @param string $elementId
51
     *
52
     * @throws \RuntimeException
53
     *
54
     * @return FormBuilderInterface|null
55
     */
56
    public function getChildFormBuilder(FormBuilderInterface $formBuilder, $elementId)
57
    {
58
        foreach (new FormBuilderIterator($formBuilder) as $name => $formBuilder) {
59
            if ($name === $elementId) {
60
                return $formBuilder;
61
            }
62
        }
63
64
        return null;
65
    }
66
67
    /**
68
     * @param string $elementId
69
     *
70
     * @return FormView|null
71
     */
72
    public function getChildFormView(FormView $formView, $elementId)
73
    {
74
        foreach (new \RecursiveIteratorIterator(new FormViewIterator($formView), \RecursiveIteratorIterator::SELF_FIRST) as $name => $formView) {
75
            if ($name === $elementId) {
76
                return $formView;
77
            }
78
        }
79
80
        return null;
81
    }
82
83
    /**
84
     * NEXT_MAJOR: remove this method.
85
     *
86
     * @deprecated
87
     *
88
     * @param string $code
89
     *
90
     * @return AdminInterface
91
     */
92
    public function getAdmin($code)
93
    {
94
        return $this->pool->getInstance($code);
95
    }
96
97
    /**
98
     * Note:
99
     *   This code is ugly, but there is no better way of doing it.
100
     *
101
     * @param object $subject
102
     * @param string $elementId
103
     *
104
     * @throws \RuntimeException
105
     * @throws \Exception
106
     *
107
     * @return array
108
     */
109
    public function appendFormFieldElement(AdminInterface $admin, $subject, $elementId)
110
    {
111
        // child rows marked as toDelete
112
        $toDelete = [];
113
114
        $formBuilder = $admin->getFormBuilder();
115
116
        // get the field element
117
        $childFormBuilder = $this->getChildFormBuilder($formBuilder, $elementId);
118
119
        if ($childFormBuilder) {
120
            $formData = $admin->getRequest()->get($formBuilder->getName(), []);
121
            if (\array_key_exists($childFormBuilder->getName(), $formData)) {
122
                $formData = $admin->getRequest()->get($formBuilder->getName(), []);
123
                $i = 0;
124
                foreach ($formData[$childFormBuilder->getName()] as $name => &$field) {
125
                    $toDelete[$i] = false;
126
                    if (\array_key_exists(self::FORM_FIELD_DELETE, $field)) {
127
                        $toDelete[$i] = true;
128
                        unset($field[self::FORM_FIELD_DELETE]);
129
                    }
130
                    ++$i;
131
                }
132
            }
133
            $admin->getRequest()->request->set($formBuilder->getName(), $formData);
134
        }
135
136
        $form = $formBuilder->getForm();
137
        $form->setData($subject);
138
        $form->handleRequest($admin->getRequest());
139
140
        //Child form not found (probably nested one)
141
        //if childFormBuilder was not found resulted in fatal error getName() method call on non object
142
        if (!$childFormBuilder) {
143
            $propertyAccessor = $this->pool->getPropertyAccessor();
144
145
            $path = $this->getElementAccessPath($elementId, $subject);
146
147
            $collection = $propertyAccessor->getValue($subject, $path);
148
149
            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...
150
                //since doctrine 2.4
151
                $entityClassName = $collection->getTypeClass()->getName();
152
            } elseif ($collection instanceof Collection) {
153
                $entityClassName = $this->getEntityClassName($admin, explode('.', preg_replace('#\[\d*?\]#', '', $path)));
154
            } else {
155
                throw new \Exception('unknown collection class');
156
            }
157
158
            $collection->add(new $entityClassName());
159
            $propertyAccessor->setValue($subject, $path, $collection);
160
161
            $fieldDescription = null;
162
        } else {
163
            // retrieve the FieldDescription
164
            $fieldDescription = $admin->getFormFieldDescription($childFormBuilder->getName());
165
166
            try {
167
                $value = $fieldDescription->getValue($form->getData());
168
            } catch (NoValueException $e) {
169
                $value = null;
170
            }
171
172
            // retrieve the posted data
173
            $data = $admin->getRequest()->get($formBuilder->getName());
174
175
            if (!isset($data[$childFormBuilder->getName()])) {
176
                $data[$childFormBuilder->getName()] = [];
177
            }
178
179
            $objectCount = null === $value ? 0 : \count($value);
180
            $postCount = \count($data[$childFormBuilder->getName()]);
181
182
            // add new elements to the subject
183
            while ($objectCount < $postCount) {
184
                // append a new instance into the object
185
                $this->addNewInstance($form->getData(), $fieldDescription);
186
                ++$objectCount;
187
            }
188
189
            $newInstance = $this->addNewInstance($form->getData(), $fieldDescription);
190
191
            $associationAdmin = $fieldDescription->getAssociationAdmin();
192
            $associationAdmin->setSubject($newInstance);
193
        }
194
195
        $finalForm = $admin->getFormBuilder()->getForm();
196
        $finalForm->setData($subject);
197
198
        // bind the data
199
        $finalForm->setData($form->getData());
200
201
        // back up delete field
202
        if (\count($toDelete) > 0) {
203
            $i = 0;
204
            foreach ($finalForm->get($childFormBuilder->getName()) as $childField) {
205
                if ($childField->has(self::FORM_FIELD_DELETE)) {
206
                    $childField->get(self::FORM_FIELD_DELETE)->setData($toDelete[$i] ?? false);
207
                }
208
                ++$i;
209
            }
210
        }
211
212
        return [$fieldDescription, $finalForm];
213
    }
214
215
    /**
216
     * Add a new instance to the related FieldDescriptionInterface value.
217
     *
218
     * @param object $object
219
     *
220
     * @throws \RuntimeException
221
     */
222
    public function addNewInstance($object, FieldDescriptionInterface $fieldDescription): object
223
    {
224
        $instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
225
        $mapping = $fieldDescription->getAssociationMapping();
226
        $parentMappings = $fieldDescription->getParentAssociationMappings();
227
228
        foreach ($parentMappings as $parentMapping) {
229
            $method = sprintf('get%s', Inflector::classify($parentMapping['fieldName']));
230
231
            if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
232
                /*
233
                 * NEXT_MAJOR: Use BadMethodCallException instead
234
                 */
235
                throw new \RuntimeException(
236
                    sprintf('Method %s::%s() does not exist.', ClassUtils::getClass($object), $method)
237
                );
238
            }
239
240
            $object = $object->$method();
241
        }
242
243
        $method = sprintf('add%s', Inflector::classify($mapping['fieldName']));
244
245
        if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
246
            $method = rtrim($method, 's');
247
248
            if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
249
                $method = sprintf('add%s', Inflector::classify(Inflector::singularize($mapping['fieldName'])));
250
251
                if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
252
                    /*
253
                     * NEXT_MAJOR: Use BadMethodCallException instead
254
                     */
255
                    throw new \RuntimeException(
256
                        sprintf('Method %s::%s() does not exist.', ClassUtils::getClass($object), $method)
257
                    );
258
                }
259
            }
260
        }
261
262
        $object->$method($instance);
263
264
        return $instance;
265
    }
266
267
    /**
268
     * Get access path to element which works with PropertyAccessor.
269
     *
270
     * @param string $elementId expects string in format used in form id field.
271
     *                          (uniqueIdentifier_model_sub_model or uniqueIdentifier_model_1_sub_model etc.)
272
     * @param mixed  $entity
273
     *
274
     * @throws \Exception
275
     *
276
     * @return string
277
     */
278
    public function getElementAccessPath($elementId, $entity)
279
    {
280
        $propertyAccessor = $this->pool->getPropertyAccessor();
281
282
        $idWithoutIdentifier = preg_replace('/^[^_]*_/', '', $elementId);
283
        $initialPath = preg_replace('#(_(\d+)_)#', '[$2]_', $idWithoutIdentifier);
284
285
        $parts = explode('_', $initialPath);
286
        $totalPath = '';
287
        $currentPath = '';
288
289
        foreach ($parts as $part) {
290
            $currentPath .= empty($currentPath) ? $part : '_'.$part;
291
            $separator = empty($totalPath) ? '' : '.';
292
293
            if ($propertyAccessor->isReadable($entity, $totalPath.$separator.$currentPath)) {
294
                $totalPath .= $separator.$currentPath;
295
                $currentPath = '';
296
            }
297
        }
298
299
        if (!empty($currentPath)) {
300
            throw new \Exception(
301
                sprintf('Could not get element id from %s Failing part: %s', $elementId, $currentPath)
302
            );
303
        }
304
305
        return $totalPath;
306
    }
307
308
    /**
309
     * Recursively find the class name of the admin responsible for the element at the end of an association chain.
310
     *
311
     * @param array $elements
312
     *
313
     * @return string
314
     */
315
    protected function getEntityClassName(AdminInterface $admin, $elements)
316
    {
317
        $element = array_shift($elements);
318
        $associationAdmin = $admin->getFormFieldDescription($element)->getAssociationAdmin();
319
        if (0 === \count($elements)) {
320
            return $associationAdmin->getClass();
321
        }
322
323
        return $this->getEntityClassName($associationAdmin, $elements);
0 ignored issues
show
Bug introduced by
It seems like $associationAdmin defined by $admin->getFormFieldDesc...->getAssociationAdmin() on line 318 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...
324
    }
325
}
326