13
13
14
14
use Symfony \Component \EventDispatcher \EventSubscriberInterface ;
15
15
use Symfony \Component \Form \FormEvents ;
16
+ use Symfony \Component \Form \Event \DataEvent ;
16
17
use Symfony \Component \Form \Event \FilterDataEvent ;
17
18
use Symfony \Component \Form \Exception \UnexpectedTypeException ;
18
19
use Symfony \Component \Form \Exception \FormException ;
19
20
use Symfony \Component \Form \Util \FormUtil ;
21
+ use Symfony \Component \Form \Util \PropertyPath ;
20
22
21
23
/**
22
24
* @author Bernhard Schussek <bschussek@gmail.com>
23
25
*/
24
26
class MergeCollectionListener implements EventSubscriberInterface
25
27
{
28
+ /**
29
+ * Strategy for merging the new collection into the old collection
30
+ *
31
+ * @var integer
32
+ */
33
+ const MERGE_NORMAL = 1 ;
34
+
35
+ /**
36
+ * Strategy for calling add/remove methods on the parent data for all
37
+ * new/removed elements in the new collection
38
+ *
39
+ * @var integer
40
+ */
41
+ const MERGE_INTO_PARENT = 2 ;
42
+
26
43
/**
27
44
* Whether elements may be added to the collection
28
45
* @var Boolean
@@ -39,7 +56,7 @@ class MergeCollectionListener implements EventSubscriberInterface
39
56
* Whether to search for and use adder and remover methods
40
57
* @var Boolean
41
58
*/
42
- private $ useAccessors ;
59
+ private $ mergeStrategy ;
43
60
44
61
/**
45
62
* The name of the adder method to look for
@@ -53,35 +70,112 @@ class MergeCollectionListener implements EventSubscriberInterface
53
70
*/
54
71
private $ removeMethod ;
55
72
56
- public function __construct ($ allowAdd = false , $ allowDelete = false , $ useAccessors = true , $ addMethod = null , $ removeMethod = null )
73
+ /**
74
+ * A copy of the data before starting binding for this form
75
+ * @var mixed
76
+ */
77
+ private $ dataSnapshot ;
78
+
79
+ /**
80
+ * Creates a new listener.
81
+ *
82
+ * @param Boolean $allowAdd Whether values might be added to the
83
+ * collection.
84
+ * @param Boolean $allowDelete Whether values might be removed from the
85
+ * collection.
86
+ * @param integer $mergeStrategy Which strategy to use for merging the
87
+ * bound collection with the original
88
+ * collection. Might be any combination of
89
+ * MERGE_NORMAL and MERGE_INTO_PARENT.
<
F438
code>90 + * MERGE_INTO_PARENT has precedence over
91
+ * MERGE_NORMAL if an adder/remover method
92
+ * is found. The default strategy is to use
93
+ * both strategies.
94
+ * @param string $addMethod The name of the adder method to use. If
95
+ * not given, the listener tries to discover
96
+ * the method automatically.
97
+ * @param string $removeMethod The name of the remover method to use. If
98
+ * not given, the listener tries to discover
99
+ * the method automatically.
100
+ *
101
+ * @throws FormException If the given strategy is invalid.
102
+ */
103
+ public function __construct ($ allowAdd = false , $ allowDelete = false , $ mergeStrategy = null , $ addMethod = null , $ removeMethod = null )
57
104
{
105
+ if ($ mergeStrategy && !($ mergeStrategy & (self ::MERGE_NORMAL | self ::MERGE_INTO_PARENT ))) {
106
+ throw new FormException ('The merge strategy needs to be at least MERGE_NORMAL or MERGE_INTO_PARENT ' );
107
+ }
108
+
58
109
$ this ->allowAdd = $ allowAdd ;
59
110
$ this ->allowDelete = $ allowDelete ;
60
- $ this ->useAccessors = $ useAccessors ;
111
+ $ this ->mergeStrategy = $ mergeStrategy ?: self :: MERGE_NORMAL | self :: MERGE_INTO_PARENT ;
61
112
$ this ->addMethod = $ addMethod ;
62
113
$ this ->removeMethod = $ removeMethod ;
63
114
}
64
115
65
116
static public function getSubscribedEvents ()
66
117
{
67
- return array (FormEvents::BIND_NORM_DATA => 'onBindNormData ' );
118
+ return array (
119
+ FormEvents::PRE_BIND => 'preBind ' ,
120
+ FormEvents::BIND_NORM_DATA => 'onBindNormData ' ,
121
+ );
122
+ }
123
+
124
+ public function preBind (DataEvent $ event )
125
+ {
126
+ // Get a snapshot of the current state of the normalized data
127
+ // to compare against later
128
+ $ this ->dataSnapshot = $ event ->getForm ()->getNormData ();
129
+
130
+ if (is_object ($ this ->dataSnapshot )) {
131
+ // Make sure the snapshot remains stable and doesn't change
132
+ $ this ->dataSnapshot = clone $ this ->dataSnapshot ;
133
+ }
134
+
135
+ if (null !== $ this ->dataSnapshot && !is_array ($ this ->dataSnapshot ) && !($ this ->dataSnapshot instanceof \Traversable && $ this ->dataSnapshot instanceof \ArrayAccess)) {
136
+ throw new UnexpectedTypeException ($ this ->dataSnapshot , 'array or (\Traversable and \ArrayAccess) ' );
137
+ }
68
138
}
69
139
70
140
public function onBindNormData (FilterDataEvent $ event )
71
141
{
72
- $ originalData = $ event ->getForm ()->getData ();
142
+ $ originalData = $ event ->getForm ()->getNormData ();
73
143
74
144
// If we are not allowed to change anything, return immediately
75
145
if (!$ this ->allowAdd && !$ this ->allowDelete ) {
146
+ // Don't set to the snapshot as then we are switching from the
147
+ // original object to its copy, which might break things
76
148
$ event ->setData ($ originalData );
77
149
return ;
78
150
}
79
151
80
152
$ form = $ event ->getForm ();
81
153
$ data = $ event ->getData ();
82
- $ parentData = $ form ->hasParent () ? $ form ->getParent ()->getData () : null ;
154
+ $ childPropertyPath = null ;
155
+ $ parentData = null ;
83
156
$ addMethod = null ;
84
157
$ removeMethod = null ;
158
+ $ propertyPath = null ;
159
+ $ plural = null ;
160
+
161
+ if ($ form ->hasParent () && $ form ->getAttribute ('property_path ' )) {
162
+ $ propertyPath = new PropertyPath ($ form ->getAttribute ('property_path ' ));
163
+ $ childPropertyPath = $ propertyPath ;
164
+ $ parentData = $ form ->getParent ()->getClientData ();
165
+ $ lastElement = $ propertyPath ->getElement ($ propertyPath ->getLength () - 1 );
166
+
167
+ // If the property path contains more than one element, the parent
168
+ // data is the object at the parent property path
169
+ if ($ propertyPath ->getLength () > 1 ) {
170
+ $ parentData = $ propertyPath ->getParent ()->getValue ($ parentData );
171
+
172
+ // Property path relative to $parentData
173
+ $ childPropertyPath = new PropertyPath ($ lastElement );
174
+ }
175
+
176
+ // The plural form is the last element of the property path
177
+ $ plural = ucfirst ($ lastElement );
178
+ }
85
179
86
180
if (null === $ data ) {
87
181
$ data = array ();
@@ -96,24 +190,34 @@ public function onBindNormData(FilterDataEvent $event)
96
190
}
97
191
98
192
// Check if the parent has matching methods to add/remove items
99
- if ($ this ->useAccessors && is_object ($ parentData )) {
193
+ if (( $ this ->mergeStrategy & self :: MERGE_INTO_PARENT ) && is_object ($ parentData )) {
100
194
$ reflClass = new \ReflectionClass ($ parentData );
101
195
$ addMethodNeeded = $ this ->allowAdd && !$ this ->addMethod ;
102
196
$ removeMethodNeeded = $ this ->allowDelete && !$ this ->removeMethod ;
103
197
104
198
// Any of the two methods is required, but not yet known
105
199
if ($ addMethodNeeded || $ removeMethodNeeded ) {
106
- $ singulars = (array ) FormUtil::singularify (ucfirst ( $ form -> getName ()) );
200
+ $ singulars = (array ) FormUtil::singularify ($ plural );
107
201
108
202
foreach ($ singulars as $ singular ) {
109
203
// Try to find adder, but don't override preconfigured one
110
204
if ($ addMethodNeeded ) {
111
- $ addMethod = $ this ->checkMethod ($ reflClass , 'add ' . $ singular );
205
+ $ addMethod = 'add ' . $ singular ;
206
+
207
+ // False alert
208
+ if (!$ this ->isAccessible ($ reflClass , $ addMethod , 1 )) {
209
+ $ addMethod = null ;
210
+ }
112
211
}
113
212
114
213
// Try to find remover, but don't override preconfigured one
115
214
if ($ removeMethodNeeded ) {
116
- $ removeMethod = $ this ->checkMethod ($ reflClass , 'remove ' . $ singular );
215
+ $ removeMethod = 'remove ' . $ singular ;
216
+
217
+ // False alert
218
+ if (!$ this ->isAccessible ($ reflClass , $ removeMethod , 1 )) {
219
+ $ removeMethod = null ;
220
+ }
117
221
}
118
222
119
223
// Found all that we need. Abort search.
@@ -129,38 +233,37 @@ public function onBindNormData(FilterDataEvent $event)
129
233
130
234
// Set preconfigured adder
131
235
if ($ this ->allowAdd && $ this ->addMethod ) {
132
- $ addMethod = $ this ->checkMethod ( $ reflClass , $ this -> addMethod ) ;
236
+ $ addMethod = $ this ->addMethod ;
133
237
134
- if (!$ addMethod ) {
238
+ if (!$ this -> isAccessible ( $ reflClass , $ addMethod, 1 ) ) {
135
239
throw new FormException (sprintf (
136
- 'The method "%s" could not be found on class %s ' ,
137
- $ this -> addMethod ,
240
+ 'The public method "%s" could not be found on class %s ' ,
241
+ $ addMethod ,
138
242
$ reflClass ->getName ()
139
243
));
140
244
}
141
245
}
142
246
143
247
// Set preconfigured remover
144
248
if ($ this ->allowDelete && $ this ->removeMethod ) {
145
- $ removeMethod = $ this ->checkMethod ( $ reflClass , $ this -> removeMethod ) ;
249
+ $ removeMethod = $ this ->removeMethod ;
146
250
147
- if (!$ removeMethod ) {
251
+ if (!$ this -> isAccessible ( $ reflClass , $ removeMethod, 1 ) ) {
148
252
throw new FormException (sprintf (
149
- 'The method "%s" could not be found on class %s ' ,
150
- $ this -> removeMethod ,
253
+ 'The public method "%s" could not be found on class %s ' ,
254
+ $ removeMethod ,
151
255
$ reflClass ->getName ()
152
256
));
153
257
}
154
258
}
155
259
}
156
260
157
- // Check which items are in $data that are not in $originalData and
158
- // vice versa
261
+ // Calculate delta between $data and the snapshot created in PRE_BIND
159
262
$ itemsToDelete = array ();
160
263
<
10000
span class=pl-s1>$ itemsToAdd = is_object ($ data ) ? clone $ data : $ data ;
161
264
162
- if ($ originalData ) {
163
- foreach ($ originalData as $ originalKey => $ originalItem ) {
265
+ if ($ this -> dataSnapshot ) {
266
+ foreach ($ this -> dataSnapshot as $ originalItem ) {
164
267
foreach ($ data as $ key => $ item ) {
165
268
if ($ item === $ originalItem ) {
166
269
// Item found, next original item
@@ -170,7 +273,12 @@ public function onBindNormData(FilterDataEvent $event)
170
273
}
171
274
172
275
// Item not found, remember for deletion
173
- $ itemsToDelete [$ originalKey ] = $ originalItem ;
276
+ foreach ($ originalData as $ key => $ item ) {
277
+ if ($ item === $ originalItem ) {
278
+ $ itemsToDelete [$ key ] = $ item ;
279
+ continue 2 ;
280
+ }
281
+ }
174
282
}
175
283
}
176
284
@@ -187,43 +295,47 @@ public function onBindNormData(FilterDataEvent $event)
187
295
$ parentData ->$ addMethod ($ item );
188
296
}
189
297
}
190
- } elseif (!$ originalData ) {
191
- // No original data was set. Set it if allowed
192
- if ($ this ->allowAdd ) {
193
- $ originalData = $ data ;
194
- }
195
- } else {
196
- // Original data is an array-like structure
197
- // Add and remove items in the original variable
198
- if ($ this ->allowDelete ) {
199
- foreach ($ itemsToDelete as $ key => $ item ) {
200
- unset($ originalData [$ key ]);
298
+
299
+ $ event ->setData ($ childPropertyPath ->getValue ($ parentData ));
300
+ } elseif ($ this ->mergeStrategy & self ::MERGE_NORMAL ) {
301
+ if (!$ originalData ) {
302
+ // No original data was set. Set it if allowed
303
+ if ($ this ->allowAdd ) {
304
+ $ originalData = $ data ;
305
+ }
306
+ } else {
307
+ // Original data is an array-like structure
308
+ // Add and remove items in the original variable
309
+ if ($ this ->allowDelete ) {
310
+ foreach ($ itemsToDelete as $ key => $ item ) {
311
+ unset($ originalData [$ key ]);
312
+ }
201
313
}
202
- }
203
314
204
- if ($ this ->allowAdd ) {
205
- foreach ($ itemsToAdd as $ key => $ item ) {
206
- if (!isset ($ originalData [$ key ])) {
207
- $ originalData [$ key ] = $ item ;
208
- } else {
209
- $ originalData [] = $ item ;
315
+ if ($ this ->allowAdd ) {
316
+ foreach ($ itemsToAdd as $ key => $ item ) {
317
+ if (!isset ($ originalData [$ key ])) {
318
+ $ originalData [$ key ] = $ item ;
319
+ } else {
320
+ $ originalData [] = $ item ;
321
+ }
210
322
}
211
323
}
212
324
}
213
- }
214
325
215
- $ event ->setData ($ originalData );
326
+ $ event ->setData ($ originalData );
327
+ }
216
328
}
217
329
218
- private function checkMethod (\ReflectionClass $ reflClass , $ methodName ) {
330
+ private function isAccessible (\ReflectionClass $ reflClass , $ methodName, $ numberOfRequiredParameters ) {
219
331
if ($ reflClass ->hasMethod ($ methodName )) {
220
332
$ method = $ reflClass ->getMethod ($ methodName );
221
333
222
- if ($ method ->isPublic () && $ method ->getNumberOfRequiredParameters () === 1 ) {
223
- return $ methodName ;
334
+ if ($ method ->isPublic () && $ method ->getNumberOfRequiredParameters () === $ numberOfRequiredParameters ) {
335
+ return true ;
224
336
}
225
337
}
226
338
227
- return null ;
339
+ return false ;
228
340
}
229
341
}
0 commit comments