Completed
Pull Request — 3.x (#6171)
by Vincent
03:03
created

AdminHelper::getEntityClassName()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
cc 3
nc 4
nop 2
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\Inflector\Inflector;
18
use Doctrine\Inflector\InflectorFactory;
19
use Doctrine\ODM\MongoDB\PersistentCollection;
20
use Doctrine\ORM\PersistentCollection as DoctrinePersistentCollection;
21
use Sonata\AdminBundle\Exception\NoValueException;
22
use Sonata\AdminBundle\Manipulator\ObjectManipulator;
23
use Sonata\AdminBundle\Util\FormBuilderIterator;
24
use Sonata\AdminBundle\Util\FormViewIterator;
25
use Symfony\Component\Form\FormBuilderInterface;
26
use Symfony\Component\Form\FormView;
27
28
/**
29
 * @final since sonata-project/admin-bundle 3.52
30
 *
31
 * @author Thomas Rabaix <[email protected]>
32
 */
33
class AdminHelper
34
{
35
    /**
36
     * @var string
37
     */
38
    private const FORM_FIELD_DELETE = '_delete';
39
40
    /**
41
     * @var Pool
42
     */
43
    protected $pool;
44
45
    public function __construct(Pool $pool)
46
    {
47
        $this->pool = $pool;
48
    }
49
50
    /**
51
     * @param string $elementId
52
     *
53
     * @throws \RuntimeException
54
     *
55
     * @return FormBuilderInterface|null
56
     */
57
    public function getChildFormBuilder(FormBuilderInterface $formBuilder, $elementId)
58
    {
59
        foreach (new FormBuilderIterator($formBuilder) as $name => $formBuilder) {
60
            if ($name === $elementId) {
61
                return $formBuilder;
62
            }
63
        }
64
65
        return null;
66
    }
67
68
    /**
69
     * @param string $elementId
70
     *
71
     * @return FormView|null
72
     */
73
    public function getChildFormView(FormView $formView, $elementId)
74
    {
75
        foreach (new \RecursiveIteratorIterator(new FormViewIterator($formView), \RecursiveIteratorIterator::SELF_FIRST) as $name => $formView) {
76
            if ($name === $elementId) {
77
                return $formView;
78
            }
79
        }
80
81
        return null;
82
    }
83
84
    /**
85
     * NEXT_MAJOR: remove this method.
86
     *
87
     * @deprecated
88
     *
89
     * @param string $code
90
     *
91
     * @return AdminInterface
92
     */
93
    public function getAdmin($code)
94
    {
95
        return $this->pool->getInstance($code);
96
    }
97
98
    /**
99
     * Note:
100
     *   This code is ugly, but there is no better way of doing it.
101
     *
102
     * @param object $subject
103
     * @param string $elementId
104
     *
105
     * @throws \RuntimeException
106
     * @throws \Exception
107
     *
108
     * @return array
109
     */
110
    public function appendFormFieldElement(AdminInterface $admin, $subject, $elementId)
111
    {
112
        // child rows marked as toDelete
113
        $toDelete = [];
114
115
        $formBuilder = $admin->getFormBuilder();
116
117
        // get the field element
118
        $childFormBuilder = $this->getChildFormBuilder($formBuilder, $elementId);
119
120
        if ($childFormBuilder) {
121
            $formData = $admin->getRequest()->get($formBuilder->getName(), []);
122
            if (\array_key_exists($childFormBuilder->getName(), $formData)) {
123
                $formData = $admin->getRequest()->get($formBuilder->getName(), []);
124
                $i = 0;
125
                foreach ($formData[$childFormBuilder->getName()] as $name => &$field) {
126
                    $toDelete[$i] = false;
127
                    if (\array_key_exists(self::FORM_FIELD_DELETE, $field)) {
128
                        $toDelete[$i] = true;
129
                        unset($field[self::FORM_FIELD_DELETE]);
130
                    }
131
                    ++$i;
132
                }
133
            }
134
            $admin->getRequest()->request->set($formBuilder->getName(), $formData);
135
        }
136
137
        $form = $formBuilder->getForm();
138
        $form->setData($subject);
139
        $form->handleRequest($admin->getRequest());
140
141
        //Child form not found (probably nested one)
142
        //if childFormBuilder was not found resulted in fatal error getName() method call on non object
143
        if (!$childFormBuilder) {
144
            $propertyAccessor = $this->pool->getPropertyAccessor();
145
146
            $path = $this->getElementAccessPath($elementId, $subject);
147
148
            $collection = $propertyAccessor->getValue($subject, $path);
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
                $modelClassName = $collection->getTypeClass()->getName();
153
            } elseif ($collection instanceof Collection) {
154
                $modelClassName = $this->getEntityClassName($admin, explode('.', preg_replace('#\[\d*?\]#', '', $path)));
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...r::getEntityClassName() has been deprecated with message: since sonata-project/admin-bundle 3.69. Use `getModelClassName()` instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
155
            } else {
156
                throw new \Exception('unknown collection class');
157
            }
158
159
            $collection->add(new $modelClassName());
160
            $propertyAccessor->setValue($subject, $path, $collection);
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
            $associationAdmin = $fieldDescription->getAssociationAdmin();
184
185
            // add new elements to the subject
186
            while ($objectCount < $postCount) {
187
                // append a new instance into the object
188
                ObjectManipulator::addInstance($form->getData(), $associationAdmin->getNewInstance(), $fieldDescription);
189
                ++$objectCount;
190
            }
191
192
            $newInstance = ObjectManipulator::addInstance($form->getData(), $associationAdmin->getNewInstance(), $fieldDescription);
193
194
            $associationAdmin->setSubject($newInstance);
195
        }
196
197
        $finalForm = $admin->getFormBuilder()->getForm();
198
        $finalForm->setData($subject);
199
200
        // bind the data
201
        $finalForm->setData($form->getData());
202
203
        // back up delete field
204
        if (\count($toDelete) > 0) {
205
            $i = 0;
206
            foreach ($finalForm->get($childFormBuilder->getName()) as $childField) {
207
                if ($childField->has(self::FORM_FIELD_DELETE)) {
208
                    $childField->get(self::FORM_FIELD_DELETE)->setData($toDelete[$i] ?? false);
209
                }
210
                ++$i;
211
            }
212
        }
213
214
        return [$fieldDescription, $finalForm];
215
    }
216
217
    /**
218
     * NEXT_MAJOR: remove this method.
219
     *
220
     * @deprecated since sonata-project/admin-bundle 3.x, use to be removed with 4.0.
221
     *
222
     * Add a new instance to the related FieldDescriptionInterface value.
223
     *
224
     * @param object $object
225
     *
226
     * @throws \RuntimeException
227
     *
228
     * @return object
229
     */
230
    public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
231
    {
232
        @trigger_error(sprintf(
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...
233
            'Method %s() is deprecated since sonata-project/admin-bundle 3.x. It will be removed in version 4.0.'
234
            .' Use %s::addInstance() instead.',
235
            __METHOD__,
236
            ObjectManipulator::class
237
        ), E_USER_DEPRECATED);
238
239
        $instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
240
241
        return ObjectManipulator::addInstance($object, $instance, $fieldDescription);
242
    }
243
244
    /**
245
     * Camelize a string.
246
     *
247
     * NEXT_MAJOR: remove this method.
248
     *
249
     * @static
250
     *
251
     * @param string $property
252
     *
253
     * @return string
254
     *
255
     * @deprecated since sonata-project/admin-bundle 3.1. Use \Doctrine\Inflector\Inflector::classify() instead
256
     */
257
    public function camelize($property)
258
    {
259
        @trigger_error(sprintf(
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...
260
            'The %s method is deprecated since 3.1 and will be removed in 4.0. Use %s::classify() instead.',
261
            __METHOD__,
262
            Inflector::class
263
        ), E_USER_DEPRECATED);
264
265
        return InflectorFactory::create()->build()->classify($property);
266
    }
267
268
    /**
269
     * Get access path to element which works with PropertyAccessor.
270
     *
271
     * @param string $elementId expects string in format used in form id field.
272
     *                          (uniqueIdentifier_model_sub_model or uniqueIdentifier_model_1_sub_model etc.)
273
     * @param mixed  $model
274
     *
275
     * @throws \Exception
276
     *
277
     * @return string
278
     */
279
    public function getElementAccessPath($elementId, $model)
280
    {
281
        $propertyAccessor = $this->pool->getPropertyAccessor();
282
283
        $idWithoutIdentifier = preg_replace('/^[^_]*_/', '', $elementId);
284
        $initialPath = preg_replace('#(_(\d+)_)#', '[$2]_', $idWithoutIdentifier);
285
286
        $parts = explode('_', $initialPath);
287
        $totalPath = '';
288
        $currentPath = '';
289
290
        foreach ($parts as $part) {
291
            $currentPath .= empty($currentPath) ? $part : '_'.$part;
292
            $separator = empty($totalPath) ? '' : '.';
293
294
            if ($propertyAccessor->isReadable($model, $totalPath.$separator.$currentPath)) {
295
                $totalPath .= $separator.$currentPath;
296
                $currentPath = '';
297
            }
298
        }
299
300
        if (!empty($currentPath)) {
301
            throw new \Exception(sprintf(
302
                'Could not get element id from %s Failing part: %s',
303
                $elementId,
304
                $currentPath
305
            ));
306
        }
307
308
        return $totalPath;
309
    }
310
311
    /**
312
     * Recursively find the class name of the admin responsible for the element at the end of an association chain.
313
     */
314
    protected function getModelClassName(AdminInterface $admin, array $elements): string
315
    {
316
        return $this->getEntityClassName($admin, $elements);
0 ignored issues
show
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...r::getEntityClassName() has been deprecated with message: since sonata-project/admin-bundle 3.69. Use `getModelClassName()` instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
317
    }
318
319
    /**
320
     * NEXT_MAJOR: Remove this method and move its body to `getModelClassName()`.
321
     *
322
     * @deprecated since sonata-project/admin-bundle 3.69. Use `getModelClassName()` instead.
323
     *
324
     * @param array $elements
325
     *
326
     * @return string
327
     */
328
    protected function getEntityClassName(AdminInterface $admin, $elements)
329
    {
330
        if (self::class !== static::class) {
331
            @trigger_error(sprintf(
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...
332
                'Method %s() is deprecated since sonata-project/admin-bundle 3.69 and will be removed in version 4.0.'
333
                .' Use %s::getModelClassName() instead.',
334
                __METHOD__,
335
                __CLASS__
336
            ), E_USER_DEPRECATED);
337
        }
338
339
        $element = array_shift($elements);
340
        $associationAdmin = $admin->getFormFieldDescription($element)->getAssociationAdmin();
341
        if (0 === \count($elements)) {
342
            return $associationAdmin->getClass();
343
        }
344
345
        return $this->getEntityClassName($associationAdmin, $elements);
0 ignored issues
show
Bug introduced by
It seems like $associationAdmin defined by $admin->getFormFieldDesc...->getAssociationAdmin() on line 340 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...
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...r::getEntityClassName() has been deprecated with message: since sonata-project/admin-bundle 3.69. Use `getModelClassName()` instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
346
    }
347
}
348