@@ -30,9 +30,6 @@ abstract class Component implements IComponent
3030 */
3131 private array $ monitors = [];
3232
33- /** Prevents nested listener execution during refreshMonitors */
34- private bool $ callingListeners = false ;
35-
3633
3734 /**
3835 * Finds the closest ancestor of specified type.
@@ -201,33 +198,19 @@ protected function validateParent(IContainer $parent): void
201198 /**
202199 * Refreshes monitors when attaching/detaching from component tree.
203200 * @param ?array<string, true> $missing null = detaching, array = attaching
204- * @param array<int, array{\Closure, IComponent}> $listeners
201+ * @param array<array{\Closure, int}> $called deduplication tracking
202+ * @param array<int, true> $processed prevents reentry
205203 */
206- private function refreshMonitors (int $ depth , ?array &$ missing = null , array &$ listeners = []): void
204+ private function refreshMonitors (
205+ int $ depth ,
206+ ?array &$ missing = null ,
207+ array &$ called = [],
208+ array &$ processed = [],
209+ ): void
207210 {
208- if ($ this instanceof IContainer) {
209- foreach ($ this ->getComponents () as $ component ) {
210- if ($ component instanceof self) {
211- $ component ->refreshMonitors ($ depth + 1 , $ missing , $ listeners );
212- }
213- }
214- }
211+ $ processed [spl_object_id ($ this )] = true ;
215212
216- if ($ missing === null ) { // detaching
217- foreach ($ this ->monitors as $ type => [$ ancestor , $ inDepth , , $ callbacks ]) {
218- if (isset ($ inDepth ) && $ inDepth > $ depth ) { // only process if ancestor was deeper than current detachment point
219- assert ($ ancestor !== null );
220- if ($ callbacks ) {
221- $ this ->monitors [$ type ] = [null , null , null , $ callbacks ]; // clear cached object, keep listener registrations
222- foreach ($ callbacks [1 ] as $ detached ) {
223- $ listeners [] = [$ detached , $ ancestor ];
224- }
225- } else { // no listeners, just cached lookup result - clear it
226- unset($ this ->monitors [$ type ]);
227- }
228- }
229- }
230- } else { // attaching
213+ if ($ missing !== null ) { // attaching
231214 foreach ($ this ->monitors as $ type => [$ ancestor , , , $ callbacks ]) {
232215 if (isset ($ ancestor )) { // already cached and valid - skip
233216 continue ;
@@ -243,7 +226,10 @@ private function refreshMonitors(int $depth, ?array &$missing = null, array &$li
243226 assert ($ type !== '' );
244227 if ($ ancestor = $ this ->lookup ($ type , throw: false )) {
245228 foreach ($ callbacks [0 ] as $ attached ) {
246- $ listeners [] = [$ attached , $ ancestor ];
229+ if (!in_array ($ key = [$ attached , spl_object_id ($ ancestor )], $ called , strict: false )) { // ceduplicate: same callback + same object = call once
230+ $ attached ($ ancestor );
231+ $ called [] = $ key ;
232+ }
247233 }
248234 } else {
249235 $ missing [$ type ] = true ; // ancestor not found - remember so we don't check again
@@ -254,18 +240,33 @@ private function refreshMonitors(int $depth, ?array &$missing = null, array &$li
254240 }
255241 }
256242
257- if ($ depth === 0 && !$ this ->callingListeners ) { // call listeners
258- $ this ->callingListeners = true ;
259- try {
260- $ called = [];
261- foreach ($ listeners as [$ callback , $ component ]) {
262- if (!in_array ($ key = [$ callback , spl_object_id ($ component )], $ called , strict: false )) { // deduplicate: same callback + same object = call once
263- $ callback ($ component );
264- $ called [] = $ key ;
243+ if ($ this instanceof IContainer) {
244+ foreach ($ this ->getComponents () as $ component ) {
245+ if ($ component instanceof self
246+ && !isset ($ processed [spl_object_id ($ component )]) // component may have been processed already
247+ && $ component ->getParent () === $ this // may have been removed by previous sibling's listener
248+ ) {
249+ $ component ->refreshMonitors ($ depth + 1 , $ missing , $ called , $ processed );
250+ }
251+ }
252+ }
253+
254+ if ($ missing === null ) { // detaching
255+ foreach ($ this ->monitors as $ type => [$ ancestor , $ inDepth , , $ callbacks ]) {
256+ if (isset ($ inDepth ) && $ inDepth > $ depth ) { // only process if ancestor was deeper than current detachment point
257+ assert ($ ancestor !== null );
258+ if ($ callbacks ) {
259+ $ this ->monitors [$ type ] = [null , null , null , $ callbacks ]; // clear cached object, keep listener registrations
260+ foreach ($ callbacks [1 ] as $ detached ) {
261+ if (!in_array ($ key = [$ detached , spl_object_id ($ ancestor )], $ called , strict: false )) { // ceduplicate: same callback + same object = call once
262+ $ detached ($ ancestor );
263+ $ called [] = $ key ;
264+ }
265+ }
266+ } else { // no listeners, just cached lookup result - clear it
267+ unset($ this ->monitors [$ type ]);
265268 }
266269 }
267- } finally {
268- $ this ->callingListeners = false ;
269270 }
270271 }
271272 }
0 commit comments