2525 */
2626class MergeCollectionListener implements EventSubscriberInterface
2727{
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-
4328 /**
4429 * Whether elements may be added to the collection
4530 * @var Boolean
@@ -52,131 +37,33 @@ class MergeCollectionListener implements EventSubscriberInterface
5237 */
5338 private $ allowDelete ;
5439
55- /**
56- * Whether to search for and use adder and remover methods
57- * @var Boolean
58- */
59- private $ mergeStrategy ;
60-
61- /**
62- * The name of the adder method to look for
63- * @var string
64- */
65- private $ addMethod ;
66-
67- /**
68- * The name of the remover method to look for
69- * @var string
70- */
71- private $ removeMethod ;
72-
73- /**
74- * A copy of the data before starting binding for this form
75- * @var mixed
76- */
77- private $ dataSnapshot ;
78-
7940 /**
8041 * Creates a new listener.
8142 *
8243 * @param Boolean $allowAdd Whether values might be added to the
8344 * collection.
8445 * @param Boolean $allowDelete Whether values might be removed from the
8546 * 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.
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.
10247 */
103- public function __construct ($ allowAdd = false , $ allowDelete = false , $ mergeStrategy = null , $ addMethod = null , $ removeMethod = null )
48+ public function __construct ($ allowAdd = false , $ allowDelete = false )
10449 {
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-
10950 $ this ->allowAdd = $ allowAdd ;
11051 $ this ->allowDelete = $ allowDelete ;
111- $ this ->mergeStrategy = $ mergeStrategy ?: self ::MERGE_NORMAL | self ::MERGE_INTO_PARENT ;
112- $ this ->addMethod = $ addMethod ;
113- $ this ->removeMethod = $ removeMethod ;
11452 }
11553
11654 static public function getSubscribedEvents ()
11755 {
11856 return array (
119- FormEvents::PRE_BIND => 'preBind ' ,
12057 FormEvents::BIND_NORM_DATA => 'onBindNormData ' ,
12158 );
12259 }
12360
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- }
138- }
139-
14061 public function onBindNormData (FilterDataEvent $ event )
14162 {
142- $ originalData = $ event ->getForm ()->getNormData ();
143-
144- // If we are not allowed to change anything, return immediately
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
148- $ event ->setData ($ originalData );
149-
150- return ;
151- }
63+ $ dataToMergeInto = $ event ->getForm ()->getNormData ();
15264
15365 $ form = $ event ->getForm ();
15466 $ data = $ event ->getData ();
155- $ childPropertyPath = null ;
156- $ parentData = null ;
157- $ addMethod = null ;
158- $ removeMethod = null ;
159- $ propertyPath = null ;
160- $ plural = null ;
161-
162- if ($ form ->hasParent () && $ form ->getAttribute ('property_path ' )) {
163- $ propertyPath = new PropertyPath ($ form ->getAttribute ('property_path ' ));
164- $ childPropertyPath = $ propertyPath ;
165- $ parentData = $ form ->getParent ()->getClientData ();
166- $ lastElement = $ propertyPath ->getElement ($ propertyPath ->getLength () - 1 );
167-
168- // If the property path contains more than one element, the parent
169- // data is the object at the parent property path
170- if ($ propertyPath ->getLength () > 1 ) {
171- $ parentData = $ propertyPath ->getParent ()->getValue ($ parentData );
172-
173- // Property path relative to $parentData
174- $ childPropertyPath = new PropertyPath ($ lastElement );
175- }
176-
177- // The plural form is the last element of the property path
178- $ plural = ucfirst ($ lastElement );
179- }
18067
18168 if (null === $ data ) {
18269 $ data = array ();
@@ -186,157 +73,60 @@ public function onBindNormData(FilterDataEvent $event)
18673 throw new UnexpectedTypeException ($ data , 'array or (\Traversable and \ArrayAccess) ' );
18774 }
18875
189- if (null !== $ originalData && !is_array ($ originalData ) && !($ originalData instanceof \Traversable && $ originalData instanceof \ArrayAccess)) {
190- throw new UnexpectedTypeException ($ originalData , 'array or (\Traversable and \ArrayAccess) ' );
76+ if (null !== $ dataToMergeInto && !is_array ($ dataToMergeInto ) && !($ dataToMergeInto instanceof \Traversable && $ dataToMergeInto instanceof \ArrayAccess)) {
77+ throw new UnexpectedTypeException ($ dataToMergeInto , 'array or (\Traversable and \ArrayAccess) ' );
19178 }
19279
193- // Check if the parent has matching methods to add/remove items
194- if (($ this ->mergeStrategy & self ::MERGE_INTO_PARENT ) && is_object ($ parentData )) {
195- $ reflClass = new \ReflectionClass ($ parentData );
196- $ addMethodNeeded = $ this ->allowAdd && !$ this ->addMethod ;
197- $ removeMethodNeeded = $ this ->allowDelete && !$ this ->removeMethod ;
198-
199- // Any of the two methods is required, but not yet known
200- if ($ addMethodNeeded || $ removeMethodNeeded ) {
201- $ singulars = (array ) FormUtil::singularify ($ plural );
202-
203- foreach ($ singulars as $ singular ) {
204- // Try to find adder, but don't override preconfigured one
205- if ($ addMethodNeeded ) {
206- $ addMethod = 'add ' . $ singular ;
207-
208- // False alert
209- if (!$ this ->isAccessible ($ reflClass , $ addMethod , 1 )) {
210- $ addMethod = null ;
211- }
212- }
213-
214- // Try to find remover, but don't override preconfigured one
215- if ($ removeMethodNeeded ) {
216- $ removeMethod = 'remove ' . $ singular ;
217-
218- // False alert
219- if (!$ this ->isAccessible ($ reflClass , $ removeMethod , 1 )) {
220- $ removeMethod = null ;
221- }
222- }
223-
224- // Found all that we need. Abort search.
225- if ((!$ addMethodNeeded || $ addMethod ) && (!$ removeMethodNeeded || $ removeMethod )) {
226- break ;
227- }
228-
229- // False alert
230- $ addMethod = null ;
231- $ removeMethod = null ;
232- }
233- }
234-
235- // Set preconfigured adder
236- if ($ this ->allowAdd && $ this ->addMethod ) {
237- $ addMethod = $ this ->addMethod ;
238-
239- if (!$ this ->isAccessible ($ reflClass , $ addMethod , 1 )) {
240- throw new FormException (sprintf (
241- 'The public method "%s" could not be found on class %s ' ,
242- $ addMethod ,
243- $ reflClass ->getName ()
244- ));
245- }
246- }
247-
248- // Set preconfigured remover
249- if ($ this ->allowDelete && $ this ->removeMethod ) {
250- $ removeMethod = $ this ->removeMethod ;
80+ // If we are not allowed to change anything, return immediately
81+ if ((!$ this ->allowAdd && !$ this ->allowDelete ) || $ data === $ dataToMergeInto ) {
82+ $ event ->setData ($ dataToMergeInto );
25183
252- if (!$ this ->isAccessible ($ reflClass , $ removeMethod , 1 )) {
253- throw new FormException (sprintf (
254- 'The public method "%s" could not be found on class %s ' ,
255- $ removeMethod ,
256- $ reflClass ->getName ()
257- ));
258- }
259- }
84+ return ;
26085 }
26186
262- // Calculate delta between $data and the snapshot created in PRE_BIND
263- $ itemsToDelete = array ();
264- $ itemsToAdd = is_object ($ data ) ? clone $ data : $ data ;
265-
266- if ($ this ->dataSnapshot ) {
267- foreach ($ this ->dataSnapshot as $ originalItem ) {
268- foreach ($ data as $ key => $ item ) {
269- if ($ item === $ originalItem ) {
87+ if (!$ dataToMergeInto ) {
88+ // No original data was set. Set it if allowed
89+ if ($ this ->allowAdd ) {
90+ $ dataToMergeInto = $ data ;
91+ }
92+ } else {
93+ // Calculate delta
94+ $ itemsToAdd = is_object ($ data ) ? clone $ data : $ data ;
95+ $ itemsToDelete = array ();
96+
97+ foreach ($ dataToMergeInto as $ beforeKey => $ beforeItem ) {
98+ foreach ($ data as $ afterKey => $ afterItem ) {
99+ if ($ afterItem === $ beforeItem ) {
270100 // Item found, next original item
271- unset($ itemsToAdd [$ key ]);
101+ unset($ itemsToAdd [$ afterKey ]);
272102 continue 2 ;
273103 }
274104 }
275105
276106 // Item not found, remember for deletion
277- foreach ($ originalData as $ key => $ item ) {
278- if ($ item === $ originalItem ) {
279- $ itemsToDelete [$ key ] = $ item ;
280- continue 2 ;
281- }
282- }
283- }
284- }
285-
286- if ($ addMethod || $ removeMethod ) {
287- // If methods to add and to remove exist, call them now, if allowed
288- if ($ removeMethod ) {
289- foreach ($ itemsToDelete as $ item ) {
290- $ parentData ->$ removeMethod ($ item );
291- }
107+ $ itemsToDelete [] = $ beforeKey ;
292108 }
293109
294- if ($ addMethod ) {
295- foreach ($ itemsToAdd as $ item ) {
296- $ parentData ->$ addMethod ($ item );
110+ // Remove deleted items before adding to free keys that are to be
111+ // replaced
112+ if ($ this ->allowDelete ) {
113+ foreach ($ itemsToDelete as $ key ) {
114+ unset($ dataToMergeInto [$ key ]);
297115 }
298116 }
299117
300- $ event ->setData ($ childPropertyPath ->getValue ($ parentData ));
301- } elseif ($ this ->mergeStrategy & self ::MERGE_NORMAL ) {
302- if (!$ originalData ) {
303- // No original data was set. Set it if allowed
304- if ($ this ->allowAdd ) {
305- $ originalData = $ data ;
306- }
307- } else {
308- // Original data is an array-like structure
309- // Add and remove items in the original variable
310- if ($ this ->allowDelete ) {
311- foreach ($ itemsToDelete as $ key => $ item ) {
312- unset($ originalData [$ key ]);
118+ // Add remaining items
119+ if ($ this ->allowAdd ) {
120+ foreach ($ itemsToAdd as $ key => $ item ) {
121+ if (!isset ($ dataToMergeInto [$ key ])) {
122+ $ dataToMergeInto [$ key ] = $ item ;
123+ } else {
124+ $ dataToMergeInto [] = $ item ;
313125 }
314126 }
315-
316- if ($ this ->allowAdd ) {
317- foreach ($ itemsToAdd as $ key => $ item ) {
318- if (!isset ($ originalData [$ key ])) {
319- $ originalData [$ key ] = $ item ;
320- } else {
321- $ originalData [] = $ item ;
322- }
323- }
324- }
325- }
326-
327- $ event ->setData ($ originalData );
328- }
329- }
330-
331- private function isAccessible (\ReflectionClass $ reflClass , $ methodName , $ numberOfRequiredParameters ) {
332- if ($ reflClass ->hasMethod ($ methodName )) {
333- $ method = $ reflClass ->getMethod ($ methodName );
334-
335- if ($ method ->isPublic () && $ method ->getNumberOfRequiredParameters () === $ numberOfRequiredParameters ) {
336- return true ;
337127 }
338128 }
339129
340- return false ;
130+ $ event -> setData ( $ dataToMergeInto ) ;
341131 }
342132}
0 commit comments