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

AdminHelper::getModelClassName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
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\Common\Util\ClassUtils;
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\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
                $modelClassName = $collection->getTypeClass()->getName();
152
            } elseif ($collection instanceof Collection) {
153
                $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...
154
            } else {
155
                throw new \Exception('unknown collection class');
156
            }
157
158
            $collection->add(new $modelClassName());
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
            $associationAdmin = $fieldDescription->getAssociationAdmin();
183
184
            // add new elements to the subject
185
            while ($objectCount < $postCount) {
186
                // append a new instance into the object
187
                self::addInstance($form->getData(), $fieldDescription, $associationAdmin->getNewInstance());
188
                ++$objectCount;
189
            }
190
191
            $newInstance = self::addInstance($form->getData(), $fieldDescription, $associationAdmin->getNewInstance());
192
193
            $associationAdmin->setSubject($newInstance);
194
        }
195
196
        $finalForm = $admin->getFormBuilder()->getForm();
197
        $finalForm->setData($subject);
198
199
        // bind the data
200
        $finalForm->setData($form->getData());
201
202
        // back up delete field
203
        if (\count($toDelete) > 0) {
204
            $i = 0;
205
            foreach ($finalForm->get($childFormBuilder->getName()) as $childField) {
206
                if ($childField->has(self::FORM_FIELD_DELETE)) {
207
                    $childField->get(self::FORM_FIELD_DELETE)->setData($toDelete[$i] ?? false);
208
                }
209
                ++$i;
210
            }
211
        }
212
213
        return [$fieldDescription, $finalForm];
214
    }
215
216
    /**
217
     * NEXT_MAJOR: remove this method.
218
     *
219
     * @deprecated since sonata-project/admin-bundle 3.x, use to be removed with 4.0.
220
     *
221
     * Add a new instance to the related FieldDescriptionInterface value.
222
     *
223
     * @param object $object
224
     *
225
     * @throws \RuntimeException
226
     *
227
     * @return object
228
     */
229
    public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
230
    {
231
        @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...
232
            'Method %s() is deprecated since sonata-project/admin-bundle 3.x. It will be removed in version 4.0.'
233
            .' Use %s::addInstance() instead.',
234
            __METHOD__,
235
            __CLASS__
236
        ), E_USER_DEPRECATED);
237
238
        $instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
239
240
        return self::addInstance($object, $fieldDescription, $instance);
241
    }
242
243
    /**
244
     * @phpstan-template T of object
245
     * @phpstan-param T $instance
246
     * @phpstan-return T
247
     */
248
    public static function addInstance(
249
        object $object,
250
        FieldDescriptionInterface $fieldDescription,
251
        object $instance,
252
        bool $setOnly = false
253
    ): object {
254
        $mapping = $fieldDescription->getAssociationMapping();
255
        $parentMappings = $fieldDescription->getParentAssociationMappings();
256
257
        $inflector = InflectorFactory::create()->build();
258
259
        foreach ($parentMappings as $parentMapping) {
260
            $method = sprintf('get%s', $inflector->classify($parentMapping['fieldName']));
261
262
            if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
263
                /*
264
                 * NEXT_MAJOR: Use BadMethodCallException instead
265
                 */
266
                throw new \RuntimeException(
267
                    sprintf('Method %s::%s() does not exist.', ClassUtils::getClass($object), $method)
268
                );
269
            }
270
271
            $object = $object->$method();
272
        }
273
274
        if ($setOnly) {
275
            $method = sprintf('set%s', $inflector->classify($mapping['mappedBy']));
276
277
            if (!(\is_callable([$instance, $method]) && method_exists($instance, $method))) {
278
                /*
279
                 * NEXT_MAJOR: Use BadMethodCallException instead
280
                 */
281
                throw new \RuntimeException(
282
                    sprintf('Method %s::%s() does not exist.', ClassUtils::getClass($instance), $method)
283
                );
284
            }
285
286
            $instance->$method($object);
287
        } else {
288
            $method = sprintf('add%s', $inflector->classify($mapping['fieldName']));
289
290
            if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
291
                $method = rtrim($method, 's');
292
293
                if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
294
                    $method = sprintf('add%s', $inflector->classify($inflector->singularize($mapping['fieldName'])));
295
296
                    if (!(\is_callable([$object, $method]) && method_exists($object, $method))) {
297
                        /*
298
                         * NEXT_MAJOR: Use BadMethodCallException instead
299
                         */
300
                        throw new \RuntimeException(
301
                            sprintf('Method %s::%s() does not exist.', ClassUtils::getClass($object), $method)
302
                        );
303
                    }
304
                }
305
            }
306
307
            $object->$method($instance);
308
        }
309
310
        return $instance;
311
    }
312
313
    /**
314
     * Camelize a string.
315
     *
316
     * NEXT_MAJOR: remove this method.
317
     *
318
     * @static
319
     *
320
     * @param string $property
321
     *
322
     * @return string
323
     *
324
     * @deprecated since sonata-project/admin-bundle 3.1. Use \Doctrine\Inflector\Inflector::classify() instead
325
     */
326
    public function camelize($property)
327
    {
328
        @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...
329
            sprintf(
330
                'The %s method is deprecated since 3.1 and will be removed in 4.0. '.
331
                'Use \Doctrine\Inflector\Inflector::classify() instead.',
332
                __METHOD__
333
            ),
334
            E_USER_DEPRECATED
335
        );
336
337
        return InflectorFactory::create()->build()->classify($property);
338
    }
339
340
    /**
341
     * Get access path to element which works with PropertyAccessor.
342
     *
343
     * @param string $elementId expects string in format used in form id field.
344
     *                          (uniqueIdentifier_model_sub_model or uniqueIdentifier_model_1_sub_model etc.)
345
     * @param mixed  $model
346
     *
347
     * @throws \Exception
348
     *
349
     * @return string
350
     */
351
    public function getElementAccessPath($elementId, $model)
352
    {
353
        $propertyAccessor = $this->pool->getPropertyAccessor();
354
355
        $idWithoutIdentifier = preg_replace('/^[^_]*_/', '', $elementId);
356
        $initialPath = preg_replace('#(_(\d+)_)#', '[$2]_', $idWithoutIdentifier);
357
358
        $parts = explode('_', $initialPath);
359
        $totalPath = '';
360
        $currentPath = '';
361
362
        foreach ($parts as $part) {
363
            $currentPath .= empty($currentPath) ? $part : '_'.$part;
364
            $separator = empty($totalPath) ? '' : '.';
365
366
            if ($propertyAccessor->isReadable($model, $totalPath.$separator.$currentPath)) {
367
                $totalPath .= $separator.$currentPath;
368
                $currentPath = '';
369
            }
370
        }
371
372
        if (!empty($currentPath)) {
373
            throw new \Exception(
374
                sprintf('Could not get element id from %s Failing part: %s', $elementId, $currentPath)
375
            );
376
        }
377
378
        return $totalPath;
379
    }
380
381
    /**
382
     * Recursively find the class name of the admin responsible for the element at the end of an association chain.
383
     */
384
    protected function getModelClassName(AdminInterface $admin, array $elements): string
385
    {
386
        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...
387
    }
388
389
    /**
390
     * NEXT_MAJOR: Remove this method and move its body to `getModelClassName()`.
391
     *
392
     * @deprecated since sonata-project/admin-bundle 3.69. Use `getModelClassName()` instead.
393
     *
394
     * @param array $elements
395
     *
396
     * @return string
397
     */
398
    protected function getEntityClassName(AdminInterface $admin, $elements)
399
    {
400
        if (self::class !== static::class) {
401
            @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...
402
                'Method %s() is deprecated since sonata-project/admin-bundle 3.69 and will be removed in version 4.0.'
403
                .' Use %s::getModelClassName() instead.',
404
                __METHOD__,
405
                __CLASS__
406
            ), E_USER_DEPRECATED);
407
        }
408
409
        $element = array_shift($elements);
410
        $associationAdmin = $admin->getFormFieldDescription($element)->getAssociationAdmin();
411
        if (0 === \count($elements)) {
412
            return $associationAdmin->getClass();
413
        }
414
415
        return $this->getEntityClassName($associationAdmin, $elements);
0 ignored issues
show
Bug introduced by
It seems like $associationAdmin defined by $admin->getFormFieldDesc...->getAssociationAdmin() on line 410 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...
416
    }
417
}
418