@@ -32,9 +32,6 @@ abstract class Component implements IComponent
3232 */
3333 private array $ monitors = [];
3434
35- /** Prevents nested listener execution during refreshMonitors */
36- private bool $ callingListeners = false ;
37-
3835
3936 /**
4037 * Finds the closest ancestor of specified type.
@@ -199,35 +196,25 @@ protected function validateParent(IContainer $parent): void
199196
200197
201198 /**
202- * Refreshes monitors.
203- * @param ?array<string, true> $missing (array = attaching, null = detaching)
204- * @param array<int, array{?\Closure, IComponent}> $listeners
199+ * Refreshes monitors when attaching/detaching from component tree.
200+ * @param ?array<string, true> $missing null = detaching, array = attaching
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- }
211+ $ componentId = spl_object_id ($ this );
212+ if (isset ($ processed [$ componentId ])) {
213+ return ; // Prevent reentry on same component (can happen if listener modifies tree)
214214 }
215+ $ processed [$ componentId ] = true ;
215216
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 ];
222- foreach ($ callbacks 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
217+ if ($ missing !== null ) { // attaching
231218 foreach ($ this ->monitors as $ type => [$ ancestor , , , $ callbacks ]) {
232219 if (isset ($ ancestor )) { // already cached and valid - skip
233220 continue ;
@@ -243,7 +230,10 @@ private function refreshMonitors(int $depth, ?array &$missing = null, array &$li
243230 assert ($ type !== '' );
244231 if ($ ancestor = $ this ->lookup ($ type , throw: false )) {
245232 foreach ($ callbacks as [$ attached ]) {
246- $ listeners [] = [$ attached , $ ancestor ];
233+ if ($ attached && !in_array ($ key = [$ attached , spl_object_id ($ ancestor )], $ called , false )) {
234+ $ attached ($ ancestor );
235+ $ called [] = $ key ; // Deduplicate: same callback + same object = call once
236+ }
247237 }
248238 } else {
249239 $ missing [$ type ] = true ; // ancestor not found - remember so we don't check again
@@ -254,19 +244,33 @@ private function refreshMonitors(int $depth, ?array &$missing = null, array &$li
254244 }
255245 }
256246
257- if ($ depth === 0 && !$ this ->callingListeners ) { // call listeners
258- $ this ->callingListeners = true ;
259- try {
260- $ called = [];
261- foreach ($ listeners as [$ callback , $ component ]) {
262- $ key = [$ callback , spl_object_id ($ component )];
263- if ($ callback && !in_array ($ key , $ called , strict: false )) { // deduplicate: same callback + same object = call once
264- $ callback ($ component );
265- $ called [] = $ key ;
247+ if ($ this instanceof IContainer) {
248+ foreach ($ this ->getComponents () as $ component ) {
249+ if ($ component instanceof self
250+ && !isset ($ processed [spl_object_id ($ component )]) // component may have been processed already
251+ && $ component ->getParent () === $ this // may have been removed by previous sibling's listener
252+ ) {
253+ $ component ->refreshMonitors ($ depth + 1 , $ missing , $ called , $ processed );
254+ }
255+ }
256+ }
257+
258+ if ($ missing === null ) { // detaching
259+ foreach ($ this ->monitors as $ type => [$ ancestor , $ inDepth , , $ callbacks ]) {
260+ if (isset ($ inDepth ) && $ inDepth > $ depth ) { // only process if ancestor was deeper than current detachment point
261+ assert ($ ancestor !== null );
262+ if ($ callbacks ) {
263+ $ this ->monitors [$ type ] = [null , null , null , $ callbacks ]; // clear cached object, keep listener registrations
264+ foreach ($ callbacks as [, $ detached ]) {
265+ if ($ detached && !in_array ($ key = [$ detached , spl_object_id ($ ancestor )], $ called , false )) {
266+ $ detached ($ ancestor );
267+ $ called [] = $ key ; // Deduplicate: same callback + same object = call once
268+ }
269+ }
270+ } else { // no listeners, just cached lookup result - clear it
271+ unset($ this ->monitors [$ type ]);
266272 }
267273 }
268- } finally {
269- $ this ->callingListeners = false ;
270274 }
271275 }
272276 }
0 commit comments