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
|
|
|
* Note: |
75
|
|
|
* This code is ugly, but there is no better way of doing it. |
76
|
|
|
* |
77
|
|
|
* @throws \RuntimeException |
78
|
|
|
* @throws \Exception |
79
|
|
|
*/ |
80
|
|
|
public function appendFormFieldElement(AdminInterface $admin, object $subject, string $elementId): array |
81
|
|
|
{ |
82
|
|
|
// child rows marked as toDelete |
83
|
|
|
$toDelete = []; |
84
|
|
|
|
85
|
|
|
$formBuilder = $admin->getFormBuilder(); |
86
|
|
|
|
87
|
|
|
// get the field element |
88
|
|
|
$childFormBuilder = $this->getChildFormBuilder($formBuilder, $elementId); |
89
|
|
|
|
90
|
|
|
if ($childFormBuilder) { |
91
|
|
|
$formData = $admin->getRequest()->get($formBuilder->getName(), []); |
92
|
|
|
if (\array_key_exists($childFormBuilder->getName(), $formData)) { |
93
|
|
|
$formData = $admin->getRequest()->get($formBuilder->getName(), []); |
94
|
|
|
$i = 0; |
95
|
|
|
foreach ($formData[$childFormBuilder->getName()] as $name => &$field) { |
96
|
|
|
$toDelete[$i] = false; |
97
|
|
|
if (\array_key_exists(self::FORM_FIELD_DELETE, $field)) { |
98
|
|
|
$toDelete[$i] = true; |
99
|
|
|
unset($field[self::FORM_FIELD_DELETE]); |
100
|
|
|
} |
101
|
|
|
++$i; |
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
$admin->getRequest()->request->set($formBuilder->getName(), $formData); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
$form = $formBuilder->getForm(); |
108
|
|
|
$form->setData($subject); |
109
|
|
|
$form->handleRequest($admin->getRequest()); |
110
|
|
|
|
111
|
|
|
//Child form not found (probably nested one) |
112
|
|
|
//if childFormBuilder was not found resulted in fatal error getName() method call on non object |
113
|
|
|
if (!$childFormBuilder) { |
114
|
|
|
$propertyAccessor = $this->pool->getPropertyAccessor(); |
115
|
|
|
|
116
|
|
|
$path = $this->getElementAccessPath($elementId, $subject); |
117
|
|
|
|
118
|
|
|
$collection = $propertyAccessor->getValue($subject, $path); |
119
|
|
|
|
120
|
|
|
if ($collection instanceof DoctrinePersistentCollection || $collection instanceof PersistentCollection) { |
|
|
|
|
121
|
|
|
//since doctrine 2.4 |
122
|
|
|
$modelClassName = $collection->getTypeClass()->getName(); |
123
|
|
|
} elseif ($collection instanceof Collection) { |
124
|
|
|
$modelClassName = $this->getModelClassName($admin, explode('.', preg_replace('#\[\d*?\]#', '', $path))); |
125
|
|
|
} else { |
126
|
|
|
throw new \Exception('unknown collection class'); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
$collection->add(new $modelClassName()); |
130
|
|
|
$propertyAccessor->setValue($subject, $path, $collection); |
131
|
|
|
|
132
|
|
|
$fieldDescription = null; |
133
|
|
|
} else { |
134
|
|
|
// retrieve the FieldDescription |
135
|
|
|
$fieldDescription = $admin->getFormFieldDescription($childFormBuilder->getName()); |
136
|
|
|
|
137
|
|
|
try { |
138
|
|
|
$value = $fieldDescription->getValue($form->getData()); |
139
|
|
|
} catch (NoValueException $e) { |
140
|
|
|
$value = null; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
// retrieve the posted data |
144
|
|
|
$data = $admin->getRequest()->get($formBuilder->getName()); |
145
|
|
|
|
146
|
|
|
if (!isset($data[$childFormBuilder->getName()])) { |
147
|
|
|
$data[$childFormBuilder->getName()] = []; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
$objectCount = null === $value ? 0 : \count($value); |
151
|
|
|
$postCount = \count($data[$childFormBuilder->getName()]); |
152
|
|
|
|
153
|
|
|
$associationAdmin = $fieldDescription->getAssociationAdmin(); |
154
|
|
|
|
155
|
|
|
// add new elements to the subject |
156
|
|
|
while ($objectCount < $postCount) { |
157
|
|
|
// append a new instance into the object |
158
|
|
|
ObjectManipulator::addInstance($form->getData(), $associationAdmin->getNewInstance(), $fieldDescription); |
159
|
|
|
++$objectCount; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
$newInstance = ObjectManipulator::addInstance($form->getData(), $associationAdmin->getNewInstance(), $fieldDescription); |
163
|
|
|
|
164
|
|
|
$associationAdmin->setSubject($newInstance); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
$finalForm = $admin->getFormBuilder()->getForm(); |
168
|
|
|
$finalForm->setData($subject); |
169
|
|
|
|
170
|
|
|
// bind the data |
171
|
|
|
$finalForm->setData($form->getData()); |
172
|
|
|
|
173
|
|
|
// back up delete field |
174
|
|
|
if (\count($toDelete) > 0) { |
175
|
|
|
$i = 0; |
176
|
|
|
foreach ($finalForm->get($childFormBuilder->getName()) as $childField) { |
177
|
|
|
if ($childField->has(self::FORM_FIELD_DELETE)) { |
178
|
|
|
$childField->get(self::FORM_FIELD_DELETE)->setData($toDelete[$i] ?? false); |
179
|
|
|
} |
180
|
|
|
++$i; |
181
|
|
|
} |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
return [$fieldDescription, $finalForm]; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Get access path to element which works with PropertyAccessor. |
189
|
|
|
* |
190
|
|
|
* @param string $elementId expects string in format used in form id field. |
191
|
|
|
* (uniqueIdentifier_model_sub_model or uniqueIdentifier_model_1_sub_model etc.) |
192
|
|
|
* @param mixed $model |
193
|
|
|
* |
194
|
|
|
* @throws \Exception |
195
|
|
|
*/ |
196
|
|
|
public function getElementAccessPath(string $elementId, $model): string |
197
|
|
|
{ |
198
|
|
|
$propertyAccessor = $this->pool->getPropertyAccessor(); |
199
|
|
|
|
200
|
|
|
$idWithoutIdentifier = preg_replace('/^[^_]*_/', '', $elementId); |
201
|
|
|
$initialPath = preg_replace('#(_(\d+)_)#', '[$2]_', $idWithoutIdentifier); |
202
|
|
|
|
203
|
|
|
$parts = explode('_', $initialPath); |
204
|
|
|
$totalPath = ''; |
205
|
|
|
$currentPath = ''; |
206
|
|
|
|
207
|
|
|
foreach ($parts as $part) { |
208
|
|
|
$currentPath .= empty($currentPath) ? $part : '_'.$part; |
209
|
|
|
$separator = empty($totalPath) ? '' : '.'; |
210
|
|
|
|
211
|
|
|
if ($propertyAccessor->isReadable($model, $totalPath.$separator.$currentPath)) { |
212
|
|
|
$totalPath .= $separator.$currentPath; |
213
|
|
|
$currentPath = ''; |
214
|
|
|
} |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
if (!empty($currentPath)) { |
218
|
|
|
throw new \Exception(sprintf( |
219
|
|
|
'Could not get element id from %s Failing part: %s', |
220
|
|
|
$elementId, |
221
|
|
|
$currentPath |
222
|
|
|
)); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
return $totalPath; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* Recursively find the class name of the admin responsible for the element at the end of an association chain. |
230
|
|
|
*/ |
231
|
|
|
protected function getModelClassName(AdminInterface $admin, array $elements): string |
232
|
|
|
{ |
233
|
|
|
$element = array_shift($elements); |
234
|
|
|
$associationAdmin = $admin->getFormFieldDescription($element)->getAssociationAdmin(); |
235
|
|
|
if (0 === \count($elements)) { |
236
|
|
|
return $associationAdmin->getClass(); |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
return $this->getModelClassName($associationAdmin, $elements); |
240
|
|
|
} |
241
|
|
|
} |
242
|
|
|
|
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 thecomposer.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
orrequire-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 you have not tested against this specific condition, such errors might go unnoticed.