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

nette / component-model / 21200028055

21 Jan 2026 06:45AM UTC coverage: 86.7%. Remained the same
21200028055

push

github

dg
added CLAUDE.md

176 of 203 relevant lines covered (86.7%)

0.87 hits per line

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

88.46
/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
        private ?IContainer $parent = null;
22
        private ?string $name = null;
23

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

33

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

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

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

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

72
                return $this->monitors[$type][0];
1✔
73
        }
74

75

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

89

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

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

113
                $this->monitors[$type][3][] = [$attached, $detached]; // mark as monitored
1✔
114
        }
1✔
115

116

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

126

127
        /********************* interface IComponent ****************d*g**/
128

129

130
        final public function getName(): ?string
131
        {
132
                return $this->name;
1✔
133
        }
134

135

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

144

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

157
                } elseif ($parent === $this->parent && $name === null) {
1✔
158
                        return $this; // nothing to do
×
159
                }
160

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

166
                // remove from parent
167
                if ($parent === null) {
1✔
168
                        $this->refreshMonitors(0);
1✔
169
                        $this->parent = null;
1✔
170

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

178
                        $tmp = [];
1✔
179
                        $this->refreshMonitors(0, $tmp);
1✔
180
                }
181

182
                return $this;
1✔
183
        }
184

185

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

195

196
        /**
197
         * Refreshes monitors when attaching/detaching from component tree.
198
         * @param  ?array<string, true>  $missing  null = detaching, array = attaching
199
         * @param  array<array{\Closure, int}>  $called  deduplication tracking
200
         * @param  array<int, true>  $processed  prevents reentry
201
         */
202
        private function refreshMonitors(
1✔
203
                int $depth,
204
                ?array &$missing = null,
205
                array &$called = [],
206
                array &$processed = [],
207
        ): void
208
        {
209
                $processed[spl_object_id($this)] = true;
1✔
210

211
                if ($missing !== null) { // attaching
1✔
212
                        foreach ($this->monitors as $type => [$ancestor, , , $callbacks]) {
1✔
213
                                if (isset($ancestor)) { // already cached and valid - skip
1✔
214
                                        continue;
1✔
215

216
                                } elseif (!$callbacks) { // no listeners, just old cached lookup - clear it
1✔
217
                                        unset($this->monitors[$type]);
×
218

219
                                } elseif (isset($missing[$type])) { // already checked during this attach operation - ancestor not found
1✔
220
                                        $this->monitors[$type] = [null, null, null, $callbacks]; // keep listener registrations but clear cache
×
221

222
                                } else { // need to check if ancestor exists
223
                                        unset($this->monitors[$type]); // force fresh lookup
1✔
224
                                        assert($type !== '');
225
                                        if ($ancestor = $this->lookup($type, throw: false)) {
1✔
226
                                                foreach ($callbacks as [$attached]) {
1✔
227
                                                        if ($attached && !in_array($key = [$attached, spl_object_id($ancestor)], $called, strict: false)) {
1✔
228
                                                                $attached($ancestor);
1✔
229
                                                                $called[] = $key; // Deduplicate: same callback + same object = call once
1✔
230
                                                        }
231
                                                }
232
                                        } else {
233
                                                $missing[$type] = true; // ancestor not found - remember so we don't check again
1✔
234
                                        }
235

236
                                        $this->monitors[$type][3] = $callbacks; // restore listener (lookup() cached result in $this->monitors[$type])
1✔
237
                                }
238
                        }
239
                }
240

241
                if ($this instanceof IContainer) {
1✔
242
                        foreach ($this->getComponents() as $component) {
1✔
243
                                if ($component instanceof self
1✔
244
                                        && !isset($processed[spl_object_id($component)]) // component may have been processed already
1✔
245
                                        && $component->getParent() === $this  // may have been removed by previous sibling's listener
1✔
246
                                ) {
247
                                        $component->refreshMonitors($depth + 1, $missing, $called, $processed);
1✔
248
                                }
249
                        }
250
                }
251

252
                if ($missing === null) { // detaching
1✔
253
                        foreach ($this->monitors as $type => [$ancestor, $inDepth, , $callbacks]) {
1✔
254
                                if (isset($inDepth) && $inDepth > $depth) { // only process if ancestor was deeper than current detachment point
1✔
255
                                        assert($ancestor !== null);
256
                                        if ($callbacks) {
1✔
257
                                                $this->monitors[$type] = [null, null, null, $callbacks]; // clear cached object, keep listener registrations
1✔
258
                                                foreach ($callbacks as [, $detached]) {
1✔
259
                                                        if ($detached && !in_array($key = [$detached, spl_object_id($ancestor)], $called, strict: false)) {
1✔
260
                                                                $detached($ancestor);
1✔
261
                                                                $called[] = $key; // Deduplicate: same callback + same object = call once
1✔
262
                                                        }
263
                                                }
264
                                        } else { // no listeners, just cached lookup result - clear it
265
                                                unset($this->monitors[$type]);
1✔
266
                                        }
267
                                }
268
                        }
269
                }
270
        }
1✔
271

272

273
        /********************* cloneable, serializable ****************d*g**/
274

275

276
        /**
277
         * Object cloning.
278
         */
279
        public function __clone()
280
        {
281
                if ($this->parent === null) {
1✔
282
                        return;
×
283

284
                } elseif ($this->parent instanceof Container) {
1✔
285
                        $this->parent = $this->parent->_isCloning();
1✔
286
                        if ($this->parent === null) { // not cloning
1✔
287
                                $this->refreshMonitors(0);
1✔
288
                        }
289
                } else {
290
                        $this->parent = null;
×
291
                        $this->refreshMonitors(0);
×
292
                }
293
        }
1✔
294

295

296
        /**
297
         * Prevents serialization.
298
         */
299
        final public function __serialize()
300
        {
301
                throw new Nette\NotImplementedException('Object serialization is not supported by class ' . static::class);
×
302
        }
303
}
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