Completed
Push — master ( 7e886b...273a72 )
by Grégoire
16s
created

AdminHelper::getChildFormBuilder()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 3
nc 3
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\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): void
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
     * Get access path to element which works with PropertyAccessor.
244
     *
245
     * @param string $elementId expects string in format used in form id field.
246
     *                          (uniqueIdentifier_model_sub_model or uniqueIdentifier_model_1_sub_model etc.)
247
     * @param mixed  $entity
248
     *
249
     * @throws \Exception
250
     *
251
     * @return string
252
     */
253
    public function getElementAccessPath($elementId, $entity)
254
    {
255
        $propertyAccessor = $this->pool->getPropertyAccessor();
256
257
        $idWithoutIdentifier = preg_replace('/^[^_]*_/', '', $elementId);
258
        $initialPath = preg_replace('#(_(\d+)_)#', '[$2]_', $idWithoutIdentifier);
259
260
        $parts = explode('_', $initialPath);
261
        $totalPath = '';
262
        $currentPath = '';
263
264
        foreach ($parts as $part) {
265
            $currentPath .= empty($currentPath) ? $part : '_'.$part;
266
            $separator = empty($totalPath) ? '' : '.';
267
268
            if ($propertyAccessor->isReadable($entity, $totalPath.$separator.$currentPath)) {
269
                $totalPath .= $separator.$currentPath;
270
                $currentPath = '';
271
            }
272
        }
273
274
        if (!empty($currentPath)) {
275
            throw new \Exception(
276
                sprintf('Could not get element id from %s Failing part: %s', $elementId, $currentPath)
277
            );
278
        }
279
280
        return $totalPath;
281
    }
282
283
    /**
284
     * Recursively find the class name of the admin responsible for the element at the end of an association chain.
285
     *
286
     * @param array $elements
287
     *
288
     * @return string
289
     */
290
    protected function getEntityClassName(AdminInterface $admin, $elements)
291
    {
292
        $element = array_shift($elements);
293
        $associationAdmin = $admin->getFormFieldDescription($element)->getAssociationAdmin();
294
        if (0 === \count($elements)) {
295
            return $associationAdmin->getClass();
296
        }
297
298
        return $this->getEntityClassName($associationAdmin, $elements);
0 ignored issues
show
Bug introduced by
It seems like $associationAdmin defined by $admin->getFormFieldDesc...->getAssociationAdmin() on line 293 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...
299
    }
300
}
301