Completed
Pull Request — master (#6209)
by Jordi Sala
03:49 queued 01:04
created

AdminHelper::addNewInstance()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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