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

nette / component-model / 21198407662

21 Jan 2026 05:27AM UTC coverage: 86.425%. Remained the same
21198407662

push

github

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

Implementation handles tree mutations during listener execution:
- Listeners can modify tree (remove self, siblings, parent)
- Validity check before processing children
- Deduplication prevents calling same listener twice
- Reentry guard prevents infinite loops

47 of 52 new or added lines in 1 file covered. (90.38%)

9 existing lines in 1 file now uncovered.

191 of 221 relevant lines covered (86.43%)

0.86 hits per line

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

87.7
/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 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
abstract class Component implements IComponent
20
{
21
        use Nette\SmartObject;
22

23
        private ?IContainer $parent = null;
24
        private ?string $name = null;
25

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

35

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

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

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

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

74
                return $this->monitors[$type][0];
1✔
75
        }
76

77

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

91

92
        /**
93
         * Starts monitoring ancestors for attach/detach events.
94
         * @template T of IComponent
95
         * @param class-string<T>  $type
96
         * @param ?(callable(T): void)  $attached  called when attached to a monitored ancestor
97
         * @param ?(callable(T): void)  $detached  called before detaching from a monitored ancestor
98
         */
99
        final public function monitor(string $type, ?callable $attached = null, ?callable $detached = null): void
1✔
100
        {
101
                if (!$attached && !$detached) {
1✔
UNCOV
102
                        throw new Nette\InvalidStateException('At least one handler is required.');
×
103
                } else {
104
                        $attached = $attached ? $attached(...) : null;
1✔
105
                        $detached = $detached ? $detached(...) : null;
1✔
106
                }
107

108
                if (
109
                        $attached
1✔
110
                        && ($ancestor = $this->lookup($type, throw: false))
1✔
111
                        && !in_array([$attached, $detached], $this->monitors[$type][3], strict: false)
1✔
112
                ) {
113
                        $attached($ancestor);
1✔
114
                }
115

116
                $this->monitors[$type][3][] = [$attached, $detached]; // mark as monitored
1✔
117
        }
1✔
118

119

120
        /**
121
         * Stops monitoring ancestors of specified type.
122
         * @param class-string<IComponent>  $type
123
         */
124
        final public function unmonitor(string $type): void
125
        {
UNCOV
126
                unset($this->monitors[$type]);
×
127
        }
128

129

130
        /********************* interface IComponent ****************d*g**/
131

132

133
        final public function getName(): ?string
134
        {
135
                return $this->name;
1✔
136
        }
137

138

139
        /**
140
         * Returns the parent container if any.
141
         */
142
        final public function getParent(): ?IContainer
143
        {
144
                return $this->parent;
1✔
145
        }
146

147

148
        /**
149
         * Sets or removes the parent of this component. This method is managed by containers and should
150
         * not be called by applications
151
         * @throws Nette\InvalidStateException
152
         * @internal
153
         */
154
        public function setParent(?IContainer $parent, ?string $name = null): static
1✔
155
        {
156
                if ($parent === null && $this->parent === null && $name !== null) {
1✔
UNCOV
157
                        $this->name = $name; // just rename
×
158
                        return $this;
×
159

160
                } elseif ($parent === $this->parent && $name === null) {
1✔
UNCOV
161
                        return $this; // nothing to do
×
162
                }
163

164
                // A component cannot be given a parent if it already has a parent.
165
                if ($this->parent !== null && $parent !== null) {
1✔
166
                        throw new Nette\InvalidStateException("Component '$this->name' already has a parent.");
1✔
167
                }
168

169
                // remove from parent
170
                if ($parent === null) {
1✔
171
                        $this->refreshMonitors(0);
1✔
172
                        $this->parent = null;
1✔
173

174
                } else { // add to parent
175
                        $this->validateParent($parent);
1✔
176
                        $this->parent = $parent;
1✔
177
                        if ($name !== null) {
1✔
178
                                $this->name = $name;
1✔
179
                        }
180

181
                        $tmp = [];
1✔
182
                        $this->refreshMonitors(0, $tmp);
1✔
183
                }
184

185
                return $this;
1✔
186
        }
187

188

189
        /**
190
         * Validates the new parent before it's set.
191
         * Descendant classes can override this to implement custom validation logic.
192
         * @throws Nette\InvalidStateException
193
         */
194
        protected function validateParent(IContainer $parent): void
1✔
195
        {
196
        }
1✔
197

198

199
        /**
200
         * Refreshes monitors when attaching/detaching from component tree.
201
         * @param  ?array<string, true>  $unfoundTypes  null = detaching, array = attaching
202
         * @param  array<array{?\Closure, int}>  $called  deduplication tracking
203
         * @param  array<int, true>  $processed  prevents reentry
204
         */
205
        private function refreshMonitors(
1✔
206
                int $depth,
207
                ?array &$unfoundTypes = null,
208
                array &$called = [],
209
                array &$processed = [],
210
        ): void
211
        {
212
                $componentId = spl_object_id($this);
1✔
213
                if (isset($processed[$componentId])) {
1✔
NEW
214
                        return; // Prevent reentry on same component (can happen if listener modifies tree)
×
215
                }
216
                $processed[$componentId] = true;
1✔
217

218
                if ($unfoundTypes === null) { // DETACHING
1✔
219
                        $this->processChildren($depth, $unfoundTypes, $called, $processed);
1✔
220
                        $this->invokeDetachedListeners($depth, $called);
1✔
221

222
                } else { // ATTACHING
223
                        $this->invokeAttachedListeners($unfoundTypes, $called);
1✔
224
                        $this->processChildren($depth, $unfoundTypes, $called, $processed);
1✔
225
                }
226
        }
1✔
227

228

229
        /**
230
         * Processes all child components recursively.
231
         * Validates each child before processing (may be removed/moved by sibling's listener).
232
         * @param  ?array<string, true>  $unfoundTypes
233
         * @param  array<array{?\Closure, int}>  $called
234
         * @param  array<int, true>  $processed
235
         */
236
        private function processChildren(int $depth, ?array &$unfoundTypes, array &$called, array &$processed): void
1✔
237
        {
238
                if (!($this instanceof IContainer)) {
1✔
239
                        return;
1✔
240
                }
241

242
                foreach ($this->getComponents() as $component) {
1✔
243
                        if (!($component instanceof self)) {
1✔
NEW
244
                                continue;
×
245
                        }
246

247
                        // VALIDITY CHECK: Component may have been removed by previous sibling's listener
248
                        if ($component->getParent() !== $this) {
1✔
249
                                continue; // Component was removed or moved - skip it
1✔
250
                        }
251

252
                        // DEDUPLICATION CHECK: Component may have been processed already
253
                        if (isset($processed[spl_object_id($component)])) {
1✔
NEW
254
                                continue; // Already processed - skip
×
255
                        }
256

257
                        // Component is valid and not yet processed - recurse
258
                        $component->refreshMonitors($depth + 1, $unfoundTypes, $called, $processed);
1✔
259
                }
260
        }
1✔
261

262

263
        /**
264
         * Invokes all attached listeners for this component.
265
         * Looks up monitored ancestors and calls registered attached callbacks.
266
         * @param  ?array<string, true>  $unfoundTypes
267
         * @param  array<array{?\Closure, int}>  $called
268
         */
269
        private function invokeAttachedListeners(?array &$unfoundTypes, array &$called): void
1✔
270
        {
271
                foreach ($this->monitors as $type => [$ancestor, , , $callbacks]) {
1✔
272
                        if (isset($ancestor)) {
1✔
273
                                // Already cached and valid - skip
274
                                continue;
1✔
275

276
                        } elseif (!$callbacks) {
1✔
277
                                // No listeners registered, just old cached lookup - clear it
NEW
278
                                unset($this->monitors[$type]);
×
279

280
                        } elseif (isset($unfoundTypes[$type])) {
1✔
281
                                // Already checked during this attach operation - ancestor not found
282
                                // Keep listener registrations but clear cache
NEW
283
                                $this->monitors[$type] = [null, null, null, $callbacks];
×
284

285
                        } else {
286
                                // Need to check if ancestor exists
287
                                unset($this->monitors[$type]); // force fresh lookup
1✔
288
                                assert($type !== '');
289
                                if ($ancestor = $this->lookup($type, throw: false)) {
1✔
290
                                        // Found monitored ancestor! Call ALL attached callbacks
291
                                        foreach ($callbacks as [$attached, $detached]) {
1✔
292
                                                // Deduplicate: same callback + same object = call once
293
                                                $key = [$attached, spl_object_id($ancestor)];
1✔
294
                                                if ($attached && !in_array($key, $called, strict: false)) {
1✔
295
                                                        $attached($ancestor);
1✔
296
                                                        $called[] = $key;
1✔
297
                                                }
298
                                        }
299
                                } else {
300
                                        // Ancestor not found - remember so we don't check again
301
                                        $unfoundTypes[$type] = true;
1✔
302
                                }
303

304
                                // Restore listener registrations (lookup() cached result in $this->monitors[$type])
305
                                $this->monitors[$type][3] = $callbacks;
1✔
306
                        }
307
                }
308
        }
1✔
309

310

311
        /**
312
         * Invokes all detached listeners for this component.
313
         * Only processes monitors deeper than current detachment point.
314
         * @param  array<array{?\Closure, int}>  $called
315
         */
316
        private function invokeDetachedListeners(int $depth, array &$called): void
1✔
317
        {
318
                foreach ($this->monitors as $type => [$ancestor, $monitorDepth, , $callbacks]) {
1✔
319
                        // Only process if ancestor was deeper than current detachment point
320
                        if (isset($monitorDepth) && $monitorDepth > $depth) {
1✔
321
                                assert($ancestor !== null);
322
                                if ($callbacks) { // has registered listeners
1✔
323
                                        // Clear cached object, keep listener registrations
324
                                        $this->monitors[$type] = [null, null, null, $callbacks];
1✔
325

326
                                        // Call ALL detached callbacks for this monitor type
327
                                        foreach ($callbacks as [$attached, $detached]) {
1✔
328
                                                // Deduplicate: same callback + same object = call once
329
                                                $key = [$detached, spl_object_id($ancestor)];
1✔
330
                                                if ($detached && !in_array($key, $called, strict: false)) {
1✔
331
                                                        $detached($ancestor);
1✔
332
                                                        $called[] = $key;
1✔
333
                                                }
334
                                        }
335
                                } else {
336
                                        // No listeners, just cached lookup result - clear it
337
                                        unset($this->monitors[$type]);
1✔
338
                                }
339
                        }
340
                }
341
        }
1✔
342

343

344
        /********************* cloneable, serializable ****************d*g**/
345

346

347
        /**
348
         * Object cloning.
349
         */
350
        public function __clone()
351
        {
352
                if ($this->parent === null) {
1✔
UNCOV
353
                        return;
×
354

355
                } elseif ($this->parent instanceof Container) {
1✔
356
                        $this->parent = $this->parent->_isCloning();
1✔
357
                        if ($this->parent === null) { // not cloning
1✔
358
                                $this->refreshMonitors(0);
1✔
359
                        }
360
                } else {
UNCOV
361
                        $this->parent = null;
×
UNCOV
362
                        $this->refreshMonitors(0);
×
363
                }
364
        }
1✔
365

366

367
        /**
368
         * Prevents serialization.
369
         */
370
        final public function __serialize()
371
        {
UNCOV
372
                throw new Nette\NotImplementedException('Object serialization is not supported by class ' . static::class);
×
373
        }
374
}
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