Completed
Push — 3.x ( 05b755...dab7ae )
by Grégoire
03:23
created

AdminHelper   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 325
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 44
lcom 1
cbo 11
dl 0
loc 325
rs 8.8798
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getChildFormBuilder() 0 10 3
A getChildFormView() 0 10 3
A getAdmin() 0 4 1
D appendFormFieldElement() 0 111 17
A camelize() 0 13 1
B getElementAccessPath() 0 29 6
A getEntityClassName() 0 10 2
B addNewInstance() 0 42 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
        // retrieve the subject
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
            $entity = $admin->getSubject();
145
146
            $path = $this->getElementAccessPath($elementId, $entity);
147
148
            $collection = $propertyAccessor->getValue($entity, $path);
0 ignored issues
show
Bug introduced by
It seems like $entity defined by $admin->getSubject() on line 144 can also be of type null; however, Symfony\Component\Proper...orInterface::getValue() does only seem to accept object|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
149
150
            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...
151
                //since doctrine 2.4
152
                $entityClassName = $collection->getTypeClass()->getName();
153
            } elseif ($collection instanceof Collection) {
154
                $entityClassName = $this->getEntityClassName($admin, explode('.', preg_replace('#\[\d*?\]#', '', $path)));
155
            } else {
156
                throw new \Exception('unknown collection class');
157
            }
158
159
            $collection->add(new $entityClassName());
160
            $propertyAccessor->setValue($entity, $path, $collection);
0 ignored issues
show
Bug introduced by
It seems like $entity defined by $admin->getSubject() on line 144 can also be of type null; however, Symfony\Component\Proper...orInterface::setValue() does only seem to accept object|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
161
162
            $fieldDescription = null;
163
        } else {
164
            // retrieve the FieldDescription
165
            $fieldDescription = $admin->getFormFieldDescription($childFormBuilder->getName());
166
167
            try {
168
                $value = $fieldDescription->getValue($form->getData());
169
            } catch (NoValueException $e) {
170
                $value = null;
171
            }
172
173
            // retrieve the posted data
174
            $data = $admin->getRequest()->get($formBuilder->getName());
175
176
            if (!isset($data[$childFormBuilder->getName()])) {
177
                $data[$childFormBuilder->getName()] = [];
178
            }
179
180
            $objectCount = null === $value ? 0 : \count($value);
181
            $postCount = \count($data[$childFormBuilder->getName()]);
182
183
            $fields = array_keys($fieldDescription->getAssociationAdmin()->getFormFieldDescriptions());
184
185
            // for now, not sure how to do that
186
            $value = [];
187
            foreach ($fields as $name) {
188
                $value[$name] = '';
189
            }
190
191
            // add new elements to the subject
192
            while ($objectCount < $postCount) {
193
                // append a new instance into the object
194
                $this->addNewInstance($form->getData(), $fieldDescription);
195
                ++$objectCount;
196
            }
197
198
            $this->addNewInstance($form->getData(), $fieldDescription);
199
        }
200
201
        $finalForm = $admin->getFormBuilder()->getForm();
202
        $finalForm->setData($subject);
203
204
        // bind the data
205
        $finalForm->setData($form->getData());
206
207
        // back up delete field
208
        if (\count($toDelete) > 0) {
209
            $i = 0;
210
            foreach ($finalForm->get($childFormBuilder->getName()) as $childField) {
211
                if ($childField->has(self::FORM_FIELD_DELETE)) {
212
                    $childField->get(self::FORM_FIELD_DELETE)->setData($toDelete[$i] ?? false);
213
                }
214
                ++$i;
215
            }
216
        }
217
218
        return [$fieldDescription, $finalForm];
219
    }
220
221
    /**
222
     * Add a new instance to the related FieldDescriptionInterface value.
223
     *
224
     * @param object $object
225
     *
226
     * @throws \RuntimeException
227
     */
228
    public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
229
    {
230
        $instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
231
        $mapping = $fieldDescription->getAssociationMapping();
232
        $parentMappings = $fieldDescription->getParentAssociationMappings();
233
234
        foreach ($parentMappings as $parentMapping) {
235
            $method = sprintf('get%s', Inflector::classify($parentMapping['fieldName']));
236
237
            if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
238
                /*
239
                 * NEXT_MAJOR: Use BadMethodCallException instead
240
                 */
241
                throw new \RuntimeException(
242
                    sprintf('Method %s::%s() does not exist.', ClassUtils::getClass($object), $method)
243
                );
244
            }
245
246
            $object = $object->$method();
247
        }
248
249
        $method = sprintf('add%s', Inflector::classify($mapping['fieldName']));
250
251
        if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
252
            $method = rtrim($method, 's');
253
254
            if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
255
                $method = sprintf('add%s', Inflector::classify(Inflector::singularize($mapping['fieldName'])));
256
257
                if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
258
                    /*
259
                     * NEXT_MAJOR: Use BadMethodCallException instead
260
                     */
261
                    throw new \RuntimeException(
262
                        sprintf('Method %s::%s() does not exist.', ClassUtils::getClass($object), $method)
263
                    );
264
                }
265
            }
266
        }
267
268
        $object->$method($instance);
269
    }
270
271
    /**
272
     * Camelize a string.
273
     *
274
     * NEXT_MAJOR: remove this method.
275
     *
276
     * @static
277
     *
278
     * @param string $property
279
     *
280
     * @return string
281
     *
282
     * @deprecated since sonata-project/admin-bundle 3.1. Use \Doctrine\Common\Inflector\Inflector::classify() instead
283
     */
284
    public function camelize($property)
285
    {
286
        @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...
287
            sprintf(
288
                'The %s method is deprecated since 3.1 and will be removed in 4.0. '.
289
                'Use \Doctrine\Common\Inflector\Inflector::classify() instead.',
290
                __METHOD__
291
            ),
292
            E_USER_DEPRECATED
293
        );
294
295
        return Inflector::classify($property);
296
    }
297
298
    /**
299
     * Get access path to element which works with PropertyAccessor.
300
     *
301
     * @param string $elementId expects string in format used in form id field.
302
     *                          (uniqueIdentifier_model_sub_model or uniqueIdentifier_model_1_sub_model etc.)
303
     * @param mixed  $entity
304
     *
305
     * @throws \Exception
306
     *
307
     * @return string
308
     */
309
    public function getElementAccessPath($elementId, $entity)
310
    {
311
        $propertyAccessor = $this->pool->getPropertyAccessor();
312
313
        $idWithoutIdentifier = preg_replace('/^[^_]*_/', '', $elementId);
314
        $initialPath = preg_replace('#(_(\d+)_)#', '[$2]_', $idWithoutIdentifier);
315
316
        $parts = explode('_', $initialPath);
317
        $totalPath = '';
318
        $currentPath = '';
319
320
        foreach ($parts as $part) {
321
            $currentPath .= empty($currentPath) ? $part : '_'.$part;
322
            $separator = empty($totalPath) ? '' : '.';
323
324
            if ($propertyAccessor->isReadable($entity, $totalPath.$separator.$currentPath)) {
325
                $totalPath .= $separator.$currentPath;
326
                $currentPath = '';
327
            }
328
        }
329
330
        if (!empty($currentPath)) {
331
            throw new \Exception(
332
                sprintf('Could not get element id from %s Failing part: %s', $elementId, $currentPath)
333
            );
334
        }
335
336
        return $totalPath;
337
    }
338
339
    /**
340
     * Recursively find the class name of the admin responsible for the element at the end of an association chain.
341
     *
342
     * @param array $elements
343
     *
344
     * @return string
345
     */
346
    protected function getEntityClassName(AdminInterface $admin, $elements)
347
    {
348
        $element = array_shift($elements);
349
        $associationAdmin = $admin->getFormFieldDescription($element)->getAssociationAdmin();
350
        if (0 === \count($elements)) {
351
            return $associationAdmin->getClass();
352
        }
353
354
        return $this->getEntityClassName($associationAdmin, $elements);
0 ignored issues
show
Bug introduced by
It seems like $associationAdmin defined by $admin->getFormFieldDesc...->getAssociationAdmin() on line 349 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...
355
    }
356
}
357