• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / component-model / 21196680391

21 Jan 2026 03:57AM UTC coverage: 87.448% (+2.3%) from 85.116%
21196680391

push

github

dg
attached handles are called top-down (ancestor → descendant) (BC break)

50 of 55 new or added lines in 1 file covered. (90.91%)

10 existing lines in 2 files now uncovered.

209 of 239 relevant lines covered (87.45%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.06
/src/ComponentModel/Component.php
1
<?php
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\ComponentModel;
11

12
use Nette;
13
use function func_num_args, in_array, substr;
14

15

16
/**
17
 * Base class for all components. Components have a parent, name, and can be monitored by ancestors.
18
 *
19
 * @property-read string $name
20
 * @property-deprecated ?IContainer $parent
21
 */
22
abstract class Component implements IComponent
23
{
24
        use Nette\SmartObject;
25

26
        private ?IContainer $parent = null;
27
        private ?string $name = null;
28

29
        /**
30
         * Monitors: tracks monitored ancestors and registered callbacks.
31
         * Combines cached lookup results with callback registrations for each monitored type.
32
         * Depth is used to detect when monitored ancestor becomes unreachable during detachment.
33
         * Structure: [type => [found object, depth to object, path to object, [[attached, detached], ...]]]
34
         * @var array<''|class-string<Nette\ComponentModel\IComponent>, array{?IComponent, ?int, ?string, array<int, array{?\Closure, ?\Closure}>}>
35
         */
36
        private array $monitors = [];
37

38

39
        /**
40
         * Finds the closest ancestor of specified type.
41
         * @template T of IComponent
42
         * @param ?class-string<T>  $type
43
         * @param bool  $throw  throw exception if component doesn't exist?
44
         * @return ($type is null ? ($throw is true ? IComponent : ?IComponent) : ($throw is true ? T : ?T))
45
         */
46
        final public function lookup(?string $type, bool $throw = true): ?IComponent
1✔
47
        {
48
                $type ??= '';
1✔
49
                if (!isset($this->monitors[$type])) { // not monitored or not processed yet
1✔
50
                        $ancestor = $this->parent;
1✔
51
                        $path = self::NameSeparator . $this->name;
1✔
52
                        $depth = 1;
1✔
53
                        while ($ancestor !== null) {
1✔
54
                                $parent = $ancestor->getParent();
1✔
55
                                if ($type ? $ancestor instanceof $type : $parent === null) {
1✔
56
                                        break;
1✔
57
                                }
58

59
                                $path = self::NameSeparator . $ancestor->getName() . $path;
1✔
60
                                $depth++;
1✔
61
                                $ancestor = $parent; // IComponent::getParent()
1✔
62
                                if ($ancestor === $this) {
1✔
UNCOV
63
                                        $ancestor = null; // prevent cycling
×
64
                                }
65
                        }
66

67
                        $this->monitors[$type] = $ancestor
1✔
68
                                ? [$ancestor, $depth, substr($path, 1), []]
1✔
69
                                : [null, null, null, []]; // not found
1✔
70
                }
71

72
                if ($throw && $this->monitors[$type][0] === null) {
1✔
73
                        $desc = $this->name === null ? "type of '" . static::class . "'" : "'$this->name'";
1✔
74
                        throw new Nette\InvalidStateException("Component $desc is not attached to '$type'.");
1✔
75
                }
76

77
                return $this->monitors[$type][0];
1✔
78
        }
79

80

81
        /**
82
         * Finds the closest ancestor specified by class or interface name and returns backtrace path.
83
         * A path is the concatenation of component names separated by self::NameSeparator.
84
         * @param ?class-string<IComponent>  $type
85
         * @param bool  $throw  throw exception if component doesn't exist?
86
         * @return ($throw is true ? string : ?string)
87
         */
88
        final public function lookupPath(?string $type = null, bool $throw = true): ?string
1✔
89
        {
90
                $this->lookup($type, $throw);
1✔
91
                return $this->monitors[$type ?? ''][2];
1✔
92
        }
93

94

95
        /**
96
         * Starts monitoring ancestors for attach/detach events.
97
         * @template T of IComponent
98
         * @param class-string<T>  $type
99
         * @param ?(callable(T): void)  $attached  called when attached to a monitored ancestor
100
         * @param ?(callable(T): void)  $detached  called before detaching from a monitored ancestor
101
         */
102
        final public function monitor(string $type, ?callable $attached = null, ?callable $detached = null): void
1✔
103
        {
104
                if (func_num_args() === 1) {
1✔
105
                        $class = (new \ReflectionMethod($this, 'attached'))->getDeclaringClass()->getName();
1✔
106
                        trigger_error(__METHOD__ . "(): Methods $class::attached() and $class::detached() are deprecated, use monitor(\$type, [attached], [detached])", E_USER_DEPRECATED);
1✔
107
                        $attached = $this->attached(...);
1✔
108
                        $detached = $this->detached(...);
1✔
109
                } else {
110
                        $attached = $attached ? $attached(...) : null;
1✔
111
                        $detached = $detached ? $detached(...) : null;
1✔
112
                }
113

114
                if (
115
                        $attached
1✔
116
                        && ($ancestor = $this->lookup($type, throw: false))
1✔
117
                        && !in_array([$attached, $detached], $this->monitors[$type][3], strict: false)
1✔
118
                ) {
119
                        $attached($ancestor);
1✔
120
                }
121

122
                $this->monitors[$type][3][] = [$attached, $detached]; // mark as monitored
1✔
123
        }
1✔
124

125

126
        /**
127
         * Stops monitoring ancestors of specified type.
128
         * @param class-string<IComponent>  $type
129
         */
130
        final public function unmonitor(string $type): void
131
        {
UNCOV
132
                unset($this->monitors[$type]);
×
133
        }
134

135

136
        /**
137
         * This method will be called when the component (or component's parent)
138
         * becomes attached to a monitored object. Do not call this method yourself.
139
         * @deprecated  use monitor($type, $attached)
140
         */
141
        protected function attached(IComponent $obj): void
142
        {
143
        }
144

145

146
        /**
147
         * This method will be called before the component (or component's parent)
148
         * becomes detached from a monitored object. Do not call this method yourself.
149
         * @deprecated  use monitor($type, null, $detached)
150
         */
151
        protected function detached(IComponent $obj): void
152
        {
153
        }
154

155

156
        /********************* interface IComponent ****************d*g**/
157

158

159
        final public function getName(): ?string
160
        {
161
                return $this->name;
1✔
162
        }
163

164

165
        /**
166
         * Returns the parent container if any.
167
         */
168
        final public function getParent(): ?IContainer
169
        {
170
                return $this->parent;
1✔
171
        }
172

173

174
        /**
175
         * Sets or removes the parent of this component. This method is managed by containers and should
176
         * not be called by applications
177
         * @throws Nette\InvalidStateException
178
         * @internal
179
         */
180
        public function setParent(?IContainer $parent, ?string $name = null): static
1✔
181
        {
182
                if ($parent === null && $this->parent === null && $name !== null) {
1✔
UNCOV
183
                        $this->name = $name; // just rename
×
UNCOV
184
                        return $this;
×
185

186
                } elseif ($parent === $this->parent && $name === null) {
1✔
UNCOV
187
                        return $this; // nothing to do
×
188
                }
189

190
                // A component cannot be given a parent if it already has a parent.
191
                if ($this->parent !== null && $parent !== null) {
1✔
192
                        throw new Nette\InvalidStateException("Component '$this->name' already has a parent.");
1✔
193
                }
194

195
                // remove from parent
196
                if ($parent === null) {
1✔
197
                        $this->refreshMonitors(0);
1✔
198
                        $this->parent = null;
1✔
199

200
                } else { // add to parent
201
                        $this->validateParent($parent);
1✔
202
                        $this->parent = $parent;
1✔
203
                        if ($name !== null) {
1✔
204
                                $this->name = $name;
1✔
205
                        }
206

207
                        $tmp = [];
1✔
208
                        $this->refreshMonitors(0, $tmp);
1✔
209
                }
210

211
                return $this;
1✔
212
        }
213

214

215
        /**
216
         * Validates the new parent before it's set.
217
         * Descendant classes can override this to implement custom validation logic.
218
         * @throws Nette\InvalidStateException
219
         */
220
        protected function validateParent(IContainer $parent): void
1✔
221
        {
222
        }
1✔
223

224

225
        /**
226
         * Refreshes monitors - calls attached/detached listeners.
227
         *
228
         * FULL VERSION (NO Contract Restrictions):
229
         * - Listeners CAN modify anything (including self, parent, siblings)
230
         * - Implementation handles all edge cases and tree mutations
231
         * - More complex but maximum flexibility
232
         *
233
         * IMPLEMENTATION STRATEGY (Full Version - Defensive Traversal):
234
         * 1. ATTACHING: Call parent listeners first, then children (top-down)
235
         * 2. DETACHING: Call children listeners first, then parent (bottom-up)
236
         * 3. Track which components already processed (prevents reentry)
237
         * 4. Before recursing to children, verify they still exist
238
         * 5. Handle newly added components during traversal
239
         * 6. Detect and prevent infinite loops
240
         *
241
         * KEY BENEFITS:
242
         * - Handles ANY listener mutation (remove self, siblings, parent)
243
         * - No contract to enforce
244
         * - Maximum flexibility
245
         *
246
         * TRADE-OFFS:
247
         * - More complex implementation (~2x code)
248
         * - Additional runtime checks (validity, deduplication)
249
         * - Slightly slower (O(n�) worst case vs O(n))
250
         *
251
         * INTERNAL STATE:
252
         * $this->monitors[$type] = [
253
         *   0 => monitored object (or null if not found),
254
         *   1 => depth to monitored object,
255
         *   2 => path to monitored object,
256
         *   3 => [[attached callback, detached callback], ...]
257
         * ]
258
         *
259
         * @param  int  $depth  current depth in component tree (0 = root call)
260
         * @param  ?array<string, true>  $unfoundTypes  null = detaching, array = attaching
261
         * @param  array<array{\Closure, ?IComponent}>  $called  deduplication tracking for already called listeners
262
         * @param  array<int, true>  $processed  tracking for already processed components (prevents reentry)
263
         */
264
        private function refreshMonitors(
1✔
265
                int $depth,
266
                ?array &$unfoundTypes = null,
267
                array &$called = [],
268
                array &$processed = [],
269
        ): void
270
        {
271
                // GUARD: Prevent reentry on same component (can happen if listener modifies tree)
272
                $componentId = spl_object_id($this);
1✔
273
                if (isset($processed[$componentId])) {
1✔
NEW
274
                        return; // Already processed this component
×
275
                }
276
                $processed[$componentId] = true;
1✔
277

278
                if ($unfoundTypes === null) {
1✔
279
                        // DETACHING: Children first, then own listeners (bottom-up teardown)
280
                        $this->processChildren($depth, $unfoundTypes, $called, $processed);
1✔
281
                        $this->invokeDetachedListeners($depth, $called);
1✔
282

283
                } else {
284
                        // ATTACHING: Own listeners first, then children (top-down setup)
285
                        $this->invokeAttachedListeners($depth, $unfoundTypes, $called);
1✔
286
                        $this->processChildren($depth, $unfoundTypes, $called, $processed);
1✔
287
                }
288
        }
1✔
289

290

291
        /**
292
         * Processes all child components recursively.
293
         * Validates each child before processing (may be removed/moved by sibling's listener).
294
         * @param  ?array<string, true>  $unfoundTypes
295
         * @param  array<array{\Closure, ?IComponent}>  $called
296
         * @param  array<int, true>  $processed
297
         */
298
        private function processChildren(int $depth, ?array &$unfoundTypes, array &$called, array &$processed): void
1✔
299
        {
300
                if (!($this instanceof IContainer)) {
1✔
301
                        return;
1✔
302
                }
303

304
                // Snapshot children BEFORE processing (listener may modify during iteration)
305
                $children = iterator_to_array($this->getComponents());
1✔
306

307
                foreach ($children as $component) {
1✔
308
                        if (!($component instanceof self)) {
1✔
NEW
309
                                continue;
×
310
                        }
311

312
                        // VALIDITY CHECK: Component may have been removed by previous sibling's listener
313
                        if ($component->getParent() !== $this) {
1✔
314
                                continue; // Component was removed or moved - skip it
1✔
315
                        }
316

317
                        // DEDUPLICATION CHECK: Component may have been processed already
318
                        if (isset($processed[spl_object_id($component)])) {
1✔
NEW
319
                                continue; // Already processed - skip
×
320
                        }
321

322
                        // Component is valid and not yet processed - recurse
323
                        $component->refreshMonitors($depth + 1, $unfoundTypes, $called, $processed);
1✔
324
                }
325
        }
1✔
326

327

328
        /**
329
         * Invokes all attached listeners for this component.
330
         * Looks up monitored ancestors and calls registered attached callbacks.
331
         * @param  ?array<string, true>  $unfoundTypes
332
         * @param  array<array{\Closure, ?IComponent}>  $called
333
         */
334
        private function invokeAttachedListeners(int $depth, ?array &$unfoundTypes, array &$called): void
1✔
335
        {
336
                foreach ($this->monitors as $type => $monitor) {
1✔
337
                        if (isset($monitor[0])) {
1✔
338
                                // Already cached and valid - skip
339
                                continue;
1✔
340

341
                        } elseif (!$monitor[3]) {
1✔
342
                                // No listeners registered, just old cached lookup - clear it
NEW
343
                                unset($this->monitors[$type]);
×
344

345
                        } elseif (isset($unfoundTypes[$type])) {
1✔
346
                                // Already checked during this attach operation - ancestor not found
347
                                // Keep listener registrations but clear cache
NEW
348
                                $this->monitors[$type] = [null, null, null, $monitor[3]];
×
349

350
                        } else {
351
                                // Need to check if ancestor exists
352
                                unset($this->monitors[$type]); // force fresh lookup
1✔
353

354
                                if ($ancestor = $this->lookup($type, throw: false)) {
1✔
355
                                        // Found monitored ancestor! Call ALL attached callbacks
356
                                        foreach ($monitor[3] as $callbacks) {
1✔
357
                                                if ($callbacks[0]) { // attached callback may be null
1✔
358
                                                        // Deduplicate: same callback + same object = call once
359
                                                        $key = [$callbacks[0], $ancestor];
1✔
360
                                                        if (!in_array($key, $called, strict: false)) {
1✔
361
                                                                $callbacks[0]($ancestor); // *** CALL ATTACHED CALLBACK ***
1✔
362
                                                                $called[] = $key;
1✔
363
                                                        }
364
                                                }
365
                                        }
366
                                } else {
367
                                        // Ancestor not found - remember so we don't check again
368
                                        $unfoundTypes[$type] = true;
1✔
369
                                }
370

371
                                // Restore listener registrations (lookup() cached result in $this->monitors[$type])
372
                                $this->monitors[$type][3] = $monitor[3];
1✔
373
                        }
374
                }
375
        }
1✔
376

377

378
        /**
379
         * Invokes all detached listeners for this component.
380
         * Only processes monitors deeper than current detachment point.
381
         * @param  array<array{\Closure, ?IComponent}>  $called
382
         */
383
        private function invokeDetachedListeners(int $depth, array &$called): void
1✔
384
        {
385
                foreach ($this->monitors as $type => $monitor) {
1✔
386
                        // $monitor[1] is depth to monitored ancestor
387
                        // Only process if ancestor was deeper than current detachment point
388
                        if (isset($monitor[1]) && $monitor[1] > $depth) {
1✔
389
                                if ($monitor[3]) { // has registered listeners
1✔
390
                                        // Clear cached object, keep listener registrations
391
                                        $this->monitors[$type] = [null, null, null, $monitor[3]];
1✔
392

393
                                        // Call ALL detached callbacks for this monitor type
394
                                        foreach ($monitor[3] as $callbacks) {
1✔
395
                                                if ($callbacks[1]) { // detached callback may be null
1✔
396
                                                        // Deduplicate: same callback + same object = call once
397
                                                        $key = [$callbacks[1], $monitor[0]];
1✔
398
                                                        if (!in_array($key, $called, strict: false)) {
1✔
399
                                                                $callbacks[1]($monitor[0]); // *** CALL DETACHED CALLBACK ***
1✔
400
                                                                $called[] = $key;
1✔
401
                                                        }
402
                                                }
403
                                        }
404
                                } else {
405
                                        // No listeners, just cached lookup result - clear it
406
                                        unset($this->monitors[$type]);
1✔
407
                                }
408
                        }
409
                }
410
        }
1✔
411

412

413
        /********************* cloneable, serializable ****************d*g**/
414

415

416
        /**
417
         * Object cloning.
418
         */
419
        public function __clone()
420
        {
421
                if ($this->parent === null) {
1✔
UNCOV
422
                        return;
×
423

424
                } elseif ($this->parent instanceof Container) {
1✔
425
                        $this->parent = $this->parent->_isCloning();
1✔
426
                        if ($this->parent === null) { // not cloning
1✔
427
                                $this->refreshMonitors(0);
1✔
428
                        }
429
                } else {
UNCOV
430
                        $this->parent = null;
×
UNCOV
431
                        $this->refreshMonitors(0);
×
432
                }
433
        }
1✔
434

435

436
        /**
437
         * Prevents serialization.
438
         */
439
        final public function __serialize()
440
        {
UNCOV
441
                throw new Nette\NotImplementedException('Object serialization is not supported by class ' . static::class);
×
442
        }
443
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc