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

nette / component-model / 22884715739

04 Mar 2026 07:34AM UTC coverage: 87.444% (+0.3%) from 87.156%
22884715739

push

github

dg
made static analysis mandatory

195 of 223 relevant lines covered (87.44%)

0.87 hits per line

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

88.89
/src/ComponentModel/Component.php
1
<?php declare(strict_types=1);
1✔
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
namespace Nette\ComponentModel;
9

10
use Nette;
11
use function func_num_args, in_array, substr;
12

13

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

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

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

36
        /** Prevents nested listener execution during refreshMonitors */
37
        private bool $callingListeners = false;
38

39

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

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

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

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

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

81

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

95

96
        /**
97
         * Starts monitoring ancestors for attach/detach events.
98
         * @template T of IComponent
99
         * @param class-string<T>  $type
100
         * @param ?(callable(T): void)  $attached  called when attached to a monitored ancestor
101
         * @param ?(callable(T): void)  $detached  called before detaching from a monitored ancestor
102
         */
103
        final public function monitor(string $type, ?callable $attached = null, ?callable $detached = null): void
1✔
104
        {
105
                if (func_num_args() === 1) {
1✔
106
                        $attached = $this->attached(...);
1✔
107
                        $detached = $this->detached(...);
1✔
108
                }
109

110
                $ancestor = $this->lookup($type, throw: false);
1✔
111
                $this->monitors[$type][3] ??= [[], []];
1✔
112

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

120
                if ($detached) {
1✔
121
                        $this->monitors[$type][3][1][] = $detached(...);
1✔
122
                }
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
        {
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✔
183
                        $this->name = $name; // just rename
×
184
                        return $this;
×
185

186
                } elseif ($parent === $this->parent && $name === null) {
1✔
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 when attaching/detaching from component tree.
227
         * @param  ?array<string, true>  $missing  null = detaching, array = attaching
228
         * @param  array<int, array{\Closure(IComponent): void, IComponent}>  $listeners
229
         */
230
        private function refreshMonitors(int $depth, ?array &$missing = null, array &$listeners = []): void
1✔
231
        {
232
                if ($this instanceof IContainer) {
1✔
233
                        foreach ($this->getComponents() as $component) {
1✔
234
                                if ($component instanceof self) {
1✔
235
                                        $component->refreshMonitors($depth + 1, $missing, $listeners);
1✔
236
                                }
237
                        }
238
                }
239

240
                if ($missing === null) { // detaching
1✔
241
                        foreach ($this->monitors as $type => [$ancestor, $inDepth, , $callbacks]) {
1✔
242
                                if (isset($inDepth) && $inDepth > $depth) { // only process if ancestor was deeper than current detachment point
1✔
243
                                        assert($ancestor !== null);
244
                                        if ($callbacks) {
1✔
245
                                                $this->monitors[$type] = [null, null, null, $callbacks]; // clear cached object, keep listener registrations
1✔
246
                                                foreach ($callbacks[1] as $detached) {
1✔
247
                                                        $listeners[] = [$detached, $ancestor];
1✔
248
                                                }
249
                                        } else { // no listeners, just cached lookup result - clear it
250
                                                unset($this->monitors[$type]);
×
251
                                        }
252
                                }
253
                        }
254
                } else { // attaching
255
                        foreach ($this->monitors as $type => [$ancestor, , , $callbacks]) {
1✔
256
                                if (isset($ancestor)) { // already cached and valid - skip
1✔
257
                                        continue;
1✔
258

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

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

265
                                } else { // need to check if ancestor exists
266
                                        unset($this->monitors[$type]); // force fresh lookup
1✔
267
                                        assert($type !== '');
268
                                        if ($ancestor = $this->lookup($type, throw: false)) {
1✔
269
                                                foreach ($callbacks[0] as $attached) {
1✔
270
                                                        $listeners[] = [$attached, $ancestor];
1✔
271
                                                }
272
                                        } else {
273
                                                $missing[$type] = true; // ancestor not found - remember so we don't check again
1✔
274
                                        }
275

276
                                        $this->monitors[$type][3] = $callbacks; // restore listener (lookup() cached result in $this->monitors[$type])
1✔
277
                                }
278
                        }
279
                }
280

281
                if ($depth === 0 && !$this->callingListeners) { // call listeners
1✔
282
                        $this->callingListeners = true;
1✔
283
                        try {
284
                                $called = [];
1✔
285
                                foreach ($listeners as [$callback, $component]) {
1✔
286
                                        if (!in_array($key = [$callback, $component], $called, strict: false)) { // deduplicate: same callback + same object = call once
1✔
287
                                                $callback($component);
1✔
288
                                                $called[] = $key;
1✔
289
                                        }
290
                                }
291
                        } finally {
1✔
292
                                $this->callingListeners = false;
1✔
293
                        }
294
                }
295
        }
1✔
296

297

298
        /********************* cloneable, serializable ****************d*g**/
299

300

301
        /**
302
         * Object cloning.
303
         */
304
        public function __clone()
305
        {
306
                if ($this->parent === null) {
1✔
307
                        return;
×
308

309
                } elseif ($this->parent instanceof Container) {
1✔
310
                        $this->parent = $this->parent->_isCloning();
1✔
311
                        if ($this->parent === null) { // not cloning
1✔
312
                                $this->refreshMonitors(0);
1✔
313
                        }
314
                } else {
315
                        $this->parent = null;
×
316
                        $this->refreshMonitors(0);
×
317
                }
318
        }
1✔
319

320

321
        /**
322
         * Prevents serialization.
323
         */
324
        final public function __serialize()
325
        {
326
                throw new Nette\NotImplementedException('Object serialization is not supported by class ' . static::class);
×
327
        }
328
}
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